`@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`
- Lenguaje dominante
- TypeScript
- Estrellas
- 27k
- Forks
- 11.8k
- Merge medio
- 14 h 23 min
- PR fusionados (30 d)
- 162
Descripción
> This is a follow-up to the now-closed/auto-locked [#32754](https://github.com/angular/angular-cli/issues/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`](https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts#L254) 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`](https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts#L73) — 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](https://github.com/analogjs/analog/pull/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](https://github.com/analogjs/analog/pull/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
- [vitest-dev/vitest#8944](https://github.com/vitest-dev/vitest/issues/8944) — vitest 4.0.5 regression that triggers this class of flake.
- [angular/angular-cli#32754](https://github.com/angular/angular-cli/issues/32754) — closed/auto-locked predecessor of this report. Same root cause, different surface symptom, closed as "insufficient information."
- [analogjs/analog#2222](https://github.com/analogjs/analog/issues/2222) — same error stack downstream in analog's vitest-angular stack.
- [analogjs/analog#2244](https://github.com/analogjs/analog/pull/2244) — reference fix pattern.
Guía de contribución
Línea de trabajo
Empieza por packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts y plugins.ts, y después ejecuta repetidamente la suite de jsdom indicada con ng test --force, isolate: false y vitest 4.x. Verifica que el error stale-DomAdapter ya no se produzca entre archivos de spec, conservando el comportamiento de configuración de unit-test del builder; el issue propone el patrón reset-and-reinitialize de analogjs/analog#2244 y señala la documentación como seguimiento secundario.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- angular, typescript
- Área
- build-system, testing-qa
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Tranquilo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 55/100