diff --git a/examples/app-vitest-workspace/app2/nuxt.config.ts b/examples/app-vitest-workspace/app2/nuxt.config.ts index d7b01c438..c154ef59d 100644 --- a/examples/app-vitest-workspace/app2/nuxt.config.ts +++ b/examples/app-vitest-workspace/app2/nuxt.config.ts @@ -1,6 +1,7 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ modules: ['@nuxtjs/color-mode', '@nuxt/ui'], + pages: false, devtools: { enabled: true }, compatibilityDate: '2024-04-03', }) diff --git a/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts b/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts index 6d2aff508..483b4de3d 100644 --- a/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts +++ b/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest' import { mountSuspended } from '@nuxt/test-utils/runtime' import Badge from '@nuxt/ui/components/Badge.vue' +import Breadcrumb from '@nuxt/ui/components/Breadcrumb.vue' +import Header from '@nuxt/ui/components/Header.vue' describe('components', () => { it('ui/Badge', async () => { @@ -12,4 +14,41 @@ describe('components', () => { }) expect(wrapper.text()).toBe('Hello') }) + + it('ui/Header', async () => { + const wrapper = await mountSuspended(Header, { + props: { + title: 'App', + to: '/', + }, + }) + expect(wrapper.text()).toBe('App') + expect(wrapper.find('a')?.attributes('href')).toBe('/') + }) + + it('ui/Breadcrumb', async () => { + const component = await mountSuspended(Breadcrumb, { + props: { + items: [ + { + label: 'Home', + to: '/', + }, + { + label: 'Help', + to: '/help', + }, + ], + }, + }) + + const links = component.findAll('a') + expect(links).toHaveLength(2) + + expect(links[0]?.text()).toBe('Home') + expect(links[0]?.attributes('href')).toBe('/') + + expect(links[1]?.text()).toBe('Help') + expect(links[1]?.attributes('href')).toBe('/help') + }) }) diff --git a/examples/app-vitest-workspace/app4/app/pages/index.vue b/examples/app-vitest-workspace/app4/app/pages/index.vue new file mode 100644 index 000000000..7bd9480b5 --- /dev/null +++ b/examples/app-vitest-workspace/app4/app/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/examples/app-vitest-workspace/app4/nuxt.config.ts b/examples/app-vitest-workspace/app4/nuxt.config.ts new file mode 100644 index 000000000..b377d542b --- /dev/null +++ b/examples/app-vitest-workspace/app4/nuxt.config.ts @@ -0,0 +1,8 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + imports: { + autoImport: false, + }, + devtools: { enabled: true }, + compatibilityDate: '2024-04-03', +}) diff --git a/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts b/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts new file mode 100644 index 000000000..34b172c2c --- /dev/null +++ b/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { enableAutoUnmount } from '@vue/test-utils' +import { mountSuspended } from '@nuxt/test-utils/runtime' + +import { NuxtLink } from '#components' + +type DefaultSlotProp = { isActive: boolean, isExactActive: boolean } + +describe('NuxtLink without pages', () => { + enableAutoUnmount(afterEach) + + it('should mount', async () => { + const wrapper = await mountSuspended(NuxtLink, { + props: { + to: '/', + }, + slots: { + default: () => 'Home', + }, + }) + + const a = wrapper.find('a') + expect(a.exists()).toBe(true) + expect(a.attributes('href')).toBe('/') + expect(a.text()).toBe('Home') + }) + + it('should mount custom', async () => { + const wrapper = await mountSuspended(NuxtLink, { + props: { + to: '/', + custom: true, + }, + slots: { + default: ({ isActive, isExactActive }: DefaultSlotProp) => + `Home:${isActive}:${isExactActive}`, + }, + }) + + expect(wrapper.text()).toBe('Home:undefined:undefined') + }) +}) diff --git a/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts b/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts new file mode 100644 index 000000000..a238e08b0 --- /dev/null +++ b/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { enableAutoUnmount } from '@vue/test-utils' +import { mountSuspended } from '@nuxt/test-utils/runtime' + +import { NuxtLink } from '#components' + +type DefaultSlotProp = { isActive: boolean, isExactActive: boolean } + +describe('NuxtLink with pages', () => { + enableAutoUnmount(afterEach) + + it('should mount', async () => { + const wrapper = await mountSuspended(NuxtLink, { + props: { + to: '/', + }, + slots: { + default: () => 'Home', + }, + }) + + const a = wrapper.find('a') + expect(a.exists()).toBe(true) + expect(a.attributes('href')).toBe('/') + expect(a.text()).toBe('Home') + }) + + it('should mount custom', async () => { + const wrapper = await mountSuspended(NuxtLink, { + props: { + to: '/', + custom: true, + }, + slots: { + default: ({ isActive, isExactActive }: DefaultSlotProp) => + `Home:${isActive}:${isExactActive}`, + }, + }) + + expect(wrapper.text()).toBe('Home:true:true') + }) +}) diff --git a/examples/app-vitest-workspace/app4/tsconfig.json b/examples/app-vitest-workspace/app4/tsconfig.json new file mode 100644 index 000000000..6ae5970c7 --- /dev/null +++ b/examples/app-vitest-workspace/app4/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git a/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts b/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts new file mode 100644 index 000000000..c953af449 --- /dev/null +++ b/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts @@ -0,0 +1,17 @@ +import { fileURLToPath } from 'node:url' +import { defineVitestProject } from '@nuxt/test-utils/config' + +export default defineVitestProject({ + test: { + name: 'nuxt-app4:pages-off', + include: ['test/nuxt/pages-off/*.spec.ts'], + environmentOptions: { + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + overrides: { + pages: false, + }, + }, + }, + }, +}) diff --git a/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts b/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts new file mode 100644 index 000000000..dc7be2729 --- /dev/null +++ b/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts @@ -0,0 +1,17 @@ +import { fileURLToPath } from 'node:url' +import { defineVitestProject } from '@nuxt/test-utils/config' + +export default defineVitestProject({ + test: { + name: 'nuxt-app4:pages-on', + include: ['test/nuxt/pages-on/*.spec.ts'], + environmentOptions: { + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + overrides: { + pages: true, + }, + }, + }, + }, +}) diff --git a/examples/app-vitest-workspace/package.json b/examples/app-vitest-workspace/package.json index 851033ee0..ab58a455f 100644 --- a/examples/app-vitest-workspace/package.json +++ b/examples/app-vitest-workspace/package.json @@ -7,7 +7,11 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "dev:prepare": "nuxt prepare app1 && nuxt prepare app2 && nuxt prepare app3", + "dev:prepare": "pnpm run '/dev:prepare:.*/'", + "dev:prepare:app1": "nuxt prepare app1", + "dev:prepare:app2": "nuxt prepare app2", + "dev:prepare:app3": "nuxt prepare app3", + "dev:prepare:app4": "nuxt prepare app4", "postinstall": "pnpm dev:prepare", "test": "vitest run" }, diff --git a/src/runtime-utils/components/RouterLink.ts b/src/runtime-utils/components/RouterLink.ts index f26904920..1422da17a 100644 --- a/src/runtime-utils/components/RouterLink.ts +++ b/src/runtime-utils/components/RouterLink.ts @@ -1,5 +1,12 @@ -import { defineComponent, h } from '#imports' -import { useLink } from 'vue-router' +import { defineComponent, h, useRouter, useNuxtApp } from '#imports' +import type { useLink as useLinkFn } from 'vue-router' + +function getUseLink(nuxtApp: ReturnType) { + const linkComponent = nuxtApp.vueApp._context.components.RouterLink + if (!linkComponent || typeof linkComponent !== 'object') return undefined + if (typeof linkComponent.useLink !== 'function') return undefined + return linkComponent.useLink.bind(linkComponent) as typeof useLinkFn +} export const RouterLink = defineComponent({ functional: true, @@ -16,6 +23,31 @@ export const RouterLink = defineComponent({ ariaCurrentValue: String, }, setup: (props, { slots }) => { + const app = useNuxtApp() + const useLink = getUseLink(app) + + if (!useLink) { + const navigate = () => {} + const router = useRouter() + return () => { + const route = router.resolve(props.to) + + return props.custom + ? slots.default?.({ href: route.href, navigate, route }) + : h( + 'a', + { + href: route.href, + onClick: (e: MouseEvent) => { + e.preventDefault() + return navigate() + }, + }, + slots, + ) + } + } + const link = useLink(props) return () => {