import { createRouter, createMemoryHistory, createHistory } from '../src'
import { NavigationCancelled } from '../src/errors'
import { createDom, components, tick } from './utils'
-import { RouteRecord, RouteLocation } from '../src/types'
+import {
+ RouteRecord,
+ RouteLocation,
+ START_LOCATION_NORMALIZED,
+} from '../src/types'
+import { RouterHistory } from '../src/history/common'
const routes: RouteRecord[] = [
{ path: '/', component: components.Home },
},
]
+async function newRouter({ history }: { history?: RouterHistory } = {}) {
+ history = history || createMemoryHistory()
+ const router = createRouter({ history, routes })
+ await router.push('/')
+
+ return { history, router }
+}
+
describe('Router', () => {
beforeAll(() => {
createDom()
it('can be instantiated', () => {
const history = createMemoryHistory()
const router = createRouter({ history, routes })
- expect(router.currentRoute.value).toEqual({
- name: undefined,
- fullPath: '/',
- hash: '',
- params: {},
- path: '/',
- query: {},
- meta: {},
- })
+ expect(router.currentRoute.value).toEqual(START_LOCATION_NORMALIZED)
})
// TODO: should do other checks not based on history implem
- it.skip('takes browser location', () => {
+ it.skip('takes browser location', async () => {
const history = createMemoryHistory()
history.replace('/search?q=dog#footer')
- const router = createRouter({ history, routes })
+ const { router } = await newRouter({ history })
+ await router.push(history.location)
expect(router.currentRoute).toEqual({
fullPath: '/search?q=dog#footer',
hash: '#footer',
})
it('calls history.push with router.push', async () => {
- const history = createMemoryHistory()
- const router = createRouter({ history, routes })
+ const { router, history } = await newRouter()
jest.spyOn(history, 'push')
await router.push('/foo')
expect(history.push).toHaveBeenCalledTimes(1)
it('calls history.replace with router.replace', async () => {
const history = createMemoryHistory()
- const router = createRouter({ history, routes })
+ const { router } = await newRouter({ history })
jest.spyOn(history, 'replace')
await router.replace('/foo')
expect(history.replace).toHaveBeenCalledTimes(1)
})
it('can pass replace option to push', async () => {
- const history = createMemoryHistory()
- const router = createRouter({ history, routes })
+ const { router, history } = await newRouter()
jest.spyOn(history, 'replace')
await router.push({ path: '/foo', replace: true })
expect(history.replace).toHaveBeenCalledTimes(1)
const [p2, r2] = fakePromise()
const history = createMemoryHistory()
const router = createRouter({ history, routes })
+
// navigate first to add entries to the history stack
await router.push('/foo')
await router.push('/p/a')
import createHashHistory from './history/hash'
import View from './components/View'
import Link from './components/Link'
-import { RouteLocationNormalized } from './types'
+import {
+ RouteLocationNormalized,
+ START_LOCATION_NORMALIZED as START_LOCATION,
+} from './types'
declare module '@vue/runtime-core' {
function inject(name: 'router'): Router
app.component('RouterView', View as any)
let started = false
+ // TODO: can we use something that isn't a mixin?
app.mixin({
beforeCreate() {
if (!started) {
router.setActiveApp(this)
- router.doInitialNavigation().catch(err => {
+ // TODO: this initial navigation is only necessary on client, on server it doesn't make sense
+ // because it will create an extra unecessary navigation and could lead to problems
+ router.push(router.history.location).catch(err => {
console.error('Unhandled error', err)
})
started = true
createRouter,
RouteLocationNormalized,
Router,
+ START_LOCATION,
}
stringifyURL,
normalizeQuery,
HistoryLocationNormalized,
- START,
} from './history/common'
import {
ScrollToPosition,
}
export interface Router {
+ history: RouterHistory
currentRoute: Ref<RouteLocationNormalized>
resolve(to: RouteLocation): RouteLocationNormalized
replace(to: RouteLocation): Promise<RouteLocationNormalized>
// TODO: find a way to remove it
- doInitialNavigation(): Promise<void>
setActiveApp(vm: TODO): void
beforeEach(guard: NavigationGuard): ListenerRemover
)
return currentRoute.value
+ const isFirstNavigation =
+ currentRoute.value === START_LOCATION_NORMALIZED &&
+ to === history.location
+
const toLocation: RouteLocationNormalized = location
pendingLocation = toLocation
// trigger all guards, throw if navigation is rejected
try {
await navigate(toLocation, currentRoute.value)
} catch (error) {
+ if (isFirstNavigation) markAsReady(error)
if (NavigationGuardRedirect.is(error)) {
// push was called while waiting in guards
if (pendingLocation !== toLocation) {
// push was called while waiting in guards
if (pendingLocation !== toLocation) {
- throw new NavigationCancelled(toLocation, currentRoute.value)
+ const error = new NavigationCancelled(toLocation, currentRoute.value)
+ // TODO: refactor errors to be more lightweight
+ if (isFirstNavigation) markAsReady(error)
+ throw error
}
// change URL
- if (to.replace === true) history.replace(url)
- else history.push(url)
+ if (!isFirstNavigation) {
+ if (to.replace === true) history.replace(url)
+ else history.push(url)
+ }
const from = currentRoute.value
currentRoute.value = markNonReactive(toLocation)
- updateReactiveRoute()
- handleScroll(toLocation, from).catch(err => triggerError(err, false))
+ if (!isFirstNavigation)
+ handleScroll(toLocation, from).catch(err => triggerError(err, false))
// navigation is confirmed, call afterGuards
for (const guard of afterGuards) guard(toLocation, from)
+ markAsReady()
+
return currentRoute.value
}
...to,
...matchedRoute,
})
- updateReactiveRoute()
// TODO: refactor with a state getter
// const { scroll } = history.state
const { state } = window.history
if (shouldThrow) throw error
}
- function updateReactiveRoute() {
- if (!app) return
- // TODO: matched should be non enumerable and the defineProperty here shouldn't be necessary
- const route = { ...currentRoute.value }
- Object.defineProperty(route, 'matched', { enumerable: false })
- // @ts-ignore
- app._route = Object.freeze(route)
- markAsReady()
- }
-
function isReady(): Promise<void> {
if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
return Promise.resolve()
}
function markAsReady(err?: any): void {
- if (ready || currentRoute.value === START_LOCATION_NORMALIZED) return
+ if (ready) return
ready = true
for (const [resolve] of onReadyCbs) {
// TODO: is this okay?
onReadyCbs = []
}
- async function doInitialNavigation(): Promise<void> {
- // let the user call replace or push on SSR
- if (history.location === START) return
- // TODO: refactor code that was duplicated from push method
- const toLocation: RouteLocationNormalized = resolveLocation(
- history.location,
- currentRoute.value
- )
-
- pendingLocation = toLocation
- // trigger all guards, throw if navigation is rejected
- try {
- await navigate(toLocation, currentRoute.value)
- } catch (error) {
- markAsReady(error)
- if (NavigationGuardRedirect.is(error)) {
- // push was called while waiting in guards
- if (pendingLocation !== toLocation) {
- // TODO: trigger onError as well
- throw new NavigationCancelled(toLocation, currentRoute.value)
- }
- // TODO: setup redirect stack
- await push(error.to)
- return
- } else {
- // TODO: write tests
- // triggerError as well
- if (pendingLocation !== toLocation) {
- // TODO: trigger onError as well
- throw new NavigationCancelled(toLocation, currentRoute.value)
- }
-
- // this throws, so nothing ahead happens
- triggerError(error)
- }
- }
-
- // push was called while waiting in guards
- if (pendingLocation !== toLocation) {
- const error = new NavigationCancelled(toLocation, currentRoute.value)
- markAsReady(error)
- throw error
- }
-
- // NOTE: here we removed the pushing to history part as the history
- // already contains current location
-
- const from = currentRoute.value
- currentRoute.value = markNonReactive(toLocation)
- updateReactiveRoute()
-
- // navigation is confirmed, call afterGuards
- for (const guard of afterGuards) guard(toLocation, from)
-
- markAsReady()
- }
-
async function handleScroll(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
function setActiveApp(vm: TODO) {
app = vm
- updateReactiveRoute()
}
const router: Router = {
onError,
isReady,
- doInitialNavigation,
+ history,
setActiveApp,
}