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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ export async function getVitestBuildOptions(
workspaceRoot,
removeTestExtension: true,
});

if (options.setupFiles?.length) {
const setupEntryPoints = getTestEntrypoints(options.setupFiles, {
projectSourceRoot,
workspaceRoot,
removeTestExtension: false,
prefix: 'setup',
});

for (const [entryPoint, setupFile] of setupEntryPoints) {
entryPoints.set(entryPoint, setupFile);
}
}

entryPoints.set('init-testbed', 'angular:test-bed-init');

// The 'vitest' package is always external for testing purposes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ export class VitestExecutor implements TestExecutor {
project = `${projectName} (${browserOptions.browser.instances[0].browser})`;
}

// Filter internal entries and setup files from the include list
const internalEntries = ['angular:'];
const setupFileSet = new Set(testSetupFiles);
const include = [...this.testFileToEntryPoint.keys()].filter((entry) => {
return (
!internalEntries.some((internal) => entry.startsWith(internal)) && !setupFileSet.has(entry)
);
});

return startVitest(
'test',
undefined,
Expand Down Expand Up @@ -272,10 +281,7 @@ export class VitestExecutor implements TestExecutor {
reporters,
setupFiles: testSetupFiles,
projectPlugins,
include: [...this.testFileToEntryPoint.keys()].filter(
// Filter internal entries
(entry) => !entry.startsWith('angular:'),
),
include,
}),
],
},
Expand Down
26 changes: 22 additions & 4 deletions packages/angular/build/src/builders/unit-test/test-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ interface TestEntrypointsOptions {
projectSourceRoot: string;
workspaceRoot: string;
removeTestExtension?: boolean;
prefix?: string;
}

