diff --git a/examples/app-vitest-co-location/.nuxtignore b/examples/app-vitest-co-location/.nuxtignore new file mode 100644 index 000000000..7affc8e76 --- /dev/null +++ b/examples/app-vitest-co-location/.nuxtignore @@ -0,0 +1 @@ +**/__tests__/**/* \ No newline at end of file diff --git a/examples/app-vitest-co-location/app/app.config.ts b/examples/app-vitest-co-location/app/app.config.ts new file mode 100644 index 000000000..b4313b057 --- /dev/null +++ b/examples/app-vitest-co-location/app/app.config.ts @@ -0,0 +1,3 @@ +export default defineAppConfig({ + foo: 'bar', +}) diff --git a/examples/app-vitest-co-location/app/components/Message.nuxt.spec.ts b/examples/app-vitest-co-location/app/components/Message.nuxt.spec.ts new file mode 100644 index 000000000..14a120f32 --- /dev/null +++ b/examples/app-vitest-co-location/app/components/Message.nuxt.spec.ts @@ -0,0 +1,12 @@ +import { it, expect, vi } from 'vitest' +import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime' + +import Message from './Message.vue' + +mockNuxtImport(useMessage, vi.fn) + +it('should mount', async () => { + vi.mocked(useMessage).mockImplementationOnce(() => 'Mocked!') + const wrapper = await mountSuspended(Message) + expect(wrapper.text()).toContain('Mocked!') +}) diff --git a/examples/app-vitest-co-location/app/components/Message.vue b/examples/app-vitest-co-location/app/components/Message.vue new file mode 100644 index 000000000..bef97565a --- /dev/null +++ b/examples/app-vitest-co-location/app/components/Message.vue @@ -0,0 +1,7 @@ + + + + {{ message }} + diff --git a/examples/app-vitest-co-location/app/components/__tests__/Message.browser.spec.ts b/examples/app-vitest-co-location/app/components/__tests__/Message.browser.spec.ts new file mode 100644 index 000000000..281a28535 --- /dev/null +++ b/examples/app-vitest-co-location/app/components/__tests__/Message.browser.spec.ts @@ -0,0 +1,10 @@ +import { it, expect } from 'vitest' +import { render } from '@nuxt/test-utils/browser' + +import { Message } from '#components' + +it('should render', async () => { + const screen = await render(Message) + const message = screen.getByRole('heading') + await expect.element(message).toMatchTextContent(/Hello Nuxt/) +}) diff --git a/examples/app-vitest-co-location/app/components/__tests__/Message.nuxt.spec.ts b/examples/app-vitest-co-location/app/components/__tests__/Message.nuxt.spec.ts new file mode 100644 index 000000000..7ecd70ac4 --- /dev/null +++ b/examples/app-vitest-co-location/app/components/__tests__/Message.nuxt.spec.ts @@ -0,0 +1,9 @@ +import { it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' + +import { Message } from '#components' + +it('should mount with import from alias', async () => { + const wrapper = await mountSuspended(Message) + expect(wrapper.text()).toContain('Hello Nuxt') +}) diff --git a/examples/app-vitest-co-location/app/composables/useMessage.nuxt.spec.ts b/examples/app-vitest-co-location/app/composables/useMessage.nuxt.spec.ts new file mode 100644 index 000000000..3acc553a2 --- /dev/null +++ b/examples/app-vitest-co-location/app/composables/useMessage.nuxt.spec.ts @@ -0,0 +1,8 @@ +import { it, expect } from 'vitest' +import { useMessage } from './useMessage' + +it('useMessage', () => { + expect(useMessage()).toBe('Hello Nuxt') +}) + +export function useMessageSpec() {} diff --git a/examples/app-vitest-co-location/app/composables/useMessage.ts b/examples/app-vitest-co-location/app/composables/useMessage.ts new file mode 100644 index 000000000..ba6119b5d --- /dev/null +++ b/examples/app-vitest-co-location/app/composables/useMessage.ts @@ -0,0 +1,3 @@ +export function useMessage() { + return 'Hello Nuxt' +} diff --git a/examples/app-vitest-co-location/app/middleware/logger.global.nuxt.spec.ts b/examples/app-vitest-co-location/app/middleware/logger.global.nuxt.spec.ts new file mode 100644 index 000000000..70da24bee --- /dev/null +++ b/examples/app-vitest-co-location/app/middleware/logger.global.nuxt.spec.ts @@ -0,0 +1,11 @@ +import { it, expect, vi } from 'vitest' +import middleware from './logger.global' +import { mockNuxtImport } from '@nuxt/test-utils/runtime' + +mockNuxtImport(useRoute, vi.fn) + +it('middleware', () => { + const spy = vi.spyOn(console, 'log') + expect(middleware(useRoute(), useRoute())).toBeUndefined() + expect(spy).toHaveBeenCalledExactlyOnceWith('/', '/') +}) diff --git a/examples/app-vitest-co-location/app/middleware/logger.global.ts b/examples/app-vitest-co-location/app/middleware/logger.global.ts new file mode 100644 index 000000000..d23a33f10 --- /dev/null +++ b/examples/app-vitest-co-location/app/middleware/logger.global.ts @@ -0,0 +1,3 @@ +export default defineNuxtRouteMiddleware((to, from) => { + console.log(to.path, from.path) +}) diff --git a/examples/app-vitest-co-location/app/pages/__tests__/index.nuxt.spec.ts b/examples/app-vitest-co-location/app/pages/__tests__/index.nuxt.spec.ts new file mode 100644 index 000000000..63f889e09 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/__tests__/index.nuxt.spec.ts @@ -0,0 +1,11 @@ +import { it, expect, vi } from 'vitest' +import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime' + +import Page from '../index.vue' + +mockNuxtImport(useAppConfig, vi.fn) + +it('should mount', async () => { + const wrapper = await mountSuspended(Page) + expect(wrapper.text()).toContain('Index Page') +}) diff --git a/examples/app-vitest-co-location/app/pages/about.vue b/examples/app-vitest-co-location/app/pages/about.vue new file mode 100644 index 000000000..4ff3cd106 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/about.vue @@ -0,0 +1,6 @@ + + + About Page + + + diff --git a/examples/app-vitest-co-location/app/pages/about/index.nuxt.spec.ts b/examples/app-vitest-co-location/app/pages/about/index.nuxt.spec.ts new file mode 100644 index 000000000..9516bcea6 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/about/index.nuxt.spec.ts @@ -0,0 +1,11 @@ +import { it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { NuxtPage } from '#components' + +it('should mount child page', async () => { + const wrapper = await mountSuspended(NuxtPage, { + route: '/about', + }) + expect(wrapper.text()).toContain('About Page') + expect(wrapper.text()).toContain('About Index') +}) diff --git a/examples/app-vitest-co-location/app/pages/about/index.vue b/examples/app-vitest-co-location/app/pages/about/index.vue new file mode 100644 index 000000000..49d568736 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/about/index.vue @@ -0,0 +1,5 @@ + + + About Index + + diff --git a/examples/app-vitest-co-location/app/pages/index.e2e.spec.ts b/examples/app-vitest-co-location/app/pages/index.e2e.spec.ts new file mode 100644 index 000000000..f644efc1e --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/index.e2e.spec.ts @@ -0,0 +1,19 @@ +import { fileURLToPath } from 'node:url' +import { it, expect } from 'vitest' +import { createPage, setup } from '@nuxt/test-utils/e2e' + +await setup({ + rootDir: fileURLToPath(new URL('../../', import.meta.url)), + browser: true, +}) + +it('runs e2e browser test', { timeout: 20000 }, async ({ onTestFinished }) => { + const page = await createPage('/') + onTestFinished(() => page.close()) + + const title = page.getByRole('heading', { level: 1 }) + await expect(title.textContent()).resolves.toContain('Index Page') + + const paragraph = page.locator('#app-config') + await expect(paragraph.textContent()).resolves.toContain('foo: bar') +}) diff --git a/examples/app-vitest-co-location/app/pages/index.nuxt.spec.ts b/examples/app-vitest-co-location/app/pages/index.nuxt.spec.ts new file mode 100644 index 000000000..28852ce05 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/index.nuxt.spec.ts @@ -0,0 +1,41 @@ +import { it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import type { TestCollectScans } from '~~/modules/collect-scans.ts' + +import Page from './index.vue' + +it('should mount page', async () => { + const wrapper = await mountSuspended(Page) + const title = wrapper.find('h1') + expect(title.text()).toContain('Index Page') + + const appConfig = wrapper.find('#app-config') + expect(appConfig.text()).toContain('foo: bar') +}) + +it('should ignore plugins and middlewares', async () => { + const wrapper = await mountSuspended(Page) + const scans: TestCollectScans = JSON.parse(wrapper.find('#scans').text()) + + expect(scans).toEqual({ + pages: [ + 'app/pages/about.vue', + 'app/pages/about/index.vue', + 'app/pages/index.vue', + ], + imports: [ + 'app/composables/useMessage.ts', + ], + components: [ + 'app/components/Message.vue', + ], + plugins: [ + 'app/plugins/message.ts', + ], + middlewares: [ + 'app/middleware/logger.global.ts', + ], + // empty because client environment + serverHandlers: [], + }) +}) diff --git a/examples/app-vitest-co-location/app/pages/index.vue b/examples/app-vitest-co-location/app/pages/index.vue new file mode 100644 index 000000000..fd38a0e75 --- /dev/null +++ b/examples/app-vitest-co-location/app/pages/index.vue @@ -0,0 +1,17 @@ + + + + + Index Page + + foo: {{ appConfig.foo }} + + + {{ scans }} + + + diff --git a/examples/app-vitest-co-location/app/plugins/message.nuxt.test.ts b/examples/app-vitest-co-location/app/plugins/message.nuxt.test.ts new file mode 100644 index 000000000..26a81e1ca --- /dev/null +++ b/examples/app-vitest-co-location/app/plugins/message.nuxt.test.ts @@ -0,0 +1,13 @@ +import { mockNuxtImport } from '@nuxt/test-utils/runtime' +import { it, expect, vi } from 'vitest' + +mockNuxtImport(useMessage, vi.fn) + +it('plugin', () => { + expect(useNuxtApp().$message()).toEqual('Hello Nuxt (/)') +}) + +it('plugin mock', () => { + vi.mocked(useMessage).mockImplementationOnce(() => 'Mocked!') + expect(useNuxtApp().$message()).toEqual('Mocked! (/)') +}) diff --git a/examples/app-vitest-co-location/app/plugins/message.ts b/examples/app-vitest-co-location/app/plugins/message.ts new file mode 100644 index 000000000..32f56e68f --- /dev/null +++ b/examples/app-vitest-co-location/app/plugins/message.ts @@ -0,0 +1,9 @@ +export default defineNuxtPlugin((nuxt) => { + return { + provide: { + message() { + return `${useMessage()} (${nuxt.$router.currentRoute.value.path})` + }, + }, + } +}) diff --git a/examples/app-vitest-co-location/modules/collect-scans.ts b/examples/app-vitest-co-location/modules/collect-scans.ts new file mode 100644 index 000000000..4dba7d9bc --- /dev/null +++ b/examples/app-vitest-co-location/modules/collect-scans.ts @@ -0,0 +1,71 @@ +import { relative } from 'pathe' +import { defineNuxtModule, addTemplate, addServerTemplate } from '@nuxt/kit' + +export type TestCollectScans = ReturnType + +function createScans() { + return { + pages: [] as string[], + imports: [] as string[], + components: [] as string[], + plugins: [] as string[], + middlewares: [] as string[], + serverHandlers: [] as string[], + } +} + +export default defineNuxtModule({ + meta: { + name: 'test-collect-scans', + }, + setup(_, nuxt) { + const rootDir = nuxt.options.rootDir + + const normalize = (paths: (string | undefined)[]) => paths + .filter(p => p !== undefined) + .map(p => relative(rootDir, p)) + .filter(p => !p.includes('node_modules/') && !p.includes('../') && !p.includes('.nuxt/')) + .toSorted() + + const scans = createScans() + + addTemplate({ + filename: 'test-collect-scans.mjs', + getContents: () => `export default ${JSON.stringify(scans)};`, + }) + + addServerTemplate({ + filename: '#test-collect-scans/scans.mjs', + getContents: () => `export default ${JSON.stringify(scans)};`, + }) + + nuxt.addHooks({ + 'pages:extend'(_pages) { + const getPaths = (pages: typeof _pages, paths: string[] = []) => { + for (const p of pages) { + if (p.file) { + paths.push(p.file) + } + getPaths(p.children ?? [], paths) + } + return paths + } + + scans.pages = normalize(getPaths(_pages)) + }, + 'imports:extend'(imports) { + scans.imports = normalize(imports.map(p => p.from)) + }, + 'components:extend'(components) { + scans.components = normalize(components.map(p => p.filePath)) + }, + 'app:resolve': (app) => { + scans.plugins = normalize(app.plugins.map(p => p.src)) + scans.middlewares = normalize(app.middleware.map(p => p.path)) + }, + 'nitro:build:before'(nitro) { + scans.serverHandlers = normalize(nitro.scannedHandlers.map(p => p.handler)) + }, + }) + }, +}) diff --git a/examples/app-vitest-co-location/nuxt.config.ts b/examples/app-vitest-co-location/nuxt.config.ts new file mode 100644 index 000000000..3c0edadf7 --- /dev/null +++ b/examples/app-vitest-co-location/nuxt.config.ts @@ -0,0 +1,6 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + modules: ['@nuxt/test-utils/module'], + devtools: { enabled: true }, + compatibilityDate: '2024-04-03', +}) diff --git a/examples/app-vitest-co-location/package.json b/examples/app-vitest-co-location/package.json new file mode 100644 index 000000000..5a2ec322a --- /dev/null +++ b/examples/app-vitest-co-location/package.json @@ -0,0 +1,25 @@ +{ + "name": "example-app-vitest-co-location", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "test": "vitest run" + }, + "dependencies": { + "nuxt": "^4.5.2" + }, + "devDependencies": { + "@nuxt/test-utils": "latest", + "@vitest/browser-playwright": "5.0.2", + "@vue/test-utils": "2.5.1", + "happy-dom": "20.14.5", + "playwright-core": "1.63.0", + "typescript": "6.0.3", + "vitest": "5.0.2" + } +} diff --git a/examples/app-vitest-co-location/server/api/collect-scans.ts b/examples/app-vitest-co-location/server/api/collect-scans.ts new file mode 100644 index 000000000..8543cc5d7 --- /dev/null +++ b/examples/app-vitest-co-location/server/api/collect-scans.ts @@ -0,0 +1,6 @@ +import { defineEventHandler } from 'h3' + +// @ts-expect-error vertual file +import scans from '#test-collect-scans/scans.mjs' + +export default defineEventHandler(() => scans) diff --git a/examples/app-vitest-co-location/server/api/hello.get.e2e.spec.ts b/examples/app-vitest-co-location/server/api/hello.get.e2e.spec.ts new file mode 100644 index 000000000..8e6af8676 --- /dev/null +++ b/examples/app-vitest-co-location/server/api/hello.get.e2e.spec.ts @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'node:url' +import { it, expect } from 'vitest' +import { setup, $fetch } from '@nuxt/test-utils/e2e' + +await setup({ + rootDir: fileURLToPath(new URL('../../', import.meta.url)), +}) + +it('runs e2e api test', { timeout: 20000 }, async () => { + const res = await $fetch('/api/hello', { query: { name: 'Nuxt' } }) + expect(res).toMatchObject({ message: 'Hello Nuxt!' }) +}) diff --git a/examples/app-vitest-co-location/server/api/hello.get.ts b/examples/app-vitest-co-location/server/api/hello.get.ts new file mode 100644 index 000000000..613a53671 --- /dev/null +++ b/examples/app-vitest-co-location/server/api/hello.get.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, getQuery } from 'h3' + +export default defineEventHandler((event) => { + const name = getQuery(event).name + return { message: `Hello ${name}!` } +}) diff --git a/examples/app-vitest-co-location/server/api/hello.get.unit.spec.ts b/examples/app-vitest-co-location/server/api/hello.get.unit.spec.ts new file mode 100644 index 000000000..4793f7bdf --- /dev/null +++ b/examples/app-vitest-co-location/server/api/hello.get.unit.spec.ts @@ -0,0 +1,10 @@ +import { it, expect } from 'vitest' +import { createApp, toWebHandler } from 'h3' +import handler from './hello.get' + +it('/api/hello', async () => { + const api = toWebHandler(createApp().use('/', handler)) + const req = new Request(new URL('http://localhost/?name=Nuxt')) + const res = await api(req).then(r => r.json()) + expect(res).toEqual({ message: 'Hello Nuxt!' }) +}) diff --git a/examples/app-vitest-co-location/test/nuxt/mount.nuxt.spec.ts b/examples/app-vitest-co-location/test/nuxt/mount.nuxt.spec.ts new file mode 100644 index 000000000..a89a6e558 --- /dev/null +++ b/examples/app-vitest-co-location/test/nuxt/mount.nuxt.spec.ts @@ -0,0 +1,12 @@ +import { it, expect, vi } from 'vitest' +import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime' + +import { Message } from '#components' + +mockNuxtImport(useMessage, vi.fn) + +it('should mount', async () => { + vi.mocked(useMessage).mockImplementationOnce(() => 'Mocked!') + const wrapper = await mountSuspended(Message) + expect(wrapper.text()).toContain('Mocked!') +}) diff --git a/examples/app-vitest-co-location/test/nuxt/nuxt-ignore-keep.e2e.test.ts b/examples/app-vitest-co-location/test/nuxt/nuxt-ignore-keep.e2e.test.ts new file mode 100644 index 000000000..026efd0c5 --- /dev/null +++ b/examples/app-vitest-co-location/test/nuxt/nuxt-ignore-keep.e2e.test.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { expect, it } from 'vitest' +import { setup, $fetch } from '@nuxt/test-utils/e2e' +import type { TestCollectScans } from '~~/modules/collect-scans' + +await setup({ + rootDir: fileURLToPath(new URL('../..', import.meta.url)), +}) + +it('should keep ignore test files in runtime dirs', async () => { + expect(await $fetch('/api/collect-scans')).toEqual({ + pages: [ + 'app/pages/about.vue', + 'app/pages/about/index.vue', + 'app/pages/index.vue', + ], + imports: [ + 'app/composables/useMessage.ts', + ], + components: [ + 'app/components/Message.vue', + ], + plugins: [ + 'app/plugins/message.ts', + ], + middlewares: [ + 'app/middleware/logger.global.ts', + ], + serverHandlers: [ + 'server/api/collect-scans.ts', + 'server/api/hello.get.ts', + ], + } satisfies TestCollectScans) +}) diff --git a/examples/app-vitest-co-location/tsconfig.json b/examples/app-vitest-co-location/tsconfig.json new file mode 100644 index 000000000..6ae5970c7 --- /dev/null +++ b/examples/app-vitest-co-location/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-co-location/vitest.config.ts b/examples/app-vitest-co-location/vitest.config.ts new file mode 100644 index 000000000..213614cff --- /dev/null +++ b/examples/app-vitest-co-location/vitest.config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from 'vitest/config' +import { defineVitestProject } from '@nuxt/test-utils/config' +import { playwright } from '@vitest/browser-playwright' + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + environment: 'node', + include: ['**/*.unit.{test,spec}.ts'], + }, + }, + { + test: { + name: 'e2e', + environment: 'node', + include: ['**/*.e2e.{test,spec}.ts'], + }, + }, + await defineVitestProject({ + test: { + name: 'nuxt', + environment: 'nuxt', + include: ['**/*.nuxt.{test,spec}.ts'], + }, + }), + await defineVitestProject({ + test: { + name: 'browser', + environment: 'nuxt', + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + include: ['**/*.browser.{test,spec}.ts'], + }, + }), + ], + onConsoleLog(log) { + if (log.includes(' is an experimental feature')) { + return false + } + }, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0d069982..5dee2bd2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -422,6 +422,34 @@ importers: specifier: 3.1.0 version: 3.1.0(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vitest@5.0.2)(vue@3.5.43(typescript@6.0.3)) + examples/app-vitest-co-location: + dependencies: + nuxt: + specifier: ^4.5.2 + version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@11.0.0)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@11.0.0)))(@oxc-project/types@0.152.0)(@parcel/watcher@2.6.0)(@types/node@24.19.0)(@vitejs/devtools@0.7.6)(@vue/compiler-sfc@3.5.43)(better-sqlite3@13.0.3)(cac@7.0.0)(commander@15.0.0)(db0@0.4.1)(esbuild@0.28.2)(eslint@10.12.0(jiti@2.7.0)(supports-color@11.0.0))(idb-keyval@6.3.0)(ioredis@5.11.1(supports-color@11.0.0))(lightningcss@1.33.0)(magicast@0.5.5)(optionator@0.9.4)(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.11)(rollup@4.64.0))(rollup@4.64.0)(srvx@1.0.5)(supports-color@11.0.0)(terser@5.51.2)(typescript@6.0.3)(vite@8.3.2)(vue-tsc@3.3.12(typescript@6.0.3))(yaml@2.9.1) + devDependencies: + '@nuxt/test-utils': + specifier: workspace:* + version: link:../.. + '@vitest/browser-playwright': + specifier: 5.0.2 + version: 5.0.2(playwright@1.63.0)(vite@8.3.2)(vitest@5.0.2) + '@vue/test-utils': + specifier: 2.5.1 + version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(typescript@6.0.3)) + happy-dom: + specifier: 20.14.5 + version: 20.14.5 + playwright-core: + specifier: 1.63.0 + version: 1.63.0 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 5.0.2 + version: 5.0.2(@types/node@24.19.0)(@vitest/browser-playwright@5.0.2)(@vitest/ui@5.0.2)(happy-dom@20.14.5)(jsdom@30.1.1)(vite@8.3.2) + examples/app-vitest-full: dependencies: idb-keyval: diff --git a/src/config.ts b/src/config.ts index 24b9fa0e4..32d3effb8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,6 +58,9 @@ async function startNuxtAndGetViteConfig(rootDir = process.cwd(), options: LoadN ssr: false, test: true, modules: ['@nuxt/test-utils/module'], + testUtils: { + _fromVitestConfig: true, + }, }, options.overrides, // vitest always creates a dev server from this config, but the nitro vite environment diff --git a/src/module.ts b/src/module.ts index c99fff4ff..f1160a7b8 100644 --- a/src/module.ts +++ b/src/module.ts @@ -17,6 +17,8 @@ export interface NuxtVitestOptions { startOnBoot?: boolean logToConsole?: boolean vitestConfig?: VitestConfig + /** @internal */ + _fromVitestConfig?: boolean } export default defineNuxtModule({ @@ -33,12 +35,15 @@ export default defineNuxtModule({ await runInstallWizard(nuxt) }, async setup(options, nuxt) { - if (nuxt.options.test || nuxt.options.dev) { + const { addTypeTemplate, addVitePlugin } = await loadKit(nuxt.options.rootDir) + + const resolver = createResolver(import.meta.url) + + // Setup for Vitest + if (options._fromVitestConfig) { await setupImportMocking(nuxt) - } - // inline runtime config the way dev builds do - if (nuxt.options.test && !nuxt.options.dev) { + // inline runtime config the way dev builds do nuxt.hook('app:templates', (app) => { const template = app.templates.find(t => t.filename === 'paths.mjs') if (!template?.getContents) { @@ -53,12 +58,7 @@ export default defineNuxtModule({ .replace(/const getAppConfig = \(\) => useRuntimeConfig\(\)\.app/, () => `const getAppConfig = () => (${inlineAppConfig})`) } }) - } - - const { addTypeTemplate, addVitePlugin } = await loadKit(nuxt.options.rootDir) - const resolver = createResolver(import.meta.url) - if (nuxt.options.test || nuxt.options.dev) { addVitePlugin(NuxtRootStubPlugin({ entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }), rootStubPath: await resolvePath(resolver.resolve('./runtime/nuxt-root')), diff --git a/src/module/mock.ts b/src/module/mock.ts index 47074b978..10f25f050 100644 --- a/src/module/mock.ts +++ b/src/module/mock.ts @@ -5,10 +5,6 @@ import { createMockPlugin } from './plugins/mock.ts' import type { MockPluginContext } from './plugins/mock.ts' import { loadKit } from '../utils.ts' -function isTestPluginFile(src: string) { - return (src.includes('.spec.') || src.includes('.test.')) -} - /** * This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`, * which make it possible to mock Nuxt imports. @@ -40,17 +36,20 @@ export async function setupImportMocking(nuxt: Nuxt) { } }) - // We want to run Nuxt plugins on test files - nuxt.options.ignore = nuxt.options.ignore.filter(i => i !== '**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}') - if (nuxt._ignore) { - for (const pattern of resolveIgnorePatterns('**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}')) { - nuxt._ignore.add(`!${pattern}`) - } - } - - // But do not register test files inside plugins/ as real Nuxt plugins - nuxt.hook('app:resolve', (app) => { - app.plugins = app.plugins.filter(plugin => !isTestPluginFile(plugin.src)) + const ignorePatterns: string[] = [] + nuxt.hook('vite:configResolved', (_, { isClient }) => { + if (!isClient) return + ignorePatterns.push(...resolveIgnorePatterns()) + }) + addVitePlugin({ + name: 'nuxt:vitest:un-ignore-for-testing', + configureVitest() { + // We want to run Nuxt plugins on test files, so un-ignore all patterns + for (const pattern of new Set(ignorePatterns)) { + if (pattern.startsWith('!')) continue + nuxt._ignore?.add(`!${pattern}`) + } + }, }) addVitePlugin(createMockPlugin(ctx).vite())
+ foo: {{ appConfig.foo }} +
+ {{ scans }} +