From 13f55090309213c684f9838d2d4065159c155c6a Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 16 Mar 2020 15:42:49 +0100 Subject: [PATCH] test: test async components --- __tests__/lazyLoading.spec.ts | 224 ++++++++++++++++++++++++++++++++++ __tests__/utils.ts | 6 + src/router.ts | 6 +- src/utils/index.ts | 8 +- 4 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 __tests__/lazyLoading.spec.ts diff --git a/__tests__/lazyLoading.spec.ts b/__tests__/lazyLoading.spec.ts new file mode 100644 index 00000000..a55ae823 --- /dev/null +++ b/__tests__/lazyLoading.spec.ts @@ -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 = {}) { + 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: [], + }) + }) +}) diff --git a/__tests__/utils.ts b/__tests__/utils.ts index b3f398c1..90d3b4a9 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -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'] diff --git a/src/router.ts b/src/router.ts index c71ce6ec..df5b42d7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -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 // 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', diff --git a/src/utils/index.ts b/src/utils/index.ts index e703f288..60c9d019 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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> = [] 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 -- 2.39.5