From 150c3a77fb9289ca8a11e9f3e23f8409c9fab51c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:41:46 -0400 Subject: [PATCH] feat(@angular/build): add isolate option to unit-test builder This commit adds an `isolate` option to the `@angular/build:unit-test` builder for the Vitest runner. By default, tests run in a non-isolated environment to match the behavior of Karma/Jasmine. This new option allows developers to easily opt-in to Vitest native isolation (running tests in separate threads or processes) without requiring a custom Vitest configuration file. The option is not supported by the Karma runner and will result in an error if used there. --- goldens/public-api/angular/build/index.api.md | 1 + .../build/src/builders/unit-test/options.ts | 8 ++- .../unit-test/runners/vitest/executor.ts | 1 + .../unit-test/runners/vitest/plugins.ts | 3 + .../build/src/builders/unit-test/schema.json | 4 ++ .../unit-test/tests/options/isolate_spec.ts | 58 +++++++++++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/options/isolate_spec.ts diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 4a56a0c4b683..3ca6e8d98d12 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -233,6 +233,7 @@ export type UnitTestBuilderOptions = { filter?: string; headless?: boolean; include?: string[]; + isolate?: boolean; listTests?: boolean; outputFile?: string; progress?: boolean; diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index b2b3d6740ff1..5cd04c4ca7bf 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -54,12 +54,17 @@ export async function normalizeOptions( const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); - const { runner, browsers, progress, filter, browserViewport, ui, runnerConfig } = options; + const { runner, browsers, progress, filter, browserViewport, ui, runnerConfig, isolate } = + options; if (ui && runner !== Runner.Vitest) { throw new Error('The "ui" option is only available for the "vitest" runner.'); } + if (isolate && runner !== Runner.Vitest) { + throw new Error('The "isolate" option is only available for the "vitest" runner.'); + } + const [width, height] = browserViewport?.split('x').map(Number) ?? []; let tsConfig = options.tsConfig; @@ -121,6 +126,7 @@ export async function normalizeOptions( watch, debug: options.debug ?? false, ui: process.env['CI'] ? false : ui, + isolate: isolate ?? false, quiet: options.quiet ?? (process.env['CI'] ? false : true), providersFile: options.providersFile && path.join(workspaceRoot, options.providersFile), setupFiles: options.setupFiles 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 c5b70e9a2487..0a7a3c7ea63b 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 @@ -378,6 +378,7 @@ export class VitestExecutor implements TestExecutor { projectPlugins, include, watch, + isolate: this.options.isolate, }), ], }; 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 59a1e5136456..76cbe7d58d03 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 @@ -54,6 +54,7 @@ interface VitestConfigPluginOptions { include: string[]; optimizeDepsInclude: string[]; watch: boolean; + isolate: boolean; } async function findTestEnvironment( @@ -271,6 +272,8 @@ export async function createVitestConfigPlugin( include, // CLI provider browser options override, if present ...(browser ? { browser } : {}), + // Only override if the user explicitly enabled it via CLI + ...(options.isolate ? { isolate: true } : {}), // If the user has not specified an environment, use a smart default. ...(!testConfig?.environment ? { environment: await findTestEnvironment(projectResolver) } diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 403b61a9009b..d891c3b7928d 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -73,6 +73,10 @@ "type": "boolean", "description": "Enables the Vitest UI for interactive test execution. This option is only available for the Vitest runner." }, + "isolate": { + "type": "boolean", + "description": "Enables isolation for test execution. When true, Vitest runs tests in separate threads or processes. This option is only available for the Vitest runner. Defaults to false to align with the Karma/Jasmine experience." + }, "quiet": { "type": "boolean", "description": "Suppresses the verbose build summary and stats table on each rebuild. Defaults to `true` locally and `false` in CI environments." diff --git a/packages/angular/build/src/builders/unit-test/tests/options/isolate_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/isolate_spec.ts new file mode 100644 index 000000000000..6ca00a02d94d --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/isolate_spec.ts @@ -0,0 +1,58 @@ +/** + * @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, + expectLog, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "isolate"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + }); + + it('should fail when isolate is true and runner is karma', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runner: 'karma' as any, + isolate: true, + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeFalse(); + expectLog(logs, /The "isolate" option is only available for the "vitest" runner/); + }); + + it('should run tests successfully when isolate is true and runner is vitest', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runner: 'vitest' as any, + isolate: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should run tests successfully when isolate is false and runner is vitest', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runner: 'vitest' as any, + isolate: false, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +});