From 06359f1d7cff8601f899572bdba9920ba30ca3d7 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:28:16 -0400 Subject: [PATCH] fix(@angular/build): load zone.js dynamically for library unit tests When unit testing library targets with the unit-test builder, the build target (ng-packagr) does not have a 'polyfills' configuration. This caused the builder to fall back to the 'dynamic' zone testing strategy, which only dynamically imports zone.js/testing at runtime if Zone is already defined. Since the main zone.js library was never loaded, Zone was undefined, and library tests relying on TestBed/fakeAsync failed. This commit introduces a new 'dynamic-zone' zone testing strategy. If polyfills is undefined (meaning we are running a library target) and zone.js is installed/resolvable, both zone.js and zone.js/testing are dynamically imported at startup, resolving the failure. Fixes #33477 --- .../unit-test/runners/vitest/build-options.ts | 16 +++++-- .../tests/behavior/vitest-zone-init_spec.ts | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) 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 23b5f5024de6..0ccb6195799e 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 @@ -33,7 +33,7 @@ function createTestBedInitVirtualFile( providersFile: string | undefined, projectSourceRoot: string, teardown: boolean, - zoneTestingStrategy: 'none' | 'static' | 'dynamic', + zoneTestingStrategy: 'none' | 'static' | 'dynamic' | 'dynamic-zone', hasLocalize: boolean, ): string { let providersImport = 'const providers = [];'; @@ -53,6 +53,10 @@ function createTestBedInitVirtualFile( // It must be imported dynamically to avoid a static dependency on 'zone.js'. await import('zone.js/testing'); }`; + } else if (zoneTestingStrategy === 'dynamic-zone') { + zoneTestingSnippet = ` + await import('zone.js'); + await import('zone.js/testing');`; } // The DynamicDOMTestComponentRenderer is used to avoid stale document references @@ -150,12 +154,12 @@ function adjustOutputHashing(hashing?: OutputHashing): OutputHashing { * * @param buildOptions The partial application builder options. * @param projectSourceRoot The root directory of the project source. - * @returns The resolved zone testing strategy ('none', 'static', 'dynamic'). + * @returns The resolved zone testing strategy ('none', 'static', 'dynamic', 'dynamic-zone'). */ function getZoneTestingStrategy( buildOptions: Partial, projectSourceRoot: string, -): 'none' | 'static' | 'dynamic' { +): 'none' | 'static' | 'dynamic' | 'dynamic-zone' { if (buildOptions.polyfills?.includes('zone.js/testing')) { return 'none'; } @@ -168,6 +172,12 @@ function getZoneTestingStrategy( const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json')); projectRequire.resolve('zone.js'); + // If polyfills is undefined (e.g. library build target), load zone.js dynamically. + // If polyfills is defined but doesn't include zone.js (e.g. zoneless application), do NOT load zone.js. + if (buildOptions.polyfills === undefined) { + return 'dynamic-zone'; + } + return 'dynamic'; } catch { return 'none'; diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-zone-init_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-zone-init_spec.ts index 812dba7fa70d..3caf15a2cf3f 100644 --- a/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-zone-init_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-zone-init_spec.ts @@ -67,5 +67,48 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBe(true); }); + + it('should load Zone and Zone testing support when testing a library and zone.js is installed', async () => { + harness.withBuilderTarget( + 'build', + async () => ({ success: true }), + { + project: 'ng-package.json', + }, + { + builderName: '@angular/build:ng-packagr', + }, + ); + + await harness.writeFile( + 'ng-package.json', + JSON.stringify({ + lib: { + entryFile: 'src/public-api.ts', + }, + }), + ); + + harness.useTarget('test', { + ...BASE_OPTIONS, + include: ['src/app.component.spec.ts'], + }); + + await harness.writeFile( + 'src/app.component.spec.ts', + ` + import { describe, it, expect } from 'vitest'; + + describe('Library Zone Test', () => { + it('should have Zone defined', () => { + expect((globalThis as any).Zone).toBeDefined(); + }); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); }); });