Skip to content

Commit 65afe93

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 73233dc commit 65afe93

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

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

Lines changed: 27 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,27 @@ 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 doc = ɵgetDOM().getDefaultDocument();
82+
const rootElement = doc.createElement(tagName);
83+
rootElement.setAttribute('id', rootElId);
84+
doc.body.appendChild(rootElement);
85+
}
86+
87+
removeAllRootElements() {
88+
const doc = ɵgetDOM().getDefaultDocument();
89+
if (typeof doc.querySelectorAll === 'function') {
90+
const oldRoots = doc.querySelectorAll('[id^=root]');
91+
for (let i = 0; i < oldRoots.length; i++) {
92+
ɵgetDOM().remove(oldRoots[i]);
93+
}
94+
}
95+
}
96+
}
97+
7398
const ANGULAR_TESTBED_SETUP = Symbol.for('@angular/cli/testbed-setup');
7499
if (!globalThis[ANGULAR_TESTBED_SETUP]) {
75100
globalThis[ANGULAR_TESTBED_SETUP] = true;
@@ -82,6 +107,7 @@ function createTestBedInitVirtualFile(
82107
providers: [
83108
...(typeof Zone !== 'undefined' ? [provideZoneChangeDetection()] : []),
84109
...providers,
110+
{ provide: TestComponentRenderer, useClass: DynamicDOMTestComponentRenderer },
85111
],
86112
})
87113
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)