From bfe13a20185243b6a83d99c6f2c13ea241afc8b9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 17 Oct 2026 20:52:30 +0000 Subject: [PATCH 1/4] fix(vitest-environment): resolve vitest from package location --- src/environments/vitest/index.ts | 5 +++-- test/unit/vitest-environment.spec.ts | 33 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 test/unit/vitest-environment.spec.ts diff --git a/src/environments/vitest/index.ts b/src/environments/vitest/index.ts index 6c08161b6..d5917fa95 100644 --- a/src/environments/vitest/index.ts +++ b/src/environments/vitest/index.ts @@ -1,4 +1,5 @@ import type { Environment } from 'vitest/runtime' +import { fileURLToPath } from 'node:url' import { resolveModulePath } from 'exsolve' import { indexedDB } from 'fake-indexeddb' import { joinURL } from 'ufo' @@ -15,7 +16,7 @@ const environmentMap = { jsdom, } -const vitestMajor = Number(getPackageInfo('vitest')?.version?.split('.')[0]) +const vitestMajor = Number(getPackageInfo('vitest', fileURLToPath(new URL('.', import.meta.url)))?.version?.split('.')[0]) export default { name: 'nuxt', @@ -78,7 +79,7 @@ export default { // This can be removed when dropping support for vitest 4.0.x (We can static import from 'vitest/runtime') async function importVitestEnvironments() { - const entry = vitestMajor >= 5 || resolveModulePath('vitest/runtime', { try: true }) ? 'runtime' : 'environments' + const entry = vitestMajor >= 5 || resolveModulePath('vitest/runtime', { from: import.meta.url, try: true }) ? 'runtime' : 'environments' return await import(/* @vite-ignore */ ['vitest', entry].join('/')) as typeof import('vitest/runtime') } diff --git a/test/unit/vitest-environment.spec.ts b/test/unit/vitest-environment.spec.ts new file mode 100644 index 000000000..67dfbde1d --- /dev/null +++ b/test/unit/vitest-environment.spec.ts @@ -0,0 +1,33 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('../../src/runtime/shared/environment.ts', () => ({ + setupWindow: async () => () => {}, +})) + +describe('nuxt vitest environment', () => { + const dirs: string[] = [] + + afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true }))) + }) + + it('should set up and tear down when vitest cannot be resolved from cwd', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'nuxt-test-utils-env-')) + dirs.push(cwd) + vi.spyOn(process, 'cwd').mockReturnValue(cwd) + + const { default: environment } = await import('../../src/environments/vitest/index.ts') + + const originalFetch = () => {} + const global: Record = { fetch: originalFetch } + const { teardown } = await environment.setup(global, {}) + expect(global.fetch).not.toBe(originalFetch) + + await teardown(global) + expect(global.fetch).toBe(originalFetch) + }) +}) From 5d11282abcb08234c9cf977e5ac5ad0df3c9c024 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 17 Oct 2026 20:52:30 +0000 Subject: [PATCH 2/4] fix(module): accept `vi` import from `vite-plus/test` --- src/module/plugins/mock.ts | 8 ++++++-- test/unit/mock-transform.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/module/plugins/mock.ts b/src/module/plugins/mock.ts index a6aa42f8a..7f5bc4dca 100644 --- a/src/module/plugins/mock.ts +++ b/src/module/plugins/mock.ts @@ -20,6 +20,8 @@ const HELPER_MOCK_HOIST = '__NUXT_VITEST_MOCKS' const HELPER_MOCK_HOIST_ORIGINAL = '__NUXT_VITEST_MOCKS_ORIGINAL' const HELPER_MOCK_HOIST_PREVIOUS = '__NUXT_VITEST_MOCKS_PREVIOUS' +const VITEST_SOURCES = new Set(['vitest', 'vite-plus/test']) + const HELPERS_NAME = [ HELPER_MOCK_IMPORT, HELPER_UNMOCK_IMPORT, @@ -58,6 +60,7 @@ export const createMockPlugin = (ctx: MockPluginContext) => createUnplugin(() => let insertionPoint = 0 let hasViImport = false + let vitestSource: string | undefined const s = new MagicString(code) const mocksImport: MockImportInfo[] = [] @@ -96,7 +99,8 @@ export const createMockPlugin = (ctx: MockPluginContext) => createUnplugin(() => // find existing vi import if (isImportDeclaration(node)) { - if (node.source.value === 'vitest' && !hasViImport) { + if (VITEST_SOURCES.has(node.source.value) && !hasViImport) { + vitestSource ??= node.source.value const viImport = node.specifiers.find( i => isImportSpecifier(i) && i.imported.type === 'Identifier' && i.imported.name === 'vi', @@ -280,7 +284,7 @@ export const createMockPlugin = (ctx: MockPluginContext) => createUnplugin(() => ``, ].join('\n')) - if (!hasViImport) s.prepend(`import {vi} from "vitest";\n`) + if (!hasViImport) s.prepend(`import {vi} from ${JSON.stringify(vitestSource ?? 'vitest')};\n`) s.appendLeft(insertionPoint, '\n' + mockLines.join('\n') + '\n') diff --git a/test/unit/mock-transform.spec.ts b/test/unit/mock-transform.spec.ts index 40f6166b2..ddd789846 100644 --- a/test/unit/mock-transform.spec.ts +++ b/test/unit/mock-transform.spec.ts @@ -154,6 +154,32 @@ describe('mocking', () => { }) }) + it('should not add `vi` import if it is imported from `vite-plus/test`', async () => { + pluginContext.imports = [{ + name: 'useSomeExport', + from: 'bob', + }] + const code = await getResult(` + import { expect, vi, it } from 'vite-plus/test' + mockNuxtImport('useSomeExport', () => 'bob') + `) + expect(code).not.toContain('import {vi} from') + expect(code).toMatch(/^\s*import \{ expect, vi, it \} from 'vite-plus\/test'\nvi\.hoisted\(/) + }) + + it('should add `vi` import from the vitest specifier already used in the file', async () => { + pluginContext.imports = [{ + name: 'useSomeExport', + from: 'bob', + }] + const code = await getResult(` + import { expect, it } from 'vite-plus/test' + mockNuxtImport('useSomeExport', () => 'bob') + `) + expect(code).toMatch(/^import \{vi\} from "vite-plus\/test";\n/) + expect(code).not.toContain('"vitest"') + }) + it('should transform code with unmock imports', async () => { pluginContext.imports = [{ name: 'useParent1', From a580338f5e550b9a09be647dd12016912c7b9200 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 17 Oct 2026 21:50:20 +0000 Subject: [PATCH 3/4] fix(module): add `vi` import when existing `vi` import is aliased --- src/module/plugins/mock.ts | 2 +- test/unit/mock-transform.spec.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/module/plugins/mock.ts b/src/module/plugins/mock.ts index 7f5bc4dca..3042b2262 100644 --- a/src/module/plugins/mock.ts +++ b/src/module/plugins/mock.ts @@ -103,7 +103,7 @@ export const createMockPlugin = (ctx: MockPluginContext) => createUnplugin(() => vitestSource ??= node.source.value const viImport = node.specifiers.find( i => - isImportSpecifier(i) && i.imported.type === 'Identifier' && i.imported.name === 'vi', + isImportSpecifier(i) && i.imported.type === 'Identifier' && i.imported.name === 'vi' && i.local.name === 'vi', ) if (viImport) { insertionPoint = node.end diff --git a/test/unit/mock-transform.spec.ts b/test/unit/mock-transform.spec.ts index ddd789846..870bdf6ef 100644 --- a/test/unit/mock-transform.spec.ts +++ b/test/unit/mock-transform.spec.ts @@ -180,6 +180,18 @@ describe('mocking', () => { expect(code).not.toContain('"vitest"') }) + it.each(['vitest', 'vite-plus/test'])('should add `vi` import if `vi` from `%s` is aliased', async (source) => { + pluginContext.imports = [{ + name: 'useSomeExport', + from: 'bob', + }] + const code = await getResult(` + import { vi as testVi } from '${source}' + mockNuxtImport('useSomeExport', () => 'bob') + `) + expect(code.startsWith(`import {vi} from ${JSON.stringify(source)};\n`)).toBe(true) + }) + it('should transform code with unmock imports', async () => { pluginContext.imports = [{ name: 'useParent1', From a850f739093a83c75243994a97f7f8a92dba2828 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 17 Oct 2026 23:56:50 +0000 Subject: [PATCH 4/4] test: load nuxt environment through vitest in regression test --- .../test/environment.spec.ts | 5 +++ .../vitest-environment/vitest.config.ts | 15 +++++++++ test/unit/vitest-environment.spec.ts | 31 +++++++++---------- 3 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/vitest-environment/test/environment.spec.ts create mode 100644 test/fixtures/vitest-environment/vitest.config.ts diff --git a/test/fixtures/vitest-environment/test/environment.spec.ts b/test/fixtures/vitest-environment/test/environment.spec.ts new file mode 100644 index 000000000..49148d968 --- /dev/null +++ b/test/fixtures/vitest-environment/test/environment.spec.ts @@ -0,0 +1,5 @@ +import { expect, it } from 'vitest' + +it('runs in the nuxt environment', () => { + expect(window.__NUXT_VITEST_ENVIRONMENT__).toBe(true) +}) diff --git a/test/fixtures/vitest-environment/vitest.config.ts b/test/fixtures/vitest-environment/vitest.config.ts new file mode 100644 index 000000000..407704ef8 --- /dev/null +++ b/test/fixtures/vitest-environment/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '#nuxt-test-utils/h3': 'h3', + }, + }, + test: { + environment: 'nuxt', + environmentOptions: { + nuxt: {}, + }, + }, +}) diff --git a/test/unit/vitest-environment.spec.ts b/test/unit/vitest-environment.spec.ts index 67dfbde1d..c4650ab6c 100644 --- a/test/unit/vitest-environment.spec.ts +++ b/test/unit/vitest-environment.spec.ts @@ -1,33 +1,30 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { fileURLToPath } from 'node:url' +import { x } from 'tinyexec' +import { afterAll, describe, expect, it } from 'vitest' -vi.mock('../../src/runtime/shared/environment.ts', () => ({ - setupWindow: async () => () => {}, -})) +const TEST_TIMEOUT = process.env.CI ? 60_000 : 30_000 + +const fixtureDir = fileURLToPath(new URL('../fixtures/vitest-environment', import.meta.url)) describe('nuxt vitest environment', () => { const dirs: string[] = [] - afterEach(async () => { - vi.restoreAllMocks() + afterAll(async () => { await Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true }))) }) - it('should set up and tear down when vitest cannot be resolved from cwd', async () => { + it('should run when vitest cannot be resolved from cwd', async () => { const cwd = await mkdtemp(join(tmpdir(), 'nuxt-test-utils-env-')) dirs.push(cwd) - vi.spyOn(process, 'cwd').mockReturnValue(cwd) - - const { default: environment } = await import('../../src/environments/vitest/index.ts') - const originalFetch = () => {} - const global: Record = { fetch: originalFetch } - const { teardown } = await environment.setup(global, {}) - expect(global.fetch).not.toBe(originalFetch) + const result = await x('vitest', ['run', '--root', fixtureDir, '--no-color'], { + nodeOptions: { cwd }, + }) - await teardown(global) - expect(global.fetch).toBe(originalFetch) - }) + expect.assert(result.exitCode === 0, result.stderr) + expect(result.stdout).toContain('1 passed') + }, TEST_TIMEOUT) })