Skip to content

Commit 87bb020

Browse files
committed
feat(@angular/build): support Istanbul coverage in Vitest runner
This change enables code coverage reporting when running tests in non-Chromium browsers (like Firefox or Safari) with the Vitest runner. The system now automatically detects the best coverage provider based on the configured browsers and installed packages: - If non-Chromium browsers are configured, it selects 'istanbul'. - If only Chromium browsers are used, it selects 'istanbul' if it is the only provider package installed. - Otherwise, it defaults to 'v8'. It also respects the provider specified in the user's custom Vitest configuration.
1 parent a4f11c1 commit 87bb020

File tree

4 files changed

+129
-22
lines changed

4 files changed

+129
-22
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import assert from 'node:assert';
1010
import type { TestRunner } from '../api';
1111
import { DependencyChecker } from '../dependency-checker';
12+
import { normalizeBrowserName } from './browser-provider';
1213
import { getVitestBuildOptions } from './build-options';
1314
import { VitestExecutor } from './executor';
1415

@@ -50,7 +51,10 @@ const VitestTestRunner: TestRunner = {
5051
}
5152

5253
if (options.coverage.enabled) {
53-
checker.check('@vitest/coverage-v8');
54+
checker.checkAny(
55+
['@vitest/coverage-v8', '@vitest/coverage-istanbul'],
56+
'Code coverage requires either "@vitest/coverage-v8" or "@vitest/coverage-istanbul" to be installed.',
57+
);
5458
}
5559

5660
checker.report();

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

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,74 @@ async function findTestEnvironment(
6969
}
7070
}
7171

