Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cd5c92b

Browse files
committed
fix(@angular/build): correct Vitest coverage reporting for test files
Test coverage reporting within the Vitest runner was inaccurate because the test files themselves were being included in the coverage analysis. This was caused by how the in-memory provider directly loaded the test content. This commit resolves the issue by introducing an intermediate virtual module for each test. The test entry point now only imports this module, which in turn contains the bundled test code. This extra layer of indirection prevents Vitest from including the test files in the coverage results, leading to an accurate analysis. To support this change, the `getTestEntrypoints` function now removes the `.spec` or `.test` extension from bundle names, ensuring cleaner module identifiers. The Vitest runner has also been updated to better handle coverage exclusion options and resolve relative paths more reliably.
1 parent 26127bd commit cd5c92b

File tree

7 files changed

+64
-21
lines changed

7 files changed

+64
-21
lines changed

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ jasmine_test(
322322
name = "unit-test_integration_tests",
323323
size = "medium",
324324
data = [":unit-test_integration_test_lib"],
325+
flaky = True,
325326
shard_count = 5,
326327
)
327328

packages/angular/build/src/builders/karma/find-tests.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ export async function findTests(
3030
interface TestEntrypointsOptions {
3131
projectSourceRoot: string;
3232
workspaceRoot: string;
33+
removeTestExtension?: boolean;
3334
}
3435

3536
/** Generate unique bundle names for a set of test files. */
3637
export function getTestEntrypoints(
3738
testFiles: string[],
38-
{ projectSourceRoot, workspaceRoot }: TestEntrypointsOptions,
39+
{ projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions,
3940
): Map<string, string> {
4041
const seen = new Set<string>();
4142

@@ -46,7 +47,13 @@ export function getTestEntrypoints(
4647
.replace(/^[./\\]+/, '')
4748
// Replace any path separators with dashes.
4849
.replace(/[/\\]/g, '-');
49-
const baseName = `spec-${basename(relativePath, extname(relativePath))}`;
50+
51+
let fileName = basename(relativePath, extname(relativePath));
52+
if (removeTestExtension) {
53+
fileName = fileName.replace(/\.(spec|test)$/, '');
54+
}
55+
56+
const baseName = `spec-${fileName}`;
5057
let uniqueName = baseName;
5158
let suffix = 2;
5259
while (seen.has(uniqueName)) {

packages/angular/build/src/builders/karma/find-tests_spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,36 @@ describe('getTestEntrypoints', () => {
6262
]),
6363
);
6464
});
65+
66+
describe('with removeTestExtension enabled', () => {
67+
function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) {
68+
return getTestEntrypoints(
69+
[
70+
...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)),
71+
...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)),
72+
],
73+
{ ...options, removeTestExtension: true },
74+
);
75+
}
76+
77+
it('removes .spec extension', () => {
78+
expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual(
79+
new Map<string, string>([
80+
['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')],
81+
['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')],
82+
]),
83+
);
84+
});
85+
86+
it('removes .test extension', () => {
87+
expect(getEntrypoints(['a/b.test.js'], ['c/d.test.js'])).toEqual(
88+
new Map<string, string>([
89+
['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.test.js')],
90+
['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.test.js')],
91+
]),
92+
);
93+
});
94+
});
6595
});
6696
}
6797
});

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ export async function getVitestBuildOptions(
8282
);
8383
}
8484

85-
const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot });
85+
const entryPoints = getTestEntrypoints(testFiles, {
86+
projectSourceRoot,
87+
workspaceRoot,
88+
removeTestExtension: true,
89+
});
8690
entryPoints.set('init-testbed', 'angular:test-bed-init');
8791

