diff --git a/packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts b/packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts index 4560d0cff952..ff4604c7c91e 100644 --- a/packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts @@ -26,7 +26,7 @@ import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap import { Configuration } from 'webpack'; import { ExecutionTransformer } from '../../transforms'; import { OutputHashing } from '../browser-esbuild/schema'; -import { findTests } from './find-tests'; +import { findTests, getTestEntrypoints } from './find-tests'; import { Schema as KarmaBuilderOptions } from './schema'; interface BuildOptions extends ApplicationBuilderInternalOptions { @@ -268,28 +268,7 @@ async function collectEntrypoints( projectSourceRoot, ); - const seen = new Set(); - - return new Map( - Array.from(testFiles, (testFile) => { - const relativePath = path - .relative( - testFile.startsWith(projectSourceRoot) ? projectSourceRoot : context.workspaceRoot, - testFile, - ) - .replace(/^[./]+/, '_') - .replace(/\//g, '-'); - let uniqueName = `spec-${path.basename(relativePath, path.extname(relativePath))}`; - let suffix = 2; - while (seen.has(uniqueName)) { - uniqueName = `${relativePath}-${suffix}`; - ++suffix; - } - seen.add(uniqueName); - - return [uniqueName, testFile]; - }), - ); + return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot }); } async function initializeApplication( diff --git a/packages/angular_devkit/build_angular/src/builders/karma/find-tests.ts b/packages/angular_devkit/build_angular/src/builders/karma/find-tests.ts index 80571870e3b2..077a938e0df5 100644 --- a/packages/angular_devkit/build_angular/src/builders/karma/find-tests.ts +++ b/packages/angular_devkit/build_angular/src/builders/karma/find-tests.ts @@ -26,6 +26,39 @@ export async function findTests( return [...new Set(files.flat())]; } +interface TestEntrypointsOptions { + projectSourceRoot: string; + workspaceRoot: string; +} + +/** Generate unique bundle names for a set of test files. */ +export function getTestEntrypoints( + testFiles: string[], + { projectSourceRoot, workspaceRoot }: TestEntrypointsOptions, +): Map { + const seen = new Set(); + + return new Map( + Array.from(testFiles, (testFile) => { + const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot]) + // Strip leading dots and path separators. + .replace(/^[./\\]+/, '') + // Replace any path separators with dashes. + .replace(/[/\\]/g, '-'); + const baseName = `spec-${basename(relativePath, extname(relativePath))}`; + let uniqueName = baseName; + let suffix = 2; + while (seen.has(uniqueName)) { + uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1'); + ++suffix; + } + seen.add(uniqueName); + + return [uniqueName, testFile]; + }), + ); +} + const normalizePath = (path: string): string => path.replace(/\\/g, '/'); const removeLeadingSlash = (pattern: string): string => { @@ -44,6 +77,16 @@ const removeRelativeRoot = (path: string, root: string): string => { return path; }; +function removeRoots(path: string, roots: string[]): string { + for (const root of roots) { + if (path.startsWith(root)) { + return path.substring(root.length); + } + } + + return basename(path); +} + async function findMatchingTests( pattern: string, ignore: string[], diff --git a/packages/angular_devkit/build_angular/src/builders/karma/find-tests_spec.ts b/packages/angular_devkit/build_angular/src/builders/karma/find-tests_spec.ts new file mode 100644 index 000000000000..8264539ae9dd --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/karma/find-tests_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { getTestEntrypoints } from './find-tests'; + +const UNIX_ENTRYPOINTS_OPTIONS = { + pathSeparator: '/', + workspaceRoot: '/my/workspace/root', + projectSourceRoot: '/my/workspace/root/src-root', +}; + +const WINDOWS_ENTRYPOINTS_OPTIONS = { + pathSeparator: '\\', + workspaceRoot: 'C:\\my\\workspace\\root', + projectSourceRoot: 'C:\\my\\workspace\\root\\src-root', +}; + +describe('getTestEntrypoints', () => { + for (const options of [UNIX_ENTRYPOINTS_OPTIONS, WINDOWS_ENTRYPOINTS_OPTIONS]) { + describe(`with path separator "${options.pathSeparator}"`, () => { + function joinWithSeparator(base: string, rel: string) { + return `${base}${options.pathSeparator}${rel.replace(/\//g, options.pathSeparator)}`; + } + + function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) { + return getTestEntrypoints( + [ + ...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)), + ...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)), + ], + options, + ); + } + + it('returns an empty map without test files', () => { + expect(getEntrypoints([])).toEqual(new Map()); + }); + + it('strips workspace root and/or project source root', () => { + expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual( + new Map([ + ['spec-a-b.spec', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')], + ['spec-c-d.spec', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')], + ]), + ); + }); + + it('adds unique prefixes to distinguish between similar names', () => { + expect(getEntrypoints(['a/b/c/d.spec.js', 'a-b/c/d.spec.js'], ['a/b-c/d.spec.js'])).toEqual( + new Map([ + ['spec-a-b-c-d.spec', joinWithSeparator(options.workspaceRoot, 'a/b/c/d.spec.js')], + ['spec-a-b-c-d-2.spec', joinWithSeparator(options.workspaceRoot, 'a-b/c/d.spec.js')], + [ + 'spec-a-b-c-d-3.spec', + joinWithSeparator(options.projectSourceRoot, 'a/b-c/d.spec.js'), + ], + ]), + ); + }); + }); + } +}); diff --git a/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/specs_spec.ts b/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/specs_spec.ts index 773f113bec83..5cb56abe9b9d 100644 --- a/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/specs_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/specs_spec.ts @@ -24,7 +24,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp) // src/app/app.component.spec.ts conflicts with this one: await harness.writeFiles({ - [`src/app/a/${collidingBasename}`]: `/** Success! */`, + [`src/app/a/foo-bar/${collidingBasename}`]: `/** Success! */`, + [`src/app/a-foo/bar/${collidingBasename}`]: `/** Success! */`, + [`src/app/a-foo-bar/${collidingBasename}`]: `/** Success! */`, [`src/app/b/${collidingBasename}`]: `/** Success! */`, }); @@ -36,7 +38,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp) const bundleLog = logs.find((log) => log.message.includes('Application bundle generation complete.'), ); - expect(bundleLog?.message).toContain('spec-app-a-collision.spec.js'); + expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision.spec.js'); + expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-2.spec.js'); + expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-3.spec.js'); expect(bundleLog?.message).toContain('spec-app-b-collision.spec.js'); } });