-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Commit
* test: add example for navigation errors * test: add unit tests for navigation errors * feat(errors): create router errors
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Vue from 'vue' | ||
import VueRouter from 'vue-router' | ||
|
||
const component = { | ||
template: ` | ||
<div> | ||
{{ $route.fullPath }} | ||
</div> | ||
` | ||
} | ||
|
||
Vue.use(VueRouter) | ||
|
||
const router = new VueRouter({ | ||
routes: [ | ||
{ path: '/', component }, { path: '/foo', component } | ||
] | ||
}) | ||
|
||
router.beforeEach((to, from, next) => { | ||
console.log('from', from.fullPath) | ||
console.log('going to', to.fullPath) | ||
if (to.query.wait) { | ||
setTimeout(() => next(), 100) | ||
} else if (to.query.redirect) { | ||
next(to.query.redirect) | ||
} else if (to.query.abort) { | ||
next(false) | ||
} else { | ||
next() | ||
} | ||
}) | ||
|
||
new Vue({ | ||
el: '#app', | ||
router | ||
}) | ||
|
||
// 4 NAVIGATION ERROR CASES : | ||
|
||
// navigation duplicated | ||
// router.push('/foo') | ||
// router.push('/foo') | ||
|
||
// navigation cancelled | ||
// router.push('/foo?wait=y') | ||
// router.push('/') | ||
|
||
// navigation redirected | ||
// router.push('/foo?redirect=/') | ||
|
||
// navigation aborted | ||
// router.push('/foo?abort=y') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<!DOCTYPE html> | ||
<div id="app"> | ||
<router-link to="/">/</router-link> | ||
<router-link to="/foo">/foo</router-link> | ||
<router-view></router-view> | ||
</div> | ||
<script src="/__build__/shared.chunk.js"></script> | ||
<script src="/__build__/router-errors.js"></script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,14 +4,20 @@ import { _Vue } from '../install' | |
import type Router from '../index' | ||
import { inBrowser } from '../util/dom' | ||
import { runQueue } from '../util/async' | ||
import { warn, isError, isExtendedError } from '../util/warn' | ||
import { warn, isError, isRouterError } from '../util/warn' | ||
import { START, isSameRoute } from '../util/route' | ||
import { | ||
flatten, | ||
flatMapComponents, | ||
resolveAsyncComponents | ||
} from '../util/resolve-components' | ||
import { NavigationDuplicated } from './errors' | ||
import { | ||
createNavigationDuplicatedError, | ||
createNavigationCancelledError, | ||
createNavigationRedirectedError, | ||
createNavigationAbortedError, | ||
NavigationFailureType | ||
} from './errors' | ||
|
||
export class History { | ||
router: Router | ||
|
@@ -108,7 +114,7 @@ export class History { | |
// When the user navigates through history through back/forward buttons | ||
// we do not want to throw the error. We only throw it if directly calling | ||
// push/replace. That's why it's not included in isError | ||
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) { | ||
if (!isRouterError(err, NavigationFailureType.NAVIGATION_DUPLICATED) && isError(err)) { | ||
if (this.errorCbs.length) { | ||
this.errorCbs.forEach(cb => { | ||
cb(err) | ||
|
@@ -126,7 +132,7 @@ export class History { | |
route.matched.length === current.matched.length | ||
) { | ||
this.ensureURL() | ||
return abort(new NavigationDuplicated(route)) | ||
return abort(createNavigationDuplicatedError(current, route)) | ||
} | ||
|
||
const { updated, deactivated, activated } = resolveQueue( | ||
|
@@ -150,12 +156,15 @@ export class History { | |
this.pending = route | ||
const iterator = (hook: NavigationGuard, next) => { | ||
if (this.pending !== route) { | ||
return abort() | ||
return abort(createNavigationCancelledError(current, route)) | ||
} | ||
try { | ||
hook(route, current, (to: any) => { | ||
if (to === false || isError(to)) { | ||
if (to === false) { | ||
// next(false) -> abort navigation, ensure current URL | ||
this.ensureURL(true) | ||
abort(createNavigationAbortedError(current, route)) | ||
} else if (isError(to)) { | ||
this.ensureURL(true) | ||
abort(to) | ||
} else if ( | ||
|
@@ -164,7 +173,7 @@ export class History { | |
(typeof to.path === 'string' || typeof to.name === 'string')) | ||
) { | ||
// next('/') or next({ path: '/' }) -> redirect | ||
abort() | ||
abort(createNavigationRedirectedError(current, route)) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
soletan
|
||
if (typeof to === 'object' && to.replace) { | ||
this.replace(to) | ||
} else { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,67 @@ | ||
export class NavigationDuplicated extends Error { | ||
constructor (normalizedLocation) { | ||
super() | ||
this.name = this._name = 'NavigationDuplicated' | ||
// passing the message to super() doesn't seem to work in the transpiled version | ||
this.message = `Navigating to current location ("${ | ||
normalizedLocation.fullPath | ||
}") is not allowed` | ||
// add a stack property so services like Sentry can correctly display it | ||
Object.defineProperty(this, 'stack', { | ||
value: new Error().stack, | ||
writable: true, | ||
configurable: true | ||
}) | ||
// we could also have used | ||
// Error.captureStackTrace(this, this.constructor) | ||
// but it only exists on node and chrome | ||
} | ||
export const NavigationFailureType = { | ||
redirected: 1, | ||
aborted: 2, | ||
cancelled: 3, | ||
duplicated: 4 | ||
} | ||
|
||
export function createNavigationRedirectedError (from, to) { | ||
return createRouterError( | ||
from, | ||
to, | ||
NavigationFailureType.redirected, | ||
`Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.` | ||
) | ||
} | ||
|
||
export function createNavigationDuplicatedError (from, to) { | ||
return createRouterError( | ||
from, | ||
to, | ||
NavigationFailureType.duplicated, | ||
`Avoided redundant navigation to current location: "${from.fullPath}".` | ||
) | ||
} | ||
|
||
export function createNavigationCancelledError (from, to) { | ||
return createRouterError( | ||
from, | ||
to, | ||
NavigationFailureType.cancelled, | ||
`Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.` | ||
) | ||
} | ||
|
||
// support IE9 | ||
NavigationDuplicated._name = 'NavigationDuplicated' | ||
export function createNavigationAbortedError (from, to) { | ||
return createRouterError( | ||
from, | ||
to, | ||
NavigationFailureType.aborted, | ||
`Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.` | ||
) | ||
} | ||
|
||
function createRouterError (from, to, type, message) { | ||
const error = new Error(message) | ||
error._isRouter = true | ||
error.from = from | ||
error.to = to | ||
error.type = type | ||
|
||
const newStack = error.stack.split('\n') | ||
newStack.splice(1, 2) // remove 2 last useless calls | ||
error.stack = newStack.join('\n') | ||
return error | ||
} | ||
|
||
const propertiesToLog = ['params', 'query', 'hash'] | ||
|
||
function stringifyRoute (to) { | ||
if (typeof to === 'string') return to | ||
if ('path' in to) return to.path | ||
const location = {} | ||
for (const key of propertiesToLog) { | ||
if (key in to) location[key] = to[key] | ||
} | ||
return JSON.stringify(location, null, 2) | ||
} |
This now appears to create a browser navigation error on every redirect, for example if a user is redirected to a login page this will generate an error this surely is not the intention here