From 7ec6703b60949ee59dfbf77eb9736039111565b3 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:08:39 -0400 Subject: [PATCH 1/2] fix(@angular/build): preserve coverage ignore comments in development When script optimization is disabled, set esbuild `legalComments` to 'inline' to prevent it from moving ignore comments to the end of the file. This ensures that coverage tools (like Istanbul and V8) can find the comments in place and correctly associate them with the code they are supposed to ignore. --- .../angular/build/src/tools/esbuild/application-code-bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index c3f542e1bdfb..39e9c3c674e7 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -626,7 +626,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu conditions, resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'], metafile: true, - legalComments: options.extractLicenses ? 'none' : 'eof', + legalComments: options.extractLicenses ? 'none' : (optimizationOptions.scripts ? 'eof' : 'inline'), logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent', minifyIdentifiers: optimizationOptions.scripts && allowMangle, minifySyntax: optimizationOptions.scripts, From 41f05689a4e886dcc6e7783ae94944376aae68c3 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:15:34 -0400 Subject: [PATCH 2/2] test(@angular/build): verify coverage ignore comments are preserved during compilation The underlying Vitest coverage engine depends on specific developer comments like `/* istanbul ignore next */` or `/* v8 ignore next */` being present in the executing code to accurately isolate unmeasured blocks. This commit adds strict behavioral tests to assert that the Angular CLI's in-memory compilation pipeline (via esbuild) properly preserves these structural comments and forwards them reliably to Vitest's coverage processing engine. --- .../behavior/coverage-ignore-comments_spec.ts | 238 ++++++++++++++++++ .../tools/esbuild/application-code-bundle.ts | 6 +- 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/behavior/coverage-ignore-comments_spec.ts diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/coverage-ignore-comments_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/coverage-ignore-comments_spec.ts new file mode 100644 index 000000000000..a8b2fa1d2a57 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/coverage-ignore-comments_spec.ts @@ -0,0 +1,238 @@ +/** + * @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, + UNIT_TEST_BUILDER_INFO, + describeBuilder, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Behavior: "coverage ignore comments"', () => { + beforeEach(async () => { + setupApplicationTarget(harness, { extractLicenses: false, optimization: false }); + }); + + function getSpecContent(extraTest = '') { + return ` +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render title', async () => { + const fixture = TestBed.createComponent(AppComponent); + await fixture.whenStable(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('hello'); + }); + + ${extraTest} +}); +`; + } + + async function assertNoUncoveredStatements(contextMessage: string) { + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('coverage/test/coverage-final.json').toExist(); + + const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json')); + const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts')); + expect(appComponentPath).toBeDefined(); + + const appComponentCoverage = coverageMap[appComponentPath as string]; + + const statementCounts = Object.values(appComponentCoverage.s); + const hasUncoveredStatements = statementCounts.some((count) => count === 0); + expect(hasUncoveredStatements).withContext(contextMessage).toBeFalse(); + } + + for (const type of ['istanbul', 'v8']) { + it(`should respect ${type} ignore next comments when computing coverage`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + coverageReporters: ['json'] as any, + }); + + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + template: '