Skip to content
Merged
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 @@ -235,6 +235,7 @@ export type UnitTestBuilderOptions = {
providersFile?: string;
reporters?: SchemaReporter[];
runner?: Runner;
runnerConfig?: RunnerConfig;
setupFiles?: string[];
tsConfig?: string;
ui?: boolean;
Expand Down
4 changes: 3 additions & 1 deletion packages/angular/build/src/builders/unit-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function normalizeOptions(
const buildTargetSpecifier = options.buildTarget ?? `::development`;
const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build');

const { runner, browsers, progress, filter, browserViewport, ui } = options;
const { runner, browsers, progress, filter, browserViewport, ui, runnerConfig } = options;

if (ui && runner !== 'vitest') {
throw new Error('The "ui" option is only available for the "vitest" runner.');
Expand Down Expand Up @@ -127,6 +127,8 @@ export async function normalizeOptions(
: [],
dumpVirtualFiles: options.dumpVirtualFiles,
listTests: options.listTests,
runnerConfig:
typeof runnerConfig === 'string' ? path.join(workspaceRoot, runnerConfig) : runnerConfig,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import fs from 'node:fs/promises';
import path from 'node:path';
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
import type { KarmaBuilderOptions, KarmaBuilderTransformsOptions } from '../../../karma';
import { NormalizedUnitTestBuilderOptions } from '../../options';
Expand Down Expand Up @@ -50,7 +52,23 @@ export class KarmaExecutor implements TestExecutor {
await context.getBuilderNameForTarget(unitTestOptions.buildTarget),
)) as unknown as ApplicationBuilderInternalOptions;

let karmaConfig: string | undefined;
if (typeof unitTestOptions.runnerConfig === 'string') {
karmaConfig = unitTestOptions.runnerConfig;
context.logger.info(`Using Karma configuration file: ${karmaConfig}`);
} else if (unitTestOptions.runnerConfig) {
const potentialPath = path.join(unitTestOptions.projectRoot, 'karma.conf.js');
try {
await fs.access(potentialPath);
karmaConfig = potentialPath;
context.logger.info(`Using Karma configuration file: ${karmaConfig}`);
} catch {
context.logger.info('No Karma configuration file found. Using default configuration.');
}
}

const karmaOptions: KarmaBuilderOptions = {
karmaConfig,
tsConfig: unitTestOptions.tsConfig ?? buildTargetOptions.tsConfig,
polyfills: buildTargetOptions.polyfills,
assets: buildTargetOptions.assets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export class VitestExecutor implements TestExecutor {
browserViewport,
ui,
} = this.options;

let vitestNodeModule;
try {
vitestNodeModule = await import('vitest/node');
Expand Down Expand Up @@ -192,21 +193,22 @@ export class VitestExecutor implements TestExecutor {
'test',
undefined,
{
// Disable configuration file resolution/loading
config: false,
config: this.options.runnerConfig === true ? undefined : this.options.runnerConfig,
root: workspaceRoot,
project: ['base', this.projectName],
name: 'base',
include: [],
testNamePattern: this.options.filter,
reporters: reporters ?? ['default'],
outputFile,
watch,
ui,
coverage: await generateCoverageOption(coverage, this.projectName),
...debugOptions,
},
{
test: {
coverage: await generateCoverageOption(coverage, this.projectName),
outputFile,
...debugOptions,
...(reporters ? { reporters } : {}),
},
server: {
// Disable the actual file watcher. The boolean watch option above should still
// be enabled as it controls other internal behavior related to rerunning tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ const VitestTestRunner: TestRunner = {
const projectName = context.target?.project;
assert(projectName, 'The builder requires a target.');

if (typeof options.runnerConfig === 'string') {
context.logger.info(`Using Vitest configuration file: ${options.runnerConfig}`);
} else if (options.runnerConfig) {
context.logger.info('Automatically searching for and using Vitest configuration file.');
}

return new VitestExecutor(projectName, options, testEntryPointMappings);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,11 @@ export function createVitestPlugins(
root: workspaceRoot,
globals: true,
setupFiles: testSetupFiles,
// Use `jsdom` if no browsers are explicitly configured.
// `node` is effectively no "environment" and the default.
environment: browserOptions.browser ? 'node' : 'jsdom',
browser: browserOptions.browser,
include: options.include,
...(options.exclude ? { exclude: options.exclude } : {}),
browser: browserOptions.browser,
// Use `jsdom` if no browsers are explicitly configured.
...(browserOptions.browser ? {} : { environment: 'jsdom' }),
},
plugins: [
{
Expand Down
5 changes: 5 additions & 0 deletions packages/angular/build/src/builders/unit-test/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"default": "vitest",
"enum": ["karma", "vitest"]
},
"runnerConfig": {
"type": ["boolean", "string"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Not sure how to handle this, but having type both boolean or string will make runnerConfig config not possible to be used from the CLI. As options cannot be either boolean or string.

"description": "Specifies the configuration file for the selected test runner. If a string is provided, it will be used as the path to the configuration file. If `true`, the builder will search for a default configuration file (e.g., `vitest.config.ts` or `karma.conf.js`). If `false`, no external configuration file will be used.\\nFor Vitest, this enables advanced options and the use of custom plugins. Please note that while the file is loaded, the Angular team does not provide direct support for its specific contents or any third-party plugins used within it.",
"default": false
},
"browsers": {
"description": "Specifies the browsers to use for test execution. When not specified, tests are run in a Node.js environment using jsdom. For both Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode.",
"type": "array",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @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 { execute } from '../../index';
import {
BASE_OPTIONS,
describeBuilder,
UNIT_TEST_BUILDER_INFO,
setupApplicationTarget,
} from '../setup';

const VITEST_CONFIG_CONTENT = `
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: [['junit', { outputFile: './vitest-results.xml' }]],
},
});
`;

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Option: "runnerConfig"', () => {
beforeEach(() => {
setupApplicationTarget(harness);
});

describe('Vitest Runner', () => {
it('should use a specified config file path', async () => {
harness.writeFile('custom-vitest.config.ts', VITEST_CONFIG_CONTENT);
harness.useTarget('test', {
...BASE_OPTIONS,
runnerConfig: 'custom-vitest.config.ts',
});

const { result } = await harness.executeOnce();

expect(result?.success).toBeTrue();
harness.expectFile('vitest-results.xml').toExist();
});

it('should search for a config file when `true`', async () => {
harness.writeFile('vitest.config.ts', 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 ignore config file when `false`', async () => {
harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT);
harness.useTarget('test', {
...BASE_OPTIONS,
runnerConfig: false,
});

const { result } = await harness.executeOnce();

expect(result?.success).toBeTrue();
harness.expectFile('vitest-results.xml').toNotExist();
});

it('should ignore config file by default', async () => {
harness.writeFile('vitest.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();
});
});
});
});