Skip to content

Commit eef8d0e

Browse files
clydindgp1130
authored andcommitted
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.
1 parent 829bdc6 commit eef8d0e

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { execute } from '../../index';
10+
import {
11+
BASE_OPTIONS,
12+
UNIT_TEST_BUILDER_INFO,
13+
describeBuilder,
14+
setupApplicationTarget,
15+
} from '../setup';
16+
17+
describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
18+
describe('Behavior: "coverage ignore comments"', () => {
19+
beforeEach(async () => {
20+
setupApplicationTarget(harness, { extractLicenses: false, optimization: false });
21+
});
22+
23+
function getSpecContent(extraTest = '') {
24+
return `
25+
import { TestBed } from '@angular/core/testing';
26+
import { AppComponent } from './app.component';
27+
28+
describe('AppComponent', () => {
29+
beforeEach(async () => {
30+
await TestBed.configureTestingModule({
31+
imports: [AppComponent],
32+
}).compileComponents();
33+
});
34+
35+
it('should create the app', () => {
36+
const fixture = TestBed.createComponent(AppComponent);
37+
const app = fixture.componentInstance;
38+
expect(app).toBeTruthy();
39+
});
40+
41+
it('should render title', async () => {
42+
const fixture = TestBed.createComponent(AppComponent);
43+
await fixture.whenStable();
44+
const compiled = fixture.nativeElement as HTMLElement;
45+
expect(compiled.querySelector('h1')?.textContent).toContain('hello');
46+
});
47+
48+
${extraTest}
49+
});
50+
`;
51+
}
52+
53+
async function assertNoUncoveredStatements(contextMessage: string) {
54+
const { result } = await harness.executeOnce();
55+
expect(result?.success).toBeTrue();
56+
harness.expectFile('coverage/test/coverage-final.json').toExist();
57+
58+
const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json'));
59+
const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts'));
60+
expect(appComponentPath).toBeDefined();
61+
62+
const appComponentCoverage = coverageMap[appComponentPath as string];
63+
64+
const statementCounts = Object.values(appComponentCoverage.s);
65+
const hasUncoveredStatements = statementCounts.some((count) => count === 0);
66+
expect(hasUncoveredStatements).withContext(contextMessage).toBeFalse();
67+
}
68+
69+
for (const type of ['istanbul', 'v8']) {
70+
it(`should respect ${type} ignore next comments when computing coverage`, async () => {
71+
harness.useTarget('test', {
72+
...BASE_OPTIONS,
73+
coverage: true,
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75+
coverageReporters: ['json'] as any,
76+
});
77+
78+
await harness.writeFile(
79+
'src/app/app.component.ts',
80+
`
81+
import { Component } from '@angular/core';
82+
83+
@Component({
84+
selector: 'app-root',
85+
template: '<h1>hello</h1>',
86+
standalone: true,
87+
})
88+
export class AppComponent {
89+
title = 'app';
90+
91+
/* ${type} ignore next */
92+
untestedFunction() {
93+
return false;
94+
}
95+
}
96+
`,
97+
);
98+
99+
await harness.writeFile('src/app/app.component.spec.ts', getSpecContent());
100+
101+
await assertNoUncoveredStatements(
102+
'There should be no uncovered statements as the uncalled function was ignored',
103+
);
104+
});
105+
106+
it(`should respect ${type} ignore if comments when computing coverage`, async () => {
107+
harness.useTarget('test', {
108+
...BASE_OPTIONS,
109+
coverage: true,
110+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
111+
coverageReporters: ['json'] as any,
112+
});
113+
114+
await harness.writeFile(
115+
'src/app/app.component.ts',
116+
`
117+
import { Component } from '@angular/core';
118+
119+
@Component({
120+
selector: 'app-root',
121+
template: '<h1>hello</h1>',
122+
standalone: true,
123+
})
124+
export class AppComponent {
125+
checkValue(val: boolean) {
126+
/* ${type} ignore if -- @preserve */
127+
if (val) {
128+
return true;
129+
}
130+
return false;
131+
}
132+
}
133+
`,
134+
);
135+
136+
await harness.writeFile(
137+
'src/app/app.component.spec.ts',
138+
getSpecContent(`
139+
it('should exercise the function but not the if block', () => {
140+
const fixture = TestBed.createComponent(AppComponent);
141+
const app = fixture.componentInstance;
142+
app.checkValue(false);
143+
});
144+
`),
145+
);
146+
147+
await assertNoUncoveredStatements(
148+
'There should be no uncovered statements as the uncalled branch was ignored',
149+
);
150+
});
151+
152+
it(`should respect ${type} ignore else comments when computing coverage`, async () => {
153+
harness.useTarget('test', {
154+
...BASE_OPTIONS,
155+
coverage: true,
156+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
157+
coverageReporters: ['json'] as any,
158+
});
159+
160+
await harness.writeFile(
161+
'src/app/app.component.ts',
162+
`
163+
import { Component } from '@angular/core';
164+
165+
@Component({
166+
selector: 'app-root',
167+
template: '<h1>hello</h1>',
168+
standalone: true,
169+
})
170+
export class AppComponent {
171+
checkValue(val: boolean) {
172+
/* ${type} ignore else -- @preserve */
173+
if (val) {
174+
return true;
175+
} else {
176+
return false;
177+
}
178+
}
179+
}
180+
`,
181+
);
182+
183+
await harness.writeFile(
184+
'src/app/app.component.spec.ts',
185+
getSpecContent(`
186+
it('should exercise the function but not the else block', () => {
187+
const fixture = TestBed.createComponent(AppComponent);
188+
const app = fixture.componentInstance;
189+
app.checkValue(true);
190+
});
191+
`),
192+
);
193+
194+
await assertNoUncoveredStatements(
195+
'There should be no uncovered statements as the uncalled else branch was ignored',
196+
);
197+
});
198+
}
199+
200+
it('should respect v8 ignore start/stop comments when computing coverage', async () => {
201+
harness.useTarget('test', {
202+
...BASE_OPTIONS,
203+
coverage: true,
204+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
205+
coverageReporters: ['json'] as any,
206+
});
207+
208+
await harness.writeFile(
209+
'src/app/app.component.ts',
210+
`
211+
import { Component } from '@angular/core';
212+
213+
@Component({
214+
selector: 'app-root',
215+
template: '<h1>hello</h1>',
216+
standalone: true,
217+
})
218+
export class AppComponent {
219+
title = 'app';
220+
221+
/* v8 ignore start */
222+
untestedFunction() {
223+
return false;
224+
}
225+
/* v8 ignore stop */
226+
}
227+
`,
228+
);
229+
230+
await harness.writeFile('src/app/app.component.spec.ts', getSpecContent());
231+
232+
await assertNoUncoveredStatements(
233+
'There should be no uncovered statements as the uncalled function was ignored',
234+
);
235+
});
236+
});
237+
});

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,11 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
626626
conditions,
627627
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'],
628628
metafile: true,
629-
legalComments: options.extractLicenses ? 'none' : (optimizationOptions.scripts ? 'eof' : 'inline'),
629+
legalComments: options.extractLicenses
630+
? 'none'
631+
: optimizationOptions.scripts
632+
? 'eof'
633+
: 'inline',
630634
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
631635
minifyIdentifiers: optimizationOptions.scripts && allowMangle,
632636
minifySyntax: optimizationOptions.scripts,

0 commit comments

Comments
 (0)