Skip to content

Commit f28bcfb

Browse files
committed
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
1 parent ae0c03c commit f28bcfb

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function createTestBedInitVirtualFile(
3333
providersFile: string | undefined,
3434
projectSourceRoot: string,
3535
teardown: boolean,
36-
zoneTestingStrategy: 'none' | 'static' | 'dynamic',
36+
zoneTestingStrategy: 'none' | 'static' | 'dynamic' | 'dynamic-zone',
3737
hasLocalize: boolean,
3838
): string {
3939
let providersImport = 'const providers = [];';
@@ -53,6 +53,13 @@ function createTestBedInitVirtualFile(
5353
// It must be imported dynamically to avoid a static dependency on 'zone.js'.
5454
await import('zone.js/testing');
5555
}`;
56+
} else if (zoneTestingStrategy === 'dynamic-zone') {
57+
zoneTestingSnippet = `try {
58+
await import('zone.js');
59+
await import('zone.js/testing');
60+
} catch (e) {
61+
console.error('DYNAMIC IMPORT ERROR:', e);
62+
}`;
5663
}
5764

5865
// The DynamicDOMTestComponentRenderer is used to avoid stale document references
@@ -150,12 +157,12 @@ function adjustOutputHashing(hashing?: OutputHashing): OutputHashing {
150157
*
151158
* @param buildOptions The partial application builder options.
152159
* @param projectSourceRoot The root directory of the project source.
153-
* @returns The resolved zone testing strategy ('none', 'static', 'dynamic').
160+
* @returns The resolved zone testing strategy ('none', 'static', 'dynamic', 'dynamic-zone').
154161
*/
155162
function getZoneTestingStrategy(
156163
buildOptions: Partial<ApplicationBuilderInternalOptions>,
157164
projectSourceRoot: string,
158-
): 'none' | 'static' | 'dynamic' {
165+
): 'none' | 'static' | 'dynamic' | 'dynamic-zone' {
159166
if (buildOptions.polyfills?.includes('zone.js/testing')) {
160167
return 'none';
161168
}
@@ -168,6 +175,12 @@ function getZoneTestingStrategy(
168175
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
169176
projectRequire.resolve('zone.js');
170177

178+
// If polyfills is undefined (e.g. library build target), load zone.js dynamically.
179+
// If polyfills is defined but doesn't include zone.js (e.g. zoneless application), do NOT load zone.js.
180+
if (buildOptions.polyfills === undefined) {
181+
return 'dynamic-zone';
182+
}
183+
171184
return 'dynamic';
172185
} catch {
173186
return 'none';

packages/angular/build/src/builders/unit-test/tests/behavior/vitest-zone-init_spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,48 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
6767
const { result } = await harness.executeOnce();
6868
expect(result?.success).toBe(true);
6969
});
70+
71+
it('should load Zone and Zone testing support when testing a library and zone.js is installed', async () => {
72+
harness.withBuilderTarget(
73+
'build',
74+
async () => ({ success: true }),
75+
{
76+
project: 'ng-package.json',
77+
},
78+
{
79+
builderName: '@angular/build:ng-packagr',
80+
},
81+
);
82+
83+
await harness.writeFile(
84+
'ng-package.json',
85+
JSON.stringify({
86+
lib: {
87+
entryFile: 'src/public-api.ts',
88+
},
89+
}),
90+
);
91+
92+
harness.useTarget('test', {
93+
...BASE_OPTIONS,
94+
include: ['src/app.component.spec.ts'],
95+
});
96+
97+
await harness.writeFile(
98+
'src/app.component.spec.ts',
99+
`
100+
import { describe, it, expect } from 'vitest';
101+
102+
describe('Library Zone Test', () => {
103+
it('should have Zone defined', () => {
104+
expect((globalThis as any).Zone).toBeDefined();
105+
});
106+
});
107+
`,
108+
);
109+
110+
const { result } = await harness.executeOnce();
111+
expect(result?.success).toBeTrue();
112+
});
70113
});
71114
});

0 commit comments

Comments
 (0)