Skip to content

Commit 401df7b

Browse files
lifeartclaude
andcommitted
fix(gxt-backend): drain leaked classic reactors between tests
_renderComponentGxt registers a reactor in _classicReactors (validator.ts) and tries to unhook it via registerDestructor(owner, doDestroy). When the caller passes a synthetic owner ({} — the documented default for the renderComponent API), registerDestructor silently fails (plain objects are not destroyable), so the reactor outlives its test. On the next test's classic-tag dirty (e.g. typing in a textarea, swap of a dynamic component, or any @Tracked write), the leaked reactor fires _doRender against the prior test's detached target and triggers __gxtSyncDomNow, which clobbers the active test's DOM through the shared force-rerender path. Symptom cluster on testem (single-page run, modules load sequentially): - <Textarea> / {{textarea}} cut/input/change tests - curly didReceiveAttrs deprecation - dynamic component swap-out destroy - error recovery during initial render - renderComponent siblings (1 of 2) - browser timeout 1200s after the textarea cluster wedges sync state All affected modules pass in isolation; the failures are pure cumulative-state leak symptoms. __gxtCleanupActiveComponents already clears every other module-level Set/Map between tests; _classicReactors is the one that was missing. Fix: expose __gxtClearClassicReactors from validator.ts and call it from __gxtCleanupActiveComponents. Smoke (14 modules / 333 tests) passes; affected modules pass individually. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 4f68951 commit 401df7b

2 files changed

Lines changed: 18 additions & 0 deletions

File tree

packages/@ember/-internals/gxt-backend/compile.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5822,6 +5822,15 @@ setInterval(() => {
58225822
if (typeof (globalThis as any).__gxtClearIfWatchers === 'function') {
58235823
(globalThis as any).__gxtClearIfWatchers();
58245824
}
5825+
// Drain leaked classic reactors. _renderComponentGxt registers a reactor in
5826+
// _classicReactors and tries to unhook it via registerDestructor(owner, …);
5827+
// when owner is a synthetic {} (the documented default), registerDestructor
5828+
// silently fails, leaving the reactor live. On the next test's classic-tag
5829+
// dirty (e.g. typing in a textarea) the leaked reactor fires _doRender on
5830+
// the prior test's detached target and clobbers shared GXT sync state.
5831+
if (typeof (globalThis as any).__gxtClearClassicReactors === 'function') {
5832+
(globalThis as any).__gxtClearClassicReactors();
5833+
}
58255834
// Destroy cached engine instances from {{mount}} so Namespace.destroy()
58265835
// removes them from NAMESPACES (prevents "Should not have any NAMESPACES" failures).
58275836
if ((globalThis as any).__gxtEngineInstances) {

packages/@ember/-internals/gxt-backend/validator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,15 @@ export function registerClassicReactor(cb: () => void): () => void {
655655
_classicReactors.delete(cb);
656656
};
657657
}
658+
// Drain all reactors. Used by __gxtCleanupActiveComponents between tests:
659+
// reactors registered by _renderComponentGxt with a synthetic owner ({}) can
660+
// outlive their test because registerDestructor silently fails on plain
661+
// objects, leaving the reactor in this Set. On the next test's classic-tag
662+
// dirty (e.g. textarea input), the leaked reactor fires _doRender against an
663+
// already-detached target and clobbers shared GXT sync state.
664+
(globalThis as any).__gxtClearClassicReactors = function () {
665+
_classicReactors.clear();
666+
};
658667
function _fireClassicReactors() {
659668
if (_classicReactors.size === 0) return;
660669
// Copy to avoid mutation during iteration

0 commit comments

Comments
 (0)