Skip to content

Commit 6e395fc

Browse files
committed
fix(@angular-devkit/build-angular): ensure vitest code coverage handles virtual files correctly
Vitest's default coverage provider checks for the physical existence of files to determine inclusion in the coverage report. This behavior excludes in-memory files generated by the build process from the final report. This change patches the `isIncluded` method of Vitest's `BaseCoverageProvider` to correctly account for these virtual files, ensuring they are included in the coverage analysis. Additionally, bundler-generated helper code chunks without any original source code can have empty sourcemaps. Vitest includes files with such sourcemaps in the coverage report, which is incorrect. This change adds a virtual source to these sourcemaps to prevent them from being included in the final coverage output. (cherry picked from commit 2c846fb)
1 parent 542d528 commit 6e395fc

File tree

3 files changed

+45
-4
lines changed

3 files changed

+45
-4
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type { BuilderOutput } from '@angular-devkit/architect';
1010
import assert from 'node:assert';
1111
import path from 'node:path';
12+
import { isMatch } from 'picomatch';
1213
import type { InlineConfig, Vitest } from 'vitest/node';
1314
import { assertIsError } from '../../../../utils/error';
1415
import { toPosixPath } from '../../../../utils/path';
@@ -141,7 +142,9 @@ export class VitestExecutor implements TestExecutor {
141142
} = this.options;
142143

143144
let vitestNodeModule;
145+
let vitestCoverageModule;
144146
try {
147+
vitestCoverageModule = await import('vitest/coverage');
145148
vitestNodeModule = await import('vitest/node');
146149
} catch (error: unknown) {
147150
assertIsError(error);
@@ -154,6 +157,21 @@ export class VitestExecutor implements TestExecutor {
154157
}
155158
const { startVitest } = vitestNodeModule;
156159

160+
// Augment BaseCoverageProvider to include logic to support the built virtual files.
161+
// Temporary workaround to avoid the direct filesystem checks in the base provider that
162+
// were introduced in v4. Also ensures that all built virtual files are available.
163+
const builtVirtualFiles = this.buildResultFiles;
164+
vitestCoverageModule.BaseCoverageProvider.prototype.isIncluded = function (filename) {
165+
const relativeFilename = path.relative(workspaceRoot, filename);
166+
if (!this.options.include || builtVirtualFiles.has(relativeFilename)) {
167+
return !isMatch(relativeFilename, this.options.exclude);
168+
} else {
169+
return isMatch(relativeFilename, this.options.include, {
170+
ignore: this.options.exclude,
171+
});
172+
}
173+
};
174+
157175
// Setup vitest browser options if configured
158176
const browserOptions = await setupBrowserConfiguration(
159177
browsers,

packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,26 @@ export function createVitestPlugins(
117117
outputFile.origin === 'memory'
118118
? Buffer.from(outputFile.contents).toString('utf-8')
119119
: await readFile(outputFile.inputPath, 'utf-8');
120-
const map = sourceMapFile
120+
const sourceMapText = sourceMapFile
121121
? sourceMapFile.origin === 'memory'
122122
? Buffer.from(sourceMapFile.contents).toString('utf-8')
123123
: await readFile(sourceMapFile.inputPath, 'utf-8')
124124
: undefined;
125125

126+
// Vitest will include files in the coverage report if the sourcemap contains no sources.
127+
// For builder-internal generated code chunks, which are typically helper functions,
128+
// a virtual source is added to the sourcemap to prevent them from being incorrectly
129+
// included in the final coverage report.
130+
const map = sourceMapText ? JSON.parse(sourceMapText) : undefined;
131+
if (map) {
132+
if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) {
133+
map.sources = ['virtual:builder'];
134+
}
135+
}
136+
126137
return {
127138
code,
128-
map: map ? JSON.parse(map) : undefined,
139+
map,
129140
};
130141
}
131142
},

packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
2828

2929
const { result } = await harness.executeOnce();
3030
expect(result?.success).toBeTrue();
31-
expect(harness.hasFile('coverage/test/index.html')).toBeFalse();
31+
harness.expectFile('coverage/test/index.html').toNotExist();
3232
});
3333

3434
it('should generate a code coverage report when coverage is true', async () => {
@@ -39,7 +39,19 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
3939

4040
const { result } = await harness.executeOnce();
4141
expect(result?.success).toBeTrue();
42-
expect(harness.hasFile('coverage/test/index.html')).toBeTrue();
42+
harness.expectFile('coverage/test/index.html').toExist();
43+
});
44+
45+
it('should generate a code coverage report when coverage is true', async () => {
46+
harness.useTarget('test', {
47+
...BASE_OPTIONS,
48+
coverage: true,
49+
coverageReporters: ['json'] as any,
50+
});
51+
52+
const { result } = await harness.executeOnce();
53+
expect(result?.success).toBeTrue();
54+
harness.expectFile('coverage/test/coverage-final.json').content.toContain('app.component.ts');
4355
});
4456
});
4557
});

0 commit comments

Comments
 (0)