8892
const buildOptions: Partial<ApplicationBuilderInternalOptions> = {

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export class VitestExecutor implements TestExecutor {
6868
if (this.testFileToEntryPoint.size === 0) {
6969
const { include, exclude = [], workspaceRoot, projectSourceRoot } = this.options;
7070
const testFiles = await findTests(include, exclude, workspaceRoot, projectSourceRoot);
71-
const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot });
71+
const entryPoints = getTestEntrypoints(testFiles, {
72+
projectSourceRoot,
73+
workspaceRoot,
74+
removeTestExtension: true,
75+
});
7276
for (const [entryPoint, testFile] of entryPoints) {
7377
this.testFileToEntryPoint.set(testFile, entryPoint);
7478
this.entryPointToTestFile.set(entryPoint + '.js', testFile);
@@ -90,6 +94,7 @@ export class VitestExecutor implements TestExecutor {
9094
if (source) {
9195
modifiedSourceFiles.add(source);
9296
}
97+
vitest.invalidateFile(toPosixPath(path.join(this.options.workspaceRoot, modifiedFile)));
9398
}
9499

95100
const specsToRerun = [];
@@ -162,16 +167,15 @@ export class VitestExecutor implements TestExecutor {
162167
name: 'angular:test-in-memory-provider',
163168
enforce: 'pre',
164169
resolveId: (id, importer) => {
165-
if (importer && id.startsWith('.')) {
170+
if (importer && (id[0] === '.' || id[0] === '/')) {
166171
let fullPath;
167-
let relativePath;
168172
if (this.testFileToEntryPoint.has(importer)) {
169173
fullPath = toPosixPath(path.join(this.options.workspaceRoot, id));
170-
relativePath = path.normalize(id);
171174
} else {
172175
fullPath = toPosixPath(path.join(path.dirname(importer), id));
173-
relativePath = path.relative(this.options.workspaceRoot, fullPath);
174176
}
177+
178+
const relativePath = path.relative(this.options.workspaceRoot, fullPath);
175179
if (this.buildResultFiles.has(toPosixPath(relativePath))) {
176180
return fullPath;
177181
}
@@ -201,6 +205,12 @@ export class VitestExecutor implements TestExecutor {
201205
let outputPath;
202206
if (entryPoint) {
203207
outputPath = entryPoint + '.js';
208+
209+
// To support coverage exclusion of the actual test file, the virtual
210+
// test entry point only references the built and bundled intermediate file.
211+
return {
212+
code: `import "./${outputPath}";`,
213+
};
204214
} else {
205215
// Attempt to load as a built artifact.
206216
const relativePath = path.relative(this.options.workspaceRoot, id);
@@ -247,15 +257,6 @@ export class VitestExecutor implements TestExecutor {
247257
},
248258
],
249259
});
250-
251-
// Adjust coverage excludes to not include the otherwise automatically inserted included unit tests.
252-
// Vite does this as a convenience but is problematic for the bundling strategy employed by the
253-
// builder's test setup. To workaround this, the excludes are adjusted here to only automatically
254-
// exclude the TypeScript source test files.
255-
project.config.coverage.exclude = [
256-
...(codeCoverage?.exclude ?? []),
257-
'**/*.{test,spec}.?(c|m)ts',
258-
];
259260
},
260261
},
261262
];
@@ -343,7 +344,8 @@ function generateCoverageOption(
343344
return {
344345
enabled: true,
345346
excludeAfterRemap: true,
346-
// Special handling for `reporter` due to an undefined value causing upstream failures
347+
// Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
348+
...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}),
347349
...(codeCoverage.reporters
348350
? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption)
349351
: {}),

packages/angular/build/src/builders/unit-test/schema.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@
6060
"description": "Globs to exclude from code coverage.",
6161
"items": {
6262
"type": "string"
63-
},
64-
"default": []
63+
}
6564
},
6665
"codeCoverageReporters": {
6766
"type": "array",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '../setup';
1616

1717
describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
18-
xdescribe('Option: "watch"', () => {
18+
describe('Option: "watch"', () => {
1919
beforeEach(async () => {
2020
setupApplicationTarget(harness);
2121
});

0 commit comments

Comments
 (0)