Use vitest

This commit is contained in:
tusooa 2025-02-27 22:54:23 -05:00
commit cca5e31f56
No known key found for this signature in database
GPG key ID: 42AEC43D48433C51
15 changed files with 825 additions and 96 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ selenium-debug.log
config/local.json config/local.json
src/assets/emoji.json src/assets/emoji.json
logs/ logs/
__screenshots__/

View file

@ -50,10 +50,15 @@ test:
APT_CACHE_DIR: apt-cache APT_CACHE_DIR: apt-cache
script: script:
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update - mkdir -pv $APT_CACHE_DIR && apt-get -qq update
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn - yarn
- yarn unit - yarn playwright install firefox
- yarn playwright install-deps
- yarn unit-ci
artifacts:
# When the test fails, upload screenshots for better context on why it fails
paths:
- test/**/__screenshots__
when: on_failure
build: build:
stage: build stage: build

View file

@ -19,6 +19,7 @@ export default [
}, },
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.vitest,
...globals.mocha, ...globals.mocha,
...globals.chai, ...globals.chai,
...globals.commonjs, ...globals.commonjs,

View file

@ -7,8 +7,9 @@
"scripts": { "scripts": {
"dev": "node build/update-emoji.js && vite dev", "dev": "node build/update-emoji.js && vite dev",
"build": "node build/update-emoji.js && vite build", "build": "node build/update-emoji.js && vite build",
"unit": "karma start test/unit/karma.conf.js --single-run", "unit": "node build/update-emoji.js && vitest --run",
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
"unit:watch": "node build/update-emoji.js && vitest",
"e2e": "node test/e2e/runner.js", "e2e": "node test/e2e/runner.js",
"test": "yarn run unit && yarn run e2e", "test": "yarn run unit && yarn run e2e",
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", "stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
@ -61,6 +62,8 @@
"@ungap/event-target": "0.2.4", "@ungap/event-target": "0.2.4",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/browser": "^3.0.7",
"@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.2.5", "@vue/babel-plugin-jsx": "1.2.5",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.13",
@ -107,6 +110,7 @@
"nightwatch": "2.6.25", "nightwatch": "2.6.25",
"opn": "5.5.0", "opn": "5.5.0",
"ora": "0.4.1", "ora": "0.4.1",
"playwright": "1.49.1",
"postcss": "8.5.2", "postcss": "8.5.2",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "7.3.4", "postcss-loader": "7.3.4",
@ -129,6 +133,7 @@
"stylelint-webpack-plugin": "^3.3.0", "stylelint-webpack-plugin": "^3.3.0",
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-plugin-pwa": "^0.21.1", "vite-plugin-pwa": "^0.21.1",
"vitest": "^3.0.7",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-style-loader": "4.1.3", "vue-style-loader": "4.1.3",
"webpack": "5.97.1", "webpack": "5.97.1",

View file

@ -119,6 +119,8 @@ const getLatestScrobble = (state, user) => {
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000 state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
} }
}).catch(e => {
console.warn('cannot fetch scrobbles', e)
}) })
} }

View file

@ -1,3 +1,4 @@
/* global process */
function urlBase64ToUint8Array (base64String) { function urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4) const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding) const base64 = (base64String + padding)

View file

@ -109,23 +109,18 @@ export const waitForEvent = (wrapper, event, {
timesEmitted = 1 timesEmitted = 1
} = {}) => { } = {}) => {
const tick = 10 const tick = 10
const totalTries = timeout / tick
return new Promise((resolve, reject) => { return vi.waitFor(
let currentTries = 0 () => {
const wait = () => {
const e = wrapper.emitted(event) const e = wrapper.emitted(event)
if (e && e.length >= timesEmitted) { if (e && e.length >= timesEmitted) {
resolve()
return return
} }
if (currentTries >= totalTries) { throw new Error('event is not emitted')
reject(new Error('Event did not fire')) },
return {
} timeout,
++currentTries interval: tick
setTimeout(wait, tick)
} }
wait() )
})
} }

View file

