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(); + }); }); });