diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts new file mode 100644 index 000000000000..6df583350e07 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts @@ -0,0 +1,55 @@ +/** + * @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 + */ + +/** + * @fileoverview + * This file contains utility functions for finding the Vitest base configuration file. + */ + +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; + +/** + * A list of potential Vitest configuration filenames. + * The order of the files is important as the first one found will be used. + */ +const POTENTIAL_CONFIGS = [ + 'vitest-base.config.ts', + 'vitest-base.config.mts', + 'vitest-base.config.cts', + 'vitest-base.config.js', + 'vitest-base.config.mjs', + 'vitest-base.config.cjs', +]; + +/** + * Finds the Vitest configuration file in the given search directories. + * + * @param searchDirs An array of directories to search for the configuration file. + * @returns The path to the configuration file, or `false` if no file is found. + * Returning `false` is used to disable Vitest's default configuration file search. + */ +export async function findVitestBaseConfig(searchDirs: string[]): Promise { + const uniqueDirs = new Set(searchDirs); + for (const dir of uniqueDirs) { + try { + const entries = await readdir(dir, { withFileTypes: true }); + const files = new Set(entries.filter((e) => e.isFile()).map((e) => e.name)); + + for (const potential of POTENTIAL_CONFIGS) { + if (files.has(potential)) { + return path.join(dir, potential); + } + } + } catch { + // Ignore directories that cannot be read + } + } + + return false; +} 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 4ffebb0ad28b..dee43338338f 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 @@ -22,6 +22,7 @@ import { import { NormalizedUnitTestBuilderOptions } from '../../options'; import type { TestExecutor } from '../api'; import { setupBrowserConfiguration } from './browser-provider'; +import { findVitestBaseConfig } from './configuration'; import { createVitestPlugins } from './plugins'; type VitestCoverageOption = Exclude; @@ -207,11 +208,16 @@ export class VitestExecutor implements TestExecutor { } : {}; + const runnerConfig = this.options.runnerConfig; + return startVitest( 'test', undefined, { - config: this.options.runnerConfig === true ? undefined : this.options.runnerConfig, + config: + runnerConfig === true + ? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot]) + : runnerConfig, root: workspaceRoot, project: ['base', this.projectName], name: 'base', diff --git a/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts index 54e4d9b21d13..b061b88e990c 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts @@ -44,7 +44,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should search for a config file when `true`', async () => { - harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, runnerConfig: true, @@ -57,7 +57,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should ignore config file when `false`', async () => { - harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, runnerConfig: false, @@ -70,9 +70,53 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should ignore config file by default', async () => { + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toNotExist(); + }); + + it('should find and use a `vitest-base.config.mts` in the project root', async () => { + harness.writeFile('vitest-base.config.mts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should find and use a `vitest-base.config.js` in the workspace root', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*`. + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + // The workspace root is the directory containing the project root in the test harness. + harness.writeFile('vitest-base.config.js', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should fallback to in-memory config when no base config is found', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*` + // and when `runnerConfig` is true, it should not fall back to the default search. harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, + runnerConfig: true, }); const { result } = await harness.executeOnce();