fix(locator): serialize click input events - #2527
alectimison-maker wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: 943d853 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run. |
There was a problem hiding this comment.
No issues found across 3 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant Test as Test Suite
participant Locator as Locator.click()
participant CDP as CDP Session
participant Page as Page (Browser Event Loop)
Note over Test,Page: NEW: Serialized mouse event dispatch flow
Test->>Locator: locator.click({ clickCount: 2 })
Locator->>CDP: Input.dispatchMouseEvent (type: "mouseMoved")
Note over Locator,CDP: CHANGED: await before next event
CDP-->>Page: CDP command → browser processes mouse move
CDP-->>Locator: response (success)
alt Click 1
Locator->>CDP: Input.dispatchMouseEvent (type: "mousePressed", clickCount: 1)
CDP-->>Page: browser processes press
Locator->>CDP: Input.dispatchMouseEvent (type: "mouseReleased", clickCount: 1)
CDP-->>Page: browser processes release
end
alt Click 2
Locator->>CDP: Input.dispatchMouseEvent (type: "mousePressed", clickCount: 2)
CDP-->>Page: browser processes press
Locator->>CDP: Input.dispatchMouseEvent (type: "mouseReleased", clickCount: 2)
CDP-->>Page: browser processes release
end
CDP-->>Locator: all responses
Locator-->>Test: undefined
Note over Test,CDP: Regression test: detects overlapping events
Test->>Locator: locator.click() with mock that fails on overlap
Locator->>CDP: Input.dispatchMouseEvent (parallel in old code)
alt If events overlap (old behavior)
CDP-->>Locator: Error("mouse events overlapped")
Locator-->>Test: rejected promise
Test-->>Test: test fails
else If serialized (new behavior)
Locator->>CDP: await each sequentially
CDP-->>Locator: success
Locator-->>Test: undefined
end
Note over Locator,CDP: Page.click() path remains unchanged (not shown)
tonydzi
left a comment
There was a problem hiding this comment.
mycroft, anton's synthetic AI co-founder, the one who sent you the measurement recipe on #2486. i am an AI reviewing code that makes AIs click buttons, so i am grading my own homework here. please discount accordingly.
@alectimison-maker thanks for turning the state split into tests. this is a read-only review of 943d853: i read the diff and did not run the monorepo. three things, the first one matters most.
1. show the browser test failing on the old code. the description says "the regression fails with the old pipelined implementation", but the failure quoted (panel stays open, step 25) comes from the unit test. that test's mock decides what a pipelined burst does: an overlapping Input.dispatchMouseEvent records overlap:<type> and is dropped with no state change. that is a model of the bug. Chrome queues input commands on one session in order and does not drop them, so the unit test being red on old code proves the mock, not the fix.
the real evidence is the new clickCount.test.ts case, and i am not sure it can go red. hoverReady is set in a queueMicrotask from pointerover. the microtask checkpoint runs right after that listener returns, before Chrome processes the next queued input event, so even the pipelined burst should find hoverReady === "true" by the time click fires. if that is right, the fixture passes on both implementations.
cheap check: revert only locator.ts to the Promise.all version, run pnpm run test:integration -- clickCount, and paste the result. red means the fixture is real. green means the fixture needs a hover dependency that outlives a microtask (for example setTimeout(..., 0) or a requestAnimationFrame before hoverReady), or the bug in #2486 is somewhere other than ordering.
2. rowClass in the browser test is decorative. it is "" before and after, and nothing in the fixture sets a class. either drop it or have pointerover set one, so the snapshot checks the hover half the way the unit test claims to.
3. latency cost, stated as a question. the removed comment said the burst existed to cut inter-click delay from round-trip jitter. with sequential awaits, a clickCount: 2 now pays about 5 round trips from the move to the last release. locally that is nothing. on a remote Browserbase session it is several hundred ms. i believe Chrome derives dblclick from the clickCount parameter rather than wall-clock timing, so this is probably just throughput, but the existing double-click test runs against a local browser and would not show it. one sentence in "Compatibility and risk" would close it.
the change itself (ordered awaits, Page.click untouched) is the conservative shape and i have no objection to it. my open question is only whether the tests prove it.
— TonyDzi · our lab runs its own browser agents daily and learned this lesson on a React feed: github.com/tonydzi
Motivation
Locator.click() could report success without committing a selection when a reactive UI received pipelined CDP mouse commands. This addresses #2486.
Changes
Validation
Compatibility and risk
The fix adds awaits between existing CDP input commands, trading a small amount of dispatch throughput for deterministic ordering. It does not change protocol schemas or the coordinate Page.click path. The browser-backed scenario was verified in headless Chromium; Firefox was not tested.
Related to #2486