Skip to content

Commit d597ec1

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 d597ec1

File tree

4 files changed

+128
-22
lines changed

4 files changed

+128
-22
lines changed

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

Lines changed: 11 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,16 @@ const VitestTestRunner: TestRunner = {
5051
}
5152

5253
if (options.coverage.enabled) {
53-
checker.check('@vitest/coverage-v8');
54+
const browsersToCheck: string[] = options.browsers ?? [];
55+
const hasNonChromium = browsersToCheck
56+
.map((b) => normalizeBrowserName(b).browser)
57+
.filter((b) => !['chrome', 'chromium', 'edge'].includes(b)).length > 0;
58+
59+
if (hasNonChromium) {
60+
checker.check('@vitest/coverage-istanbul');
61+
} else {
62+
checker.check('@vitest/coverage-v8');
63+
}
5464
}
5565

5666
checker.report();

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

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,67 @@ 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 = browsersToCheck
83+
.map((b) => normalizeBrowserName(b).browser)
84+
.filter((b) => !['chrome', 'chromium', 'edge'].includes(b)).length > 0;
85+
86+
if (hasNonChromium) {
87+
determinedProvider = 'istanbul';
88+
} else {
89+
const projectRequire = createRequire(projectSourceRoot + '/');
90+
const checkInstalled = (pkg: string) => {
91+
try { projectRequire.resolve(pkg); return true; } catch { return false; }
92+
};
93+
const hasIstanbul = checkInstalled('@vitest/coverage-istanbul');
94+
const hasV8 = checkInstalled('@vitest/coverage-v8');
95+
96+
if (hasIstanbul && !hasV8) {
97+
determinedProvider = 'istanbul';
98+
} else {
99+
determinedProvider = 'v8';
100+
}
101+
}
102+
}
103+
104+
return determinedProvider;
105+
}
106+
107+
function getBrowsersToCheck(
108+
browser: BrowserConfigOptions | undefined,
109+
testConfigBrowser: BrowserConfigOptions | undefined,
110+
): string[] {
111+
const browsersToCheck: string[] = [];
112+
113+
// 1. Check browsers passed by the Angular CLI options
114+
const cliBrowser = browser as CustomBrowserConfigOptions | undefined;
115+
if (cliBrowser?.instances) {
116+
browsersToCheck.push(...cliBrowser.instances.map((i) => i.browser));
117+
}
118+
119+
// 2. Check browsers defined in the user's vitest.config.ts
120+
const userBrowser = testConfigBrowser as CustomBrowserConfigOptions | undefined;
121+
if (userBrowser) {
122+
if (userBrowser.instances) {
123+
browsersToCheck.push(...userBrowser.instances.map((i) => i.browser));
124+
}
125+
if (userBrowser.name) {
126+
browsersToCheck.push(userBrowser.name);
127+
}
128+
}
129+
130+
return browsersToCheck;
131+
}
132+
72133
export async function createVitestConfigPlugin(
73134
options: VitestConfigPluginOptions,
74135
): Promise<VitestPlugins[0]> {
@@ -89,6 +150,13 @@ export async function createVitestConfigPlugin(
89150
async config(config) {
90151
const testConfig = config.test;
91152

153+
const determinedProvider = determineCoverageProvider(
154+
browser,
155+
testConfig,
156+
options.coverage.enabled,
157+
projectSourceRoot,
158+
);
159+
92160
if (reporters !== undefined) {
93161
delete testConfig?.reporters;
94162
}
@@ -155,8 +223,8 @@ export async function createVitestConfigPlugin(
155223
(browser || testConfig?.browser?.enabled) &&
156224
(options.coverage.enabled || testConfig?.coverage?.enabled)
157225
) {
158-
// Validate that enabled browsers support V8 coverage
159-
validateBrowserCoverage(browser, testConfig?.browser);
226+
// Validate that enabled browsers support the selected coverage provider
227+
validateBrowserCoverage(browser, testConfig?.browser, determinedProvider);
160228

161229
projectPlugins.unshift(createSourcemapSupportPlugin());
162230
setupFiles.unshift('virtual:source-map-support');
@@ -208,6 +276,7 @@ export async function createVitestConfigPlugin(
208276
options.coverage,
209277
testConfig?.coverage,
210278
projectName,
279+
determinedProvider,
211280
),
212281
// eslint-disable-next-line @typescript-eslint/no-explicit-any
213282
...(reporters ? ({ reporters } as any) : {}),
@@ -434,25 +503,12 @@ interface CustomBrowserConfigOptions {
434503
function validateBrowserCoverage(
435504
browser: BrowserConfigOptions | undefined,
436505
testConfigBrowser: BrowserConfigOptions | undefined,
506+
provider?: string,
437507
): 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-
}
508+
if (provider === 'istanbul') {
509+
return;
455510
}
511+
const browsersToCheck = getBrowsersToCheck(browser, testConfigBrowser);
456512

457513
// Normalize and filter unsupported browsers
458514
const unsupportedBrowsers = browsersToCheck
@@ -473,6 +529,7 @@ async function generateCoverageOption(
473529
optionsCoverage: NormalizedUnitTestBuilderOptions['coverage'],
474530
configCoverage: VitestCoverageOption | undefined,
475531
projectName: string,
532+
provider?: 'istanbul' | 'v8' | 'custom',
476533
): Promise<VitestCoverageOption> {
477534
let defaultExcludes: string[] = [];
478535
// When a coverage exclude option is provided, Vitest's default coverage excludes
@@ -486,6 +543,7 @@ async function generateCoverageOption(
486543
}
487544

488545
return {
546+
provider,
489547
excludeAfterRemap: true,
490548
reportsDirectory:
491549
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+
/The following packages are required but were not found:.*@vitest\/coverage-istanbul/,
30+
'Expected validation error for missing Istanbul coverage package.',
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)