// @ts-check
require('./helper')
const expect = require('expect')
+const fakePromise = require('faked-promise')
const { HTML5History } = require('../src/history/html5')
const { Router } = require('../src/router')
const { createDom, components } = require('./utils')
})
})
+ describe('navigation', () => {
+ it('waits before navigating in an array of beforeEnter', async () => {
+ const [p1, r1] = fakePromise()
+ const [p2, r2] = fakePromise()
+ const history = mockHistory()
+ const router = new Router({
+ history,
+ routes: [
+ {
+ path: '/a',
+ component: components.Home,
+ async beforeEnter(to, from, next) {
+ await p1
+ next()
+ },
+ },
+ {
+ path: '/b',
+ component: components.Foo,
+ name: 'Foo',
+ async beforeEnter(to, from, next) {
+ await p2
+ next()
+ },
+ },
+ ],
+ })
+ const pA = router.push('/a')
+ const pB = router.push('/b')
+ // we resolve the second navigation first then the first one
+ // and the first navigation should be ignored
+ r2()
+ await pB
+ expect(router.currentRoute.fullPath).toBe('/b')
+ r1()
+ try {
+ await pA
+ } catch (err) {
+ // TODO: expect error
+ }
+ expect(router.currentRoute.fullPath).toBe('/b')
+ })
+ })
+
describe('matcher', () => {
it('handles one redirect from route record', async () => {
const history = mockHistory()
}
}
+/**
+ * Navigation aborted by next(false)
+ */
+export class NavigationAborted extends Error {
+ to: RouteLocationNormalized
+ from: RouteLocationNormalized
+ constructor(to: RouteLocationNormalized, from: RouteLocationNormalized) {
+ super(
+ `Navigation aborted from "${from.fullPath}" to "${
+ to.fullPath
+ }" via a navigation guard`
+ )
+ Object.setPrototypeOf(this, new.target.prototype)
+
+ this.from = from
+ this.to = to
+ }
+}
+
+/**
+ * Navigation canceled by the user by pushing/replacing a new location
+ * TODO: is the name good?
+ */
+export class NavigationCancelled extends Error {
+ to: RouteLocationNormalized
+ from: RouteLocationNormalized
+ constructor(to: RouteLocationNormalized, from: RouteLocationNormalized) {
+ super(
+ `Navigation cancelled from "${from.fullPath}" to "${
+ to.fullPath
+ }" with a new \`push\` or \`replace\``
+ )
+ Object.setPrototypeOf(this, new.target.prototype)
+
+ this.from = from
+ this.to = to
+ }
+}
+
function stringifyRoute(to: RouteLocation): string {
if (typeof to === 'string') return to
if ('path' in to) return to.path
} from './types/index'
import { guardToPromiseFn, extractComponentsGuards } from './utils'
-import { NavigationGuardRedirect } from './errors'
+import {
+ NavigationGuardRedirect,
+ NavigationAborted,
+ NavigationCancelled,
+} from './errors'
export interface RouterOptions {
history: BaseHistory
private beforeGuards: NavigationGuard[] = []
private afterGuards: PostNavigationGuard[] = []
currentRoute: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
+ pendingLocation: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
private app: any
constructor(options: RouterOptions) {
// preserve history when moving forward
if (error instanceof NavigationGuardRedirect) {
this.push(error.to)
+ } else if (error instanceof NavigationAborted) {
+ // TODO: test on different browsers ensure consistent behavior
+ if (info.direction === NavigationDirection.back) {
+ this.history.forward(false)
+ } else {
+ // TODO: go back because we cancelled, then
+ // or replace and not discard the rest of history. Check issues, there was one talking about this
+ // behaviour, maybe we can do better
+ this.history.back(false)
+ }
} else {
- // TODO: handle abort and redirect correctly
// if we were going back, we push and discard the rest of the history
if (info.direction === NavigationDirection.back) {
this.history.push(from)
return this.currentRoute
const toLocation: RouteLocationNormalized = location
+ this.pendingLocation = toLocation
// trigger all guards, throw if navigation is rejected
try {
await this.navigate(toLocation, this.currentRoute)
} catch (error) {
if (error instanceof NavigationGuardRedirect) {
+ // push was called while waiting in guards
+ if (this.pendingLocation !== toLocation) {
+ throw new NavigationCancelled(toLocation, this.currentRoute)
+ }
// TODO: setup redirect stack
return this.push(error.to)
} else {
}
}
+ // push was called while waiting in guards
+ if (this.pendingLocation !== toLocation) {
+ throw new NavigationCancelled(toLocation, this.currentRoute)
+ }
+
// change URL
if (to.replace === true) this.history.replace(url)
else this.history.push(url)
} from '../types'
import { isRouteLocation } from './index'
-import { NavigationGuardRedirect } from '../errors'
+import { NavigationGuardRedirect, NavigationAborted } from '../errors'
export function guardToPromiseFn(
guard: NavigationGuard,
const next: NavigationGuardCallback = (
valid?: boolean | RouteLocation
) => {
- // TODO: better error
// TODO: handle callback
- if (valid === false) reject(new Error('Aborted'))
+ if (valid === false) reject(new NavigationAborted(to, from))
else if (isRouteLocation(valid)) {
reject(new NavigationGuardRedirect(to, valid))
} else resolve()