--- /dev/null
+// @ts-check
+require('./helper')
+const expect = require('expect')
+const { extractComponentsGuards } = require('../src/utils')
+const { START_LOCATION_NORMALIZED } = require('../src/types')
+const { components } = require('./utils')
+
+/** @typedef {import('../src/types').RouteRecord} RouteRecord */
+
+const beforeRouteEnter = jest.fn()
+
+// stub those two
+const to = START_LOCATION_NORMALIZED
+const from = START_LOCATION_NORMALIZED
+
+/** @type {RouteRecord} */
+const NoGuard = { path: '/', component: components.Home }
+/** @type {RouteRecord} */
+const SingleGuard = {
+ path: '/',
+ component: { ...components.Home, beforeRouteEnter },
+}
+/** @type {RouteRecord} */
+const SingleGuardNamed = {
+ path: '/',
+ components: {
+ default: { ...components.Home, beforeRouteEnter },
+ other: { ...components.Foo, beforeRouteEnter },
+ },
+}
+
+/**
+ *
+ * @param {RouteRecord} record
+ * @returns {RouteRecord}
+ */
+function makeAsync(record) {
+ if ('components' in record) {
+ const copy = { ...record }
+ copy.components = Object.keys(record.components).reduce(
+ (components, name) => {
+ components[name] = () => Promise.resolve(record.components[name])
+ return components
+ },
+ {}
+ )
+ return copy
+ } else {
+ if (typeof record.component === 'function') return { ...record }
+ // @ts-ignore
+ return {
+ ...record,
+ component: () => Promise.resolve(record.component),
+ }
+ }
+}
+
+beforeEach(() => {
+ beforeRouteEnter.mockReset()
+ beforeRouteEnter.mockImplementation((to, from, next) => {
+ next()
+ })
+})
+
+/**
+ *
+ * @param {import('../src/types').RouteRecord[]} components
+ */
+async function checkGuards(components, n) {
+ beforeRouteEnter.mockClear()
+ const guards = await extractComponentsGuards(
+ components,
+ 'beforeRouteEnter',
+ to,
+ from
+ )
+ expect(guards).toHaveLength(n)
+ for (const guard of guards) {
+ expect(guard).toBeInstanceOf(Function)
+ expect(await guard())
+ }
+ expect(beforeRouteEnter).toHaveBeenCalledTimes(n)
+}
+
+describe('extractComponentsGuards', () => {
+ it('extracts guards from one single component', async () => {
+ await checkGuards([SingleGuard], 1)
+ })
+
+ it('extracts guards from multiple components (named views)', async () => {
+ await checkGuards([SingleGuardNamed], 2)
+ })
+
+ it('handles no guards', async () => {
+ await checkGuards([NoGuard], 0)
+ })
+
+ it('handles mixed things', async () => {
+ await checkGuards([SingleGuard, SingleGuardNamed], 3)
+ await checkGuards([SingleGuard, SingleGuard], 2)
+ await checkGuards([SingleGuardNamed, SingleGuardNamed], 4)
+ })
+
+ it('works with async components', async () => {
+ await checkGuards([makeAsync(NoGuard)], 0)
+ await checkGuards([makeAsync(SingleGuard)], 1)
+ await checkGuards([makeAsync(SingleGuard), makeAsync(SingleGuardNamed)], 3)
+ await checkGuards([makeAsync(SingleGuard), makeAsync(SingleGuard)], 2)
+ await checkGuards(
+ [makeAsync(SingleGuardNamed), makeAsync(SingleGuardNamed)],
+ 4
+ )
+ })
+})
const Foo = { template: `<div>Foo</div>` }
const beforeRouteEnter = jest.fn()
+const named = {
+ default: jest.fn(),
+ other: jest.fn(),
+}
/** @type {import('../../src/types').RouteRecord[]} */
const routes = [
{ path: '/', component: Home },
beforeRouteEnter,
},
},
+ {
+ path: '/named',
+ components: {
+ default: {
+ ...Home,
+ beforeRouteEnter: named.default,
+ },
+ other: {
+ ...Foo,
+ beforeRouteEnter: named.other,
+ },
+ },
+ },
]
beforeEach(() => {
beforeRouteEnter.mockReset()
+ named.default.mockReset()
+ named.other.mockReset()
})
describe('beforeRouteEnter', () => {
expect(beforeRouteEnter).toHaveBeenCalledTimes(1)
})
+ it('calls beforeRouteEnter guards on navigation for named views', async () => {
+ const router = createRouter({ routes })
+ named.default.mockImplementationOnce(noGuard)
+ named.other.mockImplementationOnce(noGuard)
+ await router[navigationMethod]('/named')
+ expect(named.default).toHaveBeenCalledTimes(1)
+ expect(named.other).toHaveBeenCalledTimes(1)
+ expect(router.currentRoute.fullPath).toBe('/named')
+ })
+
+ it('aborts navigation if one of the named views aborts', async () => {
+ const router = createRouter({ routes })
+ named.default.mockImplementationOnce((to, from, next) => {
+ next(false)
+ })
+ named.other.mockImplementationOnce(noGuard)
+ await router[navigationMethod]('/named').catch(err => {}) // catch abort
+ expect(named.default).toHaveBeenCalledTimes(1)
+ expect(router.currentRoute.fullPath).not.toBe('/named')
+ })
+
it('resolves async components before guarding', async () => {
const spy = jest.fn(noGuard)
const component = {
ListenerRemover,
NavigationGuard,
TODO,
- NavigationGuardCallback,
PostNavigationGuard,
} from './types/index'
+import { guardToPromiseFn, last, extractComponentsGuards } from './utils'
+
export interface RouterOptions {
history: BaseHistory
routes: RouteRecord[]
// TODO: ensure we are leaving since we could just be changing params or not changing anything
// TODO: is it okay to resolve all matched component or should we do it in order
- guards = await extractComponentGuards(
+ guards = await extractComponentsGuards(
from.matched,
'beforeRouteLeave',
to,
}
// check in components beforeRouteUpdate
- guards = await extractComponentGuards(
+ guards = await extractComponentsGuards(
to.matched.filter(record => from.matched.indexOf(record) > -1),
'beforeRouteUpdate',
to,
// check in-component beforeRouteEnter
// TODO: is it okay to resolve all matched component or should we do it in order
- guards = await extractComponentGuards(
+ guards = await extractComponentsGuards(
to.matched.filter(record => from.matched.indexOf(record) < 0),
'beforeRouteEnter',
to,
}
}
}
-
-// UTILS
-
-function guardToPromiseFn(
- guard: NavigationGuard,
- to: RouteLocationNormalized,
- from: RouteLocationNormalized
-): () => Promise<void> {
- return () =>
- new Promise((resolve, reject) => {
- const next: NavigationGuardCallback = (valid?: boolean) => {
- // TODO: better error
- // TODO: handle callback
- if (valid === false) reject(new Error('Aborted'))
- else resolve()
- }
-
- guard(to, from, next)
- })
-}
-
-function last<T>(array: T[]): T {
- return array[array.length - 1]
-}
-
-type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'
-async function extractComponentGuards(
- matched: RouteRecord[],
- guardType: GuardType,
- to: RouteLocationNormalized,
- from: RouteLocationNormalized
-) {
- const guards: Array<() => Promise<void>> = []
- await Promise.all(
- matched.map(async record => {
- // TODO: cache async routes per record
- if ('component' in record) {
- const { component } = record
- const resolvedComponent = await (typeof component === 'function'
- ? component()
- : component)
-
- const guard = resolvedComponent[guardType]
- if (guard) {
- guards.push(guardToPromiseFn(guard, to, from))
- }
- }
- })
- )
-
- return guards
-}
--- /dev/null
+# Notes
+
+Split in multiple files to enable mocking in tests
--- /dev/null
+import {
+ NavigationGuard,
+ RouteLocationNormalized,
+ NavigationGuardCallback,
+} from '../types'
+
+export function guardToPromiseFn(
+ guard: NavigationGuard,
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized
+): () => Promise<void> {
+ return () =>
+ new Promise((resolve, reject) => {
+ const next: NavigationGuardCallback = (valid?: boolean) => {
+ // TODO: better error
+ // TODO: handle callback
+ if (valid === false) reject(new Error('Aborted'))
+ else resolve()
+ }
+
+ guard(to, from, next)
+ })
+}
--- /dev/null
+import { RouteRecord, RouteLocationNormalized } from '../types'
+import { guardToPromiseFn } from './guardToPromiseFn'
+
+export * from './guardToPromiseFn'
+
+type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'
+export async function extractComponentsGuards(
+ matched: RouteRecord[],
+ guardType: GuardType,
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized
+) {
+ const guards: Array<() => Promise<void>> = []
+ await Promise.all(
+ matched.map(async record => {
+ // TODO: cache async routes per record
+ if ('component' in record) {
+ const { component } = record
+ const resolvedComponent = await (typeof component === 'function'
+ ? component()
+ : component)
+
+ const guard = resolvedComponent[guardType]
+ if (guard) {
+ guards.push(guardToPromiseFn(guard, to, from))
+ }
+ } else {
+ for (const name in record.components) {
+ const component = record.components[name]
+ const resolvedComponent = await (typeof component === 'function'
+ ? component()
+ : component)
+
+ const guard = resolvedComponent[guardType]
+ if (guard) {
+ guards.push(guardToPromiseFn(guard, to, from))
+ }
+ }
+ }
+ })
+ )
+
+ return guards
+}
+
+export function last<T>(array: T[]): T {
+ return array[array.length - 1]
+}