diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index f270816d860f..0f27a390bc05 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -322,6 +322,7 @@ jasmine_test( name = "unit-test_integration_tests", size = "medium", data = [":unit-test_integration_test_lib"], + flaky = True, shard_count = 5, ) diff --git a/packages/angular/build/src/builders/karma/find-tests.ts b/packages/angular/build/src/builders/karma/find-tests.ts index 67ae410c6125..a84373964c8e 100644 --- a/packages/angular/build/src/builders/karma/find-tests.ts +++ b/packages/angular/build/src/builders/karma/find-tests.ts @@ -30,12 +30,13 @@ export async function findTests( interface TestEntrypointsOptions { projectSourceRoot: string; workspaceRoot: string; + removeTestExtension?: boolean; } /** Generate unique bundle names for a set of test files. */ export function getTestEntrypoints( testFiles: string[], - { projectSourceRoot, workspaceRoot }: TestEntrypointsOptions, + { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions, ): Map { const seen = new Set(); @@ -46,7 +47,13 @@ export function getTestEntrypoints( .replace(/^[./\\]+/, '') // Replace any path separators with dashes. .replace(/[/\\]/g, '-'); - const baseName = `spec-${basename(relativePath, extname(relativePath))}`; + + let fileName = basename(relativePath, extname(relativePath)); + if (removeTestExtension) { + fileName = fileName.replace(/\.(spec|test)$/, ''); + } + + const baseName = `spec-${fileName}`; let uniqueName = baseName; let suffix = 2; while (seen.has(uniqueName)) { diff --git a/packages/angular/build/src/builders/karma/find-tests_spec.ts b/packages/angular/build/src/builders/karma/find-tests_spec.ts index 8264539ae9dd..88c97c8575fe 100644 --- a/packages/angular/build/src/builders/karma/find-tests_spec.ts +++ b/packages/angular/build/src/builders/karma/find-tests_spec.ts @@ -62,6 +62,36 @@ describe('getTestEntrypoints', () => { ]), ); }); + + describe('with removeTestExtension enabled', () => { + function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) { + return getTestEntrypoints( + [ + ...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)), + ...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)), + ], + { ...options, removeTestExtension: true }, + ); + } + + it('removes .spec extension', () => { + expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual( + new Map([ + ['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')], + ['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')], + ]), + ); + }); + + it('removes .test extension', () => { + expect(getEntrypoints(['a/b.test.js'], ['c/d.test.js'])).toEqual( + new Map([ + ['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.test.js')], + ['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.test.js')], + ]), + ); + }); + }); }); } }); diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts index 996e6266b1ca..e94a35e7f8a4 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -82,7 +82,11 @@ export async function getVitestBuildOptions( ); } - const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot }); + const entryPoints = getTestEntrypoints(testFiles, { + projectSourceRoot, + workspaceRoot, + removeTestExtension: true, + }); entryPoints.set('init-testbed', 'angular:test-bed-init'); const buildOptions: Partial = { diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 27db9cb84c50..7e25ac93ab88 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -68,7 +68,11 @@ export class VitestExecutor implements TestExecutor { if (this.testFileToEntryPoint.size === 0) { const { include, exclude = [], workspaceRoot, projectSourceRoot } = this.options; const testFiles = await findTests(include, exclude, workspaceRoot, projectSourceRoot); - const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot }); + const entryPoints = getTestEntrypoints(testFiles, { + projectSourceRoot, + workspaceRoot, + removeTestExtension: true, + }); for (const [entryPoint, testFile] of entryPoints) { this.testFileToEntryPoint.set(testFile, entryPoint); this.entryPointToTestFile.set(entryPoint + '.js', testFile); @@ -90,6 +94,7 @@ export class VitestExecutor implements TestExecutor { if (source) { modifiedSourceFiles.add(source); } + vitest.invalidateFile(toPosixPath(path.join(this.options.workspaceRoot, modifiedFile))); } const specsToRerun = []; @@ -162,16 +167,15 @@ export class VitestExecutor implements TestExecutor { name: 'angular:test-in-memory-provider', enforce: 'pre', resolveId: (id, importer) => { - if (importer && id.startsWith('.')) { + if (importer && (id[0] === '.' || id[0] === '/')) { let fullPath; - let relativePath; if (this.testFileToEntryPoint.has(importer)) { fullPath = toPosixPath(path.join(this.options.workspaceRoot, id)); - relativePath = path.normalize(id); } else { fullPath = toPosixPath(path.join(path.dirname(importer), id)); - relativePath = path.relative(this.options.workspaceRoot, fullPath); } + + const relativePath = path.relative(this.options.workspaceRoot, fullPath); if (this.buildResultFiles.has(toPosixPath(relativePath))) { return fullPath; } @@ -201,6 +205,12 @@ export class VitestExecutor implements TestExecutor { let outputPath; if (entryPoint) { outputPath = entryPoint + '.js'; + + // To support coverage exclusion of the actual test file, the virtual + // test entry point only references the built and bundled intermediate file. + return { + code: `import "./${outputPath}";`, + }; } else { // Attempt to load as a built artifact. const relativePath = path.relative(this.options.workspaceRoot, id); @@ -247,15 +257,6 @@ export class VitestExecutor implements TestExecutor { }, ], }); - - // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests. - // Vite does this as a convenience but is problematic for the bundling strategy employed by the - // builder's test setup. To workaround this, the excludes are adjusted here to only automatically - // exclude the TypeScript source test files. - project.config.coverage.exclude = [ - ...(codeCoverage?.exclude ?? []), - '**/*.{test,spec}.?(c|m)ts', - ]; }, }, ]; @@ -343,7 +344,8 @@ function generateCoverageOption( return { enabled: true, excludeAfterRemap: true, - // Special handling for `reporter` due to an undefined value causing upstream failures + // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures + ...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}), ...(codeCoverage.reporters ? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption) : {}), diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index bd0836273091..f49eaeda1baa 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -60,8 +60,7 @@ "description": "Globs to exclude from code coverage.", "items": { "type": "string" - }, - "default": [] + } }, "codeCoverageReporters": { "type": "array", diff --git a/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts index 98434f302d57..d1840beb4a96 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "watch"', () => { + describe('Option: "watch"', () => { beforeEach(async () => { setupApplicationTarget(harness); });