Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/angular/build/src/builders/karma/find-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { findTests as findTestsBase } from '../unit-test/test-discovery';

// This file is a compatibility layer that re-exports the test discovery logic from its new location.
// This is necessary to avoid breaking the Karma builder, which still depends on this file.
export { findTests, getTestEntrypoints } from '../unit-test/test-discovery';
export { getTestEntrypoints } from '../unit-test/test-discovery';

const removeLeadingSlash = (path: string): string => {
return path.startsWith('/') ? path.substring(1) : path;
};

export async function findTests(
include: string[],
exclude: string[],
workspaceRoot: string,
projectSourceRoot: string,
): Promise<string[]> {
// Karma has legacy support for workspace "root-relative" file paths
return findTestsBase(
include.map(removeLeadingSlash),
exclude.map(removeLeadingSlash),
workspaceRoot,
projectSourceRoot,
);
}
57 changes: 40 additions & 17 deletions packages/angular/build/src/builders/unit-test/test-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { type PathLike, constants, promises as fs } from 'node:fs';
import os from 'node:os';
import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
import { glob, isDynamicPattern } from 'tinyglobby';
import { toPosixPath } from '../../utils/path';
Expand Down Expand Up @@ -157,15 +158,32 @@ function generateNameFromPath(
return result;
}

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

/** Removes a prefix from the beginning of a string. */
const removePrefix = (str: string, prefix: string): string => {
return str.startsWith(prefix) ? str.substring(prefix.length) : str;
};
/**
* Removes a prefix from the beginning of a string, with conditional case-insensitivity
* based on the operating system's filesystem characteristics.
*
* @param text The string to remove the prefix from.
* @param prefix The prefix to remove.
* @returns The string with the prefix removed, or the original string if the prefix was not found.
*/
function removePrefix(text: string, prefix: string): string {
if (isCaseInsensitiveFilesystem) {
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
return text.substring(prefix.length);
}
} else {
if (text.startsWith(prefix)) {
return text.substring(prefix.length);
}
}

return text;
}

/**
* Removes potential root paths from a file path, returning a relative path.
Expand All @@ -177,8 +195,10 @@ const removePrefix = (str: string, prefix: string): string => {
*/
function removeRoots(path: string, roots: string[]): string {
for (const root of roots) {
if (path.startsWith(root)) {
return path.substring(root.length);
const result = removePrefix(path, root);
// If the prefix was removed, the result will be a different string.
if (result !== path) {
return result;
}
}

Expand All @@ -194,15 +214,18 @@ function removeRoots(path: string, roots: string[]): string {
* @returns A normalized glob pattern.
*/
function normalizePattern(pattern: string, projectRootPrefix: string): string {
let normalizedPattern = toPosixPath(pattern);
normalizedPattern = removeLeadingSlash(normalizedPattern);
const posixPattern = toPosixPath(pattern);

// Do not modify absolute paths. The globber will handle them correctly.
if (isAbsolute(posixPattern)) {
return posixPattern;
}

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

return normalizedPattern;
return normalizedRelative;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions tests/legacy-cli/e2e/tests/vitest/absolute-include.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import assert from 'node:assert/strict';
import { applyVitestBuilder } from '../../utils/vitest';
import { ng } from '../../utils/process';
import path from 'node:path';

export default async function (): Promise<void> {
await applyVitestBuilder();

const { stdout } = await ng('test', '--include', path.resolve('src/app/app.spec.ts'));

assert.match(stdout, /1 passed/, 'Expected 1 test to pass.');
}