Skip to content

Commit 6df2b14

Browse files
committed
fix(@angular/build): validate V8 coverage support for browsers in Vitest
This change introduces a validation check in the Vitest runner to ensure that code coverage is only enabled when using supported Chromium-based browsers. Since the Angular CLI integration currently relies exclusively on the V8 coverage provider, running tests in non-Chromium browsers like Firefox or Safari with coverage enabled would result in incomplete data or missing reports. By adding this check, developers will receive a clear, actionable error message early in the process, preventing confusion and ensuring reliable coverage reports. (cherry picked from commit 74e7dbe)
1 parent 5e3f70c commit 6df2b14

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface BrowserInstanceConfiguration {
4747
provider?: BrowserProviderOption;
4848
}
4949

50-
function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration {
50+
export function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration {
5151
// Normalize browser names to match Vitest's expectations for headless but also supports karma's names
5252
// e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' -> 'firefox'
5353
// and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'.

packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { createBuildAssetsMiddleware } from '../../../../tools/vite/middlewares/
2323
import { toPosixPath } from '../../../../utils/path';
2424
import type { ResultFile } from '../../../application/results';
2525
import type { NormalizedUnitTestBuilderOptions } from '../../options';
26+
import { normalizeBrowserName } from './browser-provider';
2627

2728
type VitestPlugins = Awaited<ReturnType<typeof VitestPlugin>>;
2829

@@ -148,6 +149,9 @@ export async function createVitestConfigPlugin(
148149
(browser || testConfig?.browser?.enabled) &&
149150
(options.coverage.enabled || testConfig?.coverage?.enabled)
150151
) {
152+
// Validate that enabled browsers support V8 coverage
153+
validateBrowserCoverage(browser, testConfig?.browser);
154+
151155
projectPlugins.unshift(createSourcemapSupportPlugin());
152156
setupFiles.unshift('virtual:source-map-support');
153157
}
@@ -412,6 +416,53 @@ function createSourcemapSupportPlugin(): VitestPlugins[0] {
412416
};
413417
}
414418

419+
interface CustomBrowserConfigOptions {
420+
instances?: { browser: string }[];
421+
name?: string;
422+
}
423+
424+
/**
425+
* Validates that all enabled browsers support V8 coverage when coverage is enabled.
426+
* Throws an error if an unsupported browser is detected.
427+
*/
428+
function validateBrowserCoverage(
429+
browser: BrowserConfigOptions | undefined,
430+
testConfigBrowser: BrowserConfigOptions | undefined,
431+
): void {
432+
const browsersToCheck: string[] = [];
433+
434+
// 1. Check browsers passed by the Angular CLI options
435+
const cliBrowser = browser as CustomBrowserConfigOptions | undefined;
436+
if (cliBrowser?.instances) {
437+
browsersToCheck.push(...cliBrowser.instances.map((i) => i.browser));
438+
}
439+
440+
// 2. Check browsers defined in the user's vitest.config.ts
441+
const userBrowser = testConfigBrowser as CustomBrowserConfigOptions | undefined;
442+
if (userBrowser) {
443+
if (userBrowser.instances) {
444+
browsersToCheck.push(...userBrowser.instances.map((i) => i.browser));
445+
}
446+
if (userBrowser.name) {
447+
browsersToCheck.push(userBrowser.name);
448+
}
449+
}
450+
451+
// Normalize and filter unsupported browsers
452+
const unsupportedBrowsers = browsersToCheck
453+
.map((b) => normalizeBrowserName(b).browser)
454+
.filter((b) => !['chrome', 'chromium', 'edge'].includes(b));
455+
456+
if (unsupportedBrowsers.length > 0) {
457+
throw new Error(
458+
`Code coverage is enabled, but the following configured browsers do not support the V8 coverage provider: ` +
459+
`${unsupportedBrowsers.join(', ')}. ` +
460+
`V8 coverage is only supported on Chromium-based browsers (e.g., Chrome, Chromium, Edge). ` +
461+
`Please disable coverage or remove the unsupported browsers.`,
462+
);
463+
}
464+
}
465+
415466
async function generateCoverageOption(
416467
optionsCoverage: NormalizedUnitTestBuilderOptions['coverage'],
417468
configCoverage: VitestCoverageOption | undefined,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import assert from 'node:assert/strict';
2+
import path from 'node:path';
3+
import { applyVitestBuilder } from '../../utils/vitest';
4+
import { execAndCaptureError } from '../../utils/process';
5+
import { installPackage } from '../../utils/packages';
6+
import { writeFile } from '../../utils/fs';
7+
import { stripVTControlCharacters } from 'node:util';
8+
import { unlink } from 'node:fs/promises';
9+
10+
export default async function (): Promise<void> {
11+
await applyVitestBuilder();
12+
13+
// Install necessary packages to pass the provider check
14+
await installPackage('playwright@1');
15+
await installPackage('@vitest/browser-playwright@4');
16+
await installPackage('@vitest/coverage-v8@4');
17+
18+
// === Case 1: Browser configured via CLI option ===
19+
const error1 = await execAndCaptureError('ng', [
20+
'test',
21+
'--no-watch',
22+
'--coverage',
23+
'--browsers',
24+
'firefox',
25+
]);
26+
const output1 = stripVTControlCharacters(error1.message);
27+
assert.match(
28+
output1,
29+
/Code coverage is enabled, but the following configured browsers do not support the V8 coverage provider: firefox/,
30+
'Expected validation error for unsupported browser with coverage (CLI option).',
31+
);
32+
33+
const configPath = 'vitest.config.ts';
34+
const absoluteConfigPath = path.resolve(configPath);
35+
36+
try {
37+
// === Case 2: Browser configured via vitest.config.ts (name) ===
38+
await writeFile(
39+
configPath,
40+
`
41+
import { defineConfig } from 'vitest/config';
42+
export default defineConfig({
43+
test: {
44+
browser: {
45+
enabled: true,
46+
name: 'firefox',
47+
provider: 'playwright',
48+
},
49+
},
50+
});
51+
`,
52+
);
53+
54+
const error2 = await execAndCaptureError('ng', [
55+
'test',
56+
'--no-watch',
57+
'--coverage',
58+
`--runner-config=${absoluteConfigPath}`,
59+
]);
60+
const output2 = stripVTControlCharacters(error2.message);
61+
assert.match(
62+
output2,
63+
/Code coverage is enabled, but the following configured browsers do not support the V8 coverage provider: firefox/,
64+
'Expected validation error for unsupported browser with coverage (config name).',
65+
);
66+
67+
// === Case 3: Browser configured via vitest.config.ts (instances) ===
68+
await writeFile(
69+
configPath,
70+
`
71+
import { defineConfig } from 'vitest/config';
72+
export default defineConfig({
73+
test: {
74+
browser: {
75+
enabled: true,
76+
provider: 'playwright',
77+
instances: [{ browser: 'firefox' }],
78+
} as any,
79+
},
80+
});
81+
`,
82+
);
83+
84+
const error3 = await execAndCaptureError('ng', [
85+
'test',
86+
'--no-watch',
87+
'--coverage',
88+
`--runner-config=${absoluteConfigPath}`,
89+
]);
90+
const output3 = stripVTControlCharacters(error3.message);
91+
assert.match(
92+
output3,
93+
/Code coverage is enabled, but the following configured browsers do not support the V8 coverage provider: firefox/,
94+
'Expected validation error for unsupported browser with coverage (config instances).',
95+
);
96+
} finally {
97+
// Clean up the config file so it doesn't affect other tests
98+
try {
99+
await unlink(configPath);
100+
} catch {}
101+
}
102+
}

0 commit comments

Comments
 (0)