Skip to content

Commit 683d0e0

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular/build): correctly handle absolute paths and casing in test discovery
The `findTests` function's path normalization logic had two flaws that affected cross-platform compatibility and user experience: 1. Absolute paths provided in the `include` patterns were incorrectly processed. The leading slash was removed, causing the globber to fail to find the specified test files. 2. The prefix removal logic was case-sensitive, which would fail to normalize workspace-relative paths on case-insensitive filesystems (like Windows and macOS) if the user's input casing for the project path differed from the actual directory casing. This commit corrects both issues: - The `normalizePattern` function now uses `isAbsolute` to detect and preserve absolute paths, ensuring they are passed to the globber unmodified. - The `removePrefix` helper function is now conditionally case-insensitive based on the host operating system (`win32`, `darwin`), aligning its behavior with the underlying filesystem and making path normalization more robust. - Minor code style refactoring and JSDoc comments have been added for clarity.
1 parent 9efcb03 commit 683d0e0

File tree

2 files changed

+62
-18
lines changed

2 files changed

+62
-18
lines changed

packages/angular/build/src/builders/karma/find-tests.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { findTests as findTestsBase } from '../unit-test/test-discovery';
10+
911
// This file is a compatibility layer that re-exports the test discovery logic from its new location.
1012
// This is necessary to avoid breaking the Karma builder, which still depends on this file.
11-
export { findTests, getTestEntrypoints } from '../unit-test/test-discovery';
13+
export { getTestEntrypoints } from '../unit-test/test-discovery';
14+
15+
const removeLeadingSlash = (path: string): string => {
16+
return path.startsWith('/') ? path.substring(1) : path;
17+
};
18+
19+
export async function findTests(
20+
include: string[],
21+
exclude: string[],
22+
workspaceRoot: string,
23+
projectSourceRoot: string,
24+
): Promise<string[]> {
25+
// Karma has legacy support for workspace "root-relative" file paths
26+
return findTestsBase(
27+
include.map(removeLeadingSlash),
28+
exclude.map(removeLeadingSlash),
29+
workspaceRoot,
30+
projectSourceRoot,
31+
);
32+
}

packages/angular/build/src/builders/unit-test/test-discovery.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { type PathLike, constants, promises as fs } from 'node:fs';
10+
import os from 'node:os';
1011
import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
1112
import { glob, isDynamicPattern } from 'tinyglobby';
1213
import { toPosixPath } from '../../utils/path';
@@ -157,15 +158,32 @@ function generateNameFromPath(
157158
return result;
158159
}
159160

160-
/** Removes a leading slash from a path. */
161-
const removeLeadingSlash = (path: string): string => {
162-
return path.startsWith('/') ? path.substring(1) : path;
163-
};
161+
/**
162+
* Whether the current operating system's filesystem is case-insensitive.
163+
*/
164+
const isCaseInsensitiveFilesystem = os.platform() === 'win32' || os.platform() === 'darwin';
164165

165-
/** Removes a prefix from the beginning of a string. */
166-
const removePrefix = (str: string, prefix: string): string => {
167-
return str.startsWith(prefix) ? str.substring(prefix.length) : str;
168-
};
166+
/**
167+
* Removes a prefix from the beginning of a string, with conditional case-insensitivity
168+
* based on the operating system's filesystem characteristics.
169+
*
170+
* @param text The string to remove the prefix from.
171+
* @param prefix The prefix to remove.
172+
* @returns The string with the prefix removed, or the original string if the prefix was not found.
173+
*/
174+
function removePrefix(text: string, prefix: string): string {
175+
if (isCaseInsensitiveFilesystem) {
176+
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
177+
return text.substring(prefix.length);
178+
}
179+
} else {
180+
if (text.startsWith(prefix)) {
181+
return text.substring(prefix.length);
182+
}
183+
}
184+
185+
return text;
186+
}
169187

170188
/**
171189
* Removes potential root paths from a file path, returning a relative path.
@@ -177,8 +195,10 @@ const removePrefix = (str: string, prefix: string): string => {
177195
*/
178196
function removeRoots(path: string, roots: string[]): string {
179197
for (const root of roots) {
180-
if (path.startsWith(root)) {
181-
return path.substring(root.length);
198+
const result = removePrefix(path, root);
199+
// If the prefix was removed, the result will be a different string.
200+
if (result !== path) {
201+
return result;
182202
}
183203
}
184204

@@ -194,15 +214,18 @@ function removeRoots(path: string, roots: string[]): string {
194214
* @returns A normalized glob pattern.
195215
*/
196216
function normalizePattern(pattern: string, projectRootPrefix: string): string {
197-
let normalizedPattern = toPosixPath(pattern);
198-
normalizedPattern = removeLeadingSlash(normalizedPattern);
217+
const posixPattern = toPosixPath(pattern);
218+
219+
// Do not modify absolute paths. The globber will handle them correctly.
220+
if (isAbsolute(posixPattern)) {
221+
return posixPattern;
222+
}
199223

200-
// Some IDEs and tools may provide patterns relative to the workspace root.
201-
// To ensure the glob operates correctly within the project's source root,
202-
// we remove the project's relative path from the front of the pattern.
203-
normalizedPattern = removePrefix(normalizedPattern, projectRootPrefix);
224+
// For relative paths, ensure they are correctly relative to the project source root.
225+
// This involves removing the project root prefix if the user provided a workspace-relative path.
226+
const normalizedRelative = removePrefix(posixPattern, projectRootPrefix);
204227

205-
return normalizedPattern;
228+
return normalizedRelative;
206229
}
207230

208231
/**

0 commit comments

Comments
 (0)