Make the deferred-click window overridable to fix the click-vs-panel race (task 233) #173
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/task-233-distant-clicks-flake"
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?
Fixes task 233. Picks the parked branch back up after the coordinate-change attempt below was disproved.
Why a coordinate change cannot work (preserved from this PR's earlier analysis)
All three logged failures (
be2a075,fd0bd0e,00aa81d) showedSecondClickX(1050) landing inside.modal-overlay-panel's own footprint once the add-place panel is open, naming the longitude input as the interceptor. MovingSecondClickXto 450, clear of the panel's own bounds, did not fix it:OverlayBackdrop(position: fixed; inset: 0; z-index: 1299; pointer-events: autoonce visible) covers the entire viewport the instant the panel opens, above the map at every point, not only behind the panel's own footprint. Proven directly on a throwaway branch (PR #174, closed): a click at x=450, delivered 600ms after the first click — double the 300ms deferral window, panel unambiguously open — was still intercepted, now bydiv.overlay-backdropinstead of the longitude input. The coordinate change had no effect on the actual failure rate (measured 3 of 11 completed CI runs, ~27%); it only changed which element got named in the failure.The actual fix: widen the delivery margin instead of racing it
The test's premise is that both clicks land inside
deferSingleClick's deferral window while the first click's own callback is still pending. With clicks staggered 250ms apart against a 300ms production window, that left only ~50ms of real margin for the second click's delivery (Chromium's round trip through Playwright, not merely dispatch) — margin a busy CI runner can and does consume.DOUBLE_TAP_WINDOW_MS(map.js) is now overridable, for tests only:Nothing this application ships ever sets, reads, or exposes that global — no component, no query string, no
localStoragekey — so production start-up always falls through to the unchanged 300ms literal. The only writer anywhere in this repository isDistantMapClicksBothSurviveJourneyTests, via Playwright'spage.AddInitScriptAsync, injected before either click and before any of the page's own scripts run — a surface no real reader's browser has a route to reach.The test raises the window to 3000ms (ten times production) while leaving the 250ms stagger unchanged, turning the ~50ms delivery margin into ~2.75s — not a smaller race won more often, but a margin no observed CI load comes close to consuming.
SecondClickX's move to 450 and theAriaLabel-scoped map locator both stay (still correct on their own merits), but neither is load-bearing now: with the window this wide, the second click is always delivered while the panel is nowhere close to open, so no coordinate could collide with it regardless.See ADR-0160 for the alternatives considered — including the "prove it without a second real click" option task 233 itself named — and why the query-string/
localStorageroute was rejected specifically.Checked against #184 (merged onto
mainwhile this branch was in flight, which changes what a map click does while the detail panel is open): that change is gated onPlacesState.SelectedPlace is not null; this test never selects a place, soHandleMapClickedstill takes the same add-place branch it always did here. Confirmed by rerunning after rebasing onto it, and again after rebasing onto #184/#186 a second time.Verification
deferSingleClickrestored to a single pending slot, cancelling on any second click regardless of distance) and the override otherwise unchanged, the test reddened — neither click opened the panel, exactly the pre-fix shape. Restored before this shipped.ea88df8), 12m14s, and reconfirmed after two further rebases.DOUBLE_TAP_WINDOW_MS(MapClickDeferralJourneyTests,DoubleTapDragZoomJourneyTests,MarkerDragCancelsPendingClickJourneyTests) pass unaffected — they never set the override, so they continue to exercise the real 300ms production window.PlaceMark.E2E.Testssuite green (28/28);PlaceMark.Architecture.Testsgreen (129/129, including the new ADR's own index/supersession checks);PlaceMark.WebUI.Testsgreen (814/818, 4 pre-existing skips).Scoped to test code plus one production constant's own override hook — no other ADR needed beyond ADR-0160, which records that hook as its own decision.
Verdict: changes needed
Reviewed at head
e3aae385.1. The geometry only clears the panel's own content — it doesn't clear
OverlayBackdrop, which is what should actually be intercepting once the panel is open.ModalOverlay.razorrenders<OverlayBackdrop Visible="!_closing" .../>alongside the panel, and that component isposition: fixed; inset: 0(full viewport, not just the panel's footprint),z-index: var(--z-overlay) - 1= 1299,pointer-events: autoonce visible, with an@onclickwired toRequestDismissAsync. It doesn't passHideAboveWideViewport, so it's live at 1280×720.Home.razor's own<LeafletMap ... Inert="OverlayFocus.IsOpen" .../>also goesinertthe moment the panel opens..leaflet-map-hostcarries no z-index of its own (confirmed inLeafletMap.razor.css), so once the panel has genuinely opened, the backdrop sits above the map everywhere in the viewport, not just behind the panel — x=450 is exempt from the panel's own content but not from this.Given the click actually registers through Leaflet's native
map.on("click", …)listener at real DOM-dispatch time, and the deferred callback that opens the panel doesn't fire until ~300ms after the first click (DOUBLE_TAP_WINDOW_MS), the real question for "does this land or not" is whether the second click's actual delivery beats that 300ms mark — same margin as before, just a different consequence on the losing side. Before the fix, losing it at x=1050 hit the panel's own longitude field (confirmed by all three logged failures). After the fix, losing it at x=450 should, by this reasoning, hit the full-viewport backdrop instead (interception timeout, or a click-away dismiss that never updates the coordinate) — a different failure signature, not a removed race.All three confirmed production failures used the old x=1050, which sits inside the panel, so none of them says anything about what a late click does at x=450. The two post-fix green runs are equally consistent with "still beating 300ms" as with "genuinely immune once open" — they don't distinguish the two. Given the value the discrimination proof already put on actually watching a guarded thing fail (PR body itself, and ADR-0053/0043/0066 in CLAUDE.md), the same rigour should apply here: force the second click's delivery past 300ms on a throwaway branch (e.g. bump
StaggerMspast the deferral window once) and confirm it still lands — if it does, the "outright, timing-independent" claim is proven and this note is moot; if it doesn't, the fix needs to also account for the backdrop, not just the panel.2. "3/3 green after" overstates what's in the job logs. The commit that actually contains the fix,
15d1449, had exactly one real CI run against it (run #627/id 719):buildfailed onConfirmationModalTests.ConfirmationModal_BackdropClicked_RaisesOnCancelAfterTheClosingDelay(independently confirmed from the log — a genuine, unrelated bUnit closing-delay flake, matching the retrigger's own stated reason), ande2ewas skipped, contributing no data point either way. Only the two subsequent content-free retrigger commits (a7d7de9,e3aae38) actually rane2e, and both passed 25/25. So the honest tally is 2 green e2e runs post-fix, not 3 — worth correcting in the PR body and the Vikunja comment. At the stated ~27% pre-fix rate, 2 straight passes has roughly a 53% chance of happening by luck alone, which matters more, not less, given point 1 is still open.What holds up:
e2efailed withDistantMapClicksBothSurviveJourneyTests.TwoClicksFarApart_WithinTheDeferralWindow_BothOpenAndThenReseedTheAddPlacePanelas the named failing test. The updated test still catches task 229's guard being removed.--drawer-width: 20rem,.modal-overlay-panel'sright: var(--space-medium); width: min(28rem, …)), not asserted.AriaLabelscoping is sound:Home.razorsets"Your places", the picker (PlaceForm.razor) sets a distinct"Pick this place's location", both surfaced viarole="region" aria-label="@AriaLabel"—GetByRole(Region, Name: "Your places", Exact: true)can't silently match the picker's, and a real regression would still redden the assertion.Expect(...).Not.ToHaveValueAsync(...)still discriminates a silently-discarded second click (it polls to timeout rather than reading once, but a value that never changes still fails it).Fix DistantMapClicksBothSurviveJourneyTests' click-vs-panel raceto PARKED: coordinate change alone does not fix the click-vs-panel racee3aae3854519adf468d7PARKED: coordinate change alone does not fix the click-vs-panel raceto Make the deferred-click window overridable to fix the click-vs-panel race (task 233)19adf468d7ea88df84c6Verdict: mergeable
Reviewed at head
ea88df84(matches the branch tip). Checked the five points independently rather than accepting the report:deferSingleClickdirectly:matchIndexis purelypoint.distanceTo(entry.point) < CLICK_CANCEL_SLOP_PIXELS— never touchesDOUBLE_TAP_WINDOW_MS. Reverting to the pre-ADR-0145 shape (findIndex(() => true), matching the leftover mutation on the localchore/task-233-confirm-guardbranch) cancels the second click regardless of window width, since the second click (t≈250ms) always lands well inside a still-pending 3000ms entry. The widened window cannot make this pass independent of the positional guard — confirmed structurally, not just from the author's report.git grep __placeMarkTestDoubleTapWindowMsacross the tree at this commit: one writer (the test, viaAddInitScriptAsync), one reader (map.js's??). Production default is unreachable through any surface the app itself exposes. (It's still a plain global a console or extension could poke — true of any client-side JS constant, not a new attack surface, so no concern beyond what already exists.)Home.razor.csbetween #184's merge parents — the dismiss branch is gated on_editingPlaceId is null && _addingPlaceAtCoordinates is null && PlacesState.SelectedPlace is not null. This test never selects a place, soHandleMapClickednever reaches that branch; #184 is a no-op for this journey exactly as claimed.Answered bycorrectly unused. Neither ADR-0144 nor ADR-0145 names an explicit open question about making the window configurable (grepped both for it) — ADR-0107's condition 3 isn't met, so a plain new ADR is the right shape.Two non-blocking points:
deferSingleClickor the component directly). Worth a line on why that wasn't taken, even if the answer is "pointer-input guards need real-Chromium proof" (ADR-0145's own justification for the same call).@ -0,0 +103,4 @@unconditional full-viewport block, ADR-0102, ADR-0116), not a test fix, and belongs in its own ticketif ever wanted.**Delete the test, on the grounds that a wall-clock race can never be made trustworthy.** ConsideredMissing task 233's own second named option: proving the two-independent-deferred-clicks guarantee without a second real map click (unit-level against
deferSingleClick, or a bUnit-level component test), which would sidestep the E2E click-delivery round trip entirely. Worth a line here on why that wasn't pursued — non-blocking, but the Alternatives section is meant to be the record of what was weighed.ea88df84c60994846890Both points actioned in
0994846(on top of a rebase onto #184/#186):OverlayBackdrop), which nothing below the E2E layer can observe.Rebased onto current
main(merge_basecaa287c), mergeable.CI green: run #775, head
0994846, 6m42s. Mergeable against currentmain.Verdict: changes needed
Re-reviewed at head
09948468. Confirmed clean:map.jsand the test file are byte-identical toea88df84(rebase onto #184/#186 touched neither), so thedeferSingleClickpositional-guard reasoning, the single-writer/single-reader property of__placeMarkTestDoubleTapWindowMs, and the preserved coordinate-change analysis all still hold exactly as before — re-grepped and re-diffed rather than assumed.The two requested points are handled well:
deferSingleClick's cancellation logic (which a unit/bUnit test could observe fine) but about whether a real click reaches the map throughOverlayBackdrop's CSS layering, which only a real-DOM E2E test can see. That's specific enough to actually stop a re-proposal.One new problem, introduced by this round, blocking: ADR-0160's Consequences (line 154) cites "(PR #179, PR #183)" as prior instances of this project sending work back for overstating what a local run count establishes. Checked both — #179 was a connection-string-escaping/injection fix, #183 was Leaflet dark-mode theming and a stale ADR-0091 sentence; neither PR's review history says anything about local-run-count evidence. The citation doesn't support the claim it's attached to, in a record that becomes immutable once accepted. Fix the citation (or drop it and let the reasoning stand on its own — it doesn't need the precedent) before merging.
@ -0,0 +151,4 @@that caused the failure. A run count gathered locally is therefore weak evidence for "this fixes theCI flake" specifically, however many times it is repeated, and citing one as if it were strongevidence is exactly the overstatement this project has already sent work back for more than once(PR #179, PR #183). What actually supports the fix is the arithmetic in this record's own Decision —"(PR #179, PR #183)" doesn't check out — read both PRs' full review history: #179 is about connection-string quoting/injection escaping, #183 is about Leaflet dark-mode theming and a stale ADR-0091 sentence. Neither involves overstating a local run count as evidence. This citation is unsupported and needs fixing (or dropping) before this record is accepted and its body becomes frozen.
Fixed in
8f003bf. The citation was wrong as written — broadened it to the pattern that genuinely connects #179 and #183 (a durable record asserting more than the evidence in front of it supports), described each accurately (#179'spg_quoteround-trip claim, #183's non-existent CI schedule claim), and said plainly this record's own overstated run-count risk is the same kind of failure, not the same detail.CI green: run #776, head
8f003bf, 6m46s. Mergeable against currentmain.Verdict: mergeable
Diff from
09948468to8f003bfftouches only ADR-0160's Consequences paragraph —map.js, the test, and the Alternatives entry are untouched. The corrected citation checks out against what's actually in #179 and #183:pg_quote's comment did claim "every case exercised round-trips" when the trailing-newline case didn't, and the fixture's remarks did assert a schedule-only trigger thatci.ymlno longer has — both genuinely "asserted more than the evidence supports," and the generalisation is specific enough to those two facts rather than a vaguer claim that's merely harder to falsify.