From a37e4b544ffc48d132764b4ffd1f85af9f26bf89 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:05:58 -0500 Subject: [PATCH] fix(@angular/build): correct Vitest coverage include handling for virtual files Addresses an issue where user-defined `coverageInclude` patterns in Vitest could incorrectly exclude bundled virtual files during the pre-sourcemap coverage check, leading to incomplete or empty coverage reports. This change modifies the `generateCoverageOption` function to augment user-provided `coverageInclude` arrays with specific globs (`spec-*.js`, `chunk-*.js`) that match the internal bundled test entry points and code chunks. This ensures that all necessary files are considered during Vitest's initial coverage pass, while still respecting the user's original `include` patterns for the post-sourcemap remapping. --- .../unit-test/runners/vitest/plugins.ts | 5 +- .../options/code-coverage-include_spec.ts | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/options/code-coverage-include_spec.ts diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index 538b891e1473..96d3337e0149 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -285,7 +285,10 @@ async function generateCoverageOption( return { enabled: coverage.enabled, excludeAfterRemap: true, - include: coverage.include, + // Vitest performs a pre-check and a post-check for sourcemaps. + // The pre-check uses the bundled files, so specific bundled entry points and chunks need to be included. + // The post-check uses the original source files, so the user's include is used. + ...(coverage.include ? { include: ['spec-*.js', 'chunk-*.js', ...coverage.include] } : {}), reportsDirectory: toPosixPath(path.join('coverage', projectName)), thresholds: coverage.thresholds, watermarks: coverage.watermarks, diff --git a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-include_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-include_spec.ts new file mode 100644 index 000000000000..6cdbc8bfa23b --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-include_spec.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "coverageInclude"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + await harness.writeFiles({ + 'src/app/included.ts': `export const a = 1;`, + 'src/app/included.spec.ts': ` + import { a } from './included'; + describe('included', () => { + it('should work', () => { + expect(a).toBe(1); + }); + }); + `, + 'src/app/excluded.ts': `export const b = 2;`, + }); + }); + + it('should only include and report coverage for files that match the glob pattern', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + coverageInclude: ['**/included.ts'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const summary = JSON.parse(harness.readFile('coverage/test/coverage-final.json')); + const summaryKeys = Object.keys(summary); + + const includedKey = summaryKeys.find((key) => key.endsWith('src/app/included.ts')); + const excludedKey = summaryKeys.find((key) => key.endsWith('src/app/excluded.ts')); + + // Check that the included file is in the report and the excluded one is not. + expect(includedKey).toBeDefined(); + expect(excludedKey).toBeUndefined(); + + // Check that the coverage data for the included file is valid. + const includedCoverage = summary[includedKey!]; + // The file has one statement, and it should have been executed once. + expect(includedCoverage.s['0']).toBe(1); + }); + + it('should only include referenced files when no include pattern is provided', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + // coverageInclude is not provided, so only referenced files should be included. + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + const summary = JSON.parse(harness.readFile('coverage/test/coverage-final.json')); + const summaryKeys = Object.keys(summary); + + const includedKey = summaryKeys.find((key) => key.endsWith('src/app/included.ts')); + const excludedKey = summaryKeys.find((key) => key.endsWith('src/app/excluded.ts')); + + // The included file is referenced by its spec and should be in the report. + expect(includedKey).toBeDefined(); + // The excluded file is not referenced and should NOT be in the report. + expect(excludedKey).toBeUndefined(); + }); + }); +});