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?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions