From 1c07bc49a24578913a9b6d5d72cf7924a350b1f5 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:53:18 -0400 Subject: [PATCH] feat(@angular/build): add polyfills option to unit-test builder Adds an optional 'polyfills' array option to the 'unit-test' builder. When omitted, polyfills continue to be inherited from the specified 'buildTarget'. When defined, the test target's 'polyfills' option takes precedence, allowing projects to configure test-specific polyfills or provide polyfills for library test targets that build with '@angular/build:ng-packagr'. --- goldens/public-api/angular/build/index.api.md | 1 + .../build/src/builders/unit-test/builder.ts | 3 + .../build/src/builders/unit-test/options.ts | 2 + .../unit-test/runners/karma/executor.ts | 2 +- .../unit-test/runners/vitest/build-options.ts | 3 +- .../build/src/builders/unit-test/schema.json | 8 ++ .../unit-test/tests/options/polyfills_spec.ts | 96 +++++++++++++++++++ 7 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/options/polyfills_spec.ts diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index c9d83bc6fd9b..b08d70c07e64 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -236,6 +236,7 @@ export type UnitTestBuilderOptions = { isolate?: boolean; listTests?: boolean; outputFile?: string; + polyfills?: string[]; progress?: boolean; providersFile?: string; quiet?: boolean; diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 755b91c40544..dc8751778368 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -323,6 +323,9 @@ export async function* execute( const applicationBuildOptions = { ...buildTargetOptions, ...runnerBuildOptions, + ...(normalizedOptions.polyfills !== undefined + ? { polyfills: normalizedOptions.polyfills } + : {}), watch: normalizedOptions.watch, progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, quiet: normalizedOptions.quiet, diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 44f6b6b9208b..5328a5861219 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -77,6 +77,7 @@ export async function normalizeOptions( runnerConfig, isolate, splitting = true, + polyfills, } = options; if (ui && runner !== Runner.Vitest) { @@ -158,6 +159,7 @@ export async function normalizeOptions( dumpVirtualFiles: options.dumpVirtualFiles, listTests: options.listTests, preserveSymlinks, + polyfills, runnerConfig: typeof runnerConfig === 'string' ? runnerConfig.length === 0 diff --git a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts index 8b82857258b9..e558a3a1f3a7 100644 --- a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts @@ -76,7 +76,7 @@ export class KarmaExecutor implements TestExecutor { const karmaOptions: KarmaBuilderOptions = { karmaConfig, tsConfig: unitTestOptions.tsConfig ?? buildTargetOptions.tsConfig, - polyfills: injectTestingPolyfills(buildTargetOptions.polyfills), + polyfills: injectTestingPolyfills(unitTestOptions.polyfills ?? buildTargetOptions.polyfills), assets: buildTargetOptions.assets, scripts: buildTargetOptions.scripts, styles: buildTargetOptions.styles, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts index 622ca7beebd3..a97cf05cb9e2 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -178,7 +178,7 @@ function getZoneTestingStrategy( return 'dynamic-zone'; } - return 'dynamic'; + return 'none'; } catch { return 'none'; } @@ -256,6 +256,7 @@ export async function getVitestBuildOptions( const buildOptions: Partial = { ...baseBuildOptions, + ...(options.polyfills !== undefined ? { polyfills: options.polyfills } : {}), watch, incrementalResults: watch, index: false, diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 58a9592986b1..7064243c8832 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -265,6 +265,14 @@ "description": "Specifies the path to a TypeScript file that provides an array of Angular providers for the test environment. The file must contain a default export of the provider array.", "minLength": 1 }, + "polyfills": { + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'. If not specified, polyfills will be inherited from the build target.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, "setupFiles": { "type": "array", "items": { diff --git a/packages/angular/build/src/builders/unit-test/tests/options/polyfills_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/polyfills_spec.ts new file mode 100644 index 000000000000..06bc3bd8fc98 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/polyfills_spec.ts @@ -0,0 +1,96 @@ +/** + * @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'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "polyfills"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + }); + + it('should include polyfills specified on test target', async () => { + await harness.writeFiles({ + 'src/custom-polyfill.js': `globalThis['CUSTOM_POLYFILL_RAN'] = true;`, + 'src/app/app.component.spec.ts': ` + import { describe, expect, test } from 'vitest'; + describe('Polyfill Test', () => { + test('should have run test polyfill', () => { + expect((globalThis as any)['CUSTOM_POLYFILL_RAN']).toBe(true); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + polyfills: ['src/custom-polyfill.js'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should override buildTarget polyfills when polyfills is specified on test target', async () => { + setupApplicationTarget(harness, { + polyfills: ['src/app-polyfill.js'], + }); + + await harness.writeFiles({ + 'src/app-polyfill.js': `globalThis['APP_POLYFILL_RAN'] = true;`, + 'src/test-polyfill.js': `globalThis['TEST_POLYFILL_RAN'] = true;`, + 'src/app/app.component.spec.ts': ` + import { describe, expect, test } from 'vitest'; + describe('Polyfill Override Test', () => { + test('should have run test polyfill and not app polyfill', () => { + expect((globalThis as any)['TEST_POLYFILL_RAN']).toBe(true); + expect((globalThis as any)['APP_POLYFILL_RAN']).toBeUndefined(); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + polyfills: ['src/test-polyfill.js'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should allow overriding buildTarget polyfills with an empty array', async () => { + setupApplicationTarget(harness, { + polyfills: ['src/app-polyfill.js'], + }); + + await harness.writeFiles({ + 'src/app-polyfill.js': `globalThis['APP_POLYFILL_RAN'] = true;`, + 'src/app/app.component.spec.ts': ` + import { describe, expect, test } from 'vitest'; + describe('Empty Polyfill Override Test', () => { + test('should not have run app polyfill', () => { + expect((globalThis as any)['APP_POLYFILL_RAN']).toBeUndefined(); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + polyfills: [], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +});