Skip to content

Commit 6af4619

Browse files
committed
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 685ebae commit 6af4619

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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+
describeBuilder,
13+
UNIT_TEST_BUILDER_INFO,
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);
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+
for (const type of ['istanbul', 'v8']) {
54+
it(`should respect ${type} ignore next comments when computing coverage`, async () => {
55+
harness.useTarget('test', {
56+
...BASE_OPTIONS,
57+
coverage: true,
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
coverageReporters: ['json'] as any,
60+
});
61+
62+
harness.writeFile(
63+
'src/app/app.component.ts',
64+
`
65+
import { Component } from '@angular/core';
66+
67+
@Component({
68+
selector: 'app-root',
69+
template: '<h1>hello</h1>',
70+
standalone: true,
71+
})
72+
export class AppComponent {
73+
title = 'app';
74+
75+
/* ${type} ignore next */
76+
untestedFunction() {
77+
return false;
78+
}
79+
}
80+
`,
81+
);
82+
83+
harness.writeFile('src/app/app.component.spec.ts', getSpecContent());
84+
85+
const { result } = await harness.executeOnce();
86+
expect(result?.success).toBeTrue();
87+
harness.expectFile('coverage/test/coverage-final.json').toExist();
88+
89+
const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json'));
90+
const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts'));
91+
expect(appComponentPath).toBeDefined();
92+
93+
const appComponentCoverage = coverageMap[appComponentPath!];
94+
95+
const statementCounts = Object.values(appComponentCoverage.s) as number[];
96+
const hasUncoveredStatements = statementCounts.some((count) => count === 0);
97+
expect(hasUncoveredStatements)
98+
.withContext('There should be no uncovered statements as the uncalled function was ignored')
99+
.toBeFalse();
100+
});
101+
}
102+
103+
// Note: V8 does not support 'ignore if' semantic comments; it only supports generic line/block ignores.
104+
it('should respect istanbul ignore if comments when computing coverage', async () => {
105+
harness.useTarget('test', {
106+
...BASE_OPTIONS,
107+
coverage: true,
108+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
109+
coverageReporters: ['json'] as any,
110+
});
111+
112+
harness.writeFile(
113+
'src/app/app.component.ts',
114+
`
115+
import { Component } from '@angular/core';
116+
117+
@Component({
118+
selector: 'app-root',
119+
template: '<h1>hello</h1>',
120+
standalone: true,
121+
})
122+
export class AppComponent {
123+
checkValue(val: boolean) {
124+
/* istanbul ignore if */
125+
if (val) {
126+
return true;
127+
}
128+
return false;
129+
}
130+
}
131+
`,
132+
);
133+
134+
harness.writeFile(
135+
'src/app/app.component.spec.ts',
136+
getSpecContent(`
137+
it('should exercise the function but not the if block', () => {
138+
const fixture = TestBed.createComponent(AppComponent);
139+
const app = fixture.componentInstance;
140+
app.checkValue(false);
141+
});
142+
`),
143+
);
144+
145+
const { result } = await harness.executeOnce();
146+
expect(result?.success).toBeTrue();
147+
harness.expectFile('coverage/test/coverage-final.json').toExist();
148+
149+
const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json'));
150+
const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts'));
151+
expect(appComponentPath).toBeDefined();
152+
153+
const appComponentCoverage = coverageMap[appComponentPath!];
154+
155+
const statementCounts = Object.values(appComponentCoverage.s) as number[];
156+
const hasUncoveredStatements = statementCounts.some((count) => count === 0);
157+
expect(hasUncoveredStatements)
158+
.withContext('There should be no uncovered statements as the uncalled branch was ignored')
159+
.toBeFalse();
160+
});
161+
162+
it('should respect istanbul ignore else comments when computing coverage', async () => {
163+
harness.useTarget('test', {
164+
...BASE_OPTIONS,
165+
coverage: true,
166+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
167+
coverageReporters: ['json'] as any,
168+
});
169+
170+
harness.writeFile(
171+
'src/app/app.component.ts',
172+
`
173+
import { Component } from '@angular/core';
174+
175+
@Component({
176+
selector: 'app-root',
177+
template: '<h1>hello</h1>',
178+
standalone: true,
179+
})
180+
export class AppComponent {
181+
checkValue(val: boolean) {
182+
/* istanbul ignore else */
183+
if (val) {
184+
return true;
185+
} else {
186+
return false;
187+
}
188+
}
189+
}
190+
`,
191+
);
192+
193+
harness.writeFile(
194+
'src/app/app.component.spec.ts',
195+
getSpecContent(`
196+
it('should exercise the function and the if block', () => {
197+
const fixture = TestBed.createComponent(AppComponent);
198+
const app = fixture.componentInstance;
199+
app.checkValue(true);
200+
});
201+
`),
202+
);
203+
204+
const { result } = await harness.executeOnce();
205+
expect(result?.success).toBeTrue();
206+
harness.expectFile('coverage/test/coverage-final.json').toExist();
207+
208+
const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json'));
209+
const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts'));
210+
expect(appComponentPath).toBeDefined();
211+
212+
const appComponentCoverage = coverageMap[appComponentPath!];
213+
214+
const statementCounts = Object.values(appComponentCoverage.s) as number[];
215+
const hasUncoveredStatements = statementCounts.some((count) => count === 0);
216+
expect(hasUncoveredStatements)
217+
.withContext('There should be no uncovered statements as the uncalled branch was ignored')
218+
.toBeFalse();
219+
});
220+
221+
it('should respect v8 ignore start/stop comments when computing coverage', async () => {
222+
harness.useTarget('test', {
223+
...BASE_OPTIONS,
224+
coverage: true,
225+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
226+
coverageReporters: ['json'] as any,
227+
});
228+
229+
harness.writeFile(
230+
'src/app/app.component.ts',
231+
`
232+
import { Component } from '@angular/core';
233+
234+
@Component({
235+
selector: 'app-root',
236+
template: '<h1>hello</h1>',
237+
standalone: true,
238+
})
239+
export class AppComponent {
240+
title = 'app';
241+
242+
/* v8 ignore start */
243+
untestedFunction() {
244+
return false;
245+
}
246+
/* v8 ignore stop */
247+
}
248+
`,
249+
);
250+
251+
harness.writeFile('src/app/app.component.spec.ts', getSpecContent());
252+
253+
const { result } = await harness.executeOnce();
254+
expect(result?.success).toBeTrue();
255+
harness.expectFile('coverage/test/coverage-final.json').toExist();
256+
257+
const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json'));
258+
const appComponentPath = Object.keys(coverageMap).find((p) => p.includes('app.component.ts'));
259+
expect(appComponentPath).toBeDefined();
260+
261+
const appComponentCoverage = coverageMap[appComponentPath!];
262+
263+
const statementCounts = Object.values(appComponentCoverage.s) as number[];
264+
const hasUncoveredStatements = statementCounts.some((count) => count === 0);
265+
expect(hasUncoveredStatements)
266+
.withContext('There should be no uncovered statements as the uncalled function was ignored')
267+
.toBeFalse();
268+
});
269+
});
270+
});

0 commit comments

Comments
 (0)