From 59e3ccc0da638f4697d204652f44723e8d95d9bc Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 29 Dec 2025 10:44:08 -0500 Subject: [PATCH 1/2] fix(@angular/build): normalize roots to POSIX in test discovery for Windows compatibility Ensures that project and workspace root paths are converted to POSIX format before being used to determine relative test file paths. This fixes an issue on Windows where mixed path separators (backslashes in roots, forward slashes in file paths) caused test discovery to fail to correctly generate entry point names. --- packages/angular/build/src/builders/unit-test/test-discovery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/unit-test/test-discovery.ts b/packages/angular/build/src/builders/unit-test/test-discovery.ts index d4f097b388f7..64e9718e48ac 100644 --- a/packages/angular/build/src/builders/unit-test/test-discovery.ts +++ b/packages/angular/build/src/builders/unit-test/test-discovery.ts @@ -129,7 +129,7 @@ export function generateNameFromPath( roots: string[], removeTestExtension: boolean, ): string { - const relativePath = removeRoots(testFile, roots); + const relativePath = removeRoots(testFile, roots.map(toPosixPath)); let startIndex = 0; // Skip leading dots and slashes From b47fea6cfe844e802278495fbe6459e116cea615 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 29 Dec 2025 10:57:24 -0500 Subject: [PATCH 2/2] fix(@angular/build): resolve test files correctly on Windows when using non-C drives Ensures that test files requested via root-relative paths (common on Windows with non-C drives or specific Vitest configurations) are correctly resolved to their absolute path entry points. This fix prevents 'Cannot find module' errors by explicitly checking the test entry point map after normalizing the path to an absolute POSIX path. --- .../unit-test/runners/vitest/plugins.ts | 4 ++ tests/e2e/tests/vitest/windows-subst-drive.ts | 66 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/e2e/tests/vitest/windows-subst-drive.ts 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 b92c6e7f872d..e4f3cfd5f810 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 @@ -229,6 +229,10 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins // Construct the full, absolute path and normalize it to POSIX format. const fullPath = toPosixPath(path.join(baseDir, id)); + if (testFileToEntryPoint.has(fullPath)) { + return fullPath; + } + // Check if the resolved path corresponds to a known build artifact. const relativePath = path.relative(workspaceRoot, fullPath); if (buildResultFiles.has(toPosixPath(relativePath))) { diff --git a/tests/e2e/tests/vitest/windows-subst-drive.ts b/tests/e2e/tests/vitest/windows-subst-drive.ts new file mode 100644 index 000000000000..e82ff50519ad --- /dev/null +++ b/tests/e2e/tests/vitest/windows-subst-drive.ts @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { execAndCaptureError, silentExec } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { stripVTControlCharacters } from 'node:util'; + +export default async function (): Promise { + // This test uses `subst` to map the project directory to a virtual drive letter + // to simulate running tests from a non-C drive on Windows. + if (process.platform !== 'win32') { + return; + } + + await applyVitestBuilder(); + + const originalCwd = process.cwd(); + const driveLetter = 'X:'; // Pick a drive letter that is unlikely to be in use. + + try { + // 1. Map the parent directory of the project to the virtual drive. + // This avoids running the project from the root of the drive (X:\), which can cause + // issues with workspace detection. + const projectParentDir = path.dirname(originalCwd); + const projectName = path.basename(originalCwd); + + await silentExec('subst', driveLetter, projectParentDir); + + // 2. Change the current process's working directory to the project folder on the virtual drive. + const newCwd = path.join(driveLetter + '\\', projectName); + process.chdir(newCwd); + + // Verify that the file system mapping is working as expected. + assert(fs.existsSync('angular.json'), 'angular.json should exist on the subst drive'); + + // 3. Run `ng test`. + // We expect this to fail with NG0203 in the subst environment due to dual-package hazards + // (Angular loading from both X: and D:) within bazel. However, the failure proves that the + // test file was discovered and loaded. + const error = await execAndCaptureError('ng', ['test', '--watch=false']); + const output = stripVTControlCharacters(error.message); + + console.log('OUTPUT:', output); + + // 4. Verify that Vitest found the test file and identified the tests within it. + assert.match( + output, + /src\/app\/app\.spec\.ts \(2 tests/, + 'Expected tests to be discovered and loaded, even if execution fails due to subst aliasing.', + ); + } finally { + // 5. Teardown: Restore CWD and remove the virtual drive mapping. + try { + process.chdir(originalCwd); + } catch (e) { + console.error('Failed to restore CWD:', e); + } + + try { + await silentExec('subst', driveLetter, '/d'); + } catch (e) { + // Ignore errors if the drive wasn't mounted or if unmount fails (best effort) + console.error(`Failed to unmount ${driveLetter}:`, e); + } + } +}