]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: allow named views
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 May 2019 20:23:06 +0000 (22:23 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 May 2019 20:23:06 +0000 (22:23 +0200)
__tests__/extractComponentsGuards.spec.js [new file with mode: 0644]
__tests__/guards/component-beforeRouteEnter.spec.js
src/router.ts
src/utils/README.md [new file with mode: 0644]
src/utils/guardToPromiseFn.ts [new file with mode: 0644]
src/utils/index.ts [new file with mode: 0644]

diff --git a/__tests__/extractComponentsGuards.spec.js b/__tests__/extractComponentsGuards.spec.js
new file mode 100644 (file)
index 0000000..0367d9b
--- /dev/null
@@ -0,0 +1,114 @@
+// @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
+    )
+  })
+})
index e9080dbb45c6bae83c1edc8f146fa07621d059b7..5054e238279f5b4af8043f60376b631c13440e8f 100644 (file)
@@ -20,6 +20,10 @@ const Home = { template: `<div>Home</div>` }
 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 },
@@ -31,10 +35,25 @@ const routes = [
       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', () => {
@@ -54,6 +73,27 @@ 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 = {
index 1e8e41e50ae1dd1a4724cccb47b901de6acca9f6..03e4b48eec2b41e75a05951eb940c90f9305fffb 100644 (file)
@@ -9,10 +9,11 @@ import {
   ListenerRemover,
   NavigationGuard,
   TODO,
-  NavigationGuardCallback,
   PostNavigationGuard,
 } from './types/index'
 
+import { guardToPromiseFn, last, extractComponentsGuards } from './utils'
+
 export interface RouterOptions {
   history: BaseHistory
   routes: RouteRecord[]
@@ -103,7 +104,7 @@ export class Router {
 
     // 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,
@@ -131,7 +132,7 @@ export class Router {
     }
 
     // check in components beforeRouteUpdate
-    guards = await extractComponentGuards(
+    guards = await extractComponentsGuards(
       to.matched.filter(record => from.matched.indexOf(record) > -1),
       'beforeRouteUpdate',
       to,
@@ -159,7 +160,7 @@ export class Router {
 
     // 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,
@@ -194,55 +195,3 @@ export class Router {
     }
   }
 }
-
-// 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
-}
diff --git a/src/utils/README.md b/src/utils/README.md
new file mode 100644 (file)
index 0000000..6afd9d2
--- /dev/null
@@ -0,0 +1,3 @@
+# Notes
+
+Split in multiple files to enable mocking in tests
diff --git a/src/utils/guardToPromiseFn.ts b/src/utils/guardToPromiseFn.ts
new file mode 100644 (file)
index 0000000..7dd1884
--- /dev/null
@@ -0,0 +1,23 @@
+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)
+    })
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644 (file)
index 0000000..892c56a
--- /dev/null
@@ -0,0 +1,48 @@
+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]
+}