Skip to content

Commit edfa782

Browse files
committed
fix(@angular/build): use dynamic TestComponentRenderer for Vitest
This commit implements a custom TestComponentRenderer in the virtual init-testbed.js file generated for Vitest. In Vitests non-isolated mode (isolate: false) with JSDOM, Vitest creates a fresh document for each spec file but reuses the module cache. The default Angular DOMTestComponentRenderer caches the document during initialization, leading to stale references and errors like setAttribute is not a function in subsequent tests. The new DynamicDOMTestComponentRenderer looks up the document dynamically on every operation, resolving the issue without requiring a breaking change to defaults or affecting browser-based testing.
1 parent ccd6d8d commit edfa782

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,15 @@ function createTestBedInitVirtualFile(
5454
}`;
5555
}
5656

57+
// The DynamicDOMTestComponentRenderer is used to avoid stale document references
58+
// when running Vitest in non-isolated mode with JSDOM. It looks up the
59+
// document dynamically on every operation instead of caching it.
5760
return `
5861
// Initialize the Angular testing environment
5962
import { NgModule, provideZoneChangeDetection } from '@angular/core';
60-
import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
63+
import { getTestBed, ɵgetCleanupHook as getCleanupHook, TestComponentRenderer } from '@angular/core/testing';
6164
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
65+
import { ɵgetDOM } from '@angular/common';
6266
import { afterEach, beforeEach } from 'vitest';
6367
${providersImport}
6468
@@ -70,6 +74,31 @@ function createTestBedInitVirtualFile(
7074
beforeEach(getCleanupHook(false));
7175
afterEach(getCleanupHook(true));
7276
77+
class DynamicDOMTestComponentRenderer extends TestComponentRenderer {
78+
insertRootElement(rootElId, tagName = 'div') {
79+
this.removeAllRootElements();
80+
81+
const dom = ɵgetDOM();
82+
const doc = dom.getDefaultDocument();
83+
if (doc && doc.body) {
84+
const rootElement = doc.createElement(tagName);
85+
rootElement.setAttribute('id', rootElId);
86+
doc.body.appendChild(rootElement);
87+
}
88+
}
89+
90+
removeAllRootElements() {
91+
const dom = ɵgetDOM();
92+
const doc = dom.getDefaultDocument();
93+
if (doc && typeof doc.querySelectorAll === 'function') {
94+
const oldRoots = doc.querySelectorAll('[id^=root]');
95+
for (let i = 0; i < oldRoots.length; i++) {
96+
dom.remove(oldRoots[i]);
97+
}
98+
}
99+
}
100+
}
101+
73102
const ANGULAR_TESTBED_SETUP = Symbol.for('@angular/cli/testbed-setup');
74103
if (!globalThis[ANGULAR_TESTBED_SETUP]) {
75104
globalThis[ANGULAR_TESTBED_SETUP] = true;
@@ -82,6 +111,7 @@ function createTestBedInitVirtualFile(
82111
providers: [
83112
...(typeof Zone !== 'undefined' ? [provideZoneChangeDetection()] : []),
84113
...providers,
114+
{ provide: TestComponentRenderer, useClass: DynamicDOMTestComponentRenderer },
85115
],
86116
})
87117
class TestModule {}

tests/e2e/tests/vitest/larger-project.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { ng } from '../../utils/process';
22
import { applyVitestBuilder } from '../../utils/vitest';
33
import assert from 'node:assert';
44
import { installPackage } from '../../utils/packages';
5-
import { exec } from '../../utils/process';
65

76
export default async function () {
87
await applyVitestBuilder();
98

10-
const artifactCount = 100;
9+
const artifactCount = 500;
1110
// A new project starts with 1 test file (app.spec.ts)
1211
// Each generated artifact will add one more test file.
1312
const initialTestCount = 1;

0 commit comments

Comments
 (0)