]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor: use cleaner mount for tests
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 9 Apr 2020 17:20:24 +0000 (19:20 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 9 Apr 2020 17:20:24 +0000 (19:20 +0200)
__tests__/RouterView.spec.ts
__tests__/mount2.ts [new file with mode: 0644]

index 2bd39b3bb80909027089aa1c8d2fbb93d0e01bf9..5ac640bb85fb2a82b337a8e4170a0b1faa7ac1bd 100644 (file)
@@ -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: `<RouterView :name="name"></RouterView>`,
-        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(`<div>Home</div>`)
+  it('displays current route component', async () => {
+    const { wrapper } = await factory(routes.root)
+    expect(wrapper.html()).toBe(`<div>Home</div>`)
   })
 
-  it('displays named views', () => {
-    const { el } = factory(routes.named, { name: 'foo' })
-    expect(el.innerHTML).toBe(`<div>Foo</div>`)
+  it('displays named views', async () => {
+    const { wrapper } = await factory(routes.named, { name: 'foo' })
+    expect(wrapper.html()).toBe(`<div>Foo</div>`)
   })
 
-  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(`<div><h2>Nested</h2><div>Foo</div></div>`)
+  it('displays nested views', async () => {
+    const { wrapper } = await factory(routes.nested)
+    expect(wrapper.html()).toBe(`<div><h2>Nested</h2><div>Foo</div></div>`)
   })
 
-  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(
       `<div><h2>Nested</h2><div><h2>Nested</h2><div>Foo</div></div></div>`
     )
   })
 
   it('renders when the location changes', async () => {
-    const { el, router } = factory(routes.root)
-    expect(el.innerHTML).toBe(`<div>Home</div>`)
-    router.currentRoute.value = routes.foo
-    await tick()
-    expect(el.innerHTML).toBe(`<div>Foo</div>`)
+    const { route, wrapper } = await factory(routes.root)
+    expect(wrapper.html()).toBe(`<div>Home</div>`)
+    await route.set(routes.foo)
+    expect(wrapper.html()).toBe(`<div>Foo</div>`)
   })
 
   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(`<div>User: default</div>`)
-    router.currentRoute.value = markNonReactive({
+    const { wrapper, route } = await factory(noPropsWithParams)
+    expect(wrapper.html()).toBe(`<div>User: default</div>`)
+    await route.set({
       ...noPropsWithParams,
       params: { id: '4' },
     })
-    await tick()
-    expect(el.innerHTML).toBe(`<div>User: default</div>`)
+    expect(wrapper.html()).toBe(`<div>User: default</div>`)
   })
 
   it('passes params as props with props: true', async () => {
-    const { el, router } = factory(routes.withParams)
-    expect(el.innerHTML).toBe(`<div>User: 1</div>`)
-    router.currentRoute.value = markNonReactive({
+    const { wrapper, route } = await factory(routes.withParams)
+    expect(wrapper.html()).toBe(`<div>User: 1</div>`)
+    await route.set({
+      ...routes.withParams,
+      params: { id: '4' },
+    })
+    expect(wrapper.html()).toBe(`<div>User: 4</div>`)
+  })
+
+  it('passes params as props with props: true', async () => {
+    const { wrapper, route } = await factory(routes.withParams)
+
+    expect(wrapper.html()).toBe(`<div>User: 1</div>`)
+
+    await route.set({
       ...routes.withParams,
       params: { id: '4' },
     })
-    await tick()
-    expect(el.innerHTML).toBe(`<div>User: 4</div>`)
+    expect(wrapper.html()).toBe(`<div>User: 4</div>`)
   })
 
   it('can pass an object as props', async () => {
-    const { el } = factory(routes.withIdAndOther)
-    expect(el.innerHTML).toBe(`<div>id:foo;other:fixed</div>`)
+    const { wrapper } = await factory(routes.withIdAndOther)
+    expect(wrapper.html()).toBe(`<div>id:foo;other:fixed</div>`)
   })
 
   it('can pass a function as props', async () => {
-    const { el } = factory(routes.withFnProps)
-    expect(el.innerHTML).toBe(`<div>id:2;other:page</div>`)
+    const { wrapper } = await factory(routes.withFnProps)
+    expect(wrapper.html()).toBe(`<div>id:2;other:page</div>`)
   })
 })
diff --git a/__tests__/mount2.ts b/__tests__/mount2.ts
new file mode 100644 (file)
index 0000000..9d0d03d
--- /dev/null
@@ -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<string, any>
+  provide: Record<string | symbol, any>
+  components: ComponentOptionsWithProps['components']
+}
+// { app, vm: instance!, el: rootEl, setProps, provide }
+interface Wrapper {
+  app: App
+  vm: ComponentPublicInstance
+  rootEl: HTMLDivElement
+  setProps(props: MountOptions['propsData']): Promise<void>
+  html(): string
+}
+
+function initialProps<P>(propsOption: ComponentObjectPropsOptions<P>) {
+  let copy = {} as ComponentPublicInstance<typeof propsOption>['$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<typeof createApp>[0],
+  options: Partial<MountOptions> = {}
+): Promise<Wrapper> {
+  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<string, any>) {
+      Object.assign(propsData, partialProps)
+      return nextTick()
+    }
+
+    const Wrapper = defineComponent({
+      setup(_props, { emit }) {
+        const componentInstanceRef = ref<ComponentPublicInstance>()
+
+        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<string | symbol, any>): Array<symbol | string> {
+  return (Object.getOwnPropertyNames(object) as Array<string | symbol>).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,
+    },
+  }
+}