Skip to content

@angular/build:unit-test: with the default jsdom runner and zone.js, an export from a shared chunk is undefined when read during class-field initialisation from a spec with an async test #33728

Description

@jonmarozick

Command

test

Is this a regression?

Not known to be — this is the @angular/build:unit-test builder's default (jsdom) runner; I have no earlier version where this worked.

Description

Under the unit-test builder's default jsdom runner, a module that esbuild places in a chunk shared between spec entry points is wrapped in a lazy __esm initialiser, and a consumer in a different chunk reads its exports as undefined during class-field initialisation. The same specs pass when the runner is switched to browser mode with the same bundles, so this looks like the Node-side module evaluation path rather than a bundling fault.

The distinction that makes it precise:

  • a service reads the shared const inside a method → always correct;
  • a component reads the same const in a class-field initialiserundefined.

The binding is live — replacing the fields with getters (reading the same import later) makes the failing test pass with no other change. So the value arrives; at construction time it is not yet visible to the importer, even though the emitted code calls init_env_config() earlier in the same module body and construction happens later still.

Trigger conditions — each toggled on its own with everything else constant; all four are required:

# Required condition Evidence
1 ≥ 2 spec entries import the module (shared chunk, lazy __esm wrappers) a single spec file inlines everything and passes
2 a consumer in another chunk reads the export during class-field initialisation getters pass
3 the consuming spec file contains an async test callback making that one test synchronous passes; async with no await still fails
4 zone.js is in the unit-test polyfills deleting "polyfills": ["zone.js"] from the unit-test build configuration makes the same suite pass; restoring it fails again

Not required (removed with the failure surviving): TestBed.configureTestingModule() / compileComponents() (a bare TestBed.createComponent() inside an async test fails), provider overrides, barrel files, @angular/core patch level (fails on 21.2.18 and 21.2.19).

Conditions 3 and 4 are one mechanism: with zone.js present the builder downlevels async/await, so a spec containing any async function imports the __async helper — and esbuild then emits that spec entry CommonJS-wrapped, with its entry-local ESM dependencies (the component module) as lazy __esm initialisers:

// spec-…-lead-generator.container.js (dumped with ng test --dump-virtual-files)
import { DEALERS, LeadService, init_env_config, init_lead_service } from "./chunk-N64URYIK.js";
import { __async, __commonJS, __esm } from "./chunk-EFV3TZQO.js";

var LeadGeneratorContainer;
var init_lead_generator_container = __esm({
  "…/lead-generator.container.ts"() {
    init_services();
    init_env_config();                       // runs before the class statement below
    LeadGeneratorContainer = class _LeadGeneratorContainer {
      leadService = inject(LeadService);
      dealers = DEALERS;                     // undefined at construction time
      dealerOptions = this.dealers.map();   // throws
    };
  }
});

var require_lead_generator_container_spec = __commonJS({
  "…/lead-generator.container.spec.ts"(exports) {
    init_lead_generator_container();
    describe("LeadGeneratorContainer", () => {  });
  }
});
export default require_lead_generator_container_spec();

The shared chunk assigns DEALERS inside init_env_config()'s __esm wrapper and exports the binding; the emitted code is valid ESM and runs correctly in browser mode.

One caveat for anyone building a paraphrased fixture: the failure is sensitive to inert content — adding a top-level console.log to either the shared or the importing module defuses it, and one hand-written structural clone of the same shape passes. The linked reproduction is the exact shape that fails deterministically.

Minimal Reproduction

https://github.com/jonmarozick/ng-shared-chunk-repro

npm install
npm test        # ng test --no-watch

Expected: 2 passed. Actual: Test Files 1 failed | 1 passed (2)lead-generator.container.spec.ts fails, lead.service.spec.ts (the control reading the same const in a method) passes.

Nine source files, dependencies are only Angular 21.2.19 + Vitest 4 + jsdom, package-lock.json committed. The module graph:

src/environments/env-config.ts        exports `const DEALERS`
  ├── lead.service.ts                 reads DEALERS in a method
  │     ├── lead.service.spec.ts      ← spec entry 1 (passes)
  │     └── lead-generator.container.ts
  └── lead-generator.container.ts     reads DEALERS in a field initialiser
        └── lead-generator.container.spec.ts   ← spec entry 2 (fails)

Also reproduced by copying these source files unchanged into a fresh ng new workspace on CLI 21.2.19 (@angular/core 21.2.19) with "polyfills": ["zone.js"] added to the unit-test build configuration — so nothing about it depends on the original project. The emitted bundles can be inspected via ng test --dump-virtual-files (they land in .angular/cache/<version>/<project>/unit-test/output-files/).

Exception or Error

TypeError: Cannot read properties of undefined (reading 'map')
 ❯ _LeadGeneratorContainer.<instance_members_initializer> src/app/features/lead-generator/lead-generator.container.ts:291:43
 ❯ new _LeadGeneratorContainer src/app/features/lead-generator/lead-generator.container.ts:336:5
 ❯ NodeInjectorFactory.LeadGeneratorContainer_Factory [as factory] spec-app-features-lead-generator-lead-generator.container.js:1138:16
 ❯ getNodeInjectable packages/core/src/render3/di.ts:785:37
 ❯ instantiateAllDirectives packages/core/src/render3/instructions/shared.ts:122:2
 ❯ createComponentRef packages/core/src/render3/component_ref.ts:301:20
 ❯ create packages/core/testing/src/test_bed.ts:695:44

Your Environment

Angular CLI: 21.2.19
Node: 24.18.0
Package Manager: npm 11.16.0
OS: win32 x64

@angular/build         21.2.19
@angular/cli           21.2.19
@angular/core          21.2.18
@angular/compiler-cli  21.2.18
typescript             5.9.3
vitest                 4.1.10
zone.js                0.16.2
jsdom                  (builder default)

Also reproduced on Linux CI (ubuntu-latest, Node 24) and in a fresh ng new workspace with @angular/core 21.2.19.

Anything else relevant?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions