Failing test for #19344 - #21530
Open
johanrd wants to merge 3 commits into
Open
Conversation
johanrd
force-pushed
the
todo-test-gh19344
branch
from
July 27, 2026 10:45
a717f35 to
52caf45
Compare
Disabling a focused element fires blur synchronously during render, so a
{{on "blur"}} listener that updates rendered state trips the backtracking
assertion. Recorded as a QUnit todo test (via new @todo support in
moduleFor) so it passes CI while the bug exists and flags when fixed.
With autorun enabled (as in apps), a state change in the first {{on}}
listener schedules the render flush as a microtask, and browser-dispatched
events run a microtask checkpoint between listener callbacks. The flush
tears down the element's modifiers mid-dispatch, and the removed second
listener is never invoked. Uses setTesting(false) plus a browser-task
scroll dispatch to reproduce the real path; synthetic clicks in test mode
mask the bug (both listeners fire), which is why this went uncaught.
johanrd
force-pushed
the
todo-test-gh19344
branch
from
July 28, 2026 06:53
52caf45 to
8423c82
Compare
The bug it documents is the backtracking assertion, which is stripped in production. There the blur listener succeeds, the todo passes in full, and QUnit reports "expected todo to not pass" — failing the Production build jobs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a failing test for #19344: with autorun enabled (as in apps), a state change in the first
{{on}}listener schedules the render flush as a microtask, and browser-dispatched events run a microtask checkpoint between listener callbacks — the flush tears down the element's modifiers mid-dispatch and the removed second listener is never invoked. Test-mode clicks mask this (runloop-wrapped, autorun disabled), which is why it went uncaught; the test usessetTesting(false)and a browser-taskscrolldispatch to reproduce the real path.The test is a QUnit
todo: CI stays green while the bug exists, and fails once it is fixed (then flip@todoto@test).Mechanism (verified on
main): each{{on}}registers its own native listener (on.ts#L220). A state change in the first listener schedules the render flush as a microtask (backburner autorun). For browser-dispatched events the JS stack empties between listener callbacks, so a microtask checkpoint runs between the two listeners — the flush tears down the element's modifiers, teardown callsremoveEventListener(on.ts#L47-L53) on the not-yet-invoked second listener, and a listener removed mid-dispatch is never invoked.Why tests never caught it (@marcin-wicha's false positive): test mode masks it twice —
click()is runloop-wrapped and autorun is disabled — and even with autorun enabled, syntheticel.click()dispatches inside the calling JS stack, so no checkpoint runs between listeners. Only browser-task dispatch reproduces it. #21530 adds a failing (todo) test usingsetTesting(false)plus a browser-dispatchedscroll.Fix prototype: johanrd#38 — all
{{on}}handlers for the same (element, event, capture/passive bucket) share one native listener that dispatches to a snapshot of the handler list, so sibling handlers run in a single callback frame and no checkpoint can fall between them. Full suite green (9437 tests, 0 failures); the failing test flips to passing. Scope note: this covers the reported same-element case. A cross-element variant exists (a handler's flush destroying an ancestor's{{on}}before the event bubbles up to it) — fixing that requires deferring teardown past the dispatch, i.e. deciding whether handlers may fire after their component is destroyed, which needs maintainer input.Precedent — every mainstream framework funnels its handlers through a single native callback per dispatch; the surfaces that don't are exactly where this bug class lives:
el[veiKey]), handler swaps mutateinvoker.value, stale-event guard viae._vts <= invoker.attachednode[$$event]propsCowritten by Claude