/**
Expand All @@ -93,22 +94,39 @@ interface TestEntrypointsOptions {
*/
export function getTestEntrypoints(
testFiles: string[],
{ projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions,
{
projectSourceRoot,
workspaceRoot,
removeTestExtension,
prefix = 'spec',
}: TestEntrypointsOptions,
): Map<string, string> {
const seen = new Set<string>();
const counters = new Map<string, number>();
const roots = [projectSourceRoot, workspaceRoot];
const infixes = TEST_FILE_INFIXES.map((i) => i.slice(1));
if (!infixes.includes(prefix)) {
infixes.push(prefix);
}
const infixesPattern = infixes.join('|');

return new Map(
Array.from(testFiles, (testFile) => {
const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension);
const baseName = `spec-${fileName}`;
const baseName = fileName === prefix ? prefix : `${prefix}-${fileName}`;
let uniqueName = baseName;
let suffix = 2;
// Start at 2 for collisions as the first instance remains suffix-less.
let suffix = counters.get(baseName) ?? 2;

while (seen.has(uniqueName)) {
uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
uniqueName = `${baseName}-${suffix}`.replace(
new RegExp(`([^\\w](?:${infixesPattern}))-([\\d]+)$`),
'-$2$1',
);
++suffix;
}
seen.add(uniqueName);
counters.set(baseName, suffix);

return [uniqueName, testFile];
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,79 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { generateNameFromPath } from './test-discovery';
import { generateNameFromPath, getTestEntrypoints } from './test-discovery';

describe('getTestEntrypoints', () => {
const workspaceRoot = '/project';
const projectSourceRoot = '/project/src';
const options = { workspaceRoot, projectSourceRoot };

it('should generate entry points for unique files', () => {
const files = ['/project/src/a.spec.ts', '/project/src/b.spec.ts'];
const result = getTestEntrypoints(files, { ...options, removeTestExtension: true });

expect(result.size).toBe(2);
expect(result.get('spec-a')).toBe(files[0]);
expect(result.get('spec-b')).toBe(files[1]);
});

it('should handle collisions with numeric suffixes correctly positioned', () => {
// To trigger the regex replacement, the name must end in 'spec', 'test', or the prefix.
const files = ['/project/src/sub-test.spec.ts', '/project/src/sub/test.spec.ts'];

const result = getTestEntrypoints(files, { ...options, removeTestExtension: true });
// Both map to 'sub-test' (relative to src root).
// baseName = 'spec-sub-test'.
// 1st: 'spec-sub-test'.
// 2nd: 'spec-sub-test-2'. Regex matches '-test-2'. -> 'spec-sub-2-test'.
expect(result.get('spec-sub-test')).toBe(files[0]);
expect(result.get('spec-sub-2-test')).toBe(files[1]);
});

it('should handle setup file naming with prefix setup', () => {
const files = ['/project/src/setup.ts'];
const result = getTestEntrypoints(files, {
...options,
removeTestExtension: false,
prefix: 'setup',
});

// 'setup.ts' -> 'setup' (via generateNameFromPath).
// prefix='setup'.
// baseName = 'setup' === 'setup' ? 'setup' : 'setup-setup' -> 'setup'.
expect(result.get('setup')).toBe(files[0]);
});

it('should handle setup file collisions', () => {
const files = ['/project/src/sub-setup.ts', '/project/src/sub/setup.ts'];
const result = getTestEntrypoints(files, {
...options,
removeTestExtension: false,
prefix: 'setup',
});

// Both map to 'sub-setup' (baseName: 'setup-sub-setup').
// 1st: 'setup-sub-setup'.
// 2nd: 'setup-sub-setup-2'. Regex matches '-setup-2'. -> 'setup-sub-2-setup'.
expect(result.get('setup-sub-setup')).toBe(files[0]);
expect(result.get('setup-sub-2-setup')).toBe(files[1]);
});

it('should handle custom prefixes', () => {
const files = ['/project/src/sub-my-file.ts', '/project/src/sub/my-file.ts'];
const result = getTestEntrypoints(files, {
...options,
removeTestExtension: false,
prefix: 'custom',
});

// 'sub-my-file.ts' -> 'sub-my-file'.
// baseName: 'custom-sub-my-file'.
expect(result.get('custom-sub-my-file')).toBe(files[0]);
// 'custom-sub-my-file-2'. Does not match regex (no 'spec'/'test'/'custom' at end).
expect(result.get('custom-sub-my-file-2')).toBe(files[1]);
});
});

describe('generateNameFromPath', () => {
const roots = ['/project/src/', '/project/'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
xdescribe('Option: "setupFiles"', () => {
describe('Option: "setupFiles"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
});
Expand All @@ -27,19 +27,21 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
setupFiles: ['src/setup.ts'],
});

const { result, error } = await harness.executeOnce({ outputLogsOnFailure: false });
expect(result).toBeUndefined();
expect(error?.message).toMatch(`The specified setup file "src/setup.ts" does not exist.`);
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
expect(result?.success).toBeFalse();
// Verify that the build failed due to resolution error (esbuild error)
expectLog(logs, /Could not resolve/);
expectLog(logs, /src\/setup\.ts/);
});

it('should include the setup files', async () => {
await harness.writeFiles({
'src/setup.ts': `console.log('Hello from setup.ts');`,
'src/setup.ts': `globalThis['TEST_SETUP_RAN'] = true;`,
'src/app/app.component.spec.ts': `
import { describe, expect, test } from 'vitest'
describe('AppComponent', () => {
test('should create the app', () => {
expect(true).toBe(true);
test('should have run setup file', () => {
expect(globalThis['TEST_SETUP_RAN']).toBe(true);
});
});`,
});
Expand All @@ -49,9 +51,8 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
setupFiles: ['src/setup.ts'],
});

const { result, logs } = await harness.executeOnce();
const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
expectLog(logs, 'Hello from setup.ts');
});
});
});
5 changes: 5 additions & 0 deletions tests/e2e/tests/vitest/browser-playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { applyVitestBuilder } from '../../utils/vitest';
import { ng } from '../../utils/process';
import { installPackage } from '../../utils/packages';
import { writeFile } from '../../utils/fs';
import { updateJsonFile } from '../../utils/project';

export default async function (): Promise<void> {
await applyVitestBuilder();
Expand All @@ -19,6 +20,10 @@ export default async function (): Promise<void> {
`,
);

await updateJsonFile('tsconfig.spec.json', (json) => {
json.include = [...(json.include || []), 'src/setup1.ts'];
});

const { stdout } = await ng(
'test',
'--no-watch',
Expand Down
5 changes: 5 additions & 0 deletions tests/e2e/tests/vitest/browser-webdriverio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { applyVitestBuilder } from '../../utils/vitest';
import { ng } from '../../utils/process';
import { installPackage } from '../../utils/packages';
import { writeFile } from '../../utils/fs';
import { updateJsonFile } from '../../utils/project';

export default async function (): Promise<void> {
await applyVitestBuilder();
Expand All @@ -20,6 +21,10 @@ export default async function (): Promise<void> {
`,
);

await updateJsonFile('tsconfig.spec.json', (json) => {
json.include = [...(json.include || []), 'src/setup1.ts'];
});

const { stdout } = await ng(
'test',
'--no-watch',
Expand Down