diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 54fe67e7544b..43317ae44e8e 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -150,9 +150,10 @@ export class VitestSpecifications { return [] } + const transformCache = new Map() const testGraphs = await Promise.all( specs.map(async (spec) => { - const deps = await this.getTestDependencies(spec) + const deps = await this.getTestDependencies(spec, transformCache) return [spec, deps] as const }), ) @@ -169,19 +170,31 @@ export class VitestSpecifications { return runningTests } - private async getTestDependencies(spec: TestSpecification, deps = new Set()): Promise> { + private async getTestDependencies( + spec: TestSpecification, + transformCache?: Map, + deps = new Set(), + ): Promise> { const addImports = async (project: TestProject, filepath: string) => { if (deps.has(filepath)) { return } deps.add(filepath) - const mod = project.vite.environments.ssr.moduleGraph.getModuleById(filepath) - const transformed = mod?.transformResult || await project.vite.environments.ssr.transformRequest(filepath) - if (!transformed) { - return + let dependencies: string[] + const cached = transformCache?.get(filepath) + if (cached) { + dependencies = cached + } + else { + const mod = project.vite.environments.ssr.moduleGraph.getModuleById(filepath) + const transformed = mod?.transformResult || await project.vite.environments.ssr.transformRequest(filepath) + if (!transformed) { + return + } + dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] + transformCache?.set(filepath, dependencies) } - const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] await Promise.all(dependencies.map(async (dep) => { const fsPath = dep.startsWith('/@fs/') ? dep.slice(isWindows ? 5 : 4) diff --git a/test/cli/fixtures/related-multi-project/src/shared.ts b/test/cli/fixtures/related-multi-project/src/shared.ts new file mode 100644 index 000000000000..41d988ae6d5f --- /dev/null +++ b/test/cli/fixtures/related-multi-project/src/shared.ts @@ -0,0 +1 @@ +export const value = 'shared' diff --git a/test/cli/fixtures/related-multi-project/src/unrelated.ts b/test/cli/fixtures/related-multi-project/src/unrelated.ts new file mode 100644 index 000000000000..a236f932c1e8 --- /dev/null +++ b/test/cli/fixtures/related-multi-project/src/unrelated.ts @@ -0,0 +1 @@ +export const other = 'unrelated' diff --git a/test/cli/fixtures/related-multi-project/tests/jsdom.test.ts b/test/cli/fixtures/related-multi-project/tests/jsdom.test.ts new file mode 100644 index 000000000000..4cfb0050ae82 --- /dev/null +++ b/test/cli/fixtures/related-multi-project/tests/jsdom.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest' +import { value } from '../src/shared' + +test('jsdom project uses shared module', () => { + expect(value).toBe('shared') +}) diff --git a/test/cli/fixtures/related-multi-project/tests/node.test.ts b/test/cli/fixtures/related-multi-project/tests/node.test.ts new file mode 100644 index 000000000000..df1614056789 --- /dev/null +++ b/test/cli/fixtures/related-multi-project/tests/node.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest' +import { value } from '../src/shared' + +test('node project uses shared module', () => { + expect(value).toBe('shared') +}) diff --git a/test/cli/fixtures/related-multi-project/tests/unrelated.test.ts b/test/cli/fixtures/related-multi-project/tests/unrelated.test.ts new file mode 100644 index 000000000000..1320418532e7 --- /dev/null +++ b/test/cli/fixtures/related-multi-project/tests/unrelated.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest' +import { other } from '../src/unrelated' + +test('unrelated test should not run', () => { + expect(other).toBe('unrelated') +}) diff --git a/test/cli/fixtures/related-multi-project/vitest.config.ts b/test/cli/fixtures/related-multi-project/vitest.config.ts new file mode 100644 index 000000000000..b3e6c5cc1fbb --- /dev/null +++ b/test/cli/fixtures/related-multi-project/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'project-jsdom', + environment: 'jsdom', + include: ['./tests/jsdom.test.ts'], + }, + }, + { + test: { + name: 'project-node', + environment: 'node', + include: ['./tests/node.test.ts', './tests/unrelated.test.ts'], + }, + }, + ], + }, +}) diff --git a/test/cli/test/related-multi-project.test.ts b/test/cli/test/related-multi-project.test.ts new file mode 100644 index 000000000000..22ac162349e4 --- /dev/null +++ b/test/cli/test/related-multi-project.test.ts @@ -0,0 +1,38 @@ +import { resolve } from 'pathe' +import { describe, expect, it } from 'vitest' +import { runVitest } from '../../test-utils' + +// Regression test for https://github.com/vitest-dev/vitest/issues/9855 +// Verifies that `vitest related` correctly resolves dependencies across +// multiple projects. The cache poisoning aspect of the bug (shared plugin +// caches corrupted by redundant transforms) requires @vitejs/plugin-vue +// with browser mode to reproduce and was verified against a real project. +describe('related with multiple projects', () => { + it('correctly finds related tests across multiple projects', async () => { + const root = resolve(import.meta.dirname, '../fixtures/related-multi-project') + + const { stdout, stderr } = await runVitest({ + root, + related: resolve(root, 'src/shared.ts'), + passWithNoTests: true, + }) + + expect(stderr).toBe('') + expect(stdout).toContain('project-jsdom') + expect(stdout).toContain('project-node') + expect(stdout).toContain('2 passed') + }) + + it('does not run unrelated tests', async () => { + const root = resolve(import.meta.dirname, '../fixtures/related-multi-project') + + const { stdout, stderr } = await runVitest({ + root, + related: resolve(root, 'src/shared.ts'), + passWithNoTests: true, + }) + + expect(stderr).toBe('') + expect(stdout).not.toContain('unrelated') + }) +})