From 0a48239ab65a55e6c4c266b2fc49aeb61a6b471e Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:00:56 -0400 Subject: [PATCH 1/2] test(@angular/build): add E2E test for Vitest browser mode with coverage This test ensures that stack traces map correctly to source files and that coverage reports are generated when running Vitest in browser mode with coverage enabled. This provides validation for the current implementation and will help verify future refactors removing the source-map-support dependency. --- .../vitest/browser-coverage-sourcemaps.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts diff --git a/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts b/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts new file mode 100644 index 000000000000..2f0e66abc3ce --- /dev/null +++ b/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { ng } from '../../utils/process'; +import { installPackage } from '../../utils/packages'; +import { expectFileToExist, readFile } from '../../utils/fs'; + +export default async function (): Promise { + await applyVitestBuilder(); + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + await installPackage('@vitest/coverage-v8@4'); + + // Run tests with coverage in browser mode. + // We use the default passing tests generated for the project. + const { stdout } = await ng('test', '--no-watch', '--browsers', 'chromiumHeadless', '--coverage'); + + // Verify that tests passed + assert.match(stdout, /pass/, 'Expected tests to run successfully.'); + + // Verify that coverage files are generated + const coverageJsonPath = 'coverage/test-project/coverage-final.json'; + await expectFileToExist(coverageJsonPath); + + const coverageContent = await readFile(coverageJsonPath); + assert.match(coverageContent, /app\.ts/, 'Expected coverage report to contain app.ts.'); + assert.doesNotMatch( + coverageContent, + /\.spec\.ts/, + 'Expected coverage report to not contain .spec.ts files.', + ); +} From 890ec4c3b220a55259ae91543067d48b111abab9 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:48:08 -0400 Subject: [PATCH 2/2] refactor(@angular/build): remove source-map-support from Vitest runner This change removes the injection of `source-map-support` in Vitest browser tests and enables sourcemap rebasing for coverage runs as well. This allows Vitest's native remapper to handle stack traces and coverage correctly without needing the external polyfill in the browser. The E2E tests have been verified to pass with these changes. The unused `createSourcemapSupportPlugin` function has also been removed. --- .../unit-test/runners/vitest/plugins.ts | 38 +------------------ .../vitest/browser-coverage-sourcemaps.ts | 34 +++++++++++------ 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index 59a1e5136456..877dbdaf1606 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -232,16 +232,12 @@ export async function createVitestConfigPlugin( delete config.plugins; } - // Add browser source map support if coverage is enabled + // Validate browser coverage support if coverage is enabled if ( (browser || testConfig?.browser?.enabled) && (options.coverage.enabled || testConfig?.coverage?.enabled) ) { - // Validate that enabled browsers support the selected coverage provider validateBrowserCoverage(browser, testConfig?.browser, determinedProvider); - - projectPlugins.unshift(createSourcemapSupportPlugin()); - setupFiles.unshift('virtual:source-map-support'); } const projectResolver = createRequire(projectSourceRoot + '/').resolve; @@ -408,7 +404,7 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins const map = sourceMapText ? JSON.parse(sourceMapText) : undefined; if (map) { - adjustSourcemapSources(map, !vitestConfig?.coverage?.enabled, workspaceRoot, id); + adjustSourcemapSources(map, true, workspaceRoot, id); } return { @@ -475,36 +471,6 @@ function adjustSourcemapSources( } } -function createSourcemapSupportPlugin(): VitestPlugins[0] { - return { - name: 'angular:source-map-support', - enforce: 'pre', - resolveId(source) { - if (source.includes('virtual:source-map-support')) { - return '\0source-map-support'; - } - }, - async load(id) { - if (id !== '\0source-map-support') { - return; - } - - const packageResolve = createRequire(__filename).resolve; - const supportPath = packageResolve('source-map-support/browser-source-map-support.js'); - - const content = await readFile(supportPath, 'utf-8'); - - // The `source-map-support` library currently relies on `this` being defined in the global scope. - // However, when running in an ESM environment, `this` is undefined. - // To workaround this, we patch the library to use `globalThis` instead of `this`. - return ( - content.replaceAll(/this\.(define|sourceMapSupport|base64js)/g, 'globalThis.$1') + - '\n;globalThis.sourceMapSupport.install();' - ); - }, - }; -} - interface CustomBrowserConfigOptions { enabled?: boolean; instances?: { browser: string }[]; diff --git a/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts b/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts index 2f0e66abc3ce..db8b91a0abbe 100644 --- a/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts +++ b/tests/e2e/tests/vitest/browser-coverage-sourcemaps.ts @@ -12,20 +12,32 @@ export default async function (): Promise { // Run tests with coverage in browser mode. // We use the default passing tests generated for the project. - const { stdout } = await ng('test', '--no-watch', '--browsers', 'chromiumHeadless', '--coverage'); + const { stdout } = await ng( + 'test', + '--no-watch', + '--browsers', + 'chromiumHeadless', + '--coverage', + '--coverage-reporters=json-summary', + ); // Verify that tests passed assert.match(stdout, /pass/, 'Expected tests to run successfully.'); // Verify that coverage files are generated - const coverageJsonPath = 'coverage/test-project/coverage-final.json'; - await expectFileToExist(coverageJsonPath); - - const coverageContent = await readFile(coverageJsonPath); - assert.match(coverageContent, /app\.ts/, 'Expected coverage report to contain app.ts.'); - assert.doesNotMatch( - coverageContent, - /\.spec\.ts/, - 'Expected coverage report to not contain .spec.ts files.', - ); + const summaryPath = 'coverage/test-project/coverage-summary.json'; + await expectFileToExist(summaryPath); + + const summary = JSON.parse(await readFile(summaryPath)); + + // Find the key for app.ts (it might be an absolute path) + const appFileKey = Object.keys(summary).find((key) => key.endsWith('app.ts')); + assert.ok(appFileKey, 'Expected coverage summary to contain app.ts.'); + + const appCoverage = summary[appFileKey]; + assert.ok(appCoverage.lines.pct > 0, 'Expected lines percentage to be greater than 0.'); + + // Also verify that spec files are NOT present in the summary + const specFileKey = Object.keys(summary).find((key) => key.endsWith('.spec.ts')); + assert.ok(!specFileKey, 'Expected coverage report to not contain .spec.ts files.'); }