From: Eduardo San Martin Morote Date: Thu, 9 Apr 2020 17:20:24 +0000 (+0200) Subject: refactor: use cleaner mount for tests X-Git-Tag: v4.0.0-alpha.6~45 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=04afcb0638a0f3fb3e7058b9e987bd816c6e2293;p=thirdparty%2Fvuejs%2Frouter.git refactor: use cleaner mount for tests --- diff --git a/__tests__/RouterView.spec.ts b/__tests__/RouterView.spec.ts index 2bd39b3b..5ac640bb 100644 --- a/__tests__/RouterView.spec.ts +++ b/__tests__/RouterView.spec.ts @@ -4,8 +4,8 @@ import { View as RouterView } from '../src/components/View' import { components, RouteLocationNormalizedLoose } from './utils' import { START_LOCATION_NORMALIZED } from '../src/types' -import { ref, markNonReactive } from 'vue' -import { mount, tick } from './mount' +import { markNonReactive } from 'vue' +import { mount, createMockedRoute } from './mount2' import { mockWarn } from 'jest-mock-warn' // to have autocompletion @@ -145,70 +145,54 @@ const routes = createRoutes({ describe('RouterView', () => { mockWarn() - function factory(route: RouteLocationNormalizedLoose, props: any = {}) { - const router = { - currentRoute: ref( - markNonReactive({ - ...route, - // reset the instances every time - matched: route.matched.map(match => ({ ...match, instances: {} })), - }) - ), - } - - const { app, el } = mount( - router as any, - { - template: ``, - components: { RouterView }, - setup() { - const name = ref(props.name) - - return { - name, - } - }, - } as any - ) + async function factory( + initialRoute: RouteLocationNormalizedLoose, + propsData: any = {} + ) { + const route = createMockedRoute(initialRoute) + const wrapper = await mount(RouterView, { + propsData, + provide: route.provides, + components: { RouterView }, + }) - return { app, router, el } + return { route, wrapper } } - it('displays current route component', () => { - const { el } = factory(routes.root) - expect(el.innerHTML).toBe(`
Home
`) + it('displays current route component', async () => { + const { wrapper } = await factory(routes.root) + expect(wrapper.html()).toBe(`
Home
`) }) - it('displays named views', () => { - const { el } = factory(routes.named, { name: 'foo' }) - expect(el.innerHTML).toBe(`
Foo
`) + it('displays named views', async () => { + const { wrapper } = await factory(routes.named, { name: 'foo' }) + expect(wrapper.html()).toBe(`
Foo
`) }) - it('displays nothing when route is unmatched', () => { - const { el } = factory(START_LOCATION_NORMALIZED as any) + it('displays nothing when route is unmatched', async () => { + const { wrapper } = await factory(START_LOCATION_NORMALIZED as any) // NOTE: I wonder if this will stay stable in future releases expect('Router').not.toHaveBeenWarned() - expect(el.childElementCount).toBe(0) + expect(wrapper.rootEl.childElementCount).toBe(0) }) - it('displays nested views', () => { - const { el } = factory(routes.nested) - expect(el.innerHTML).toBe(`

Nested

Foo
`) + it('displays nested views', async () => { + const { wrapper } = await factory(routes.nested) + expect(wrapper.html()).toBe(`

Nested

Foo
`) }) - it('displays deeply nested views', () => { - const { el } = factory(routes.nestedNested) - expect(el.innerHTML).toBe( + it('displays deeply nested views', async () => { + const { wrapper } = await factory(routes.nestedNested) + expect(wrapper.html()).toBe( `

Nested

Nested

