From 3602fd9a014502ac3d047d6262863205af393d30 Mon Sep 17 00:00:00 2001 From: yamachi4416 Date: Wed, 14 Oct 2026 23:05:45 +0900 Subject: [PATCH 1/5] fix(runtime): test failure when module cached --- src/module/plugins/entry.ts | 4 ++-- test/fixtures/isolate-setup/vitest.config.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/module/plugins/entry.ts b/src/module/plugins/entry.ts index 50e5f7493..4f5ccc39b 100644 --- a/src/module/plugins/entry.ts +++ b/src/module/plugins/entry.ts @@ -24,8 +24,8 @@ export const NuxtRootStubPlugin = (options: NuxtRootStubPluginOptions): Plugin = filter: { id: idFilter, }, - async handler(id, importer) { - return importer?.endsWith('index.html') ? id : entryPath + async handler() { + return entryPath }, }, load: { diff --git a/test/fixtures/isolate-setup/vitest.config.ts b/test/fixtures/isolate-setup/vitest.config.ts index 8c757a0e5..0bd9ceafb 100644 --- a/test/fixtures/isolate-setup/vitest.config.ts +++ b/test/fixtures/isolate-setup/vitest.config.ts @@ -22,6 +22,7 @@ export default defineVitestConfig({ instances: [{ browser: 'chromium' }], }, ui: false, + fsModuleCache: true, onConsoleLog(log) { if (log.includes(' is an experimental feature')) { return false From 91a77dc99d748857202481bd321988932fbb8ea3 Mon Sep 17 00:00:00 2001 From: yamachi4416 Date: Wed, 14 Oct 2026 23:58:20 +0900 Subject: [PATCH 2/5] fix fsModuleCache in no-isolate test --- src/module/plugins/entry.ts | 2 +- test/fixtures/isolate-setup/global.setup.ts | 11 +++++++++++ test/fixtures/isolate-setup/vitest.config.ts | 9 ++++----- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/isolate-setup/global.setup.ts diff --git a/src/module/plugins/entry.ts b/src/module/plugins/entry.ts index 4f5ccc39b..91aa36da8 100644 --- a/src/module/plugins/entry.ts +++ b/src/module/plugins/entry.ts @@ -24,7 +24,7 @@ export const NuxtRootStubPlugin = (options: NuxtRootStubPluginOptions): Plugin = filter: { id: idFilter, }, - async handler() { + handler() { return entryPath }, }, diff --git a/test/fixtures/isolate-setup/global.setup.ts b/test/fixtures/isolate-setup/global.setup.ts new file mode 100644 index 000000000..9cfb5a242 --- /dev/null +++ b/test/fixtures/isolate-setup/global.setup.ts @@ -0,0 +1,11 @@ +import type { TestProject } from 'vitest/node' + +export default function setup(project: TestProject) { + project.provide('testPattern', process.env.ERROR_TEST_PATTERN) +} + +declare module 'vitest' { + export interface ProvidedContext { + testPattern: string | undefined + } +} diff --git a/test/fixtures/isolate-setup/vitest.config.ts b/test/fixtures/isolate-setup/vitest.config.ts index 0bd9ceafb..0530d52b5 100644 --- a/test/fixtures/isolate-setup/vitest.config.ts +++ b/test/fixtures/isolate-setup/vitest.config.ts @@ -23,6 +23,7 @@ export default defineVitestConfig({ }, ui: false, fsModuleCache: true, + globalSetup: './global.setup.ts', onConsoleLog(log) { if (log.includes(' is an experimental feature')) { return false @@ -34,9 +35,7 @@ export default defineVitestConfig({ enforce: 'pre', transform(code, id) { if (id !== setupNuxtPath) return - const failOnSetupBefore = process.env.ERROR_TEST_PATTERN === 'before' - const failOnSetupAfter = process.env.ERROR_TEST_PATTERN === 'after' - return code + return 'import { inject } from "vitest";\n' + code .replace(/export async function setupNuxt/, 'async function _setupNuxt') .concat(` export async function setupNuxt(...args) { @@ -47,9 +46,9 @@ export async function setupNuxt(...args) { console.log(\`### setupNuxt called ### \${window.__setup_calls__}:\${workerId}\`) if (window.__setup_calls__ === 1) { - if (${failOnSetupBefore}) { + if (inject('testPattern') === 'before') { throw new Error('#### setupNuxt failed before ###') - } else if (${failOnSetupAfter}) { + } else if (inject('testPattern') === 'after') { await _setupNuxt(...args) throw new Error('#### setupNuxt failed after ###') } From 72b9768a8c72812f56a66605702a22bf3f1f8351 Mon Sep 17 00:00:00 2001 From: yamachi4416 Date: Thu, 15 Oct 2026 08:33:33 +0900 Subject: [PATCH 3/5] restore html check and verify bare stub id --- src/module/plugins/entry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/module/plugins/entry.ts b/src/module/plugins/entry.ts index 91aa36da8..848a834cf 100644 --- a/src/module/plugins/entry.ts +++ b/src/module/plugins/entry.ts @@ -24,8 +24,9 @@ export const NuxtRootStubPlugin = (options: NuxtRootStubPluginOptions): Plugin = filter: { id: idFilter, }, - handler() { - return entryPath + handler(id, importer) { + const isBareStub = id === STUB_ID || id === STUB_ID + extension + return isBareStub && importer?.endsWith('index.html') ? id : entryPath }, }, load: { From 25b5eeb7e791a715fd0533a78171720279ca826d Mon Sep 17 00:00:00 2001 From: yamachi4416 Date: Thu, 15 Oct 2026 18:12:47 +0900 Subject: [PATCH 4/5] fix resolveId in browser mode --- src/config.ts | 11 ++++++++--- src/module.ts | 5 ++++- src/module/plugins/entry.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index dec9449ee..d9ddc93b5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,9 +58,6 @@ 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 @@ -144,6 +141,14 @@ export async function getVitestConfigFromNuxt( overrides: { test: true, ..._overrides, + ...{ + testUtils: { + ...(_overrides as { testUtils?: object })?.testUtils, + _fromVitestConfig: { + isBrowserEnabled: testConfig.browser?.enabled, + }, + }, + } as NuxtConfig, }, }) } diff --git a/src/module.ts b/src/module.ts index f1160a7b8..b6c4b0ab3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -18,7 +18,9 @@ export interface NuxtVitestOptions { logToConsole?: boolean vitestConfig?: VitestConfig /** @internal */ - _fromVitestConfig?: boolean + _fromVitestConfig?: { + isBrowserEnabled?: boolean + } } export default defineNuxtModule({ @@ -62,6 +64,7 @@ export default defineNuxtModule({ addVitePlugin(NuxtRootStubPlugin({ entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }), rootStubPath: await resolvePath(resolver.resolve('./runtime/nuxt-root')), + isBrowserEnabled: options._fromVitestConfig.isBrowserEnabled ?? false, })) } diff --git a/src/module/plugins/entry.ts b/src/module/plugins/entry.ts index 848a834cf..f702cf0fd 100644 --- a/src/module/plugins/entry.ts +++ b/src/module/plugins/entry.ts @@ -7,6 +7,7 @@ const PLUGIN_NAME = 'nuxt:vitest:nuxt-root-stub' interface NuxtRootStubPluginOptions { entry: string rootStubPath: string + isBrowserEnabled: boolean } const STUB_ID = 'nuxt-vitest-app-entry' @@ -25,15 +26,17 @@ export const NuxtRootStubPlugin = (options: NuxtRootStubPluginOptions): Plugin = id: idFilter, }, handler(id, importer) { - const isBareStub = id === STUB_ID || id === STUB_ID + extension - return isBareStub && importer?.endsWith('index.html') ? id : entryPath + if (options.isBrowserEnabled) { + return importer?.endsWith('index.html') ? id : entryPath + } + return entryPath }, }, load: { filter: { id: idFilter, }, - async handler() { + handler() { const entryContents = readFileSync(options.entry, 'utf-8') return entryContents.replace('#build/root-component.mjs', options.rootStubPath) }, From ef6d7ab935d103c5fd46895f36da22335c9c1da2 Mon Sep 17 00:00:00 2001 From: yamachi4416 Date: Fri, 16 Oct 2026 20:02:39 +0900 Subject: [PATCH 5/5] remove browser mode check, respect user `noDiscovery` --- src/config.ts | 46 ++++++++++++++++--------------------- src/module.ts | 5 +--- src/module/plugins/entry.ts | 6 +---- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/config.ts b/src/config.ts index d9ddc93b5..de0ed9df2 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 @@ -130,8 +133,9 @@ const excludedPlugins = [ export async function getVitestConfigFromNuxt( options?: GetVitestConfigOptions, loadNuxtOptions: LoadNuxtOptions = {}, - testConfig: VitestConfig = {}, + userConfig: ViteUserConfig & { test?: VitestConfig } | UserWorkspaceConfig = {}, ): Promise { + const { test: testConfig = {} } = userConfig const { rootDir = process.cwd(), ..._overrides } = loadNuxtOptions.overrides || {} if (!options) { @@ -141,14 +145,6 @@ export async function getVitestConfigFromNuxt( overrides: { test: true, ..._overrides, - ...{ - testUtils: { - ...(_overrides as { testUtils?: object })?.testUtils, - _fromVitestConfig: { - isBrowserEnabled: testConfig.browser?.enabled, - }, - }, - } as NuxtConfig, }, }) } @@ -178,7 +174,20 @@ export async function getVitestConfigFromNuxt( }, }, optimizeDeps: { - noDiscovery: true, + ...vitestMajor >= 5 && testConfig.browser?.enabled + ? { + // Browser mode in vitest 5 reuses the project's Vite server: dependency + // discovery must stay on so vitest's CJS internals are prebundled, and deps + // imported through nuxt virtual modules are listed upfront because they + // can't be found by vite automatically which reloads the page. + // We leave `noDiscovery` unchanged to respect vitest defaults and user config. + include: ['@testing-library/vue', '#nuxt-test-utils/h3'], + // aliased to mocks, excluded from pre-bundling to avoid runtime discovery + exclude: ['@vue/devtools-kit', '@vue/devtools-core'], + } + : { + noDiscovery: true, + }, }, test: { environmentOptions: { @@ -442,27 +451,12 @@ async function resolveConfig }, + }, config) satisfies ViteUserConfig & { test: NonNullable }, ) as T & { test: NonNullable } resolvedConfig.plugins!.push(NuxtVitestEnvironmentOptionsPlugin(resolvedConfig.test.environmentOptions)) if (resolvedConfig.test.browser?.enabled) { - // browser mode in vitest 5 reuses the project's Vite server: dependency - // discovery must stay on so vitest's CJS internals are prebundled, and deps - // imported through nuxt virtual modules are listed upfront because they - // can't be found by vite automatically which reloads the page - if (vitestMajor >= 5) { - delete resolvedConfig.optimizeDeps?.noDiscovery - resolvedConfig.optimizeDeps ??= {} - resolvedConfig.optimizeDeps.include ??= [] - resolvedConfig.optimizeDeps.include.push('@testing-library/vue', '#nuxt-test-utils/h3') - - // aliased to mocks, excluded from pre-bundling to avoid runtime discovery - resolvedConfig.optimizeDeps.exclude ??= [] - resolvedConfig.optimizeDeps.exclude.push('@vue/devtools-kit', '@vue/devtools-core') - } - if (resolvedConfig.test.environment === 'nuxt') { resolvedConfig.test.setupFiles = Array.isArray(resolvedConfig.test.setupFiles) ? resolvedConfig.test.setupFiles diff --git a/src/module.ts b/src/module.ts index b6c4b0ab3..f1160a7b8 100644 --- a/src/module.ts +++ b/src/module.ts @@ -18,9 +18,7 @@ export interface NuxtVitestOptions { logToConsole?: boolean vitestConfig?: VitestConfig /** @internal */ - _fromVitestConfig?: { - isBrowserEnabled?: boolean - } + _fromVitestConfig?: boolean } export default defineNuxtModule({ @@ -64,7 +62,6 @@ export default defineNuxtModule({ addVitePlugin(NuxtRootStubPlugin({ entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }), rootStubPath: await resolvePath(resolver.resolve('./runtime/nuxt-root')), - isBrowserEnabled: options._fromVitestConfig.isBrowserEnabled ?? false, })) } diff --git a/src/module/plugins/entry.ts b/src/module/plugins/entry.ts index f702cf0fd..9187cdc01 100644 --- a/src/module/plugins/entry.ts +++ b/src/module/plugins/entry.ts @@ -7,7 +7,6 @@ const PLUGIN_NAME = 'nuxt:vitest:nuxt-root-stub' interface NuxtRootStubPluginOptions { entry: string rootStubPath: string - isBrowserEnabled: boolean } const STUB_ID = 'nuxt-vitest-app-entry' @@ -25,10 +24,7 @@ export const NuxtRootStubPlugin = (options: NuxtRootStubPluginOptions): Plugin = filter: { id: idFilter, }, - handler(id, importer) { - if (options.isBrowserEnabled) { - return importer?.endsWith('index.html') ? id : entryPath - } + handler() { return entryPath }, },