From 3f0fd0887b23486a9e885cc0b1075be92d839102 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:03:09 -0500 Subject: [PATCH] fix(@angular/build): correct Vitest coverage path resolution for JSDOM on Windows This commit addresses an issue where Vitest coverage reports were incomplete on Windows when using the JSDOM test environment. The root cause is an improperly formatted absolute path that can result from manually converting a file URL back to a path on Windows, leading to a superfluous leading slash (e.g., '/D:/path' instead of 'D:/path'). The 'angular:test-in-memory-provider' plugin now explicitly detects and removes this leading slash from absolute Windows paths, thereby correcting the path format and enabling proper source file mapping for coverage collection. --- .../unit-test/runners/vitest/plugins.ts | 27 +++++++++++++++++++ .../tests/vitest/larger-project-coverage.ts | 16 +++++------ 2 files changed, 33 insertions(+), 10 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 d614d16f97b8..93ef7cfb9f85 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 @@ -9,6 +9,7 @@ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; +import { platform } from 'node:os'; import path from 'node:path'; import type { BrowserConfigOptions, @@ -173,6 +174,7 @@ async function loadResultFile(file: ResultFile): Promise { export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins { const { workspaceRoot, buildResultFiles, testFileToEntryPoint } = pluginOptions; + const isWindows = platform() === 'win32'; return [ { @@ -184,6 +186,31 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins return id; } + // Workaround for Vitest in Windows when a fully qualified absolute path is provided with + // a superfluous leading slash. This can currently occur with the `@vitest/coverage-v8` provider + // when it uses `removeStartsWith(url, FILE_PROTOCOL)` to convert a file URL resulting in + // `/D:/tmp_dir/...` instead of `D:/tmp_dir/...`. + if (id[0] === '/' && isWindows) { + const slicedId = id.slice(1); + if (path.isAbsolute(slicedId)) { + return slicedId; + } + } + + if (importer && (id[0] === '.' || id[0] === '/')) { + let fullPath; + if (testFileToEntryPoint.has(importer)) { + fullPath = toPosixPath(path.join(workspaceRoot, id)); + } else { + fullPath = toPosixPath(path.join(path.dirname(importer), id)); + } + + const relativePath = path.relative(workspaceRoot, fullPath); + if (buildResultFiles.has(toPosixPath(relativePath))) { + return fullPath; + } + } + // Determine the base directory for resolution. let baseDir: string; if (importer) { diff --git a/tests/legacy-cli/e2e/tests/vitest/larger-project-coverage.ts b/tests/legacy-cli/e2e/tests/vitest/larger-project-coverage.ts index 4b5d5cc72bfa..3594bdc7dfee 100644 --- a/tests/legacy-cli/e2e/tests/vitest/larger-project-coverage.ts +++ b/tests/legacy-cli/e2e/tests/vitest/larger-project-coverage.ts @@ -36,16 +36,12 @@ export default async function () { const { stdout: jsdomStdout } = await ng('test', '--no-watch', '--coverage'); assert.match(jsdomStdout, expectedMessage, `Expected ${totalTests} tests to pass in JSDOM mode.`); - // TODO: Investigate why coverage-final.json is empty on Windows in JSDOM mode. - // For now, skip the coverage report check on Windows. - if (process.platform !== 'win32') { - // Assert that every generated file is in the coverage report by reading the JSON output. - const jsdomSummary = JSON.parse(await readFile(coverageJsonPath)); - const jsdomSummaryKeys = Object.keys(jsdomSummary); - for (const file of generatedFiles) { - const found = jsdomSummaryKeys.some((key) => key.endsWith(file)); - assert.ok(found, `Expected ${file} to be in the JSDOM coverage report.`); - } + // Assert that every generated file is in the coverage report by reading the JSON output. + const jsdomSummary = JSON.parse(await readFile(coverageJsonPath)); + const jsdomSummaryKeys = Object.keys(jsdomSummary); + for (const file of generatedFiles) { + const found = jsdomSummaryKeys.some((key) => key.endsWith(file)); + assert.ok(found, `Expected ${file} to be in the JSDOM coverage report.`); } // Setup for browser mode