const expect = require('expect')
const fakePromise = require('faked-promise')
const { HTML5History } = require('../src/history/html5')
+const { AbstractHistory } = require('../src/history/abstract')
const { Router } = require('../src/router')
-const { createDom, components } = require('./utils')
+const { createDom, components, tick } = require('./utils')
function mockHistory() {
// TODO: actually do a mock
})
describe('navigation', () => {
- it('waits before navigating in an array of beforeEnter', async () => {
+ it('cancels pending navigations if a newer one is finished on push', async () => {
const [p1, r1] = fakePromise()
const [p2, r2] = fakePromise()
const history = mockHistory()
path: '/a',
component: components.Home,
async beforeEnter(to, from, next) {
+ expect(from.fullPath).toBe('/')
await p1
next()
},
component: components.Foo,
name: 'Foo',
async beforeEnter(to, from, next) {
+ expect(from.fullPath).toBe('/')
await p2
next()
},
}
expect(router.currentRoute.fullPath).toBe('/b')
})
+
+ it('cancels pending navigations if a newer one is finished on user navigation (from history)', async () => {
+ const [p1, r1] = fakePromise()
+ const [p2, r2] = fakePromise()
+ const history = new AbstractHistory()
+ const router = new Router({ history, routes })
+ // navigate first to add entries to the history stack
+ await router.push('/p/initial')
+ await router.push('/p/a')
+ await router.push('/p/b')
+
+ router.beforeEach(async (to, from, next) => {
+ if (to.fullPath === '/p/initial') {
+ // because we delay navigation, we are coming from /p/b
+ expect(from.fullPath).toBe('/p/b')
+ await p1
+ } else {
+ expect(from.fullPath).toBe('/p/b')
+ await p2
+ }
+ next()
+ })
+
+ // trigger to history.back()
+ history.back()
+ history.back()
+
+ expect(router.currentRoute.fullPath).toBe('/p/b')
+ // resolves the last call to history.back() first
+ // so we end up on /p/initial
+ r1()
+ await tick()
+ expect(router.currentRoute.fullPath).toBe('/p/initial')
+ // resolves the pending navigation, this should be cancelled
+ r2()
+ await tick()
+ expect(router.currentRoute.fullPath).toBe('/p/initial')
+ })
+
+ it('cancels pending in-guard navigations if a newer one is finished on user navigation (from history)', async () => {
+ const [p1, r1] = fakePromise()
+ const [p2, r2] = fakePromise()
+ const history = new AbstractHistory()
+ const router = new Router({ history, routes })
+ // navigate first to add entries to the history stack
+ await router.push('/p/initial')
+ await router.push('/p/a')
+ await router.push('/p/b')
+
+ router.beforeEach(async (to, from, next) => {
+ console.log('going to', to.fullPath, 'from', from.fullPath)
+ if (to.fullPath === '/p/initial') {
+ console.log('waiting for p1')
+ await p1
+ console.log('done with p1')
+ next()
+ } else if (from.fullPath === '/p/b') {
+ console.log('waiting for p2')
+ await p2
+ console.log('done with p2')
+ next('/p/other-place')
+ } else {
+ next()
+ }
+ })
+
+ // trigger to history.back()
+ history.back()
+ history.back()
+
+ expect(router.currentRoute.fullPath).toBe('/p/b')
+ // resolves the last call to history.back() first
+ // so we end up on /p/initial
+ r1()
+ await tick()
+ expect(router.currentRoute.fullPath).toBe('/p/initial')
+ // resolves the pending navigation, this should be cancelled
+ r2()
+ await tick()
+ expect(router.currentRoute.fullPath).toBe('/p/initial')
+ })
})
describe('matcher', () => {
routes: RouteRecord[]
}
+type ErrorHandler = (error: any) => any
+
export class Router {
protected history: BaseHistory
private matcher: RouterMatcher
currentRoute: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
pendingLocation: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
private app: any
+ private errorHandlers: ErrorHandler[] = []
constructor(options: RouterOptions) {
this.history = options.history
// console.log({ to, matchedRoute })
const toLocation: RouteLocationNormalized = { ...to, ...matchedRoute }
+ this.pendingLocation = toLocation
try {
await this.navigate(toLocation, this.currentRoute)
+ // a more recent navigation took place
+ if (this.pendingLocation !== toLocation) {
+ throw new NavigationCancelled(toLocation, this.currentRoute)
+ }
+
// accept current navigation
this.currentRoute = {
...to,
// TODO: use the push/replace technique with any navigation to
// preserve history when moving forward
if (error instanceof NavigationGuardRedirect) {
+ // TODO: refactor the duplication of new NavigationCancelled by
+ // checking instanceof NavigationError (it's another TODO)
+ // a more recent navigation took place
+ if (this.pendingLocation !== toLocation) {
+ return this.triggerError(
+ new NavigationCancelled(toLocation, this.currentRoute)
+ )
+ }
+
+ // TODO: handle errors
this.push(error.to)
} else if (error instanceof NavigationAborted) {
// TODO: test on different browsers ensure consistent behavior
}
}
+ /**
+ * Add an error handler to catch errors during navigation
+ * TODO: return a remover like beforeEach
+ * @param handler error handler
+ */
+ onError(handler: ErrorHandler): void {
+ this.errorHandlers.push(handler)
+ }
+
+ /**
+ * Trigger all registered error handlers
+ * @param error thrown error
+ */
+ private triggerError(error: any): void {
+ for (const handler of this.errorHandlers) {
+ handler(error)
+ }
+ }
+
private updateReactiveRoute() {
if (!this.app) return
// TODO: matched should be non enumerable and the defineProperty here shouldn't be necessary