`@angular/build:unit-test` virtual `init-testbed.js` guards `initTestEnvironment()` behind a once-per-worker symbol → stale DomAdapter under vitest ≥4.0.5 + `isolate: false`

Open
#33,047 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
55/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
angular, typescript

Research direction

Start with packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts and plugins.ts, then run the reported jsdom suite repeatedly with ng test --force, isolate: false, and vitest 4.x. Verify the stale-DomAdapter failure no longer occurs across spec files while preserving the builder's unit-test setup behavior; the issue proposes the analogjs/analog#2244 reset-and-reinitialize pattern and notes documentation as a secondary follow-up.

Written by the indexing model from the issue text.

Description

angular/build:unit-test area: @angular/build gemini-triaged

This is a follow-up to the now-closed/auto-locked #32754 with the missing details. That issue was dismissed as potentially analog-specific because the reporter had @analogjs/vitest-angular in their deps. This report uses @angular/build:unit-test only and pins the bug to specific lines in @angular/build's own source.

Which @angular/* package(s) are the source of the bug?

@angular/build

Is this a regression?

Yes — surfaces with vitest ≥4.0.5 (see vitest-dev/vitest#8944).

Description

@angular/build:unit-test's vitest runner combines two defaults that interact badly under vitest ≥4.0.5:

  • plugins.ts:254 hardcodes isolate: false for the vitest pool (intentional, "align with the Karma/Jasmine experience"). Worker module graphs are therefore reused across spec files.
  • build-options.ts:73-89 — the injected virtual init-testbed.js wraps getTestBed().initTestEnvironment(...) in an if (!globalThis[ANGULAR_TESTBED_SETUP]) guard. The comment on line 79 explicitly says "the guard condition above ensures that the setup is only performed once". That's the anti-pattern.

The first spec file in a worker initializes a platformBrowserTesting whose DomAdapter captures the jsdom document as a closure reference. Subsequent spec files in the same worker skip the init block entirely, so the DomAdapter keeps being reused. When jsdom's document swaps between spec files — and vitest ≥4.0.5 no longer re-executes setup files between spec files under isolate: false (vitest-dev/vitest#8944) — _getDOM().getDefaultDocument().createElement(tagName) returns something that is not a real HTMLElement, and DOMTestComponentRenderer.insertRootElement crashes:

```
TypeError: rootElement.setAttribute is not a function
at DOMTestComponentRenderer.insertRootElement (@angular/platform-browser/testing)
at _TestBedImpl.createComponent
```

Different spec files fail each run; each failing spec passes in isolation. Classic test-isolation bug.

The analog project had the analogous pattern in setupTestBed() and fixed it in analogjs/analog#2244 by calling resetTestEnvironment() + initTestEnvironment() on every setup invocation instead of guarding with a once-only singleton.

Reproduction

I can put up a public minimal repro if helpful, but the bug is visible from the builder source alone — the conditions are:

  • Angular 22 (or 21 with vitest ≥4.0.5) monorepo using @angular/build:unit-test with jsdom.
  • runnerConfig unset, so the builder's default isolate: false applies.
  • Suite of ~50+ spec files to make the race frequent.
  • Run ng test --force (or equivalent) several times. A different 1–10 specs crash each run with the setAttribute trace.

Confirmed environment:

```
Angular CLI: 22.0.0-next.6
@angular/build: 22.0.0-next.6
@angular/core: 22.0.0-next.9
vitest: 4.1.4 (via ^4.0.17)
Environment: jsdom
Runtime: Node 22 / Bun 1.x
OS: Windows 11 (also reported on Ubuntu CI via analogjs/analog#2222)
```

Exception

```
TypeError: rootElement.setAttribute is not a function
❯ DOMTestComponentRenderer.insertRootElement node_modules/@angular/platform-browser/fesm2022/testing.mjs:24
❯ _TestBedImpl.createComponent packages/core/testing/src/test_bed.ts:420
```

(The #32754 variant Cannot set base providers because it has already been called is the same root cause but a different downstream symptom — it fires when the user's own test-setup.ts re-calls initTestEnvironment. Projects that don't re-call it land on setAttribute is not a function instead.)

Proposed fix

Primary — mirror analogjs/analog#2244. Replace the if (!globalThis[ANGULAR_TESTBED_SETUP]) guard in build-options.ts:73-89 with a reset-and-reinit pattern:

```ts
getTestBed().resetTestEnvironment();
getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
// ...
});
```

Even under isolate: false, if the setup file re-runs (or a user hook calls it), each spec file gets a fresh platform targeting the current jsdom.

Secondary (defensive) — either flip the default to isolate: true with a documented opt-out for projects that want the Karma-style speed, or make DOMTestComponentRenderer.insertRootElement throw a clearer error when rootElement.setAttribute is not callable (e.g. "TestBed's DOM adapter is referencing a document that has been torn down — check your vitest `isolate` setting"). Today the TypeError has no breadcrumb to the root cause.

Docs — the unit-test builder docs should warn that isolate: false + vitest ≥4.0.5 + jsdom silently bleeds DOM state across spec files.

Local workaround

Override to isolate: true via a project-level vitest-base.config.ts (possible because runnerConfig: true merges user config on top of the builder defaults). 10 consecutive ng test --force runs then pass deterministically. Wall-time cost is ~10–30%. This is a workaround, not a fix — the builder's guard is what should change.

References
Dominant language
TypeScript
Stars
27k
Forks
11.8k
Avg merge
16h 35m
Merged PRs (30d)
176

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from angular/angular-cli

All issues in angular/angular-cli

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.