Skip to content

Commit 210b613

Browse files
dgp1130angular-robot[bot]
authored andcommitted
refactor: configure Zone.js for Jest tests
This configures polyfills to set up the environment before executing Jest tests. We need to do three things: 1. Set the global `jest` symbol. Jest executing in ESM does not provide the `jest` global and users are expected to import from `@jest/globals` or `import.meta.jest`. Zone.js is not compatible with this yet, so we need to manually define the `jest` global for Zone to read it. 2. Run user polyfills, (typically including `zone.js` and `zone.js/testing`). Zone reads the `jest` global to recognize the environment it is in and patch the relevant functions to load fake async properly. Users can override this part if they are building a Zoneless application or have custom polyfills for other browser functionality. 3. Initalize `TestBed`. This configures the `TestBed` environment so users don't have to manually configure it for each test file. Ordering is very important for these operations, which complicates the implementation somewhat. `zone.js/testing` does not include an import on `zone.js`, meaning there was no guarnatee the bundler would sort their executions in the correct order. Similarly, `zone.js` does not import anything from Jest, so it is not trivial to inject the `globalThis.jest = import.meta.jest;` line before Zone loads. Even setting polyfills to `[jestGlobal, 'zone.js, 'zone.js/testing', initTestBed]` doesn't work because code splitting rearranges the order of operations in an incompatible way. Instead, these are implemented as distinct entry points in `browser-esbuild` with Jest's `--setupFilesAfterEnv` option executing them in the correct order. Ideally, we could drop the global initialization altogether once Zone.js knows to look for `import.meta.jest` in an ESM context. Also we might be able to reduce down to a single polyfills entry point if `zone.js/testing` had an import on `zone.js` to apply correct ordering.
1 parent 97c0cf8 commit 210b613

File tree

4 files changed

+61
-9
lines changed

4 files changed

+61
-9
lines changed

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ ts_library(
9696
"builders.json",
9797
"src/**/schema.json",
9898
"src/**/*.js",
99+
"src/**/*.mjs",
99100
"src/**/*.html",
100101
],
101102
),

packages/angular_devkit/build_angular/src/builders/jest/index.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@ export default createBuilder(
5656

5757
// Build all the test files.
5858
const testFiles = await findTestFiles(options, context.workspaceRoot);
59+
const jestGlobal = path.join(__dirname, 'jest-global.mjs');
60+
const initTestBed = path.join(__dirname, 'init-test-bed.mjs');
5961
const buildResult = await build(context, {
60-
entryPoints: testFiles,
62+
// Build all the test files and also the `jest-global` and `init-test-bed` scripts.
63+
entryPoints: new Set([...testFiles, jestGlobal, initTestBed]),
6164
tsConfig: options.tsConfig,
62-
polyfills: options.polyfills,
65+
polyfills: options.polyfills ?? ['zone.js', 'zone.js/testing'],
6366
outputPath: testOut,
6467
aot: false,
6568
index: null,
@@ -83,21 +86,32 @@ export default createBuilder(
8386
'--experimental-vm-modules',
8487
jest,
8588

86-
`--rootDir=${testOut}`,
89+
`--rootDir="${testOut}"`,
8790
'--testEnvironment=jsdom',
8891

8992
// TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it.
9093
'--no-cache',
9194

9295
// Run basically all files in the output directory, any excluded files were already dropped by the build.
93-
`--testMatch=${path.join('<rootDir>', '**', '*.mjs')}`,
94-
95-
// Load polyfills before each test, and don't run them directly as a test.
96-
`--setupFilesAfterEnv=${path.join('<rootDir>', 'polyfills.mjs')}`,
97-
`--testPathIgnorePatterns=${path.join('<rootDir>', 'polyfills\\.mjs')}`,
96+
`--testMatch="<rootDir>/**/*.mjs"`,
97+
98+
// Load polyfills and initialize the environment before executing each test file.
99+
// IMPORTANT: Order matters here.
100+
// First, we execute `jest-global.mjs` to initialize the `jest` global variable.
101+
// Second, we execute user polyfills, including `zone.js` and `zone.js/testing`. This is dependent on the Jest global so it can patch
102+
// the environment for fake async to work correctly.
103+
// Third, we initialize `TestBed`. This is dependent on fake async being set up correctly beforehand.
104+
`--setupFilesAfterEnv="<rootDir>/jest-global.mjs"`,
105+
...(options.polyfills ? [`--setupFilesAfterEnv="<rootDir>/polyfills.mjs"`] : []),
106+
`--setupFilesAfterEnv="<rootDir>/init-test-bed.mjs"`,
107+
108+
// Don't run any infrastructure files as tests, they are manually loaded where needed.
109+
`--testPathIgnorePatterns="<rootDir>/jest-global\\.mjs"`,
110+
...(options.polyfills ? [`--testPathIgnorePatterns="<rootDir>/polyfills\\.mjs"`] : []),
111+
`--testPathIgnorePatterns="<rootDir>/init-test-bed\\.mjs"`,
98112

99113
// Skip shared chunks, as they are not entry points to tests.
100-
`--testPathIgnorePatterns=${path.join('<rootDir>', 'chunk-.*\\.mjs')}`,
114+
`--testPathIgnorePatterns="<rootDir>/chunk-.*\\.mjs"`,
101115

102116
// Optionally enable color.
103117
...(colors.enabled ? ['--colors'] : []),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
// TODO(dgp1130): These imports likely don't resolve in stricter package environments like `pnpm`, since they are resolved relative to
10+
// `@angular-devkit/build-angular` rather than the user's workspace. Should look into virtual modules to support those use cases.
11+
12+
import { getTestBed } from '@angular/core/testing';
13+
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
14+
15+
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
16+
errorOnUnknownElements: true,
17+
errorOnUnknownProperties: true,
18+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* @fileoverview Zone.js requires the `jest` global to be initialized in order to know that it must patch the environment to support Jest
11+
* execution. When running ESM code, Jest does _not_ inject the global `jest` symbol, so Zone.js would not normally know it is running
12+
* within Jest as users are supposed to import from `@jest/globals` or use `import.meta.jest`. Zone.js is not currently aware of this, so we
13+
* manually set this global to get Zone.js to run correctly.
14+
*
15+
* TODO(dgp1130): Update Zone.js to directly support Jest ESM executions so we can drop this.
16+
*/
17+
18+
// eslint-disable-next-line no-undef
19+
globalThis.jest = import.meta.jest;

0 commit comments

Comments
 (0)