expect(route1.params).toEqual({ p: 'a' })
})
+ it('warns on undefined location during dev', async () => {
+ const { router } = await newRouter()
+
+ const route1 = router.resolve(undefined as any)
+ expect('router.resolve() was passed an invalid location').toHaveBeenWarned()
+ expect(route1.path).toBe('/')
+ })
+
+ it('warns on null location during dev', async () => {
+ const { router } = await newRouter()
+
+ const route1 = router.resolve(null as any)
+ expect('router.resolve() was passed an invalid location').toHaveBeenWarned()
+ expect(route1.path).toBe('/')
+ })
+
it('removes null/undefined optional params when current location has it', async () => {
const { router } = await newRouter()
--- /dev/null
+/**
+ * @jest-environment jsdom
+ */
+import { nextTick, ref } from 'vue'
+import { mount } from '@vue/test-utils'
+import { mockWarn } from 'jest-mock-warn'
+import {
+ createMemoryHistory,
+ createRouter,
+ RouteLocationRaw,
+ useLink,
+ UseLinkOptions,
+} from '../src'
+
+async function callUseLink(args: UseLinkOptions) {
+ const router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: '/',
+ component: {},
+ name: 'root',
+ },
+ {
+ path: '/a',
+ component: {},
+ name: 'a',
+ },
+ {
+ path: '/b',
+ component: {},
+ name: 'b',
+ },
+ ],
+ })
+
+ await router.push('/')
+
+ let link: ReturnType<typeof useLink>
+
+ mount(
+ {
+ setup() {
+ link = useLink(args)
+
+ return () => ''
+ },
+ },
+ {
+ global: {
+ plugins: [router],
+ },
+ }
+ )
+
+ return link!
+}
+
+describe('useLink', () => {
+ describe('basic usage', () => {
+ it('supports a string for "to"', async () => {
+ const { href, route } = await callUseLink({
+ to: '/a',
+ })
+
+ expect(href.value).toBe('/a')
+ expect(route.value).toMatchObject({ name: 'a' })
+ })
+
+ it('supports an object for "to"', async () => {
+ const { href, route } = await callUseLink({
+ to: { path: '/a' },
+ })
+
+ expect(href.value).toBe('/a')
+ expect(route.value).toMatchObject({ name: 'a' })
+ })
+
+ it('supports a ref for "to"', async () => {
+ const to = ref<RouteLocationRaw>('/a')
+
+ const { href, route } = await callUseLink({
+ to,
+ })
+
+ expect(href.value).toBe('/a')
+ expect(route.value).toMatchObject({ name: 'a' })
+
+ to.value = { path: '/b' }
+
+ await nextTick()
+
+ expect(href.value).toBe('/b')
+ expect(route.value).toMatchObject({ name: 'b' })
+ })
+ })
+
+ describe('warnings', () => {
+ mockWarn()
+
+ it('should warn when "to" is undefined', async () => {
+ await callUseLink({
+ to: undefined as any,
+ })
+
+ expect('Invalid value for prop "to" in useLink()').toHaveBeenWarned()
+ expect(
+ 'router.resolve() was passed an invalid location'
+ ).toHaveBeenWarned()
+ })
+
+ it('should warn when "to" is an undefined ref', async () => {
+ await callUseLink({
+ to: ref(undefined as any),
+ })
+
+ expect('Invalid value for prop "to" in useLink()').toHaveBeenWarned()
+ expect(
+ 'router.resolve() was passed an invalid location'
+ ).toHaveBeenWarned()
+ })
+
+ it('should warn when "to" changes to a null ref', async () => {
+ const to = ref('/a')
+
+ const { href, route } = await callUseLink({
+ to,
+ })
+
+ expect(href.value).toBe('/a')
+ expect(route.value).toMatchObject({ name: 'a' })
+
+ to.value = null as any
+
+ await nextTick()
+
+ expect('Invalid value for prop "to" in useLink()').toHaveBeenWarned()
+ expect(
+ 'router.resolve() was passed an invalid location'
+ ).toHaveBeenWarned()
+ })
+ })
+})
ComponentOptionsMixin,
} from 'vue'
import {
+ isRouteLocation,
RouteLocationRaw,
VueUseOptions,
RouteLocation,
import { RouteRecord } from './matcher/types'
import { NavigationFailure } from './errors'
import { isArray, isBrowser, noop } from './utils'
+import { warn } from './warning'
export interface RouterLinkOptions {
/**
route: RouteLocationNormalized & { href: string }
isActive: boolean
isExactActive: boolean
+ error: string | null
}
export type UseLinkOptions = VueUseOptions<RouterLinkOptions>
const router = inject(routerKey)!
const currentRoute = inject(routeLocationKey)!
- const route = computed(() => router.resolve(unref(props.to)))
+ let hasPrevious = false
+ let previousTo: unknown = null
+
+ const route = computed(() => {
+ const to = unref(props.to)
+
+ if (__DEV__ && (!hasPrevious || to !== previousTo)) {
+ if (!isRouteLocation(to)) {
+ if (hasPrevious) {
+ warn(
+ `Invalid value for prop "to" in useLink()\n- to:`,
+ to,
+ `\n- previous to:`,
+ previousTo,
+ `\n- props:`,
+ props
+ )
+ } else {
+ warn(
+ `Invalid value for prop "to" in useLink()\n- to:`,
+ to,
+ `\n- props:`,
+ props
+ )
+ }
+ }
+
+ previousTo = to
+ hasPrevious = true
+ }
+
+ return router.resolve(to)
+ })
const activeRecordIndex = computed<number>(() => {
const { matched } = route.value
route: route.value,
isActive: isActive.value,
isExactActive: isExactActive.value,
+ error: null,
}
// @ts-expect-error: this is internal
linkContextDevtools.route = route.value
linkContextDevtools.isActive = isActive.value
linkContextDevtools.isExactActive = isExactActive.value
+ linkContextDevtools.error = isRouteLocation(unref(props.to))
+ ? null
+ : 'Invalid "to" value'
},
{ flush: 'post' }
)
;(
componentInstance.__vrl_devtools as UseLinkDevtoolsContext[]
).forEach(devtoolsData => {
+ let label = devtoolsData.route.path
let backgroundColor = ORANGE_400
let tooltip: string = ''
+ let textColor = 0
- if (devtoolsData.isExactActive) {
+ if (devtoolsData.error) {
+ label = devtoolsData.error
+ backgroundColor = RED_100
+ textColor = RED_700
+ } else if (devtoolsData.isExactActive) {
backgroundColor = LIME_500
tooltip = 'This is exactly active'
} else if (devtoolsData.isActive) {
}
node.tags.push({
- label: devtoolsData.route.path,
- textColor: 0,
+ label,
+ textColor,
tooltip,
backgroundColor,
})
const ORANGE_400 = 0xfb923c
// const GRAY_100 = 0xf4f4f5
const DARK = 0x666666
+const RED_100 = 0xfee2e2
+const RED_700 = 0xb91c1c
function formatRouteRecordForInspector(
route: RouteRecordMatcher
RouteLocationNormalizedLoaded,
RouteLocation,
RouteRecordName,
+ isRouteLocation,
isRouteName,
NavigationGuardWithThis,
RouteLocationOptions,
})
}
+ if (__DEV__ && !isRouteLocation(rawLocation)) {
+ warn(
+ `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
+ rawLocation
+ )
+ rawLocation = {}
+ }
+
let matcherLocation: MatcherLocationRaw
// path could be relative in object as well