)
})
+ it('Duplicated navigation triggers afterEach', async () => {
+ let expectedFailure = expect.objectContaining({
+ type: NavigationFailureType.duplicated,
+ to: expect.objectContaining({ path: '/' }),
+ from: expect.objectContaining({ path: '/' }),
+ })
+
+ const { router } = createRouter()
+
+ await expect(router.push('/')).resolves.toEqual(undefined)
+ expect(afterEach).toHaveBeenCalledTimes(1)
+ expect(onError).toHaveBeenCalledTimes(0)
+
+ await expect(router.push('/')).resolves.toEqual(expectedFailure)
+ expect(afterEach).toHaveBeenCalledTimes(2)
+ expect(onError).toHaveBeenCalledTimes(0)
+
+ expect(afterEach).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ expectedFailure
+ )
+ })
+
it('next("/location") triggers afterEach', async () => {
await testNavigation(
((to, from, next) => {
NAVIGATION_GUARD_REDIRECT,
NAVIGATION_ABORTED,
NAVIGATION_CANCELLED,
- // Using string enums because error codes are exposed to developers
- // and number enums could collide with other error codes in runtime
- // MATCHER_NOT_FOUND = 'MATCHER_NOT_FOUND',
- // NAVIGATION_GUARD_REDIRECT = 'NAVIGATION_GUARD_REDIRECT',
- // NAVIGATION_ABORTED = 'NAVIGATION_ABORTED',
- // NAVIGATION_CANCELLED = 'NAVIGATION_CANCELLED',
+ NAVIGATION_DUPLICATED,
}
interface RouterErrorBase extends Error {
export enum NavigationFailureType {
cancelled = ErrorTypes.NAVIGATION_CANCELLED,
aborted = ErrorTypes.NAVIGATION_ABORTED,
+ duplicated = ErrorTypes.NAVIGATION_DUPLICATED,
}
export interface NavigationFailure extends RouterErrorBase {
- type: ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
+ type:
+ | ErrorTypes.NAVIGATION_CANCELLED
+ | ErrorTypes.NAVIGATION_ABORTED
+ | ErrorTypes.NAVIGATION_DUPLICATED
from: RouteLocationNormalized
to: RouteLocationNormalized
}
[ErrorTypes.NAVIGATION_CANCELLED]({ from, to }: NavigationFailure) {
return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new \`push\` or \`replace\``
},
+ [ErrorTypes.NAVIGATION_DUPLICATED]({ from, to }: NavigationFailure) {
+ return `Avoided redundant navigation to current location: "${from.fullPath}"`
+ },
}
// Possible internal errors
// to could be a string where `replace` is a function
const replace = (to as RouteLocationOptions).replace === true
- // TODO: create navigation failure
- if (!force && isSameRouteLocation(from, targetLocation)) return
-
const lastMatched =
targetLocation.matched[targetLocation.matched.length - 1]
if (lastMatched && 'redirect' in lastMatched) {
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void
- // trigger all guards, throw if navigation is rejected
- try {
- await navigate(toLocation, from)
- } catch (error) {
- // a more recent navigation took place
- if (pendingLocation !== toLocation) {
- failure = createRouterError<NavigationFailure>(
- ErrorTypes.NAVIGATION_CANCELLED,
- {
- from,
- to: toLocation,
- }
- )
- } else if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
- failure = error as NavigationFailure
- } else if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
- // preserve the original redirectedFrom if any
- return pushWithRedirect(
- // keep options
- {
- ...locationAsObject((error as NavigationRedirectError).to),
- state: data,
- force,
- replace,
- },
- redirectedFrom || toLocation
- )
- } else {
- // unknown error, throws
- triggerError(error, true)
+ if (!force && isSameRouteLocation(from, targetLocation))
+ failure = createRouterError<NavigationFailure>(
+ ErrorTypes.NAVIGATION_DUPLICATED,
+ { to: toLocation, from }
+ )
+
+ if (!failure) {
+ // trigger all guards, throw if navigation is rejected
+ try {
+ await navigate(toLocation, from)
+ } catch (error) {
+ // a more recent navigation took place
+ if (pendingLocation !== toLocation) {
+ failure = createRouterError<NavigationFailure>(
+ ErrorTypes.NAVIGATION_CANCELLED,
+ {
+ from,
+ to: toLocation,
+ }
+ )
+ } else if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
+ failure = error as NavigationFailure
+ } else if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
+ // preserve the original redirectedFrom if any
+ return pushWithRedirect(
+ // keep options
+ {
+ ...locationAsObject((error as NavigationRedirectError).to),
+ state: data,
+ force,
+ replace,
+ },
+ redirectedFrom || toLocation
+ )
+ } else {
+ // unknown error, throws
+ triggerError(error, true)
+ }
}
}