]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(guards): next callback beforeRouteEnter
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 Jul 2020 18:02:05 +0000 (20:02 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 Jul 2020 18:02:05 +0000 (20:02 +0200)
13 files changed:
__tests__/RouterView.spec.ts
__tests__/errors.spec.ts
__tests__/guards/beforeRouteEnter.spec.ts
__tests__/matcher/records.spec.ts
__tests__/utils.ts
e2e/keep-alive/index.ts
e2e/specs/keep-alive.js
src/RouterView.ts
src/errors.ts
src/matcher/index.ts
src/matcher/types.ts
src/navigationGuards.ts
src/router.ts

index b7805d371156054d9304712874c49568b6daf7f0..a23c5dd497f81ce812d23e74f3207644ff0d2129 100644 (file)
@@ -39,6 +39,7 @@ const routes = createRoutes({
       {
         components: { default: components.Home },
         instances: {},
+        enterCallbacks: [],
         path: '/',
         props,
       },
@@ -56,6 +57,7 @@ const routes = createRoutes({
       {
         components: { default: components.Foo },
         instances: {},
+        enterCallbacks: [],
         path: '/foo',
         props,
       },
@@ -73,12 +75,14 @@ const routes = createRoutes({
       {
         components: { default: components.Nested },
         instances: {},
+        enterCallbacks: [],
         path: '/',
         props,
       },
       {
         components: { default: components.Foo },
         instances: {},
+        enterCallbacks: [],
         path: 'a',
         props,
       },
@@ -96,18 +100,21 @@ const routes = createRoutes({
       {
         components: { default: components.Nested },
         instances: {},
+        enterCallbacks: [],
         path: '/',
         props,
       },
       {
         components: { default: components.Nested },
         instances: {},
+        enterCallbacks: [],
         path: 'a',
         props,
       },
       {
         components: { default: components.Foo },
         instances: {},
+        enterCallbacks: [],
         path: 'b',
         props,
       },
@@ -122,7 +129,13 @@ const routes = createRoutes({
     hash: '',
     meta: {},
     matched: [
-      { components: { foo: components.Foo }, instances: {}, path: '/', props },
+      {
+        components: { foo: components.Foo },
+        instances: {},
+        enterCallbacks: [],
+        path: '/',
+        props,
+      },
     ],
   },
   withParams: {
@@ -138,6 +151,7 @@ const routes = createRoutes({
         components: { default: components.User },
 
         instances: {},
+        enterCallbacks: [],
         path: '/users/:id',
         props: { default: true },
       },
@@ -156,6 +170,7 @@ const routes = createRoutes({
         components: { default: components.WithProps },
 
         instances: {},
+        enterCallbacks: [],
         path: '/props/:id',
         props: { default: { id: 'foo', other: 'fixed' } },
       },
@@ -175,6 +190,7 @@ const routes = createRoutes({
         components: { default: components.WithProps },
 
         instances: {},
+        enterCallbacks: [],
         path: '/props/:id',
         props: {
           default: (to: RouteLocationNormalized) => ({
@@ -247,6 +263,7 @@ describe('RouterView', () => {
         {
           components: { default: components.User },
           instances: {},
+          enterCallbacks: [],
           path: '/users/:id',
           props,
         },
index 14139cf0808999ff844d19190b566459d270d7bc..dd868bdd38c949b68d62ba7e561a424319546aa2 100644 (file)
@@ -101,8 +101,8 @@ describe('Errors & Navigation failures', () => {
     // should hang
     let navigationPromise = router.push('/foo')
 
+    expect(afterEach).toHaveBeenCalledTimes(0)
     await expect(router.push('/')).resolves.toEqual(undefined)
-    expect(afterEach).toHaveBeenCalledTimes(1)
     expect(onError).toHaveBeenCalledTimes(0)
 
     resolve()
@@ -110,7 +110,8 @@ describe('Errors & Navigation failures', () => {
     expect(afterEach).toHaveBeenCalledTimes(2)
     expect(onError).toHaveBeenCalledTimes(0)
 
-    expect(afterEach).toHaveBeenLastCalledWith(
+    expect(afterEach).toHaveBeenNthCalledWith(
+      1,
       expect.objectContaining({ path: '/foo' }),
       from,
       expect.objectContaining({ type: NavigationFailureType.cancelled })
@@ -159,18 +160,12 @@ describe('Errors & Navigation failures', () => {
       let navigationPromise = router.push('/bar')
 
       // goes from /foo to /
+      expect(afterEach).toHaveBeenCalledTimes(0)
       history.go(-1)
 
       await tick()
 
-      expect(afterEach).toHaveBeenCalledTimes(1)
       expect(onError).toHaveBeenCalledTimes(0)
-      expect(afterEach).toHaveBeenLastCalledWith(
-        expect.objectContaining({ path: '/' }),
-        from,
-        undefined
-      )
-
       resolve()
       await expect(navigationPromise).resolves.toEqual(
         expect.objectContaining({ type: NavigationFailureType.cancelled })
@@ -179,11 +174,19 @@ describe('Errors & Navigation failures', () => {
       expect(afterEach).toHaveBeenCalledTimes(2)
       expect(onError).toHaveBeenCalledTimes(0)
 
-      expect(afterEach).toHaveBeenLastCalledWith(
+      expect(afterEach).toHaveBeenNthCalledWith(
+        1,
         expect.objectContaining({ path: '/bar' }),
         from,
         expect.objectContaining({ type: NavigationFailureType.cancelled })
       )
+
+      expect(afterEach).toHaveBeenNthCalledWith(
+        2,
+        expect.objectContaining({ path: '/' }),
+        from,
+        undefined
+      )
     })
 
     it('next(false) triggers afterEach with history.back', async () => {
index 01092274e48f71d0ed37d373b05a9528a4f368d5..1a0a3b7e1d54b0c67b026b2ae27c5f17392ae43a 100644 (file)
@@ -201,34 +201,4 @@ describe('beforeRouteEnter', () => {
     await p
     expect(router.currentRoute.value.fullPath).toBe('/foo')
   })
-
-  // TODO: wait until we have something working with keep-alive and transition first
-  it.skip('calls next callback', async done => {
-    const router = createRouter({ routes })
-    beforeRouteEnter.mockImplementationOnce((to, from, next) => {
-      next(vm => {
-        expect(router.currentRoute.value.fullPath).toBe('/foo')
-        expect(vm).toBeTruthy()
-        done()
-      })
-    })
-
-    await router.push('/')
-    await router.push('/guard/2')
-  })
-
-  it.skip('calls next callback after waiting', async done => {
-    const [promise, resolve] = fakePromise()
-    const router = createRouter({ routes })
-    beforeRouteEnter.mockImplementationOnce(async (to, from, next) => {
-      await promise
-      next(vm => {
-        expect(router.currentRoute.value.fullPath).toBe('/foo')
-        expect(vm).toBeTruthy()
-        done()
-      })
-    })
-    router.push('/foo')
-    resolve()
-  })
 })
index bc78ec17be99c4c083e1d47c441b832b4bc65a1c..3ff0aa4bd72354f296021857ff7a91359eb39390 100644 (file)
@@ -6,7 +6,7 @@ describe('normalizeRouteRecord', () => {
       path: '/home',
       component: {},
     })
-    expect(record).toEqual({
+    expect(record).toMatchObject({
       beforeEnter: undefined,
       children: [],
       aliasOf: undefined,
@@ -31,7 +31,7 @@ describe('normalizeRouteRecord', () => {
       name: 'name',
       component: {},
     })
-    expect(record).toEqual({
+    expect(record).toMatchObject({
       beforeEnter,
       children: [{ path: '/child' }],
       components: { default: {} },
@@ -73,7 +73,7 @@ describe('normalizeRouteRecord', () => {
       name: 'name',
       components: { one: {} },
     })
-    expect(record).toEqual({
+    expect(record).toMatchObject({
       beforeEnter,
       children: [{ path: '/child' }],
       components: { one: {} },
index d93270fc2f35527beb5fd24dba614f360ab34bef..719823ed7412e2f50010c1a6dd3ab0449b06db49 100644 (file)
@@ -53,6 +53,7 @@ export interface RouteRecordViewLoose
   > {
   leaveGuards?: any
   instances: Record<string, any>
+  enterCallbacks: Function[]
   props: Record<string, _RouteRecordProps>
   aliasOf: RouteRecordViewLoose | undefined
 }
index 12d2cab2a240c094ea021a0723e2225edb191fdb..a6705f78553a9cd43de28560b183104dc46da6f5 100644 (file)
@@ -21,12 +21,19 @@ const Foo: RouteComponent = { template: '<div class="foo">foo</div>' }
 
 const WithGuards: RouteComponent = {
   template: `<div>
+    <p>Enter Count <span id="enter-count">{{ enterCount }}</span></p>
     <p>Update Count <span id="update-count">{{ updateCount }}</span></p>
     <p>Leave Count <span id="leave-count">{{ leaveCount }}</span></p>
     <button id="change-query" @click="changeQuery">Change query</button>
     <button id="reset" @click="reset">Reset</button>
     </div>`,
 
+  beforeRouteEnter(to, from, next) {
+    next(vm => {
+      ;(vm as any).enterCount++
+    })
+  },
+
   beforeRouteUpdate(to, from, next) {
     this.updateCount++
     next()
@@ -37,11 +44,13 @@ const WithGuards: RouteComponent = {
   },
 
   setup() {
+    const enterCount = ref(0)
     const updateCount = ref(0)
     const leaveCount = ref(0)
     const router = useRouter()
 
     function reset() {
+      enterCount.value = 0
       updateCount.value = 0
       leaveCount.value = 0
     }
@@ -52,6 +61,7 @@ const WithGuards: RouteComponent = {
     return {
       reset,
       changeQuery,
+      enterCount,
       updateCount,
       leaveCount,
     }
index a9e1c4e652c3779fa58ad660c55b1f06e9c882ce..3f0e6bbca48c1e5f39d2040207b302c949b40244 100644 (file)
@@ -21,8 +21,10 @@ module.exports = {
       .assert.containsText('#counter', '1')
 
       .click('li:nth-child(3) a')
+      .assert.containsText('#enter-count', '1')
       .assert.containsText('#update-count', '0')
       .click('#change-query')
+      .assert.containsText('#enter-count', '1')
       .assert.containsText('#update-count', '1')
       .back()
       .assert.containsText('#update-count', '2')
@@ -30,6 +32,7 @@ module.exports = {
       .back()
       .assert.containsText('#counter', '1')
       .forward()
+      .assert.containsText('#enter-count', '2')
       .assert.containsText('#update-count', '2')
       .assert.containsText('#leave-count', '1')
 
index 74ed892d18a81cfbe1456385fd067956fe0fad87..1041cfc41f57196fbcf53bbe1fc0dff4df785aeb 100644 (file)
@@ -75,7 +75,9 @@ export const RouterViewImpl = defineComponent({
       const currentName = props.name
       const onVnodeMounted = () => {
         matchedRoute.instances[currentName] = viewRef.value
-        // TODO: trigger beforeRouteEnter hooks
+        matchedRoute.enterCallbacks.forEach(callback =>
+          callback(viewRef.value!)
+        )
       }
       const onVnodeUnmounted = () => {
         // remove the instance reference to prevent leak
index fce067c2896d2247d7fbb72e80e340b5cefb4dbb..1128d5999d7c32541aec1a857b1187acf98db24d 100644 (file)
@@ -5,18 +5,25 @@ import {
   RouteLocationNormalized,
 } from './types'
 import { assign } from './utils'
+import { PolySymbol } from './injectionSymbols'
 
 /**
- * order is important to make it backwards compatible with v3
+ * Flags so we can combine them when checking for multiple errors
  */
 export const enum ErrorTypes {
-  MATCHER_NOT_FOUND = 0,
-  NAVIGATION_GUARD_REDIRECT = 1,
-  NAVIGATION_ABORTED = 2,
-  NAVIGATION_CANCELLED = 3,
-  NAVIGATION_DUPLICATED = 4,
+  // they must be literals to be used as values so we can't write
+  // 1 << 2
+  MATCHER_NOT_FOUND = 1,
+  NAVIGATION_GUARD_REDIRECT = 2,
+  NAVIGATION_ABORTED = 4,
+  NAVIGATION_CANCELLED = 8,
+  NAVIGATION_DUPLICATED = 16,
 }
 
+const NavigationFailureSymbol = PolySymbol(
+  __DEV__ ? 'navigation failure' : 'nf'
+)
+
 interface RouterErrorBase extends Error {
   type: ErrorTypes
 }
@@ -85,14 +92,42 @@ export function createRouterError<E extends RouterError>(
   if (__DEV__ || !__BROWSER__) {
     return assign(
       new Error(ErrorTypeMessages[type](params as any)),
-      { type },
+      {
+        type,
+        [NavigationFailureSymbol]: true,
+      } as { type: typeof type },
       params
     ) as E
   } else {
-    return assign(new Error(), { type }, params) as E
+    return assign(
+      new Error(),
+      {
+        type,
+        [NavigationFailureSymbol]: true,
+      } as { type: typeof type },
+      params
+    ) as E
   }
 }
 
+export function isNavigationFailure(
+  error: any,
+  type: ErrorTypes.NAVIGATION_GUARD_REDIRECT
+): error is NavigationRedirectError
+export function isNavigationFailure(
+  error: any,
+  type: ErrorTypes
+): error is NavigationFailure
+export function isNavigationFailure(
+  error: any,
+  type?: number
+): error is NavigationFailure {
+  return (
+    NavigationFailureSymbol in error &&
+    (type == null || !!((error as NavigationFailure).type & type))
+  )
+}
+
 const propertiesToLog = ['params', 'query', 'hash'] as const
 
 function stringifyRoute(to: RouteLocationRaw): string {
index 6a7a7e6b17b225d6288dfa513cb34b3187b3deb5..fb31f58427ace91838164237c969fb478443396d 100644 (file)
@@ -323,6 +323,7 @@ export function normalizeRouteRecord(
     instances: {},
     leaveGuards: [],
     updateGuards: [],
+    enterCallbacks: [],
     components:
       'components' in record
         ? record.components || {}
index d381fcf0e055d1460149c574b06a6e8c701e1424..cbf0c39ed9e2e963a742b9ace916f6ac38797395 100644 (file)
@@ -3,6 +3,7 @@ import {
   NavigationGuard,
   _RouteRecordBase,
   _RouteRecordProps,
+  NavigationGuardNextCallback,
 } from '../types'
 import { ComponentPublicInstance } from 'vue'
 
@@ -21,6 +22,7 @@ export interface RouteRecordNormalized {
   beforeEnter: RouteRecordMultipleViews['beforeEnter']
   leaveGuards: NavigationGuard[]
   updateGuards: NavigationGuard[]
+  enterCallbacks: NavigationGuardNextCallback[]
   instances: Record<string, ComponentPublicInstance | undefined | null>
   // can only be of of the same type as this record
   aliasOf: RouteRecordNormalized | undefined
index c711d830bc117760e15531f1f16ff55506b6a158..0585a189eea9ddb77a17ded567d888f68d361468 100644 (file)
@@ -91,8 +91,11 @@ export function guardToPromiseFn(
   guard: NavigationGuard,
   to: RouteLocationNormalized,
   from: RouteLocationNormalizedLoaded,
-  instance?: ComponentPublicInstance | undefined | null
+  instance?: ComponentPublicInstance | undefined | null,
+  record?: RouteRecordNormalized
 ): () => Promise<void> {
+  // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
+  const enterCallbackArray = record && record.enterCallbacks
   return () =>
     new Promise((resolve, reject) => {
       const next: NavigationGuardNext = (
@@ -121,8 +124,12 @@ export function guardToPromiseFn(
             )
           )
         } else {
-          // TODO: call the in component enter callbacks. Maybe somewhere else
-          // record && record.enterCallbacks.push(valid)
+          if (
+            record &&
+            record.enterCallbacks === enterCallbackArray &&
+            typeof valid === 'function'
+          )
+            enterCallbackArray.push(valid)
           resolve()
         }
       }
@@ -182,7 +189,9 @@ export function extractComponentsGuards(
           (rawComponent as any).__vccOpts || rawComponent
         const guard = options[guardType]
         guard &&
-          guards.push(guardToPromiseFn(guard, to, from, record.instances[name]))
+          guards.push(
+            guardToPromiseFn(guard, to, from, record.instances[name], record)
+          )
       } else {
         // start requesting the chunk already
         let componentPromise: Promise<RouteComponent | null> = (rawComponent as Lazy<
@@ -215,7 +224,13 @@ export function extractComponentsGuards(
             const guard: NavigationGuard = resolvedComponent[guardType]
             return (
               guard &&
-              guardToPromiseFn(guard, to, from, record.instances[name])()
+              guardToPromiseFn(
+                guard,
+                to,
+                from,
+                record.instances[name],
+                record
+              )()
             )
           })
         )
index fc0da57b916298219d725d1883606d3fca6be84f..01e80b4fdeee6c2614e513e396550a7e72b01394 100644 (file)
@@ -29,6 +29,7 @@ import {
   ErrorTypes,
   NavigationFailure,
   NavigationRedirectError,
+  isNavigationFailure,
 } from './errors'
 import { applyToParams, isBrowser, assign } from './utils'
 import { useCallbacks } from './utils/callbacks'
@@ -365,6 +366,21 @@ export function createRouter(options: RouterOptions): Router {
     return typeof to === 'string' ? { path: to } : assign({}, to)
   }
 
+  function checkCanceledNavigation(
+    to: RouteLocationNormalized,
+    from: RouteLocationNormalized
+  ): NavigationFailure | void {
+    if (pendingLocation !== to) {
+      return createRouterError<NavigationFailure>(
+        ErrorTypes.NAVIGATION_CANCELLED,
+        {
+          from,
+          to,
+        }
+      )
+    }
+  }
+
   function push(to: RouteLocationRaw | RouteLocation) {
     return pushWithRedirect(to)
   }
@@ -456,19 +472,13 @@ export function createRouter(options: RouterOptions): Router {
 
     return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
       .catch((error: NavigationFailure | NavigationRedirectError) => {
-        // a more recent navigation took place
-        if (pendingLocation !== toLocation) {
-          return createRouterError<NavigationFailure>(
-            ErrorTypes.NAVIGATION_CANCELLED,
-            {
-              from,
-              to: toLocation,
-            }
-          )
-        }
         if (
-          error.type === ErrorTypes.NAVIGATION_ABORTED ||
-          error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT
+          isNavigationFailure(
+            error,
+            ErrorTypes.NAVIGATION_ABORTED |
+              ErrorTypes.NAVIGATION_CANCELLED |
+              ErrorTypes.NAVIGATION_GUARD_REDIRECT
+          )
         ) {
           return error
         }
@@ -477,7 +487,9 @@ export function createRouter(options: RouterOptions): Router {
       })
       .then((failure: NavigationFailure | NavigationRedirectError | void) => {
         if (failure) {
-          if (failure.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+          if (
+            isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+          )
             // preserve the original redirectedFrom if any
             return pushWithRedirect(
               // keep options
@@ -507,6 +519,21 @@ export function createRouter(options: RouterOptions): Router {
       })
   }
 
+  /**
+   * Helper to reject and skip all navigation guards if a new navigation happened
+   * @param to
+   * @param from
+   */
+  function checkCanceledNavigationAndReject(
+    to: RouteLocationNormalized,
+    from: RouteLocationNormalized
+  ): Promise<void> {
+    const error = checkCanceledNavigation(to, from)
+    return error ? Promise.reject(error) : Promise.resolve()
+  }
+
+  // TODO: refactor the whole before guards by internally using router.beforeEach
+
   function navigate(
     to: RouteLocationNormalized,
     from: RouteLocationNormalizedLoaded
@@ -533,77 +560,105 @@ export function createRouter(options: RouterOptions): Router {
       }
     }
 
-    // run the queue of per route beforeRouteLeave guards
-    return runGuardQueue(guards)
-      .then(() => {
-        // check global guards beforeEach
-        guards = []
-        for (const guard of beforeGuards.list()) {
-          guards.push(guardToPromiseFn(guard, to, from))
-        }
+    const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
+      null,
+      to,
+      from
+    )
 
-        return runGuardQueue(guards)
-      })
-      .then(() => {
-        // check in components beforeRouteUpdate
-        guards = extractComponentsGuards(
-          to.matched.filter(record => from.matched.indexOf(record as any) > -1),
-          'beforeRouteUpdate',
-          to,
-          from
-        )
+    guards.push(canceledNavigationCheck)
 
-        for (const record of updatingRecords) {
-          for (const guard of record.updateGuards) {
+    // run the queue of per route beforeRouteLeave guards
+    return (
+      runGuardQueue(guards)
+        .then(() => {
+          // check global guards beforeEach
+          guards = []
+          for (const guard of beforeGuards.list()) {
             guards.push(guardToPromiseFn(guard, to, from))
           }
-        }
+          guards.push(canceledNavigationCheck)
 
-        // run the queue of per route beforeEnter guards
-        return runGuardQueue(guards)
-      })
-      .then(() => {
-        // check the route beforeEnter
-        guards = []
-        for (const record of to.matched) {
-          // do not trigger beforeEnter on reused views
-          if (record.beforeEnter && from.matched.indexOf(record as any) < 0) {
-            if (Array.isArray(record.beforeEnter)) {
-              for (const beforeEnter of record.beforeEnter)
-                guards.push(guardToPromiseFn(beforeEnter, to, from))
-            } else {
-              guards.push(guardToPromiseFn(record.beforeEnter, to, from))
+          return runGuardQueue(guards)
+        })
+        .then(() => {
+          // check in components beforeRouteUpdate
+          guards = extractComponentsGuards(
+            to.matched.filter(
+              record => from.matched.indexOf(record as any) > -1
+            ),
+            'beforeRouteUpdate',
+            to,
+            from
+          )
+
+          for (const record of updatingRecords) {
+            for (const guard of record.updateGuards) {
+              guards.push(guardToPromiseFn(guard, to, from))
             }
           }
-        }
+          guards.push(canceledNavigationCheck)
 
-        // run the queue of per route beforeEnter guards
-        return runGuardQueue(guards)
-      })
-      .then(() => {
-        // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
-
-        // check in-component beforeRouteEnter
-        guards = extractComponentsGuards(
-          // the type doesn't matter as we are comparing an object per reference
-          to.matched.filter(record => from.matched.indexOf(record as any) < 0),
-          'beforeRouteEnter',
-          to,
-          from
-        )
+          // run the queue of per route beforeEnter guards
+          return runGuardQueue(guards)
+        })
+        .then(() => {
+          // check the route beforeEnter
+          guards = []
+          for (const record of to.matched) {
+            // do not trigger beforeEnter on reused views
+            if (record.beforeEnter && from.matched.indexOf(record as any) < 0) {
+              if (Array.isArray(record.beforeEnter)) {
+                for (const beforeEnter of record.beforeEnter)
+                  guards.push(guardToPromiseFn(beforeEnter, to, from))
+              } else {
+                guards.push(guardToPromiseFn(record.beforeEnter, to, from))
+              }
+            }
+          }
+          guards.push(canceledNavigationCheck)
 
-        // run the queue of per route beforeEnter guards
-        return runGuardQueue(guards)
-      })
-      .then(() => {
-        // check global guards beforeResolve
-        guards = []
-        for (const guard of beforeResolveGuards.list()) {
-          guards.push(guardToPromiseFn(guard, to, from))
-        }
+          // run the queue of per route beforeEnter guards
+          return runGuardQueue(guards)
+        })
+        .then(() => {
+          // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
+
+          // clear existing enterCallbacks, these are added by extractComponentsGuards
+          to.matched.forEach(record => (record.enterCallbacks = []))
+
+          // check in-component beforeRouteEnter
+          guards = extractComponentsGuards(
+            // the type doesn't matter as we are comparing an object per reference
+            to.matched.filter(
+              record => from.matched.indexOf(record as any) < 0
+            ),
+            'beforeRouteEnter',
+            to,
+            from
+          )
+          guards.push(canceledNavigationCheck)
 
-        return runGuardQueue(guards)
-      })
+          // run the queue of per route beforeEnter guards
+          return runGuardQueue(guards)
+        })
+        .then(() => {
+          // check global guards beforeResolve
+          guards = []
+          for (const guard of beforeResolveGuards.list()) {
+            guards.push(guardToPromiseFn(guard, to, from))
+          }
+          guards.push(canceledNavigationCheck)
+
+          return runGuardQueue(guards)
+        })
+        // catch any navigation canceled
+        .catch(err =>
+          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
+            ? err
+            : Promise.reject(err)
+        )
+    )
   }
 
   function triggerAfterEach(
@@ -629,15 +684,8 @@ export function createRouter(options: RouterOptions): Router {
     data?: HistoryState
   ): NavigationFailure | void {
     // a more recent navigation took place
-    if (pendingLocation !== toLocation) {
-      return createRouterError<NavigationFailure>(
-        ErrorTypes.NAVIGATION_CANCELLED,
-        {
-          from,
-          to: toLocation,
-        }
-      )
-    }
+    const error = checkCanceledNavigation(toLocation, from)
+    if (error) return error
 
     const [leavingRecords] = extractChangingRecords(toLocation, from)
     for (const record of leavingRecords) {
@@ -699,20 +747,17 @@ export function createRouter(options: RouterOptions): Router {
 
       navigate(toLocation, from)
         .catch((error: NavigationFailure | NavigationRedirectError) => {
-          // a more recent navigation took place
-          if (pendingLocation !== toLocation) {
-            return createRouterError<NavigationFailure>(
-              ErrorTypes.NAVIGATION_CANCELLED,
-              {
-                from,
-                to: toLocation,
-              }
+          if (
+            isNavigationFailure(
+              error,
+              ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
             )
+          ) {
+            return error
           }
-          if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
-            return error as NavigationFailure
-          }
-          if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
+          if (
+            isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+          ) {
             routerHistory.go(-info.delta, false)
             // the error is already handled by router.push we just want to avoid
             // logging the error