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/src/module/plugins/mock.ts b/src/module/plugins/mock.ts index a6aa42f8a..3042b2262 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,10 +99,11 @@ 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', + isImportSpecifier(i) && i.imported.type === 'Identifier' && i.imported.name === 'vi' && i.local.name === 'vi', ) if (viImport) { insertionPoint = node.end @@ -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/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/mock-transform.spec.ts b/test/unit/mock-transform.spec.ts index 40f6166b2..870bdf6ef 100644 --- a/test/unit/mock-transform.spec.ts +++ b/test/unit/mock-transform.spec.ts @@ -154,6 +154,44 @@ 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.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', diff --git a/test/unit/vitest-environment.spec.ts b/test/unit/vitest-environment.spec.ts new file mode 100644 index 000000000..c4650ab6c --- /dev/null +++ b/test/unit/vitest-environment.spec.ts @@ -0,0 +1,30 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { x } from 'tinyexec' +import { afterAll, describe, expect, it } from 'vitest' + +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[] = [] + + afterAll(async () => { + await Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true }))) + }) + + it('should run when vitest cannot be resolved from cwd', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'nuxt-test-utils-env-')) + dirs.push(cwd) + + const result = await x('vitest', ['run', '--root', fixtureDir, '--no-color'], { + nodeOptions: { cwd }, + }) + + expect.assert(result.exitCode === 0, result.stderr) + expect(result.stdout).toContain('1 passed') + }, TEST_TIMEOUT) +})