72+
function determineCoverageProvider(
73+
browser: BrowserConfigOptions | undefined,
74+
testConfig: InlineConfig | undefined,
75+
optionsCoverageEnabled: boolean | undefined,
76+
projectSourceRoot: string,
77+
): 'istanbul' | 'v8' | 'custom' | undefined {
78+
let determinedProvider = testConfig?.coverage?.provider;
79+
if (!determinedProvider && (optionsCoverageEnabled || testConfig?.coverage?.enabled)) {
80+
const browsersToCheck = getBrowsersToCheck(browser, testConfig?.browser);
81+
82+
const hasNonChromium =
83+
browsersToCheck
84+
.map((b) => normalizeBrowserName(b).browser)
85+
.filter((b) => !['chrome', 'chromium', 'edge'].includes(b)).length > 0;
86+
87+
if (hasNonChromium) {
88+
determinedProvider = 'istanbul';
89+
} else {
90+
const projectRequire = createRequire(projectSourceRoot + '/');
91+
const checkInstalled = (pkg: string) => {
92+
try {
93+
projectRequire.resolve(pkg);
94+
95+
return true;
96+
} catch {
97+
return false;
98+
}
99+
};
100+
const hasIstanbul = checkInstalled('@vitest/coverage-istanbul');
101+
const hasV8 = checkInstalled('@vitest/coverage-v8');
102+
103+
if (hasIstanbul && !hasV8) {
104+
determinedProvider = 'istanbul';
105+
} else {
106+
determinedProvider = 'v8';
107+
}
108+
}
109+
}
110+
111+
return determinedProvider;
112+
}
113+
114+
function getBrowsersToCheck(
115+
browser: BrowserConfigOptions | undefined,
116+
testConfigBrowser: BrowserConfigOptions | undefined,
117+
): string[] {
118+
const browsersToCheck: string[] = [];
119+
120+
// 1. Check browsers passed by the Angular CLI options
121+
const cliBrowser = browser as CustomBrowserConfigOptions | undefined;
122+
if (cliBrowser?.instances) {
123+
browsersToCheck.push(...cliBrowser.instances.map((i) => i.browser));
124+
}
125+
126+
// 2. Check browsers defined in the user's vitest.config.ts
127+
const userBrowser = testConfigBrowser as CustomBrowserConfigOptions | undefined;
128+
if (userBrowser) {
129+
if (userBrowser.instances) {
130+
browsersToCheck.push(...userBrowser.instances.map((i) => i.browser));
131+
}
132+
if (userBrowser.name) {
133+
browsersToCheck.push(userBrowser.name);
134+
}
135+
}
136+
137+
return browsersToCheck;
138+
}
139+
72140
export async function createVitestConfigPlugin(
73141
options: VitestConfigPluginOptions,
74142
): Promise<VitestPlugins[0]> {
@@ -89,6 +157,13 @@ export async function createVitestConfigPlugin(
89157
async config(config) {
90158
const testConfig = config.test;
91159

160+
const determinedProvider = determineCoverageProvider(
161+
browser,
162+
testConfig,
163+
options.coverage.enabled,
164+
projectSourceRoot,
165+
);
166+
92167
if (reporters !== undefined) {
93168
delete testConfig?.reporters;
94169
}
@@ -155,8 +230,8 @@ export async function createVitestConfigPlugin(
155230
(browser || testConfig?.browser?.enabled) &&
156231
(options.coverage.enabled || testConfig?.coverage?.enabled)
157232
) {
158-
// Validate that enabled browsers support V8 coverage
159-
validateBrowserCoverage(browser, testConfig?.browser);
233+
// Validate that enabled browsers support the selected coverage provider
234+
validateBrowserCoverage(browser, testConfig?.browser, determinedProvider);
160235

161236
projectPlugins.unshift(createSourcemapSupportPlugin());
162237
setupFiles.unshift('virtual:source-map-support');
@@ -208,6 +283,7 @@ export async function createVitestConfigPlugin(
208283
options.coverage,
209284
testConfig?.coverage,
210285
projectName,
286+
determinedProvider,
211287
),
212288
// eslint-disable-next-line @typescript-eslint/no-explicit-any
213289
...(reporters ? ({ reporters } as any) : {}),
@@ -434,25 +510,12 @@ interface CustomBrowserConfigOptions {
434510
function validateBrowserCoverage(
435511
browser: BrowserConfigOptions | undefined,
436512
testConfigBrowser: BrowserConfigOptions | undefined,
513+
provider?: string,
437514
): void {
438-
const browsersToCheck: string[] = [];
439-
440-
// 1. Check browsers passed by the Angular CLI options
441-
const cliBrowser = browser as CustomBrowserConfigOptions | undefined;
442-
if (cliBrowser?.instances) {
443-
browsersToCheck.push(...cliBrowser.instances.map((i) => i.browser));
444-
}
445-
446-
// 2. Check browsers defined in the user's vitest.config.ts
447-
const userBrowser = testConfigBrowser as CustomBrowserConfigOptions | undefined;
448-
if (userBrowser) {
449-
if (userBrowser.instances) {
450-
browsersToCheck.push(...userBrowser.instances.map((i) => i.browser));
451-
}
452-
if (userBrowser.name) {
453-
browsersToCheck.push(userBrowser.name);
454-
}
515+
if (provider === 'istanbul') {
516+
return;
455517
}
518+
const browsersToCheck = getBrowsersToCheck(browser, testConfigBrowser);
456519

457520
// Normalize and filter unsupported browsers
458521
const unsupportedBrowsers = browsersToCheck
@@ -473,6 +536,7 @@ async function generateCoverageOption(
473536
optionsCoverage: NormalizedUnitTestBuilderOptions['coverage'],
474537
configCoverage: VitestCoverageOption | undefined,
475538
projectName: string,
539+
provider?: 'istanbul' | 'v8' | 'custom',
476540
): Promise<VitestCoverageOption> {
477541
let defaultExcludes: string[] = [];
478542
// When a coverage exclude option is provided, Vitest's default coverage excludes
@@ -486,6 +550,7 @@ async function generateCoverageOption(
486550
}
487551

488552
return {
553+
provider,
489554
excludeAfterRemap: true,
490555
reportsDirectory:
491556
configCoverage?.reportsDirectory ?? toPosixPath(path.join('coverage', projectName)),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ng } from '../../utils/process';
2+
import { applyVitestBuilder } from '../../utils/vitest';
3+
import assert from 'node:assert';
4+
import { installPackage } from '../../utils/packages';
5+
import { expectFileToExist, readFile } from '../../utils/fs';
6+
import { updateJsonFile } from '../../utils/project';
7+
8+
export default async function (): Promise<void> {
9+
await applyVitestBuilder();
10+
11+
// Install ONLY Istanbul coverage package.
12+
// This will trigger the auto-detection logic to use Istanbul even for Node tests.
13+
await installPackage('@vitest/coverage-istanbul@4');
14+
15+
// Use the 'json' reporter to get a machine-readable output for assertions.
16+
await updateJsonFile('angular.json', (json) => {
17+
const project = Object.values(json['projects'])[0] as any;
18+
const test = project['architect']['test'];
19+
test.options = {
20+
coverageReporters: ['json', 'text'],
21+
};
22+
});
23+
24+
// Run tests with coverage (defaults to Node/jsdom environment)
25+
const { stdout } = await ng('test', '--no-watch', '--coverage');
26+
27+
// Verify that tests passed
28+
assert.match(stdout, /1 passed/, 'Expected tests to run successfully.');
29+
30+
// Verify that coverage files are generated
31+
const coverageJsonPath = 'coverage/test-project/coverage-final.json';
32+
await expectFileToExist(coverageJsonPath);
33+
34+
const coverageSummary = JSON.parse(await readFile(coverageJsonPath));
35+
assert.ok(Object.keys(coverageSummary).length > 0, 'Expected coverage report to not be empty.');
36+
}

tests/e2e/tests/vitest/browser-coverage-validation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export default async function (): Promise<void> {
2626
const output1 = stripVTControlCharacters(error1.message);
2727
assert.match(
2828
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).',
29+
/Code coverage requires either "@vitest\/coverage-v8" or "@vitest\/coverage-istanbul" to be installed./,
30+
'Expected validation error for missing coverage packages.',
3131
);
3232

3333
const configPath = 'vitest.config.ts';
@@ -41,6 +41,7 @@ export default async function (): Promise<void> {
4141
import { defineConfig } from 'vitest/config';
4242
export default defineConfig({
4343
test: {
44+
coverage: { provider: 'v8' },
4445
browser: {
4546
enabled: true,
4647
name: 'firefox',
@@ -71,6 +72,7 @@ export default async function (): Promise<void> {
7172
import { defineConfig } from 'vitest/config';
7273
export default defineConfig({
7374
test: {
75+
coverage: { provider: 'v8' },
7476
browser: {
7577
enabled: true,
7678
provider: 'playwright',

0 commit comments

Comments
 (0)