Flaky e2e: "edit a card, verify changes persist" fails ~1 run in 3 on Escape #165
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente#165
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
tests/e2e/board.spec.js:189(edit a card, verify changes persist) fails intermittently at its last assertion:The dialog stays open. Measured 2 failures in 6 consecutive runs on an unmodified
main(95e4eea), driven against a host-sidedotnet runin Development at127.0.0.1:5000. Every earlier assertion in the same test — including reopening the dialog and reading the saved description back — passes, so the circuit is up by the time Escape is pressed.Where to look:
CardModal.OnAfterRenderAsyncfocuses_backdropon first render for an existing card, and the Escape handler is@onkeydownon that element relying on the event bubbling up from whatever has focus. If that focus call loses a race with something else on the page taking focus after the dialog's first render, the keydown lands on<body>and nothing hears it. Worth checking whether the reopen path (second.card-titleclick on an already-mounted page) re-runsOnAfterRenderAsyncwithfirstRender: trueat all — the component is recreated, but the second open is the case that fails.This is not the prerender race
docs/testing.mddescribes: that one is a click landing before the circuit attaches, and here the two assertions before it needed the circuit. Filed from the search work onclaude/issue-159, which did not touch the card dialog and reproduces the same rate with those changes stashed.Filed by Claude while working on #159 (run).
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Fixed on branch
claude/issue-165.What was actually wrong
Your hunch about the focus call was right, and it's not a reopen-specific bug — the reopen is just where the test happens to press Escape fastest.
CardModal.OnAfterRenderAsynccalls_backdrop.FocusAsync(), and that call is not part of the render that put the dialog on screen. In Blazor Server the sequence is: render batch out → browser acknowledges → server runsOnAfterRenderAsync→ a second interop message comes back saying "focus this". So between the dialog being visible (which is when every assertion before the Escape passes) and the dialog being focused-from, there is a window one and a half round trips wide whereactiveElementis<body>— which no component owns, so Blazor routes the keydown nowhere.I measured it directly, with a
MutationObserverin the page recording when.modal-backdropwas inserted and when it receivedfocus:Five milliseconds on loopback with an idle server and one browser. It's the circuit's latency, so it's however far away the server is — which is why you see it at 2-in-6 and this container sees it at roughly 0-in-25.
firstRender: truedoes re-run on the reopen (the component is destroyed and recreated by@if (_modalCard is not null)), so that part is fine.The fix
src/Ponente.Kanban/wwwroot/js/modal-escape.js— a delegatedkeydownlistener on the document, attached from a plain script at the end of<body>rather than from a component, for the same reason the theme switcher inApp.razor's head is: it covers every modal on every page, needs no interop to arm, and survives the circuit reconnecting. On Escape with nothing focused it focuses the topmost.modal-backdropand re-dispatches the keydown there, where Blazor's own delegated listener routes it as the backdrop's@onkeydownexactly as a real event.The guard is deliberately "target is
<body>or<html>" and not "target is outside the dialog": that's precisely the broken state and nothing else, so it can never take an Escape away from a real handler (SearchBox, the column tag flyout) — those need a focused element to receive the event in the first place. It fixesFeedbackModalandChangelogModalfor free, since all three share the backdrop shape.I did not change the C# focus call — it's still the right thing for keyboard and assistive tech; it just isn't what Escape can depend on. There's a comment at the call site saying so.
The test now blurs before pressing Escape. That's not papering over it — the losing state is "nothing focused", so the test asks for it every run instead of racing for it, and the assertion becomes the one that pins the JS.
Verified
Against a host-side
dotnet runin Development, Postgres 15 local, Playwright 1.62.1:mainfailed 3/3 at that assertion. (The natural 1-in-3 does not reproduce in this container — it's too fast and too idle; 0 failures in 25 unmodified runs. Hence forcing it.)modal-escape.jsemptied (file still present, so nothing else changes), the test fails 3/3 at line 230 — the Escape assertion, not somewhere else.board.spec.js+changelog.spec.js+filter.spec.js+search.spec.js: 37/37 — that covers the filter flyout's "dismisses every way" and both search specs, i.e. the other Escape handlers.mobile.spec.js: 7/7.dotnet test Ponente.sln: 448/448, includingDocumentationTests.Not run:
feedback.spec.jsand the rest, which need the compose stack's stub-forge container — no container engine in this job.Also updated:
docs/ui.md(the card-dialog section, with why the guard is what it is),docs/frontend.md(thewwwroot/jslist), and a changelog line in both locales — a dialog ignoring your first Escape is something a member notices.I filed one out-of-scope finding I hit on the way: the calendar's two
role="dialog"overlays have no Escape handling at all, and use their own class so this fix doesn't reach them.⏱ 22m 48s · 79 turns · tokens: 106.2k in / 36.4k out (+5756.2k cache-read)
📜 Full session transcript
→ Opened PR: #172
→ Filed follow-up issue #173: Escape does not close either of the calendar's two dialogs