--- /dev/null
+import { createRouter, createMemoryHistory } from '../src'
+import { h } from 'vue'
+import { createDom } from './utils'
+// import { mockWarn } from 'jest-mock-warn'
+
+declare var __DEV__: boolean
+
+const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t))
+
+function newRouter(options: Partial<Parameters<typeof createRouter>[0]> = {}) {
+ const history = options.history || createMemoryHistory()
+ const router = createRouter({
+ history,
+ routes: [
+ {
+ path: '/:pathMatch(.*)',
+ component: {
+ render: () => h('div', 'any route'),
+ },
+ },
+ ],
+ ...options,
+ })
+
+ return { history, router }
+}
+
+describe('Multiple apps', () => {
+ beforeAll(() => {
+ createDom()
+ const rootEl = document.createElement('div')
+ rootEl.id = 'app'
+ document.body.appendChild(rootEl)
+ })
+
+ it('does not listen to url changes before being ready', async () => {
+ const { router, history } = newRouter()
+
+ const spy = jest.fn((to, from, next) => {
+ next()
+ })
+ router.beforeEach(spy)
+
+ history.push('/foo')
+ history.push('/bar')
+ history.go(-1, true)
+
+ await delay(5)
+ expect(spy).not.toHaveBeenCalled()
+
+ await router.push('/baz')
+
+ history.go(-1, true)
+ await delay(5)
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+})
.back()
.assert.containsText('#guardcount', '4')
- /**
- * TODO:
- * - add in-component guards and check each one of them is called
- * - check `this` is the actual instance by injecting a global property
- * per app equal to their id and using it somewhere in the template
- */
+ // unmounting apps should pause guards
+ // start by navigating 3 times
+ .click('#app-1 li:nth-child(1) a')
+ .click('#app-1 li:nth-child(2) a')
+ .click('#app-1 li:nth-child(1) a')
+ .assert.containsText('#guardcount', '7')
+ .click('#unmount1')
+ .click('#unmount2')
+ .assert.containsText('#guardcount', '7')
+ .back()
+ // one app is still mounted
+ .assert.containsText('#guardcount', '8')
+ .click('#unmount3')
+ .back()
+ .assert.containsText('#guardcount', '8')
- // unmounting apps should end up removing the popstate listener
- // .click('#unmount1')
- // .click('#unmount2')
- // .click('#unmount3')
- // TODO: we need a way to hook into unmount
- // .assert.containsText('#popcount', '0')
+ // mounting again should add the listeners again
+ .click('#mount1')
+ // the initial navigation
+ .assert.containsText('#guardcount', '9')
+ .click('#app-1 li:nth-child(2) a')
+ .assert.containsText('#guardcount', '10')
.end()
},
ComputedRef,
reactive,
unref,
+ computed,
} from 'vue'
import { RouteRecord, RouteRecordNormalized } from './matcher/types'
import { parseURL, stringifyURL, isSameRouteLocation } from './location'
}
export interface Router {
+ /**
+ * @internal
+ */
readonly history: RouterHistory
readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
readonly options: RouterOptions
markAsReady()
}
+ let removeHistoryListener: () => void
// attach listener to history to trigger navigations
- routerHistory.listen((to, _from, info) => {
- // TODO: in dev try catch to correctly log the matcher error
- // cannot be a redirect route because it was in history
- const toLocation = resolve(to.fullPath) as RouteLocationNormalized
+ function setupListeners() {
+ removeHistoryListener = routerHistory.listen((to, _from, info) => {
+ // TODO: in dev try catch to correctly log the matcher error
+ // cannot be a redirect route because it was in history
+ const toLocation = resolve(to.fullPath) as RouteLocationNormalized
+
+ pendingLocation = toLocation
+ const from = currentRoute.value
+
+ // TODO: should be moved to web history?
+ if (isBrowser) {
+ saveScrollPosition(
+ getScrollKey(from.fullPath, info.delta),
+ computeScrollPosition()
+ )
+ }
- pendingLocation = toLocation
- const from = currentRoute.value
+ navigate(toLocation, from)
+ .catch((error: NavigationFailure | NavigationRedirectError) => {
+ // a more recent navigation took place
+ if (pendingLocation !== toLocation) {
+ return createRouterError<NavigationFailure>(
+ ErrorTypes.NAVIGATION_CANCELLED,
+ {
+ from,
+ to: toLocation,
+ }
+ )
+ }
+ if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
+ return error as NavigationFailure
+ }
+ if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
+ routerHistory.go(-info.delta, false)
+ // the error is already handled by router.push we just want to avoid
+ // logging the error
+ pushWithRedirect(
+ (error as NavigationRedirectError).to,
+ toLocation
+ ).catch(() => {
+ // TODO: in dev show warning, in prod triggerError, same as initial navigation
+ })
+ // avoid the then branch
+ return Promise.reject()
+ }
+ // TODO: test on different browsers ensure consistent behavior
+ routerHistory.go(-info.delta, false)
+ // unrecognized error, transfer to the global handler
+ return triggerError(error)
+ })
+ .then((failure: NavigationFailure | void) => {
+ failure =
+ failure ||
+ finalizeNavigation(
+ // after navigation, all matched components are resolved
+ toLocation as RouteLocationNormalizedLoaded,
+ from,
+ false
+ )
- // TODO: should be moved to web history?
- if (isBrowser) {
- saveScrollPosition(
- getScrollKey(from.fullPath, info.delta),
- computeScrollPosition()
- )
- }
+ // revert the navigation
+ if (failure) routerHistory.go(-info.delta, false)
- navigate(toLocation, from)
- .catch((error: NavigationFailure | NavigationRedirectError) => {
- // a more recent navigation took place
- if (pendingLocation !== toLocation) {
- return createRouterError<NavigationFailure>(
- ErrorTypes.NAVIGATION_CANCELLED,
- {
- from,
- to: toLocation,
- }
- )
- }
- if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
- return error as NavigationFailure
- }
- if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
- routerHistory.go(-info.delta, false)
- // the error is already handled by router.push we just want to avoid
- // logging the error
- pushWithRedirect(
- (error as NavigationRedirectError).to,
- toLocation
- ).catch(() => {
- // TODO: in dev show warning, in prod triggerError, same as initial navigation
- })
- // avoid the then branch
- return Promise.reject()
- }
- // TODO: test on different browsers ensure consistent behavior
- routerHistory.go(-info.delta, false)
- // unrecognized error, transfer to the global handler
- return triggerError(error)
- })
- .then((failure: NavigationFailure | void) => {
- failure =
- failure ||
- finalizeNavigation(
- // after navigation, all matched components are resolved
+ triggerAfterEach(
toLocation as RouteLocationNormalizedLoaded,
from,
- false
+ failure
)
-
- // revert the navigation
- if (failure) routerHistory.go(-info.delta, false)
-
- triggerAfterEach(
- toLocation as RouteLocationNormalizedLoaded,
- from,
- failure
- )
- })
- .catch(() => {
- // TODO: same as above
- })
- })
+ })
+ .catch(() => {
+ // TODO: same as above
+ })
+ })
+ }
// Initialization and Errors
function markAsReady(err?: any): void {
if (ready) return
ready = true
+ setupListeners()
readyHandlers
.list()
.forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
}
let started: boolean | undefined
+ const installedApps = new Set<App>()
const router: Router = {
currentRoute,
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
+
+ let unmountApp = app.unmount
+ installedApps.add(app)
+ app.unmount = function () {
+ installedApps.delete(app)
+ if (installedApps.size < 1) {
+ removeHistoryListener()
+ currentRoute.value = START_LOCATION_NORMALIZED
+ started = false
+ ready = false
+ }
+ unmountApp.call(this, arguments)
+ }
},
}