]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
test: test async components
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 16 Mar 2020 14:42:49 +0000 (15:42 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 16 Mar 2020 14:42:49 +0000 (15:42 +0100)
__tests__/lazyLoading.spec.ts [new file with mode: 0644]
__tests__/utils.ts
src/router.ts
src/utils/index.ts

diff --git a/__tests__/lazyLoading.spec.ts b/__tests__/lazyLoading.spec.ts
new file mode 100644 (file)
index 0000000..a55ae82
--- /dev/null
@@ -0,0 +1,224 @@
+import fakePromise from 'faked-promise'
+import { createRouter, createMemoryHistory } from '../src'
+import { RouterOptions } from '../src/router'
+import { RouteComponent } from '../src/types'
+import { ticks } from './utils'
+
+function newRouter(options: Partial<RouterOptions> = {}) {
+  let history = createMemoryHistory()
+  const router = createRouter({ history, routes: [], ...options })
+
+  return { history, router }
+}
+
+function createLazyComponent() {
+  const [promise, resolve, reject] = fakePromise()
+
+  return {
+    component: jest.fn(() => promise.then(() => ({} as RouteComponent))),
+    promise,
+    resolve,
+    reject,
+  }
+}
+
+describe('Lazy Loading', () => {
+  it('works', async () => {
+    const { component, resolve } = createLazyComponent()
+    const { router } = newRouter({
+      routes: [{ path: '/foo', component }],
+    })
+
+    let p = router.push('/foo')
+    await ticks(1)
+
+    expect(component).toHaveBeenCalledTimes(1)
+    resolve()
+
+    await p
+    expect(router.currentRoute.value).toMatchObject({
+      path: '/foo',
+      matched: [{}],
+    })
+  })
+
+  it('works with nested routes', async () => {
+    const parent = createLazyComponent()
+    const child = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        {
+          path: '/foo',
+          component: parent.component,
+          children: [{ path: 'bar', component: child.component }],
+        },
+      ],
+    })
+
+    parent.resolve()
+    child.resolve()
+    await router.push('/foo/bar')
+
+    expect(parent.component).toHaveBeenCalled()
+    expect(child.component).toHaveBeenCalled()
+
+    expect(router.currentRoute.value).toMatchObject({
+      path: '/foo/bar',
+    })
+    expect(router.currentRoute.value.matched).toHaveLength(2)
+  })
+
+  it('caches lazy loaded components', async () => {
+    const { component, resolve } = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        { path: '/foo', component },
+        { path: '/', component: {} },
+      ],
+    })
+
+    resolve()
+
+    await router.push('/foo')
+    await router.push('/')
+    await router.push('/foo')
+
+    expect(component).toHaveBeenCalledTimes(1)
+  })
+
+  it('uses the same cache for aliases', async () => {
+    const { component, resolve } = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        { path: '/foo', alias: ['/bar', '/baz'], component },
+        { path: '/', component: {} },
+      ],
+    })
+
+    resolve()
+
+    await router.push('/foo')
+    await router.push('/')
+    await router.push('/bar')
+    await router.push('/')
+    await router.push('/baz')
+
+    expect(component).toHaveBeenCalledTimes(1)
+  })
+
+  it('uses the same cache for nested aliases', async () => {
+    const { component, resolve } = createLazyComponent()
+    const c2 = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        {
+          path: '/foo',
+          alias: ['/bar', '/baz'],
+          component,
+          children: [
+            { path: 'child', alias: ['c1', 'c2'], component: c2.component },
+          ],
+        },
+        { path: '/', component: {} },
+      ],
+    })
+
+    resolve()
+    c2.resolve()
+
+    await router.push('/baz/c2')
+    await router.push('/')
+    await router.push('/foo/c2')
+    await router.push('/')
+    await router.push('/foo/child')
+
+    expect(component).toHaveBeenCalledTimes(1)
+    expect(c2.component).toHaveBeenCalledTimes(1)
+  })
+
+  it('avoid fetching async component if navigation is cancelled through beforeEnter', async () => {
+    const { component, resolve } = createLazyComponent()
+    const spy = jest.fn((to, from, next) => next(false))
+    const { router } = newRouter({
+      routes: [
+        {
+          path: '/foo',
+          component,
+          beforeEnter: spy,
+        },
+      ],
+    })
+
+    resolve()
+    await router.push('/foo').catch(() => {})
+    expect(spy).toHaveBeenCalledTimes(1)
+    expect(component).toHaveBeenCalledTimes(0)
+  })
+
+  it('avoid fetching async component if navigation is cancelled through router.beforeEach', async () => {
+    const { component, resolve } = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        {
+          path: '/foo',
+          component,
+        },
+      ],
+    })
+
+    const spy = jest.fn((to, from, next) => next(false))
+
+    router.beforeEach(spy)
+
+    resolve()
+    await router.push('/foo').catch(() => {})
+    expect(spy).toHaveBeenCalledTimes(1)
+    expect(component).toHaveBeenCalledTimes(0)
+  })
+
+  it('aborts the navigation if async fails', async () => {
+    const { component, reject } = createLazyComponent()
+    const { router } = newRouter({
+      routes: [{ path: '/foo', component }],
+    })
+
+    const spy = jest.fn()
+
+    reject()
+    await router.push('/foo').catch(spy)
+
+    expect(spy).toHaveBeenCalled()
+
+    expect(router.currentRoute.value).toMatchObject({
+      path: '/',
+      matched: [],
+    })
+  })
+
+  it('aborts the navigation if nested async fails', async () => {
+    const parent = createLazyComponent()
+    const child = createLazyComponent()
+    const { router } = newRouter({
+      routes: [
+        {
+          path: '/foo',
+          component: parent.component,
+          children: [{ path: '', component: child.component }],
+        },
+      ],
+    })
+
+    const spy = jest.fn()
+
+    parent.resolve()
+    child.reject()
+    await router.push('/foo').catch(spy)
+
+    expect(spy).toHaveBeenCalledWith(expect.any(Error))
+
+    expect(router.currentRoute.value).toMatchObject({
+      path: '/',
+      matched: [],
+    })
+  })
+})
index b3f398c11814ebb8a6314392bcfec4377e18a7c3..90d3b4a9525b9215c0ce5dfff91c619e621e098b 100644 (file)
@@ -15,6 +15,12 @@ export const tick = (time?: number) =>
     else process.nextTick(resolve)
   })
 
