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: '

hello

', + standalone: true, + }) + export class AppComponent { + title = 'app'; + + /* ${type} ignore next */ + untestedFunction() { + return false; + } + } + `, + ); + + await harness.writeFile('src/app/app.component.spec.ts', getSpecContent()); + + await assertNoUncoveredStatements( + 'There should be no uncovered statements as the uncalled function was ignored', + ); + }); + } + + // Note: V8 does not support 'ignore if' semantic comments; it only supports generic line/block ignores. + it('should respect istanbul ignore if 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: '

hello

', + standalone: true, + }) + export class AppComponent { + checkValue(val: boolean) { + /* istanbul ignore if -- @preserve */ + if (val) { + return true; + } + return false; + } + } + `, + ); + + await harness.writeFile( + 'src/app/app.component.spec.ts', + getSpecContent(` + it('should exercise the function but not the if block', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + app.checkValue(false); + }); +`), + ); + + await assertNoUncoveredStatements( + 'There should be no uncovered statements as the uncalled branch was ignored', + ); + }); + + it('should respect v8 ignore start/stop 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: '

hello

', + standalone: true, + }) + export class AppComponent { + title = 'app'; + + /* v8 ignore start */ + untestedFunction() { + return false; + } + /* v8 ignore stop */ + } + `, + ); + + await harness.writeFile('src/app/app.component.spec.ts', getSpecContent()); + + await assertNoUncoveredStatements( + 'There should be no uncovered statements as the uncalled function was ignored', + ); + }); + + it('should respect istanbul ignore else 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: '

hello

', + standalone: true, + }) + export class AppComponent { + checkValue(val: boolean) { + /* istanbul ignore else -- @preserve */ + if (val) { + return true; + } else { + return false; + } + } + } + `, + ); + + await harness.writeFile( + 'src/app/app.component.spec.ts', + getSpecContent(` + it('should exercise the function but not the else block', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + app.checkValue(true); + }); +`), + ); + + await assertNoUncoveredStatements( + 'There should be no uncovered statements as the uncalled else branch was ignored', + ); + }); + }); +}); 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 39e9c3c674e7..d11e1b6fb63c 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,11 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu conditions, resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'], metafile: true, - legalComments: options.extractLicenses ? 'none' : (optimizationOptions.scripts ? 'eof' : 'inline'), + legalComments: options.extractLicenses + ? 'none' + : optimizationOptions.scripts + ? 'eof' + : 'inline', logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent', minifyIdentifiers: optimizationOptions.scripts && allowMangle, minifySyntax: optimizationOptions.scripts,