From 80aa13dd6d9e42282fd65286622f53d28e34ec78 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 20 Oct 2026 14:37:53 +0200 Subject: [PATCH] fix: run tests that import a deleted file (#11669) --- packages/vitest/src/node/affected-modules.ts | 14 +++- test/e2e/test/related-deps.test.ts | 76 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/affected-modules.ts b/packages/vitest/src/node/affected-modules.ts index 25de3910e..2171a4488 100644 --- a/packages/vitest/src/node/affected-modules.ts +++ b/packages/vitest/src/node/affected-modules.ts @@ -1,4 +1,4 @@ -import type { DevEnvironment } from 'vite' +import type { DevEnvironment, EnvironmentModuleNode } from 'vite' import type { Vitest } from './core' import type { TestProject } from './project' import type { TestSpecification } from './test-specification' @@ -357,9 +357,13 @@ class ProjectGraph { const imports = new Set([...(transformed.deps || []), ...(transformed.dynamicDeps || [])]) const dependencies: string[] = [] const watchedFiles: string[] = [] + let failed = false // ids keep the query, so `./file.txt?raw` is walked as its own module this.environment.moduleGraph.getModuleById(id)?.importedModules.forEach((imported) => { - if (imported.id && imported.file && this.resolver.isLocalSourceFile(imported.file)) { + // a dynamic import of a missing file can be caught, a static one always fails + if (isMissingFile(imported) && transformed.deps?.includes(imported.url)) { + failed = true + } else if (imported.id && imported.file && this.resolver.isLocalSourceFile(imported.file)) { ;(imports.has(imported.url) ? dependencies : watchedFiles).push(imported.id) } }) @@ -375,10 +379,16 @@ class ProjectGraph { watchedFiles, mocked: new Set(Array.from(replaced, (mockedId) => cleanUrl(mockedId))), restored: new Set(Array.from(restored, (restoredId) => cleanUrl(restoredId))), + failed, } } } +// in SSR, Vite keeps an import it can't resolve as is, a bare one can still be loaded by Node +function isMissingFile(mod: EnvironmentModuleNode): boolean { + return mod.id?.[0] === '.' || mod.meta?.['vite:alias']?.noResolved === true +} + /** * Walks the module graph once for every root, skipping a module only when every walk that reaches it mocks it. */ diff --git a/test/e2e/test/related-deps.test.ts b/test/e2e/test/related-deps.test.ts index caa0155f9..898a051a9 100644 --- a/test/e2e/test/related-deps.test.ts +++ b/test/e2e/test/related-deps.test.ts @@ -1459,6 +1459,82 @@ describe('modules that fail to load', () => { `) }) + test('a test that imports a deleted file runs and reports the error', async () => { + const { results } = await runInlineTests( + { + 'vitest.config.js': { resolve: { alias: { '@': '/src' } } }, + 'src/other.js': 'export {}', + 'a.test.js': testFile('a', `import './src/helper.js'`), + 'b.test.js': testFile('b', `import '@/helper'`), + 'c.test.js': testFile('c', `import './src/other.js'`), + }, + { related: ['src/helper.js'] }, + ) + + expect(moduleStates(results)).toMatchInlineSnapshot(` + [ + [ + "a.test.js", + "failed", + ], + [ + "b.test.js", + "failed", + ], + ] + `) + }) + + test('a test that catches a dynamic import of a deleted file does not run', async () => { + const { results } = await runInlineTests( + { + 'src/changed.js': 'export {}', + 'a.test.js': ` + import { test } from 'vitest' + test('a', async () => { + await import('./src/helper.js').catch(() => {}) + }) + `, + 'b.test.js': testFile('b', `import './src/changed.js'`), + }, + { related: ['src/helper.js', 'src/changed.js'] }, + ) + + expect(moduleStates(results)).toMatchInlineSnapshot(` + [ + [ + "b.test.js", + "passed", + ], + ] + `) + }) + + test('a setup file that imports a deleted file runs every test', async () => { + const { results } = await runInlineTests( + { + 'vitest.config.js': { test: { setupFiles: ['./setup.js'] } }, + 'setup.js': `import './src/helper.js'`, + 'a.test.js': testFile('a'), + 'b.test.js': testFile('b'), + }, + { related: ['src/helper.js'] }, + ) + + expect(moduleStates(results)).toMatchInlineSnapshot(` + [ + [ + "a.test.js", + "failed", + ], + [ + "b.test.js", + "failed", + ], + ] + `) + }) + function moduleStates(modules: TestModule[]) { return modules .map((testModule) => [testModule.relativeModuleId, testModule.state()]) -- 2.54.0