+export async function ticks(n: number) {
+  for (let i = 0; i < n; i++) {
+    await tick()
+  }
+}
+
 export type NAVIGATION_METHOD = 'push' | 'replace'
 export const NAVIGATION_TYPES: NAVIGATION_METHOD[] = ['push', 'replace']
 
index c71ce6ecf0b914d02918850224d7b016be5cdd37..df5b42d790be829d80299e3b95e27c0053d2a2b3 100644 (file)
@@ -250,7 +250,7 @@ export function createRouter({
 
     // all components here have been resolved once because we are leaving
     // TODO: refactor both together
-    guards = await extractComponentsGuards(
+    guards = extractComponentsGuards(
       from.matched.filter(record => to.matched.indexOf(record) < 0).reverse(),
       'beforeRouteLeave',
       to,
@@ -282,7 +282,7 @@ export function createRouter({
     await runGuardQueue(guards)
 
     // check in components beforeRouteUpdate
-    guards = await extractComponentsGuards(
+    guards = extractComponentsGuards(
       to.matched.filter(record => from.matched.indexOf(record as any) > -1),
       'beforeRouteUpdate',
       to,
@@ -312,7 +312,7 @@ export function createRouter({
     // TODO: at this point to.matched is normalized and does not contain any () => Promise<Component>
 
     // check in-component beforeRouteEnter
-    guards = await extractComponentsGuards(
+    guards = extractComponentsGuards(
       // the type does'nt matter as we are comparing an object per reference
       to.matched.filter(record => from.matched.indexOf(record as any) < 0),
       'beforeRouteEnter',
index e703f2882334e2479842482e3a16d3e804d73cf8..60c9d019c91afe93034cea841b406889189386a8 100644 (file)
@@ -18,14 +18,13 @@ function isESModule(obj: any): obj is { default: RouteComponent } {
 }
 
 type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'
-// TODO: remove async
-export async function extractComponentsGuards(
+
+export function extractComponentsGuards(
   matched: RouteRecordNormalized[],
   guardType: GuardType,
   to: RouteLocationNormalized,
   from: RouteLocationNormalized
 ) {
-  // TODO: test to avoid redundant requests for aliases. It should work because we are holding a copy of the `components` option when we create aliases
   const guards: Array<() => Promise<void>> = []
 
   for (const record of matched) {
@@ -33,9 +32,10 @@ export async function extractComponentsGuards(
       const rawComponent = record.components[name]
       if (typeof rawComponent === 'function') {
         // start requesting the chunk already
-        const componentPromise = rawComponent()
+        const componentPromise = rawComponent().catch(() => null)
         guards.push(async () => {
           const resolved = await componentPromise
+          if (!resolved) throw new Error('TODO: error while fetching')
           const resolvedComponent = isESModule(resolved)
             ? resolved.default
             : resolved