@ -1,6 +1,5 @@
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import sinon from 'sinon'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { mountOpts, waitForEvent, $t } from '../../../fixtures/setup_test' import { mountOpts, waitForEvent, $t } from '../../../fixtures/setup_test'
@ -25,7 +24,7 @@ const saveManually = async (wrapper) => {
const waitSaveTime = 4000 const waitSaveTime = 4000
afterEach(() => { afterEach(() => {
sinon.restore() vi.useRealTimers()
}) })
describe('Draft saving', () => { describe('Draft saving', () => {
@ -43,12 +42,10 @@ describe('Draft saving', () => {
await saveManually(wrapper) await saveManually(wrapper)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1) expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew') expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
console.log('done')
}) })
it('should auto-save if it is enabled', async function () { it('should auto-save if it is enabled', async function () {
this.timeout(5000) vi.useFakeTimers()
const clock = sinon.useFakeTimers(Date.now())
const wrapper = mount(PostStatusForm, mountOpts()) const wrapper = mount(PostStatusForm, mountOpts())
await wrapper.vm.$store.dispatch('setOption', { await wrapper.vm.$store.dispatch('setOption', {
name: 'autoSaveDraft', name: 'autoSaveDraft',
@ -59,10 +56,9 @@ describe('Draft saving', () => {
await textarea.setValue('mew mew') await textarea.setValue('mew mew')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0) expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
await clock.tickAsync(waitSaveTime) await vi.advanceTimersByTimeAsync(waitSaveTime)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1) expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew') expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
clock.restore()
}) })
it('should auto-save when close if auto-save is on', async () => { it('should auto-save when close if auto-save is on', async () => {
@ -81,7 +77,6 @@ describe('Draft saving', () => {
wrapper.vm.requestClose() wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1) expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'can-close') await waitForEvent(wrapper, 'can-close')
console.log('done')
}) })
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
@ -104,7 +99,6 @@ describe('Draft saving', () => {
wrapper.vm.requestClose() wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1) expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'can-close') await waitForEvent(wrapper, 'can-close')
console.log('done')
}) })
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
@ -127,40 +121,33 @@ describe('Draft saving', () => {
wrapper.vm.requestClose() wrapper.vm.requestClose()
await waitForEvent(wrapper, 'can-close') await waitForEvent(wrapper, 'can-close')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0) expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
console.log('done')
}) })
it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => { it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => {
try { const wrapper = mount(PostStatusForm, mountOpts({
const wrapper = mount(PostStatusForm, mountOpts({ props: {
props: { closeable: true
closeable: true }
} }))
})) await wrapper.vm.$store.dispatch('setOption', {
await wrapper.vm.$store.dispatch('setOption', { name: 'autoSaveDraft',
name: 'autoSaveDraft', value: false
value: false })
}) await wrapper.vm.$store.dispatch('setOption', {
await wrapper.vm.$store.dispatch('setOption', { name: 'unsavedPostAction',
name: 'unsavedPostAction', value: 'confirm'
value: 'confirm' })
}) expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
expect(wrapper.vm.$store.getters.draftCount).to.equal(0) const textarea = wrapper.get('textarea')
const textarea = wrapper.get('textarea') await textarea.setValue('mew mew')
await textarea.setValue('mew mew') wrapper.vm.requestClose()
wrapper.vm.requestClose() await nextTick()
await nextTick() const saveButton = wrapper.findByText('button', $t('post_status.close_confirm_save_button'))
const saveButton = wrapper.findByText('button', $t('post_status.close_confirm_save_button')) expect(saveButton).to.be.ok
expect(saveButton).to.be.ok await saveButton.trigger('click')
await saveButton.trigger('click') console.log('clicked')
console.log('clicked') expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1) await flushPromises()
await flushPromises() await waitForEvent(wrapper, 'can-close')
await waitForEvent(wrapper, 'can-close')
console.log('done')
} catch (e) {
console.log('error:', e)
throw e
}
}) })
}) })

View file

@ -18,7 +18,13 @@ const generateInput = (value, padEmoji = true) => {
$t: (msg) => msg $t: (msg) => msg
}, },
stubs: { stubs: {
FAIcon: true FAIcon: true,
Popover: {
template: `<div><slot trigger /></div>`,
methods: {
updateStyles () {}
}
}
}, },
directives: { directives: {
'click-outside': vClickOutside 'click-outside': vClickOutside
@ -104,43 +110,37 @@ describe('EmojiInput', () => {
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:') expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
}) })
it('correctly sets caret after insertion at beginning', (done) => { it('correctly sets caret after insertion at beginning', async () => {
const initialString = '1234' const initialString = '1234'
const wrapper = generateInput(initialString) const wrapper = generateInput(initialString)
const input = wrapper.find('input') const input = wrapper.find('input')
input.setValue(initialString) input.setValue(initialString)
wrapper.setData({ caret: 0 }) wrapper.setData({ caret: 0 })
wrapper.vm.insert({ insertion: '1234', keepOpen: false }) wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(5) expect(wrapper.vm.caret).to.eql(5)
done()
})
}) })
it('correctly sets caret after insertion at end', (done) => { it('correctly sets caret after insertion at end', async () => {
const initialString = '1234' const initialString = '1234'
const wrapper = generateInput(initialString) const wrapper = generateInput(initialString)
const input = wrapper.find('input') const input = wrapper.find('input')
input.setValue(initialString) input.setValue(initialString)
wrapper.setData({ caret: initialString.length }) wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false }) wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(10) expect(wrapper.vm.caret).to.eql(10)
done()
})
}) })
it('correctly sets caret after insertion if padEmoji setting is set to false', (done) => { it('correctly sets caret after insertion if padEmoji setting is set to false', async () => {
const initialString = '1234' const initialString = '1234'
const wrapper = generateInput(initialString, false) const wrapper = generateInput(initialString, false)
const input = wrapper.find('input') const input = wrapper.find('input')
input.setValue(initialString) input.setValue(initialString)
wrapper.setData({ caret: initialString.length }) wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false }) wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => { await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(8) expect(wrapper.vm.caret).to.eql(8)
done()
})
}) })
}) })
}) })

