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 00a9bb71c18b..4ffebb0ad28b 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 @@ -9,6 +9,7 @@ import type { BuilderOutput } from '@angular-devkit/architect'; import assert from 'node:assert'; import path from 'node:path'; +import { isMatch } from 'picomatch'; import type { InlineConfig, Vitest } from 'vitest/node'; import { assertIsError } from '../../../../utils/error'; import { toPosixPath } from '../../../../utils/path'; @@ -141,7 +142,9 @@ export class VitestExecutor implements TestExecutor { } = this.options; let vitestNodeModule; + let vitestCoverageModule; try { + vitestCoverageModule = await import('vitest/coverage'); vitestNodeModule = await import('vitest/node'); } catch (error: unknown) { assertIsError(error); @@ -154,6 +157,21 @@ export class VitestExecutor implements TestExecutor { } const { startVitest } = vitestNodeModule; + // Augment BaseCoverageProvider to include logic to support the built virtual files. + // Temporary workaround to avoid the direct filesystem checks in the base provider that + // were introduced in v4. Also ensures that all built virtual files are available. + const builtVirtualFiles = this.buildResultFiles; + vitestCoverageModule.BaseCoverageProvider.prototype.isIncluded = function (filename) { + const relativeFilename = path.relative(workspaceRoot, filename); + if (!this.options.include || builtVirtualFiles.has(relativeFilename)) { + return !isMatch(relativeFilename, this.options.exclude); + } else { + return isMatch(relativeFilename, this.options.include, { + ignore: this.options.exclude, + }); + } + }; + // Setup vitest browser options if configured const browserOptions = await setupBrowserConfiguration( browsers, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index fa51ae996d2f..649032e3b4fc 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -117,15 +117,26 @@ export function createVitestPlugins( outputFile.origin === 'memory' ? Buffer.from(outputFile.contents).toString('utf-8') : await readFile(outputFile.inputPath, 'utf-8'); - const map = sourceMapFile + const sourceMapText = sourceMapFile ? sourceMapFile.origin === 'memory' ? Buffer.from(sourceMapFile.contents).toString('utf-8') : await readFile(sourceMapFile.inputPath, 'utf-8') : undefined; + // Vitest will include files in the coverage report if the sourcemap contains no sources. + // For builder-internal generated code chunks, which are typically helper functions, + // a virtual source is added to the sourcemap to prevent them from being incorrectly + // included in the final coverage report. + const map = sourceMapText ? JSON.parse(sourceMapText) : undefined; + if (map) { + if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) { + map.sources = ['virtual:builder']; + } + } + return { code, - map: map ? JSON.parse(map) : undefined, + map, }; } }, diff --git a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts index 490f2deaba4b..f8a8acb60591 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts @@ -28,7 +28,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/test/index.html')).toBeFalse(); + harness.expectFile('coverage/test/index.html').toNotExist(); }); it('should generate a code coverage report when coverage is true', async () => { @@ -39,7 +39,19 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/test/index.html')).toBeTrue(); + harness.expectFile('coverage/test/index.html').toExist(); + }); + + it('should generate a code coverage report when coverage is true', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + coverageReporters: ['json'] as any, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('coverage/test/coverage-final.json').content.toContain('app.component.ts'); }); }); });