diff --git a/__tests__/isReady.spec.ts b/__tests__/isReady.spec.ts new file mode 100644 index 000000000..cad258a00 --- /dev/null +++ b/__tests__/isReady.spec.ts @@ -0,0 +1,111 @@ +import { createMemoryHistory, createRouter } from '../src' +import { components } from './utils' +import { RouteRecordRaw } from '../src/types' + +// generic component because we are not displaying anything so it doesn't matter +const component = components.Home + +const routes: RouteRecordRaw[] = [ + { path: '/', component }, + { path: '/bar', component }, + { path: '/foo', component, name: 'Foo' }, + { + path: '/fail-lazy', + component: async () => { + throw new Error('async') + }, + }, +] + +describe('isReady', () => { + function newRouter( + options: Partial[0]> = {} + ) { + const history = options.history || createMemoryHistory() + const router = createRouter({ history, routes, ...options }) + + return router + } + + it('resolves a normal navigation', async () => { + const router = newRouter() + router.push('/foo') + await expect(router.isReady()).resolves.toBe(undefined) + // can be called again + await expect(router.isReady()).resolves.toBe(undefined) + }) + + it('resolves a redirected navigation', async () => { + const router = newRouter() + router.beforeEach(to => (to.path === '/bar' ? true : '/bar')) + router.push('/foo') + await expect(router.isReady()).resolves.toBe(undefined) + expect(router.currentRoute.value).toMatchObject({ + redirectedFrom: expect.objectContaining({ path: '/foo' }), + }) + // can be called again + await expect(router.isReady()).resolves.toBe(undefined) + }) + + it('rejects when an error is thrown in a navigation guard', async () => { + const router = newRouter() + const errorSpy = jest.fn() + const error = new Error('failed') + router.onError(errorSpy) + const remove = router.beforeEach(async () => { + throw error + }) + router.push('/foo').catch(() => {}) + await expect(router.isReady()).rejects.toBe(error) + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledWith( + error, + // to + expect.objectContaining({ path: '/foo' }), + // from + expect.objectContaining({ path: '/' }) + ) + + // result can change + remove() + router.push('/foo').catch(() => {}) + await expect(router.isReady()).resolves.toBe(undefined) + expect(errorSpy).toHaveBeenCalledTimes(1) + }) + + it('rejects a cancelled navigation', async () => { + const router = newRouter() + const errorSpy = jest.fn() + router.onError(errorSpy) + const remove = router.beforeEach(() => false) + router.push('/foo').catch(() => {}) + await expect(router.isReady()).rejects.toMatchObject({ + to: expect.objectContaining({ path: '/foo' }), + from: expect.objectContaining({ path: '/' }), + }) + expect(errorSpy).toHaveBeenCalledTimes(0) + + // can be checked again + router.push('/foo').catch(() => {}) + await expect(router.isReady()).rejects.toMatchObject({ + to: expect.objectContaining({ path: '/foo' }), + from: expect.objectContaining({ path: '/' }), + }) + expect(errorSpy).toHaveBeenCalledTimes(0) + + // result can change + remove() + router.push('/foo').catch(() => {}) + await expect(router.isReady()).resolves.toBe(undefined) + expect(errorSpy).toHaveBeenCalledTimes(0) + }) + + it('rejects failed lazy loading', async () => { + const router = newRouter() + const errorSpy = jest.fn() + router.onError(errorSpy) + router.push('/fail-lazy').catch(() => {}) + await expect(router.isReady()).rejects.toEqual(expect.any(Error)) + expect(errorSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/router.ts b/src/router.ts index 90ddeef30..9a87439b0 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1103,14 +1103,19 @@ export function createRouter(options: RouterOptions): Router { * only be called once, otherwise does nothing. * @param err - optional error */ - function markAsReady(err?: any): void { - if (ready) return - ready = true - setupListeners() - readyHandlers - .list() - .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) - readyHandlers.reset() + function markAsReady(err: E): E + function markAsReady(): void + function markAsReady(err?: E): E | void { + if (!ready) { + // still not ready if an error happened + ready = !err + setupListeners() + readyHandlers + .list() + .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) + readyHandlers.reset() + } + return err } // Scroll behavior