From 256b1bb0ebef9848bd8bbe67a4a6b9e5449bf3eb Mon Sep 17 00:00:00 2001 From: Ali Al Dallal Date: Tue, 18 Aug 2026 15:00:59 -0400 Subject: [PATCH] fix: bridge smoke waits for node position stability before clicking (goal 0107 CI follow-up) First CI run of the calibrated registry: 5/6 green on the hosted runner; the one failure was the commit check's select click landing mid-fitView-animation -- the click dispatches to where the card WAS and selects nothing (the same stale-coordinate class the e2e suite's clickFrameGutter helper exists for). waitForNodeStable polls the card's bounding rect across consecutive samples before the commit and ring checks click; scripted fakes updated. 3/3 local runs green. Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-Authored-By: Claude Fable 5 --- internal/webviewbridgesmoke/checks.go | 28 ++++++++++++++++++++++ internal/webviewbridgesmoke/checks_test.go | 6 +++++ 2 files changed, 34 insertions(+) diff --git a/internal/webviewbridgesmoke/checks.go b/internal/webviewbridgesmoke/checks.go index 0f2e4315..a34e39c6 100644 --- a/internal/webviewbridgesmoke/checks.go +++ b/internal/webviewbridgesmoke/checks.go @@ -217,6 +217,28 @@ func checkAtlasBoardRenders(c mcpCaller) (string, error) { return "atlas-board rendered after nav click", nil } +var stableSeq int + +// waitForNodeStable polls until selector's bounding rect stops moving +// across two consecutive samples -- the board's fitView animation +// after navigation invalidates any position measured mid-flight, the +// same stale-coordinate class the Playwright suite's own +// clickFrameGutter helper exists for. A click dispatched before this +// settles lands where the card WAS and selects nothing. +func waitForNodeStable(c mcpCaller, selector string) error { + // Fresh slot per invocation: a leftover key from an earlier check + // could otherwise satisfy the very first sample. + stableSeq++ + slot := fmt.Sprintf("__millStablePos%d", stableSeq) + return pollJSEval(c, fmt.Sprintf(`const el = document.querySelector(%q); + if (!el) return false; + const r = el.getBoundingClientRect(); + const key = Math.round(r.x) + ':' + Math.round(r.y); + if (window[%q] === key) { return true; } + window[%q] = key; + return false;`, selector, slot, slot), 10*time.Second) +} + // checkNoteCardCommit drives the goal 0102 click model end to end: // plain click selects (React Flow needs the full pointer sequence, so // mouse_click, never a js_eval .click()), a second click on the @@ -225,6 +247,9 @@ func checkAtlasBoardRenders(c mcpCaller) (string, error) { // so the ring check that follows starts from an unselected board. func checkNoteCardCommit(c mcpCaller) (string, error) { selector := `[data-testid="atlas-note-card"]` + if err := waitForNodeStable(c, selector); err != nil { + return "", fmt.Errorf("board never settled before the select click: %w", err) + } if _, err := c.call("mouse_click", withWindow(map[string]any{"selector": selector})); err != nil { return "", fmt.Errorf("select click: %w", err) } @@ -274,6 +299,9 @@ func readRing(c mcpCaller, selector string) (ringSnapshot, error) { func checkNoteCardSelectionRing(c mcpCaller) (string, error) { selector := `[data-testid="atlas-note-card"]` + if err := waitForNodeStable(c, selector); err != nil { + return "", fmt.Errorf("board never settled before the ring check: %w", err) + } before, err := readRing(c, selector) if err != nil { return "", err diff --git a/internal/webviewbridgesmoke/checks_test.go b/internal/webviewbridgesmoke/checks_test.go index c39a932d..bc348af5 100644 --- a/internal/webviewbridgesmoke/checks_test.go +++ b/internal/webviewbridgesmoke/checks_test.go @@ -243,6 +243,7 @@ func TestCheckAtlasBoardRenders(t *testing.T) { func TestCheckNoteCardCommit(t *testing.T) { t.Run("select click, commit click, escape ladder", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onJSON("js_eval", true) // poll: wrapper selected f.onJSON("js_eval", true) // poll: page header visible f.onJSON("js_eval", true) // poll: board unselected again @@ -261,6 +262,7 @@ func TestCheckNoteCardCommit(t *testing.T) { t.Run("select click failing propagates", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onError("mouse_click", errors.New("no such element")) if _, err := checkNoteCardCommit(f); err == nil { t.Fatal("expected the select click's error to propagate") @@ -281,6 +283,7 @@ func TestCheckNoteCardCommit(t *testing.T) { func TestCheckNoteCardSelectionRing(t *testing.T) { t.Run("box-shadow none -> non-none on shift-click select", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: false}) f.on("mouse_click", func(map[string]any) (string, error) { return "ok", nil }) f.onJSON("js_eval", ringSnapshot{BoxShadow: "0 0 0 2px accent", Selected: true}) @@ -295,6 +298,7 @@ func TestCheckNoteCardSelectionRing(t *testing.T) { t.Run("already selected before the check ran is an error", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: true}) if _, err := checkNoteCardSelectionRing(f); err == nil { t.Fatal("expected an error for a pre-selected card") @@ -303,6 +307,7 @@ func TestCheckNoteCardSelectionRing(t *testing.T) { t.Run("shift-click that fails to select is an error", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: false}) f.on("mouse_click", func(map[string]any) (string, error) { return "ok", nil }) f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: false}) @@ -313,6 +318,7 @@ func TestCheckNoteCardSelectionRing(t *testing.T) { t.Run("ring never renders after selection is an error", func(t *testing.T) { f := newFakeCaller() + f.onJSON("js_eval", true) // poll: node position stable f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: false}) f.on("mouse_click", func(map[string]any) (string, error) { return "ok", nil }) f.onJSON("js_eval", ringSnapshot{BoxShadow: "none", Selected: true})