2022-05-09 09:59:31 +00:00
|
|
|
import isMobile from 'ismobilejs'
|
2022-07-20 21:20:43 +00:00
|
|
|
import { isObject, mergeWith } from 'lodash'
|
2022-05-09 09:59:31 +00:00
|
|
|
import { cleanup, render, RenderOptions } from '@testing-library/vue'
|
2022-05-09 12:25:19 +00:00
|
|
|
import { afterEach, beforeEach, vi } from 'vitest'
|
2022-09-03 08:32:09 +00:00
|
|
|
import { clickaway, focus } from '@/directives'
|
2022-05-09 09:59:31 +00:00
|
|
|
import { defineComponent, nextTick } from 'vue'
|
2022-05-11 09:12:26 +00:00
|
|
|
import { commonStore, userStore } from '@/stores'
|
|
|
|
import factory from '@/__tests__/factory'
|
2022-10-08 10:54:25 +00:00
|
|
|
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
2022-07-26 14:46:02 +00:00
|
|
|
import { DialogBoxStub, MessageToasterStub } from '@/__tests__/stubs'
|
2022-10-08 10:54:25 +00:00
|
|
|
import { routes } from '@/config'
|
|
|
|
import Router from '@/router'
|
2022-07-20 21:20:43 +00:00
|
|
|
|
|
|
|
// A deep-merge function that
|
|
|
|
// - supports symbols as keys (_.merge doesn't)
|
|
|
|
// - supports Vue's Ref type without losing reactivity (deepmerge doesn't)
|
|
|
|
// Credit: https://stackoverflow.com/a/60598589/794641
|
|
|
|
const deepMerge = (first: object, second: object) => {
|
|
|
|
return mergeWith(first, second, (a, b) => {
|
|
|
|
if (!isObject(b)) return b
|
|
|
|
|
2022-09-14 12:12:06 +00:00
|
|
|
// @ts-ignore
|
2022-07-20 21:20:43 +00:00
|
|
|
return Array.isArray(a) ? [...a, ...b] : { ...a, ...b }
|
|
|
|
})
|
|
|
|
}
|
2022-05-09 09:59:31 +00:00
|
|
|
|
2022-05-13 17:58:38 +00:00
|
|
|
export default abstract class UnitTestCase {
|
2022-05-09 12:25:19 +00:00
|
|
|
private backupMethods = new Map()
|
2022-10-08 10:54:25 +00:00
|
|
|
protected router: Router
|
2022-05-09 12:25:19 +00:00
|
|
|
|
2022-05-09 09:59:31 +00:00
|
|
|
public constructor () {
|
2022-10-08 10:54:25 +00:00
|
|
|
this.router = new Router(routes)
|
2022-05-09 09:59:31 +00:00
|
|
|
this.beforeEach()
|
|
|
|
this.afterEach()
|
|
|
|
this.test()
|
|
|
|
}
|
|
|
|
|
|
|
|
protected beforeEach (cb?: Closure) {
|
|
|
|
beforeEach(() => {
|
2022-06-10 10:47:46 +00:00
|
|
|
commonStore.state.allow_download = true
|
|
|
|
commonStore.state.use_i_tunes = true
|
2022-05-09 09:59:31 +00:00
|
|
|
cb && cb()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
protected afterEach (cb?: Closure) {
|
|
|
|
afterEach(() => {
|
|
|
|
cleanup()
|
2022-05-09 12:25:19 +00:00
|
|
|
this.restoreAllMocks()
|
2022-05-09 09:59:31 +00:00
|
|
|
isMobile.any = false
|
|
|
|
cb && cb()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-07-19 08:19:57 +00:00
|
|
|
protected actingAs (user?: User) {
|
|
|
|
userStore.state.current = user || factory<User>('user')
|
2022-05-11 09:12:26 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
protected actingAsAdmin () {
|
|
|
|
return this.actingAs(factory.states('admin')<User>('user'))
|
|
|
|
}
|
|
|
|
|
2022-05-14 14:45:48 +00:00
|
|
|
protected mock<T, M extends MethodOf<Required<T>>> (obj: T, methodName: M, implementation?: any) {
|
2022-05-11 07:21:57 +00:00
|
|
|
const mock = vi.fn()
|
|
|
|
|
|
|
|
if (implementation !== undefined) {
|
|
|
|
mock.mockImplementation(implementation instanceof Function ? implementation : () => implementation)
|
|
|
|
}
|
|
|
|
|
2022-05-09 12:25:19 +00:00
|
|
|
this.backupMethods.set([obj, methodName], obj[methodName])
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
obj[methodName] = mock
|
|
|
|
|
|
|
|
return mock
|
|
|
|
}
|
|
|
|
|
|
|
|
protected restoreAllMocks () {
|
|
|
|
this.backupMethods.forEach((fn, [obj, methodName]) => (obj[methodName] = fn))
|
|
|
|
this.backupMethods = new Map()
|
|
|
|
}
|
2022-05-09 09:59:31 +00:00
|
|
|
|
|
|
|
protected render (component: any, options: RenderOptions = {}) {
|
2022-07-20 21:20:43 +00:00
|
|
|
return render(component, deepMerge({
|
2022-05-09 09:59:31 +00:00
|
|
|
global: {
|
|
|
|
directives: {
|
|
|
|
'koel-clickaway': clickaway,
|
2022-09-03 08:32:09 +00:00
|
|
|
'koel-focus': focus
|
2022-07-16 09:52:39 +00:00
|
|
|
},
|
|
|
|
components: {
|
2022-07-25 18:55:40 +00:00
|
|
|
icon: this.stub('icon')
|
2022-05-09 09:59:31 +00:00
|
|
|
}
|
|
|
|
}
|
2022-07-26 14:46:02 +00:00
|
|
|
}, this.supplyRequiredProvides(options)))
|
|
|
|
}
|
|
|
|
|
|
|
|
private supplyRequiredProvides (options: RenderOptions) {
|
|
|
|
options.global = options.global || {}
|
|
|
|
options.global.provide = options.global.provide || {}
|
|
|
|
|
2022-09-08 05:06:49 +00:00
|
|
|
// @ts-ignore
|
2022-07-26 14:46:02 +00:00
|
|
|
if (!options.global.provide?.hasOwnProperty(DialogBoxKey)) {
|
2022-09-08 05:06:49 +00:00
|
|
|
// @ts-ignore
|
2022-07-26 14:46:02 +00:00
|
|
|
options.global.provide[DialogBoxKey] = DialogBoxStub
|
|
|
|
}
|
|
|
|
|
2022-09-08 05:06:49 +00:00
|
|
|
// @ts-ignore
|
2022-07-26 14:46:02 +00:00
|
|
|
if (!options.global.provide?.hasOwnProperty(MessageToasterKey)) {
|
2022-09-08 05:06:49 +00:00
|
|
|
// @ts-ignore
|
2022-07-26 14:46:02 +00:00
|
|
|
options.global.provide[MessageToasterKey] = MessageToasterStub
|
|
|
|
}
|
|
|
|
|
2022-10-08 10:54:25 +00:00
|
|
|
// @ts-ignore
|
|
|
|
if (!options.global.provide.hasOwnProperty(RouterKey)) {
|
|
|
|
// @ts-ignore
|
|
|
|
options.global.provide[RouterKey] = this.router
|
|
|
|
}
|
|
|
|
|
2022-07-26 14:46:02 +00:00
|
|
|
return options
|
2022-05-09 09:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected stub (testId = 'stub') {
|
|
|
|
return defineComponent({
|
|
|
|
template: `<br data-testid="${testId}"/>`
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async tick (count = 1) {
|
|
|
|
for (let i = 0; i < count; ++i) {
|
|
|
|
await nextTick()
|
|
|
|
}
|
|
|
|
}
|
2022-05-09 12:25:19 +00:00
|
|
|
|
2022-05-14 14:45:48 +00:00
|
|
|
protected setReadOnlyProperty<T> (obj: T, prop: keyof T, value: any) {
|
2022-07-25 18:23:30 +00:00
|
|
|
return Object.defineProperties(obj, {
|
|
|
|
[prop]: {
|
|
|
|
value,
|
|
|
|
configurable: true
|
|
|
|
}
|
2022-05-14 14:45:48 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-09 12:25:19 +00:00
|
|
|
protected abstract test ()
|
2022-05-09 09:59:31 +00:00
|
|
|
}
|