View file

@ -1,5 +1,8 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js' import * as DateUtils from 'src/services/date_utils/date_utils.js'
beforeEach(() => { vi.useFakeTimers() })
afterEach(() => { vi.useRealTimers() })
describe('DateUtils', () => { describe('DateUtils', () => {
describe('relativeTime', () => { describe('relativeTime', () => {
it('returns now with low enough amount of seconds', () => { it('returns now with low enough amount of seconds', () => {

View file

@ -1,10 +1,13 @@
import { deserialize } from 'src/services/theme_data/iss_deserializer.js' import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
import { serialize } from 'src/services/theme_data/iss_serializer.js' import { serialize } from 'src/services/theme_data/iss_serializer.js'
const componentsContext = require.context('src', true, /\.style.js(on)?$/) const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
)
describe('ISS (de)serialization', () => { describe('ISS (de)serialization', () => {
componentsContext.keys().forEach(key => { Object.keys(componentsContext).forEach(key => {
const component = componentsContext(key).default const component = componentsContext[key].default
it(`(De)serialization of component ${component.name} works`, () => { it(`(De)serialization of component ${component.name} works`, () => {
const normalized = component.defaultRules.map(x => ({ component: component.name, ...x })) const normalized = component.defaultRules.map(x => ({ component: component.name, ...x }))

View file

@ -16,10 +16,10 @@ const checkColors = (output) => {
} }
describe('Theme Data utility functions', () => { describe('Theme Data utility functions', () => {
const context = require.context('static/themes/', false, /\.json$/) const context = import.meta.glob('/public/static/themes/*.json', { import: 'default', eager: true })
context.keys().forEach((key) => { Object.keys(context).forEach((key) => {
it(`Should render all colors for ${key} properly`, () => { it(`Should render all colors for ${key} properly`, () => {
const { theme, source } = context(key) const { theme, source } = context[key]
const data = source || theme const data = source || theme
const colors = getColors(data.colors, data.opacity, 1) const colors = getColors(data.colors, data.opacity, 1)
checkColors(colors) checkColors(colors)

View file

@ -62,8 +62,6 @@ describe('Theme Data 3', () => {
}) })
describe('init', function () { describe('init', function () {
this.timeout(5000)
it('Test initialization without anything', () => { it('Test initialization without anything', () => {
const out = init({ inputRuleset: [], ultimateBackgroundColor: '#DEADAF' }) const out = init({ inputRuleset: [], ultimateBackgroundColor: '#DEADAF' })

View file

@ -27,7 +27,7 @@ const getLocalDevSettings = async () => {
const projectRoot = dirname(fileURLToPath(import.meta.url)) const projectRoot = dirname(fileURLToPath(import.meta.url))
export default defineConfig(async ({ command }) => { export default defineConfig(async ({ mode, command }) => {
const settings = await getLocalDevSettings() const settings = await getLocalDevSettings()
const target = settings.target || 'http://localhost:4000/' const target = settings.target || 'http://localhost:4000/'
const proxy = { const proxy = {
@ -87,7 +87,8 @@ export default defineConfig(async ({ command }) => {
resolve: { resolve: {
alias: { alias: {
src: '/src', src: '/src',
components: '/src/components' components: '/src/components',
...(mode === 'test' ? { vue: 'vue/dist/vue.esm-bundler.js' } : {})
} }
}, },
define: { define: {
@ -131,11 +132,21 @@ export default defineConfig(async ({ command }) => {
}, },
}, },
server: { server: {
proxy, ...(mode === 'test' ? {} : { proxy }),
port: Number(process.env.PORT) || 8080 port: Number(process.env.PORT) || 8080
}, },
preview: { preview: {
proxy proxy
},
test: {
globals: true,
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'firefox' }
]
}
} }
} }
}) })

737
yarn.lock

File diff suppressed because it is too large Load diff