From 4ef000a938ca3e54eee1c8dd02e2eb9b1afff0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Wed, 7 Oct 2026 08:10:42 +0300 Subject: [PATCH] fix(coverage): `allowExternal` fails to include uncovered files (#11513) --- packages/vitest/src/node/coverage.ts | 21 +++++++++++++------ .../coverage-test/test/allow-external.test.ts | 9 +++++++- .../test-utils/fixtures/external-uncovered.ts | 3 +++ 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 test/test-utils/fixtures/external-uncovered.ts diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index 8a57bbd17..052e7c613 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -170,17 +170,20 @@ export class BaseCoverageProvider { return false } - const relativeFilename = matchingRoot ? relative(matchingRoot, filename) : filename + // Files outside roots can be matched by absolute or "../" patterns + const candidates = matchingRoot + ? [relative(matchingRoot, filename)] + : [filename, ...roots.map((root) => relative(root, filename))] const { matchExclude, matchInclude } = this.getGlobMatchers() - if (matchExclude(relativeFilename)) { + if (candidates.some(matchExclude)) { this.globCache.set(filename, false) return false } // By default `coverage.include` matches all files, except "coverage.exclude" - let included = matchInclude(relativeFilename) + let included = candidates.some(matchInclude) if (included && this.changedFiles) { included = this.changedFiles.includes(filename) @@ -779,10 +782,11 @@ export class BaseCoverageProvider { } createUncoveredFileTransformer(ctx: Vitest) { + const rootProject = ctx.getRootProject() const projects = new Set([ ...ctx.projects, // Check core last as it will match all files anyway - ctx.getRootProject(), + rootProject, ]) return async (filename: string): Promise => { @@ -791,8 +795,13 @@ export class BaseCoverageProvider { for (const project of projects) { const root = project.config.root - // On Windows root doesn't start with "/" while filenames do - if (!filename.startsWith(root) && !filename.startsWith(`/${root}`)) { + if ( + // On Windows root doesn't start with "/" while filenames do + !filename.startsWith(root) && + !filename.startsWith(`/${root}`) && + // If it's file outside project (allowExternal), transform it with the root project + project !== rootProject + ) { continue } diff --git a/test/coverage-test/test/allow-external.test.ts b/test/coverage-test/test/allow-external.test.ts index 1307d72a9..e4108653a 100644 --- a/test/coverage-test/test/allow-external.test.ts +++ b/test/coverage-test/test/allow-external.test.ts @@ -7,7 +7,11 @@ test('{ allowExternal: true } includes files outside project root', async () => coverage: { allowExternal: true, reporter: 'json', - include: ['**/fixtures/src/math.ts', '**/fixtures/external-math.ts'], + include: [ + '**/fixtures/src/math.ts', + '**/fixtures/external-math.ts', + '../test-utils/fixtures/**.ts', + ], }, }) const coverageMap = await readCoverageMap() @@ -16,6 +20,9 @@ test('{ allowExternal: true } includes files outside project root', async () => // File outside project root expect(files).toContain('/test/test-utils/fixtures/external-math.ts') + // Uncovered file outside project root + expect(files).toContain('/test/test-utils/fixtures/external-uncovered.ts') + // Files inside project root should always be included expect(files).toContain('/fixtures/src/math.ts') }) diff --git a/test/test-utils/fixtures/external-uncovered.ts b/test/test-utils/fixtures/external-uncovered.ts new file mode 100644 index 000000000..52568ba2e --- /dev/null +++ b/test/test-utils/fixtures/external-uncovered.ts @@ -0,0 +1,3 @@ +export function subtract(a: number, b: number) { + return a - b +} -- 2.51.2