Skip to content
Open
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
1 change: 1 addition & 0 deletions goldens/public-api/angular/build/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export type UnitTestBuilderOptions = {
quiet?: boolean;
reporters?: SchemaReporter[];
runner?: Runner;
runnerArgs?: string[];
runnerConfig?: RunnerConfig;
setupFiles?: string[];
splitting?: boolean;
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ ts_project(
":node_modules/@babel/core",
":node_modules/@parcel/watcher",
":node_modules/oxc-parser",
":node_modules/vitest",
"//:node_modules/@angular/compiler-cli",
"//:node_modules/@angular/router",
"//:node_modules/@types/jasmine",
Expand Down
6 changes: 6 additions & 0 deletions packages/angular/build/src/builders/unit-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export async function normalizeOptions(
browserViewport,
ui,
runnerConfig,
runnerArgs,
isolate,
splitting = true,
polyfills,
Expand All @@ -92,6 +93,10 @@ export async function normalizeOptions(
throw new Error('The "isolate" option is only available for the "vitest" runner.');
}

if (runnerArgs?.length && (runner ?? Runner.Vitest) !== Runner.Vitest) {
throw new Error('The "runnerArgs" option is only available for the "vitest" runner.');
}

const [width, height] = browserViewport?.split('x').map(Number) ?? [];

let tsConfig = options.tsConfig;
Expand Down Expand Up @@ -170,6 +175,7 @@ export async function normalizeOptions(
? true
: path.resolve(workspaceRoot, runnerConfig)
: runnerConfig,
runnerArgs: runnerArgs?.length ? runnerArgs : undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @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
*/

import type { CliOptions } from 'vitest/node';
import type { NormalizedUnitTestBuilderOptions } from '../../options';

/**
* The subset of the normalized builder options that are used to create the Vitest CLI options.
*/
export interface VitestCliOptionsSource extends Pick<
NormalizedUnitTestBuilderOptions,
'workspaceRoot' | 'watch' | 'outputFile' | 'filter' | 'ui' | 'debug'
> {
cacheOptions: Pick<NormalizedUnitTestBuilderOptions['cacheOptions'], 'enabled'>;
}

/**
* Creates the Vitest CLI level options from the normalized builder options.
* These are the options that Vitest would otherwise receive from its own command line.
*
* @param options The normalized builder options to convert.
* @param project The name of the Vitest project to execute.
* @param configPath The path to an external Vitest configuration file, if any.
* @param runnerArgOptions The Vitest CLI options from the `runnerArgs` builder option, if any.
* The options created from the other builder options take precedence over these.
* @returns The Vitest CLI options.
*/
export function createVitestCliOptions(
options: VitestCliOptionsSource,
project: string,
configPath: string | false | undefined,
runnerArgOptions?: CliOptions,
): CliOptions {
const { workspaceRoot, watch, outputFile, cacheOptions, filter, ui, debug } = options;

return {
...runnerArgOptions,
config: configPath,
root: workspaceRoot,
project,
watch,
...(outputFile !== undefined ? { outputFile } : {}),
...(cacheOptions.enabled ? {} : { cache: false as const }),
...(filter !== undefined ? { testNamePattern: filter } : {}),
...(typeof ui === 'boolean' ? { ui } : {}),
...(debug ? { inspectBrk: true, isolate: false, fileParallelism: false } : {}),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @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
*/

import { type VitestCliOptionsSource, createVitestCliOptions } from './cli-options';

describe('createVitestCliOptions', () => {
const baseOptions: VitestCliOptionsSource = {
workspaceRoot: '/workspace',
watch: false,
outputFile: undefined,
cacheOptions: { enabled: true },
filter: undefined,
ui: undefined,
debug: false,
};

it('should provide the root, project, watch mode, and configuration file', () => {
expect(createVitestCliOptions(baseOptions, 'app', '/workspace/vitest-base.config.mts')).toEqual(
{
config: '/workspace/vitest-base.config.mts',
root: '/workspace',
project: 'app',
watch: false,
},
);
});

it('should disable configuration file lookup when no configuration file is used', () => {
expect(createVitestCliOptions(baseOptions, 'app', false).config).toBeFalse();
});

it('should only include the output file when it is defined', () => {
expect('outputFile' in createVitestCliOptions(baseOptions, 'app', false)).toBeFalse();
expect(
createVitestCliOptions({ ...baseOptions, outputFile: 'results.json' }, 'app', false)
.outputFile,
).toBe('results.json');
});

it('should disable the Vitest cache when the Angular CLI cache is disabled', () => {
expect('cache' in createVitestCliOptions(baseOptions, 'app', false)).toBeFalse();
expect(
createVitestCliOptions({ ...baseOptions, cacheOptions: { enabled: false } }, 'app', false)
.cache,
).toBeFalse();
});

it('should map the filter option to the test name pattern', () => {
expect('testNamePattern' in createVitestCliOptions(baseOptions, 'app', false)).toBeFalse();
expect(
createVitestCliOptions({ ...baseOptions, filter: 'should work' }, 'app', false)
.testNamePattern,
).toBe('should work');
});

it('should only include the UI option when it is a boolean', () => {
expect('ui' in createVitestCliOptions(baseOptions, 'app', false)).toBeFalse();
expect(createVitestCliOptions({ ...baseOptions, ui: false }, 'app', false).ui).toBeFalse();
expect(createVitestCliOptions({ ...baseOptions, ui: true }, 'app', false).ui).toBeTrue();
});

it('should configure the inspector and disable parallelism when debugging', () => {
const options = createVitestCliOptions({ ...baseOptions, debug: true }, 'app', false);

expect(options.inspectBrk).toBeTrue();
expect(options.isolate).toBeFalse();
expect(options.fileParallelism).toBeFalse();
});

it('should include the options from the runner arguments', () => {
const options = createVitestCliOptions(baseOptions, 'app', false, { update: true, bail: 1 });

expect(options.update).toBeTrue();
expect(options.bail).toBe(1);
});

it('should prefer the builder options over the options from the runner arguments', () => {
const options = createVitestCliOptions({ ...baseOptions, filter: 'builder' }, 'app', false, {
root: '/other',
watch: true,
testNamePattern: 'runner',
});

expect(options.root).toBe('/workspace');
expect(options.watch).toBeFalse();
expect(options.testNamePattern).toBe('builder');
});

it('should not configure the inspector when not debugging', () => {
const options = createVitestCliOptions(baseOptions, 'app', false);

expect('inspectBrk' in options).toBeFalse();
expect('isolate' in options).toBeFalse();
expect('fileParallelism' in options).toBeFalse();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import {
import { NormalizedUnitTestBuilderOptions } from '../../options';
import type { TestExecutor } from '../api';
import { setupBrowserConfiguration } from './browser-provider';
import { createVitestCliOptions } from './cli-options';
import { findVitestBaseConfig } from './configuration';
import { createVitestConfigPlugin, createVitestPlugins } from './plugins';
import type { RunnerArgOptions } from './runner-args';

enum DebugLogLevel {
Info = 1,
Expand All @@ -43,6 +45,7 @@ export class VitestExecutor implements TestExecutor {
private readonly projectName: string;
private readonly options: NormalizedUnitTestBuilderOptions;
private readonly logger: BuilderContext['logger'];
private readonly runnerArgOptions: RunnerArgOptions | undefined;
private readonly buildResultFiles = new Map<string, ResultFile>();
private readonly externalMetadata: DevServerExternalResultMetadata = {
implicitBrowser: [],
Expand All @@ -64,13 +67,15 @@ export class VitestExecutor implements TestExecutor {
options: NormalizedUnitTestBuilderOptions,
testEntryPointMappings: Map<string, string> | undefined,
logger: BuilderContext['logger'],
runnerArgOptions?: RunnerArgOptions,
) {
const level = parseInt(process.env['NG_TEST_LOG'] ?? '0', 10);
this.debugLevel = isNaN(level) ? 0 : level;

this.projectName = projectName;
this.options = options;
this.logger = logger;
this.runnerArgOptions = runnerArgOptions;

this.debugLog(DebugLogLevel.Info, 'VitestExecutor instantiated.');
this.debugLog(DebugLogLevel.Verbose, 'NormalizedUnitTestBuilderOptions:', options);
Expand Down Expand Up @@ -266,13 +271,11 @@ export class VitestExecutor implements TestExecutor {
const {
coverage,
reporters,
outputFile,
workspaceRoot,
browsers,
debug,
watch,
browserViewport,
ui,
projectRoot,
runnerConfig,
projectSourceRoot,
Expand Down Expand Up @@ -342,14 +345,6 @@ export class VitestExecutor implements TestExecutor {
setupFiles: testSetupFiles,
});

const debugOptions = debug
? {
inspectBrk: true,
isolate: false,
fileParallelism: false,
}
: {};

const externalConfigPath =
runnerConfig === true
? await findVitestBaseConfig([projectRoot, workspaceRoot])
Expand Down Expand Up @@ -389,17 +384,12 @@ export class VitestExecutor implements TestExecutor {
}
this.debugLog(DebugLogLevel.Verbose, 'Included test files (after filtering):', include);

const vitestConfig = {
config: externalConfigPath,
root: workspaceRoot,
const vitestConfig = createVitestCliOptions(
this.options,
project,
watch,
...(outputFile !== undefined ? { outputFile } : {}),
...(cacheOptions.enabled ? {} : { cache: false as const }),
...(this.options.filter !== undefined ? { testNamePattern: this.options.filter } : {}),
...(typeof ui === 'boolean' ? { ui } : {}),
...debugOptions,
};
externalConfigPath,
this.runnerArgOptions?.cli,
);
const vitestServerConfig = {
// Note `.vitest` is auto appended to the path.
cacheDir: cacheOptions.path,
Expand All @@ -418,6 +408,7 @@ export class VitestExecutor implements TestExecutor {
watch,
isolate: this.options.isolate,
preserveSymlinks: this.options.preserveSymlinks,
runnerArgOptions: this.runnerArgOptions?.project,
}),
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { TestRunner } from '../api';
import { DependencyChecker } from '../dependency-checker';
import { getVitestBuildOptions } from './build-options';
import { VitestExecutor } from './executor';
import { type RunnerArgOptions, parseRunnerArgs } from './runner-args';

/**
* A declarative definition of the Vitest test runner.
Expand Down Expand Up @@ -79,7 +80,20 @@ const VitestTestRunner: TestRunner = {
context.logger.info('Automatically searching for and using Vitest configuration file.');
}

return new VitestExecutor(projectName, options, testEntryPointMappings, context.logger);
// Parse and validate any additional Vitest arguments prior to the build to fail fast.
let runnerArgOptions: RunnerArgOptions | undefined;
if (options.runnerArgs) {
const { parseCLI } = await import('vitest/node');
runnerArgOptions = parseRunnerArgs(options.runnerArgs, parseCLI);
}

return new VitestExecutor(
projectName,
options,
testEntryPointMappings,
context.logger,
runnerArgOptions,
);
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ interface VitestConfigPluginOptions {
watch: boolean;
isolate: boolean | undefined;
preserveSymlinks?: boolean;

/** Vitest project options from the `runnerArgs` builder option. */
runnerArgOptions?: Record<string, unknown>;
}

async function findTestEnvironment(
Expand Down Expand Up @@ -307,8 +310,13 @@ export async function createVitestConfigPlugin(
resolve,
};

// The order of precedence from lowest to highest is the builder defaults, the Vitest
// configuration file, the `runnerArgs` builder option, and then the other builder options.
const projectBase = mergeConfig(projectDefaults, testConfig ? { test: testConfig } : {});
const projectConfig = mergeConfig(projectBase, projectOverrides);
const projectWithRunnerArgs = options.runnerArgOptions
? mergeConfig(projectBase, { test: options.runnerArgOptions })
: projectBase;
const projectConfig = mergeConfig(projectWithRunnerArgs, projectOverrides);

return {
test: {
Expand Down
Loading
Loading