`@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`

未關閉
#33,047 0 則留言 1 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

評估

難度
4/5
預估耗時
3-5 天
新手友好度
55/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
冷清
技術堆疊
angular, typescript

研究方向

從 packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts 和 plugins.ts 開始,然後使用 ng test --force、isolate: false 和 vitest 4.x 反覆執行回報的 jsdom suite。驗證 stale-DomAdapter failure 不再跨 spec 檔案發生,同時保留 builder 的 unit-test 設定行為;該 issue 提議採用 analogjs/analog#2244 中的 reset-and-reinitialize pattern,並指出 documentation 作為次要的後續工作。

由索引模型根據 Issue 內容生成。

描述

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
主要語言
TypeScript
星號
27k
分支
11.8k
平均合併
16 小時 35 分鐘
30 天內合併 PR
176

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

angular/angular-cli 的其他 Issue

查看 angular/angular-cli 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。