Skip to content

Commit

Permalink
feat: skip navigation guards by default
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Dec 22, 2020
1 parent d0f2ec5 commit 0f25718
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 13 deletions.
23 changes: 18 additions & 5 deletions __tests__/fixtures/Test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { defineComponent, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { defineComponent, getCurrentInstance, h, PropType } from 'vue'
import {
NavigationGuard,
onBeforeRouteLeave,
onBeforeRouteUpdate,
useRoute,
useRouter,
} from 'vue-router'

export default defineComponent({
name: 'Test',
props: {
leaveGuard: Function,
updateGuard: Function,
leaveGuard: Function as PropType<NavigationGuard>,
updateGuard: Function as PropType<NavigationGuard>,
},

setup() {
setup(props) {
const route = useRoute()
const router = useRouter()

if (props.leaveGuard) {
onBeforeRouteLeave(props.leaveGuard)
}
if (props.updateGuard) {
onBeforeRouteUpdate(props.updateGuard)
}

return { route, router }
},

Expand Down
96 changes: 92 additions & 4 deletions __tests__/navigations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,108 @@ describe('Navigations', () => {
})

describe('per-router guards', () => {
it('can ignore them', async () => {
it('ignored by default', async () => {
const beforeEnter = jest.fn()
const router = createRouterMock({ runInComponentGuards: false })
const router = createRouterMock()
injectRouterMock(router)
router.addRoute({ path: '/foo', beforeEnter, component: EmptyView })

mount(Test)
await router.push('/foo')
expect(beforeEnter).not.toHaveBeenCalled()
})

it('ignore them with other setNextGuardReturn', async () => {
const beforeEnter = jest.fn()
const router = createRouterMock()
injectRouterMock(router)
router.addRoute({ path: '/foo', beforeEnter, component: EmptyView })

mount(Test)
router.setNextGuardReturn(true)
router.push('/foo')
await router.getPendingNavigation()
await router.push('/foo')
expect(beforeEnter).not.toHaveBeenCalled()
})
})

describe('in-component guards', () => {
it('ignores guards by default with no guard', async () => {
const router = createRouterMock()
injectRouterMock(router)
router.addRoute({ path: '/test', component: Test })
await router.push('/test')

const leaveGuard = jest.fn()
const updateGuard = jest.fn()

mount(Test, { props: { leaveGuard, updateGuard } })

await router.push('/test#two')
expect(updateGuard).not.toHaveBeenCalled()

await router.push('/foo')
expect(leaveGuard).not.toHaveBeenCalled()
})

it('ignores guards by default with a guard', async () => {
const router = createRouterMock()
injectRouterMock(router)
router.addRoute({ path: '/test', component: Test })
await router.push('/test')

const leaveGuard = jest.fn()
const updateGuard = jest.fn()

mount(Test, { props: { leaveGuard, updateGuard } })

router.setNextGuardReturn(true)
await router.push('/test#two')
expect(updateGuard).not.toHaveBeenCalled()

router.setNextGuardReturn(true)
await router.push('/foo')
expect(leaveGuard).not.toHaveBeenCalled()
})

it('runs guards without a guard return set', async () => {
const router = createRouterMock({ runInComponentGuards: true })
injectRouterMock(router)
router.addRoute({ path: '/test', component: Test })
await router.push('/test')

const leaveGuard = jest.fn()
const updateGuard = jest.fn()

mount(Test, { props: { leaveGuard, updateGuard } })

await router.push('/test#two')
expect(updateGuard).toHaveBeenCalled()

await router.push('/foo')
expect(leaveGuard).toHaveBeenCalled()
})

it('runs guards with a guard', async () => {
const router = createRouterMock({ runInComponentGuards: true })
injectRouterMock(router)
router.addRoute({ path: '/test', component: Test })
await router.push('/test')

const leaveGuard = jest.fn()
const updateGuard = jest.fn()

mount(Test, { props: { leaveGuard, updateGuard } })

router.setNextGuardReturn(true)
await router.push('/test#two')
expect(updateGuard).toHaveBeenCalled()

router.setNextGuardReturn(true)
await router.push('/foo')
expect(leaveGuard).toHaveBeenCalled()
})
})

it('can redirect the next navigation', async () => {
const wrapper = mount(Test)
router.setNextGuardReturn('/bar')
Expand Down
9 changes: 8 additions & 1 deletion src/injections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
matchedRouteKey,
routeLocationKey,
RouteLocationNormalizedLoaded,
routerKey,
Expand All @@ -10,7 +11,7 @@ import { config } from '@vue/test-utils'
import { createReactiveRouteLocation } from './routeLocation'
import { createRouterMock, RouterMock } from './router'
// @ts-ignore: for api-extractor
import { Ref } from 'vue'
import { computed, Ref } from 'vue'

/**
* Inject global variables, overriding any previously inject router mock
Expand All @@ -30,6 +31,7 @@ export function injectRouterMock(router?: RouterMock) {
config.global.mocks.$router = router
config.global.mocks.$route = route

// TODO: stub that provides the prop route or the current route with machedRouteKey
config.global.components.RouterView = RouterView
config.global.components.RouterLink = RouterLink

Expand All @@ -48,9 +50,14 @@ export function injectRouterMock(router?: RouterMock) {
export function createProvide(router: RouterMock) {
const route = createReactiveRouteLocation(router.currentRoute)

const matchedRouteRef = computed(
() => router.currentRoute.value.matched[router.depth.value]
)

return {
[routerKey as any]: router,
[routeLocationKey as any]: route,
[routerViewLocationKey as any]: router.currentRoute,
[matchedRouteKey as any]: matchedRouteRef,
}
}
21 changes: 18 additions & 3 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from 'vue'
import { Component, Ref, ref } from 'vue'
import {
createMemoryHistory,
createRouter,
Expand All @@ -17,6 +17,11 @@ export const EmptyView: Component = {
* Router Mock instance
*/
export interface RouterMock extends Router {
/**
* Current depeth of the router view. This index is used to find the component
* to display in the array `router.currentRoute.value.matched`.
*/
depth: Ref<number>
/**
* Set a value to be returned on a navigation guard for the next navigation.
*
Expand Down Expand Up @@ -120,14 +125,21 @@ export function createRouterMock({
to: RouteLocationRaw,
options: { replace?: boolean } = {}
) {
if (nextReturn != null) {
if (nextReturn != null || runInComponentGuards) {
const removeGuard = router.beforeEach(() => {
const value = nextReturn
removeGuard()
nextReturn = undefined
return value
})
// TODO: should we avoid all in component guards by deleting them here?

// avoid existing navigation guards
const record = router.currentRoute.value.matched[depth.value]
if (record && !runInComponentGuards) {
record.leaveGuards.clear()
record.updateGuards.clear()
// TODO: option guards
}

pendingNavigation = (options.replace ? replace : push)(to)
pendingNavigation
Expand All @@ -148,8 +160,11 @@ export function createRouterMock({
return pendingNavigation || Promise.resolve()
}

const depth = ref(0)

return {
...router,
depth,
setNextGuardReturn,
getPendingNavigation,
}
Expand Down

0 comments on commit 0f25718

Please sign in to comment.