Foo
` ) }) it('renders when the location changes', async () => { - const { el, router } = factory(routes.root) - expect(el.innerHTML).toBe(`
Home
`) - router.currentRoute.value = routes.foo - await tick() - expect(el.innerHTML).toBe(`
Foo
`) + const { route, wrapper } = await factory(routes.root) + expect(wrapper.html()).toBe(`
Home
`) + await route.set(routes.foo) + expect(wrapper.html()).toBe(`
Foo
`) }) it('does not pass params as props by default', async () => { @@ -216,34 +200,44 @@ describe('RouterView', () => { ...routes.withParams, matched: [{ ...routes.withParams.matched[0], props: false }], } - const { el, router } = factory(noPropsWithParams) - expect(el.innerHTML).toBe(`
User: default
`) - router.currentRoute.value = markNonReactive({ + const { wrapper, route } = await factory(noPropsWithParams) + expect(wrapper.html()).toBe(`
User: default
`) + await route.set({ ...noPropsWithParams, params: { id: '4' }, }) - await tick() - expect(el.innerHTML).toBe(`
User: default
`) + expect(wrapper.html()).toBe(`
User: default
`) }) it('passes params as props with props: true', async () => { - const { el, router } = factory(routes.withParams) - expect(el.innerHTML).toBe(`
User: 1
`) - router.currentRoute.value = markNonReactive({ + const { wrapper, route } = await factory(routes.withParams) + expect(wrapper.html()).toBe(`
User: 1
`) + await route.set({ + ...routes.withParams, + params: { id: '4' }, + }) + expect(wrapper.html()).toBe(`
User: 4
`) + }) + + it('passes params as props with props: true', async () => { + const { wrapper, route } = await factory(routes.withParams) + + expect(wrapper.html()).toBe(`
User: 1
`) + + await route.set({ ...routes.withParams, params: { id: '4' }, }) - await tick() - expect(el.innerHTML).toBe(`
User: 4
`) + expect(wrapper.html()).toBe(`
User: 4
`) }) it('can pass an object as props', async () => { - const { el } = factory(routes.withIdAndOther) - expect(el.innerHTML).toBe(`
id:foo;other:fixed
`) + const { wrapper } = await factory(routes.withIdAndOther) + expect(wrapper.html()).toBe(`
id:foo;other:fixed
`) }) it('can pass a function as props', async () => { - const { el } = factory(routes.withFnProps) - expect(el.innerHTML).toBe(`
id:2;other:page
`) + const { wrapper } = await factory(routes.withFnProps) + expect(wrapper.html()).toBe(`
id:2;other:page
`) }) }) diff --git a/__tests__/mount2.ts b/__tests__/mount2.ts new file mode 100644 index 00000000..9d0d03d3 --- /dev/null +++ b/__tests__/mount2.ts @@ -0,0 +1,153 @@ +import { + Component, + createApp, + defineComponent, + h, + ref, + ComponentPublicInstance, + reactive, + nextTick, + ComponentObjectPropsOptions, + ComputedRef, + computed, + markNonReactive, + App, + ComponentOptionsWithProps, +} from 'vue' +import { RouteLocationNormalizedLoose } from './utils' +import { routeLocationKey } from '../src/utils/injectionSymbols' + +interface MountOptions { + propsData: Record + provide: Record + components: ComponentOptionsWithProps['components'] +} +// { app, vm: instance!, el: rootEl, setProps, provide } +interface Wrapper { + app: App + vm: ComponentPublicInstance + rootEl: HTMLDivElement + setProps(props: MountOptions['propsData']): Promise + html(): string +} + +function initialProps

(propsOption: ComponentObjectPropsOptions

) { + let copy = {} as ComponentPublicInstance['$props'] + + for (let key in propsOption) { + const prop = propsOption[key]! + // @ts-ignore + if (!prop.required && prop.default) + // TODO: function value + // @ts-ignore + copy[key] = prop.default + } + + return copy +} + +export function mount( + // TODO: generic? + targetComponent: Parameters[0], + options: Partial = {} +): Promise { + const TargetComponent = targetComponent as Component + return new Promise(resolve => { + // TODO: props can only be an object + const propsData = reactive( + Object.assign( + initialProps(TargetComponent.props || {}), + options.propsData + ) + ) + + function setProps(partialProps: Record) { + Object.assign(propsData, partialProps) + return nextTick() + } + + const Wrapper = defineComponent({ + setup(_props, { emit }) { + const componentInstanceRef = ref() + + return () => { + return h(TargetComponent, { + ref: componentInstanceRef, + onVnodeMounted() { + emit('ready', componentInstanceRef.value) + }, + ...propsData, + }) + } + }, + }) + + const app = createApp(Wrapper, { + onReady: (instance: ComponentPublicInstance) => { + resolve({ app, vm: instance!, rootEl, setProps, html }) + }, + }) + + if (options.provide) { + const keys = getKeys(options.provide) + + for (let key of keys) { + app.provide(key, options.provide[key as any]) + } + } + + if (options.components) { + for (let key in options.components) { + app.component(key, options.components[key]) + } + } + + // TODO: how to cleanup? + const rootEl = document.createElement('div') + document.body.appendChild(rootEl) + + function html() { + return rootEl.innerHTML + } + + app.mount(rootEl) + }) +} + +function getKeys(object: Record): Array { + return (Object.getOwnPropertyNames(object) as Array).concat( + Object.getOwnPropertySymbols(object) + ) +} + +export function createMockedRoute(initialValue: RouteLocationNormalizedLoose) { + const route = {} as { + [k in keyof RouteLocationNormalizedLoose]: ComputedRef< + RouteLocationNormalizedLoose[k] + > + } + + const routeRef = ref(markNonReactive(initialValue)) + + function set(newRoute: RouteLocationNormalizedLoose) { + routeRef.value = markNonReactive(newRoute) + return nextTick() + } + + for (let key in initialValue) { + // @ts-ignore + route[key] = + // new line to still get errors here + computed(() => routeRef.value[key as keyof RouteLocationNormalizedLoose]) + } + + const value = reactive(route) + + return { + value, + set, + provides: { + [routeLocationKey as symbol]: value, + }, + } +}