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 bd8a6926bfd6..d36f8a05ffa6 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 @@ -82,6 +82,18 @@ export async function createVitestConfigPlugin( async config(config) { const testConfig = config.test; + if (reporters !== undefined) { + delete testConfig?.reporters; + } + + if ( + options.coverage.reporters !== undefined && + testConfig?.coverage && + 'reporter' in testConfig.coverage + ) { + delete testConfig.coverage.reporter; + } + if (testConfig?.projects?.length) { this.warn( 'The "test.projects" option in the Vitest configuration file is not supported. ' + @@ -106,6 +118,14 @@ export async function createVitestConfigPlugin( delete testConfig.watch; } + if (testConfig?.exclude) { + this.warn( + 'The "test.exclude" option in the Vitest configuration file is evaluated after ' + + 'tests are compiled. For better build performance, please use the Angular CLI ' + + '"exclude" option instead.', + ); + } + // Merge user-defined plugins from the Vitest config with the CLI's internal plugins. if (config.plugins) { const userPlugins = config.plugins.filter( @@ -398,6 +418,9 @@ async function generateCoverageOption( projectName: string, ): Promise { let defaultExcludes: string[] = []; + // When a coverage exclude option is provided, Vitest's default coverage excludes + // will be overridden. To retain them, we manually fetch the defaults to append to the + // user's provided exclusions. if (optionsCoverage.exclude) { try { const vitestConfig = await import('vitest/config'); @@ -429,12 +452,15 @@ async function generateCoverageOption( // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures ...(optionsCoverage.exclude ? { - exclude: [ - // Augment the default exclude https://vitest.dev/config/#coverage-exclude - // with the user defined exclusions - ...optionsCoverage.exclude, - ...defaultExcludes, - ], + exclude: Array.from( + new Set([ + // Augment the default exclude https://vitest.dev/config/#coverage-exclude + // with the user defined exclusions + ...(configCoverage?.exclude || []), + ...optionsCoverage.exclude, + ...defaultExcludes, + ]), + ), } : {}), ...(optionsCoverage.reporters diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts index fcfa3644035f..609d736e00f7 100644 --- a/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts @@ -42,6 +42,21 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { harness.expectFile('vitest-results.xml').toExist(); }); + it('should override reporters defined in runnerConfig file when CLI option is present', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + reporters: ['default'], + }); + + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + // The CLI option 'default' should override the 'junit' reporter in VITEST_CONFIG_CONTENT + harness.expectFile('vitest-results.xml').toNotExist(); + }); + it('should use custom reportsDirectory defined in runnerConfig file', async () => { harness.useTarget('test', { ...BASE_OPTIONS, @@ -164,6 +179,59 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { expect(results.numPassedTests).toBe(1); }); + it('should correctly merge coverage.exclude arrays from builder and runner options', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + runnerConfig: 'vitest.config.ts', + coverageExclude: ['src/app/cli-excluded.ts'], + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + coverage: { + exclude: ['src/app/config-excluded.ts'], + }, + }, + }); + `, + ); + + // Create two files that would normally be covered + harness.writeFile('src/app/cli-excluded.ts', 'export const cliExcluded = true;'); + harness.writeFile('src/app/config-excluded.ts', 'export const configExcluded = true;'); + + // Update the test file to import them so they're picked up by coverage + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + import './cli-excluded'; + import './config-excluded'; + test('should pass', () => { + expect(true).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('coverage/test/coverage-final.json').toExist(); + + const coverageMap = JSON.parse(harness.readFile('coverage/test/coverage-final.json')); + const coveredFiles = Object.keys(coverageMap); + + const hasCliExcluded = coveredFiles.some((f) => f.includes('cli-excluded.ts')); + const hasConfigExcluded = coveredFiles.some((f) => f.includes('config-excluded.ts')); + + expect(hasCliExcluded).withContext('CLI target should be excluded').toBeFalse(); + expect(hasConfigExcluded).withContext('Config file target should be excluded').toBeFalse(); + }); + it('should allow overriding globals to false via runnerConfig file', async () => { harness.useTarget('test', { ...BASE_OPTIONS, @@ -348,6 +416,38 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { // ); }); + it('should warn about performance when "test.exclude" option is in runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + exclude: ['src/app/non-existent.spec.ts'], + }, + }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // TODO: Re-enable once Vite logs are remapped through build system + // expect(logs).toContain( + // jasmine.objectContaining({ + // level: 'warn', + // message: jasmine.stringMatching( + // 'The "test.exclude" option in the Vitest configuration file is evaluated after', + // ), + // }), + // ); + }); + it(`should append "test.setupFiles" (string) from runnerConfig to the CLI's setup`, async () => { harness.useTarget('test', { ...BASE_OPTIONS,