Skip to content

webview: add mouse.down/up/move primitives for drag automation - #29817

Open
robobun wants to merge 9 commits into
mainfrom
farm/e08928ec/webview-mouse-primitives
Open

webview: add mouse.down/up/move primitives for drag automation#29817
robobun wants to merge 9 commits into
mainfrom
farm/e08928ec/webview-mouse-primitives

Conversation

@robobun

@robobun robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #29815.

Adds three low-level pointer primitives to Bun.WebView so canvas
drag-and-drop tests (and other flows that need intermediate
pointermove events while a button is held) no longer have to
drop to raw CDP:

view.mouseDown(options?: { button?, modifiers?, clickCount? }): Promise<void>
view.mouseUp(options?: { button?, modifiers?, clickCount? }): Promise<void>
view.mouseMove(x, y, options?: { steps?, modifiers? }): Promise<void>

Use case

The classic canvas drag pattern from the issue now runs unchanged on
both Chrome and WebKit backends:

await view.mouseMove(from.x, from.y);
await view.mouseDown();
await view.mouseMove(to.x, to.y, { steps: 5 });
await view.mouseUp();

Behavior

  • mouseDown/mouseUp take no coords — they use the position
    last set by mouseMove (defaulting to (0,0) if never moved).
    Matches Playwright's page.mouse.* semantics.
  • mouseMove dispatches steps intermediate events interpolated
    linearly from the current position to (x, y), plus one final
    event at the target. Default steps: 1 = single event.
  • When a button is held (from a prior mouseDown), moves fire as
    mousedrag/dragover (WebKit uses mouseDragged: variants;
    Chrome sends mouseMoved with non-zero buttons field).
    Without a prior down, mouseMove is a plain hover — useful for
    :hover style assertions.
  • State is tracked parent-side on the JSWebView: m_mouseX,
    m_mouseY, and m_mouseButtons (bitmask of held buttons).

Backend details

Chrome (CDP): Input.dispatchMouseEvent with the correct
button/buttons fields. Multi-step moves send steps-1
fire-and-forget events + one tracked final event that resolves the
promise — Chrome processes events in order, so the final reply means
all previous ones landed too.

WebKit (WKWebView host subprocess): Synthesized NSEvents
(LeftMouseDown/Up, LeftMouseDragged / RightMouseDragged /
OtherMouseDragged) dispatched via the public responder selectors.
Plain moves (no button held) use the _simulateMouseMove: SPI —
the public mouseMoved: doesn't route to WebContent without
acceptsMouseMovedEvents:YES and a tracking area. Every op waits
on _doAfterProcessingAllPendingMouseEvents: — the promise
resolves only after WebContent has processed every event and run all
JS handlers (same barrier click() uses).

Tests

test/js/bun/webview/webview.test.ts (WebKit, macOS) and
test/js/bun/webview/webview-chrome.test.ts (cross-platform) each
cover:

  • drag sequence with intermediate buttons-held moves
  • hover without prior down (buttons: 0 throughout)
  • down + up at same position synthesizes trusted click
  • right button with modifiers fires contextmenu with the right
    button/buttons bitmask
  • x/y NaN/Infinity validation in mouseMove

Method-existence test (WebView prototype exposes mouseDown/mouseUp/ mouseMove) runs on every platform — fails on pre-fix main, passes
after.

@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:30 AM PT - May 4th, 2026

@robobun, your commit c9237ab is building: #51057

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds low-level pointer primitives—mouseDown, mouseUp, mouseMove—to Bun.WebView across Chrome and WebKit backends. Introduces internal pointer state tracking, stepped movement interpolation, IPC/protocol additions, macOS ObjC runtime glue for synthetic mouse events, TypeScript typings, and tests exercising drag/hover/click/contextmenu behaviors and argument validation.

Changes

Cohort / File(s) Summary
TypeScript API
packages/bun-types/bun.d.ts
Adds bun.WebView.MouseButtonOptions, bun.WebView.MouseMoveOptions, and async methods mouseMove(x,y,opts), mouseDown(opts), mouseUp(opts).
JS WebView core
src/bun.js/webview/JSWebView.h, src/bun.js/webview/JSWebView.cpp, src/bun.js/webview/JSWebViewPrototype.cpp
Adds m_mouseX/m_mouseY/m_mouseButtons, prototype methods mouseDown/mouseUp/mouseMove, option parsing/validation (button, modifiers, clickCount, steps), pending-op slot checks, and forwards calls to backend ops.
Chrome (CDP) backend
src/bun.js/webview/ChromeBackend.h, src/bun.js/webview/ChromeBackend.cpp
Implements CDP-backed mouseDown/mouseUp/mouseMove via Input.dispatchMouseEvent, maps buttons/modifiers/buttonsMask, emits interpolated mouseMoved events, and resolves promises on final dispatch.
WebKit backend & host IPC
src/bun.js/webview/WebKitBackend.h, src/bun.js/webview/WebKitBackend.cpp, src/bun.js/webview/ipc_protocol.h, src/bun.js/webview/host_main.cpp
Adds IPC ops MouseDown/MouseUp/MouseMove and payload structs; backend wrappers send ops with coordinates/metadata and return promises.
Host-side event handling
src/bun.js/webview/WebViewHost.h, src/bun.js/webview/WebViewHost.cpp
Adds mouseDownIPC/mouseUpIPC/mouseMoveIPC to serialize input, set/observe m_inputPending, publish NSEvent::s_trackedButtonsMask, synthesize NSEvents, and interpolate steps for move.
macOS ObjC runtime glue
src/bun.js/webview/ObjCRuntime.h, src/bun.js/webview/ObjCRuntime.cpp
Adds tracked buttons mask, NSEvent move/drag enum values, selector bindings and method replacements to synthesize mouse-move/drag NSEvents and override +[NSEvent pressedMouseButtons].
Tests
test/js/bun/webview/webview.test.ts, test/js/bun/webview/webview-chrome.test.ts
Adds cross-backend and Chrome-specific tests for drag sequences with intermediate steps, hover-only moves, mouseDown+mouseUp click synthesis, right-button/contextmenu with modifiers, and non-finite-argument validation.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'webview: add mouse.down/up/move primitives for drag automation' clearly and concisely describes the main change—adding three new mouse pointer APIs to WebView for drag automation.
Description check ✅ Passed The PR description fully addresses the template requirements: it explains what the PR does (adds mouseDown/mouseUp/mouseMove primitives), how the code was verified (references tests in webview.test.ts and webview-chrome.test.ts covering drag sequences, hover, click synthesis, and argument validation), and provides comprehensive implementation details.
Linked Issues check ✅ Passed The PR fully implements the objectives from #29815: provides three low-level mouse primitives (mouseDown/mouseUp/mouseMove) matching Playwright semantics, supports steps for intermediate pointermove events, tracks internal pressed-button state for correct buttons bitmask, enables hover without prior down, and implements cross-backend behavior for both Chrome (CDP) and WebKit (NSEvent synthesis) to eliminate Chrome-only workarounds.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the three mouse primitives: type definitions, backend implementations (CDP and WebKit), IPC protocol, state tracking, tests, and ObjC runtime support for event dispatch and button state tracking. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bun-types/bun.d.ts`:
- Around line 8212-8222: The JSDoc for MouseMoveOptions.steps is misleading (it
suggests "intermediate events" count) but the runtime treats steps as the total
number of mousemove events; update the comment on the steps field in the
MouseMoveOptions interface to state that steps is the total number of dispatched
mousemove events including the final event (e.g., steps: 1 => a single event),
keep the default `@default` 1, and mention typical ranges (5–20) for smooth drags
so callers are not misled; reference MouseMoveOptions and the steps property
when making the change.
- Around line 8641-8665: The JSDoc for mouseMove incorrectly calls held-button
moves "dragover" (an HTML drag event); update the description in the mouseMove
comment (the block above mouseMove(x: number, y: number, options?:
WebView.MouseMoveOptions): Promise<void>) to say these are backend/native
dragged mouse/pointer events (e.g., "mouse drag"/"pointer drag" or "dragged
mouse/pointer events") rather than the DOM "dragover" event and add a short note
that this is not the HTML Drag and Drop API; keep references to
mouseDown/mouseUp and the examples intact.

In `@src/bun.js/webview/JSWebView.cpp`:
- Around line 228-287: mouseDown/mouseUp/mouseMove currently mutate
m_mouseButtons/m_mouseX/m_mouseY immediately; change them to only update
internal state after the dispatched JSPromise resolves successfully.
Specifically, in JSWebView::mouseDown, JSWebView::mouseUp and
JSWebView::mouseMove, do not assign m_mouseButtons/m_mouseX/m_mouseY before
calling CDP::Ops::... or WK::Ops::...; instead attach a success continuation to
the returned JSPromise that updates the appropriate members (m_mouseButtons for
mouseDown/mouseUp, m_mouseX/m_mouseY for mouseMove) and leave state unchanged on
rejection. Ensure you reference the same promise returned from
CDP::Ops::mouseDown/mouseUp/mouseMove and WK::Ops::mouseDown/mouseUp/mouseMove
when wiring the continuation so non-Darwin/Chrome branches still return a
promise.

In `@src/bun.js/webview/JSWebViewPrototype.cpp`:
- Around line 557-605: The jsWebViewProtoFuncMouseMove handler currently
defaults steps to 1 and uses s.toInt32(), which allows huge inputs to wrap and
prevents the host's special steps==0 single-final-event path; change this by
defaulting uint32_t steps = 0, read the "steps" field using
s.toNumber(globalObject) (check std::isfinite on the resulting double), convert
to an integer type safely (e.g. truncate/floor via static_cast<int64_t> after
the finite check), then clamp into the range [0, 1000] (allowing zero) before
assigning steps; keep the rest (mods via parseModifiers, checkSlot, and calling
thisObject->mouseMove) unchanged so the normalized steps is forwarded correctly.

In `@test/js/bun/webview/webview-chrome.test.ts`:
- Line 601: The test title is misleading: the it(...) call currently says
"chrome: mouseDown validates — x/y must be finite in mouseMove" but the test
body actually validates mouseMove finite-coordinate guards; update the string in
the it(...) declaration to reflect that (e.g., "chrome: mouseMove validates —
x/y must be finite" or "chrome: mouseMove validates — x/y must be finite in
mouseMove") so the test name matches the validation being performed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4757273d-e8d6-4dc0-904b-834639dc5d27

📥 Commits

Reviewing files that changed from the base of the PR and between 70f9002 and 1ec254c.

📒 Files selected for processing (16)
  • packages/bun-types/bun.d.ts
  • src/bun.js/webview/ChromeBackend.cpp
  • src/bun.js/webview/ChromeBackend.h
  • src/bun.js/webview/JSWebView.cpp
  • src/bun.js/webview/JSWebView.h
  • src/bun.js/webview/JSWebViewPrototype.cpp
  • src/bun.js/webview/ObjCRuntime.cpp
  • src/bun.js/webview/ObjCRuntime.h
  • src/bun.js/webview/WebKitBackend.cpp
  • src/bun.js/webview/WebKitBackend.h
  • src/bun.js/webview/WebViewHost.cpp
  • src/bun.js/webview/WebViewHost.h
  • src/bun.js/webview/host_main.cpp
  • src/bun.js/webview/ipc_protocol.h
  • test/js/bun/webview/webview-chrome.test.ts
  • test/js/bun/webview/webview.test.ts

Comment thread packages/bun-types/bun.d.ts
Comment thread packages/bun-types/bun.d.ts
Comment thread src/runtime/webview/JSWebView.cpp
Comment thread src/runtime/webview/JSWebViewPrototype.cpp
Comment thread test/js/bun/webview/webview-chrome.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/webview/WebViewHost.cpp`:
- Around line 418-480: The comment above mouseMoveIPC that reads "`steps`
intermediate events + one final" is misleading—given the loop (for i = 1; i <
steps; ++i) the code emits steps-1 intermediate events and then one final event
(total of steps events); update that comment near the steps handling/for-loop in
mouseMoveIPC to explicitly state "emits steps-1 intermediate events and one
final event (total = steps events)" so future readers match the actual loop
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83dbfbfa-ec63-45d1-8d9e-c619bc430902

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec254c and 9fae968.

📒 Files selected for processing (2)
  • src/bun.js/webview/ChromeBackend.cpp
  • src/bun.js/webview/WebViewHost.cpp

Comment thread src/bun.js/webview/WebViewHost.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bun-types/bun.d.ts (1)

8203-8696: 🧹 Nitpick | 🔵 Trivial

Run declaration tests directly for this .d.ts change before merge.

Please validate via bun-types type declaration tests directly (without bun bd) for this file change.

As per coding guidelines packages/bun-types/**/*.d.ts: Run TypeScript type declaration tests directly without bun bd since they don't touch compiled code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bun-types/bun.d.ts` around lines 8203 - 8696, Summary: The PR
changed .d.ts typings and the reviewer asks you to run the bun-types declaration
tests directly (not via `bun bd`) to validate the type changes. Fix: run the
TypeScript declaration checks for the changed file(s) directly by invoking the
TypeScript compiler (e.g. tsc --project packages/bun-types/tsconfig.json
--noEmit) or your repository's direct type-test command against
packages/bun-types so that the updated declarations (notably types/interfaces
like WebView, Backend, ConsoleCapture, ConstructorOptions, MouseMoveOptions)
compile without errors; do not run through `bun bd`; if errors appear, iterate
on the affected symbols (e.g., WebView.evaluate, WebView.screenshot overloads,
Backend union variants) until tsc --noEmit succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/bun-types/bun.d.ts`:
- Around line 8203-8696: Summary: The PR changed .d.ts typings and the reviewer
asks you to run the bun-types declaration tests directly (not via `bun bd`) to
validate the type changes. Fix: run the TypeScript declaration checks for the
changed file(s) directly by invoking the TypeScript compiler (e.g. tsc --project
packages/bun-types/tsconfig.json --noEmit) or your repository's direct type-test
command against packages/bun-types so that the updated declarations (notably
types/interfaces like WebView, Backend, ConsoleCapture, ConstructorOptions,
MouseMoveOptions) compile without errors; do not run through `bun bd`; if errors
appear, iterate on the affected symbols (e.g., WebView.evaluate,
WebView.screenshot overloads, Backend union variants) until tsc --noEmit
succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7face990-b5ce-4a87-920e-8b2b0bcb92fc

📥 Commits

Reviewing files that changed from the base of the PR and between 9fae968 and c120e99.

📒 Files selected for processing (4)
  • packages/bun-types/bun.d.ts
  • src/bun.js/webview/JSWebView.cpp
  • src/bun.js/webview/JSWebViewPrototype.cpp
  • test/js/bun/webview/webview-chrome.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/bun.js/webview/WebViewHost.cpp (1)

418-425: ⚠️ Potential issue | 🟡 Minor

Fix stale event-count wording in the mouseMove header comment.

Line 418 says "steps intermediate events + one final", but the loop emits steps - 1 intermediates plus the final event. This is the same mismatch previously flagged.

Proposed comment-only fix
-// mouseMove: dispatch `steps` intermediate events + one final. When
+// mouseMove: dispatch `steps - 1` intermediate events + one final. When
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/webview/WebViewHost.cpp` around lines 418 - 425, The header
comment for mouseMove is incorrect about the number of intermediate events:
update the comment in WebViewHost.cpp (mouseMove) to state that the loop emits
steps - 1 intermediate events plus one final event (not "steps" intermediates),
and adjust surrounding phrasing that references "intermediate events + one
final" so it consistently reflects "steps - 1 intermediates and one final"; keep
existing references to buttonsMask, mouseMoved:, mouseDragged and
mouseEventQueue to preserve context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/bun.js/webview/WebViewHost.cpp`:
- Around line 418-425: The header comment for mouseMove is incorrect about the
number of intermediate events: update the comment in WebViewHost.cpp (mouseMove)
to state that the loop emits steps - 1 intermediate events plus one final event
(not "steps" intermediates), and adjust surrounding phrasing that references
"intermediate events + one final" so it consistently reflects "steps - 1
intermediates and one final"; keep existing references to buttonsMask,
mouseMoved:, mouseDragged and mouseEventQueue to preserve context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3af6d572-fdb0-48a8-b833-a76e5deade67

📥 Commits

Reviewing files that changed from the base of the PR and between c120e99 and a091ce0.

📒 Files selected for processing (1)
  • src/bun.js/webview/WebViewHost.cpp

@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Updated analysis after looking at the darwin logs:

  • Darwin test-bun failures — WERE real WebView test failures, spec-correctness gap: mousedown.buttons = 0 instead of 1. WebCore computes DOM event.buttons via +[NSEvent pressedMouseButtons] (system-wide HID state), which is always 0 for synthesized events. Fixed in 3e7a861 by method-swizzling that class method to return a host-tracked mask — same approach Safari's WebDriver uses (WebAutomationSessionMac.mm:80).
  • Windows done() callback shard failures — unrelated existing flake (asynchronously failing test with a done callback does not hang etc.); same shard has been failing across unrelated PRs.
  • 2 of 19 asan shards failing with fdleak_rm ASAN use-after-poison in ModuleLoader.cpp:1085 — unrelated existing flake hitting multiple PRs this week (noisy-neighbor transpiler-cache ASAN violation in the shell/leak test's subprocess burst). The remaining 17 asan shards pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/webview/ObjCRuntime.cpp`:
- Around line 448-467: The comment says the swizzle failure is "Logged via
m_loadError" but the code using getClassMethod(NSEvent::cls,
sel("pressedMouseButtons")) does not set m_loadError or otherwise log on
failure; update the code to set m_loadError (or call the existing diagnostic
logger) when getClassMethod returns null so a clear message is recorded (include
class NSEvent and selector "pressedMouseButtons" and that
methodSetImplementation was not applied), referencing pressedMouseButtonsImpl,
getClassMethod, methodSetImplementation and m_loadError; alternatively, if you
prefer no runtime diagnostic, change the comment to state the swizzle silently
falls back instead of claiming it is logged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f7f56745-93e8-4b57-858e-173a91e08343

📥 Commits

Reviewing files that changed from the base of the PR and between a091ce0 and 3e7a861.

📒 Files selected for processing (3)
  • src/bun.js/webview/ObjCRuntime.cpp
  • src/bun.js/webview/ObjCRuntime.h
  • src/bun.js/webview/WebViewHost.cpp

Comment thread src/runtime/webview/ObjCRuntime.cpp
@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Build 48494: all 6 darwin test-bun shards pass — my WebKit fix works on macOS 14/15/26 aarch64 and 14 x64. Full webview.test.ts ran (48 pass, 15 todo-if-CI, 0 fail).

The 9 remaining CI failures are all pre-existing flakes on unrelated shards:

None of the flaky failures touch WebView code.

Comment thread src/runtime/webview/WebViewHost.cpp
Comment thread packages/bun-types/bun.d.ts Outdated
Comment thread src/bun.js/webview/WebViewHost.cpp Outdated
Comment thread src/bun.js/webview/ChromeBackend.cpp Outdated
Comment thread src/bun.js/webview/WebViewHost.cpp Outdated
robobun and others added 9 commits May 4, 2026 10:29
Adds three low-level pointer primitives to Bun.WebView:

  view.mouseDown(options?)        — press button at current position
  view.mouseUp(options?)          — release button at current position
  view.mouseMove(x, y, options?)  — move to (x,y), optional steps

Canvas drag-and-drop tests that rely on intermediate pointermove
events can now run on both Chrome and WebKit backends without
dropping to raw CDP:

  await view.mouseMove(from.x, from.y);
  await view.mouseDown();
  await view.mouseMove(to.x, to.y, { steps: 5 });
  await view.mouseUp();

- Parent tracks cursor position (m_mouseX/Y) and pressed-button mask
  (m_mouseButtons) on JSWebView; down/up/move update accordingly.
- Chrome backend emits Input.dispatchMouseEvent with the right
  button/buttons fields; multi-step moves send (steps-1) fire-and-
  forget events + a tracked final that resolves the promise.
- WebKit backend synthesizes NSEvents (LeftMouseDown/Up,
  LeftMouseDragged/RightMouseDragged/OtherMouseDragged) and uses
  _simulateMouseMove: SPI for plain hovers. Events go through the
  existing _doAfterProcessingAllPendingMouseEvents: barrier — the
  promise resolves only after WebContent has dispatched every
  handler, same guarantee click() provides.

All events fire with isTrusted:true.
…p steps via double

- JSWebView.cpp: mouseDown/Up/Move only mutate m_mouseX/Y/Buttons when
  the returned promise isn't synchronously rejected (dead transport).
  Async rejections still stale the state, but the view is unusable
  past that point so every follow-up op will reject too.
- JSWebViewPrototype.cpp: read steps as a double, clamp in [1, 1000],
  then cast. toInt32() on values above 2^31 wraps to negative, so
  steps: 1e12 previously clamped-up to 1 instead of the 1000 cap.
- bun.d.ts: clarify that `steps` is the TOTAL event count (1 = single
  event at target), not an intermediate count; clarify mouseMove with
  a button held fires native dragged mouse/pointer events, not the
  HTML5 Drag and Drop API's dragover.
- test: rename 'mouseDown validates' -> 'mouseMove validates' (the
  body tests mouseMove guards, not mouseDown).
The loop emits (steps - 1) intermediates + 1 final = steps total events,
not 'steps intermediate + one final' as the comment suggested. Matches
ChromeBackend.cpp's wording.
…Kit)

CI surfaced a spec-correctness gap in the WebKit backend:
  event.buttons in mousedown/mouseup/contextmenu handlers always read
  as 0 for synthesized events, breaking drag tests that observe the
  button-held bitmask to distinguish hover-move from drag-move.

The cause: WebCore's PlatformEventFactoryMac computes
event.buttons via '+[NSEvent pressedMouseButtons]', which is the
system-wide HID state. For NSEvent objects we synthesize at the
WKWebView responder level, no physical button is down, so this
returns 0.

Fix, same mechanism Safari's WebDriver uses
(WebAutomationSessionMac.mm:80): swap the class method
implementation of '+[NSEvent pressedMouseButtons]' to return a
global 'NSEvent::s_trackedButtonsMask' that the host sets before
each mouseDown/Up/Move dispatch. Theirs is scope-limited with a
scope_exit; ours is permanent — the WebView host is a headless
subprocess, we never want real HID state.

- ObjCRuntime.h/.cpp: declare s_trackedButtonsMask, dlsym
  class_getClassMethod + method_setImplementation, swap the impl
  at load() time. Permanent, non-fatal if the swap fails
  (event.buttons just stays 0 in that case).
- WebViewHost.cpp: stamp the mask before mouseDownIPC/mouseUpIPC/
  mouseMoveIPC dispatch. JSWebView already computes the post-op
  mask (mousedown sees buttons WITH the pressed bit, mouseup
  WITHOUT) and threads it through IPC.
- click()/doNativeClick unchanged — its callers don't observe
  event.buttons, only button/detail/modifiers.
Drag-sequence test timed out on macOS 14/15 aarch64 only: the first
view.mouseMove(x, y) call (no button held) uses _simulateMouseMove:
which enqueues into mouseEventQueue but WebContent never drains it on
that OS/arch combo. The _doAfterProcessingAllPendingMouseEvents:
barrier then waits forever and bun:test hits its 90s timeout.

Fix: on WebKit, skip the NSEvent dispatch for hover (buttonsMask==0)
moves. Parent-side cursor tracking (m_mouseX/Y on JSWebView) already
runs, so a following mouseDown fires at the correct position — which
is the primary use case (drag automation). DOM :hover / cursor CSS
won't update on WebKit until someone wires a CGEvent-based hover
(screen-level, following WebAutomationSessionMac's wheel path); left
for a future PR.

Drag moves (button held) still dispatch normally via mouseDragged: /
rightMouseDragged: / otherMouseDragged: — those route through the
responder chain and the barrier drains them because the prior
mouseDown: already queued onto WebContent.

Test update: the drag-sequence test no longer asserts on an initial
hover mousemove event (it's not dispatched anymore). Chrome-backend
tests are unchanged since CDP's mouseMoved dispatch works for hover
at any button state.

Confirmed working on macOS 26 aarch64 (full suite passed last run)
and the reference darwin-14 aarch64 shard hung specifically at the
test's first view.mouseMove(40, 40) call.
Four fixes from review:

- WebViewHost::mouseMoveIPC picked RightMouseDragged when buttonsMask
  had bit 0 AND bit 1 set (left + right held) — the 0x2 check landed
  first and there was no explicit 0x1 guard before it, so the LeftDrag
  default only survived when bit 0 was the ONLY bit set, not when it
  was the lowest set bit. Same for 0x5 (left + middle) falling through
  to OtherDrag. Adds an explicit `if (buttonsMask & 0x1)` branch that
  keeps LeftDrag so 0x3/0x5/0x7 all dispatch as left-drag NSEvents,
  matching the Chrome backend's selector priority in
  ChromeBackend::mouseMove.

- Function header above mouseMoveIPC was stale: said "steps
  intermediate events + one final" (now correctly "(steps-1)
  intermediate + 1 final = steps total") and "fire MouseMoved via
  mouseMoved:" when buttonsMask==0 (actually sync-Acks without
  dispatching — _simulateMouseMove: hangs the barrier on macOS 14/15
  aarch64).

- ipc_protocol.h MouseMovePayload comment said "the parent expands
  before send" (host does the interpolation) and "fires steps+1
  NSEvents" (fires exactly `steps`).

- ObjCRuntime.cpp comment claimed swizzle failure is "Logged via
  m_loadError" but the getClassMethod branch is silent — clarified
  that failure is intentionally silent because every AppKit target
  has the method.

- bun.d.ts JSDoc promised standalone hover "works for :hover styles"
  unconditionally, but WebKit short-circuits no-button moves (see the
  hover-path rationale in WebViewHost.cpp). Qualified to say Chrome
  only; users needing :hover on WebKit know to pick the Chrome
  backend.
Same stale wording swept out of bun.d.ts in c120e99 and WebViewHost.cpp /
ipc_protocol.h in 6b13ba6 survived in ChromeBackend.cpp's mouseMove
function header. CDP `Input.dispatchMouseEvent type:"mouseMoved"` with
a non-zero `buttons` field fires `mousemove`/`pointermove` only —
HTML5 DnD events require a `draggable` element starting the drag
page-side (or `Input.dispatchDragEvent`, not used). Comment-only.
The process-wide +[NSEvent pressedMouseButtons] swizzle (from
ObjCRuntime.cpp) lets synthesized NSEvents report the correct DOM
event.buttons. The low-level mouseDown/Up/Move primitives stamp the
static before dispatch; doNativeClick (backing view.click(x,y) /
view.click(selector)) did not, so a prior unbalanced mouseDown on this
view or a sibling view would leak its mask into click()'s DOM events —
mousedown/mouseup would report whatever the static was last set to.

Fix: stamp the pressing bit before each mouseDown and clear before
each mouseUp in doNativeClick, matching the primitives. Bonus:
click()'s event.buttons is now spec-correct (mousedown includes the
pressing bit, mouseup excludes it) instead of whatever the real HID
state was pre-PR (always 0 in a headless host).
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/e08928ec/webview-mouse-primitives branch from ff0a5d6 to c9237ab Compare May 4, 2026 10:29

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found on this pass — all prior review feedback has been addressed — but this adds a new public Bun.WebView API surface plus process-wide +[NSEvent pressedMouseButtons] swizzling and cross-backend native input synthesis, so it's worth a human sign-off on the API shape and the WebKit hover-is-a-no-op limitation before merge.

Extended reasoning...

Overview

This PR adds three new public methods to Bun.WebView (mouseDown, mouseUp, mouseMove) for drag automation, spanning 16 files: TypeScript declarations, the JSWebView prototype/state layer, both the Chrome (CDP) and WebKit (WKWebView host subprocess) backends, the IPC wire protocol, ObjC runtime glue (including a permanent method-swizzle of +[NSEvent pressedMouseButtons]), and ~270 lines of new tests across both backend suites.

Security risks

None identified. Inputs are validated (finite x/y, clamped steps to [1,1000], clamped clickCount), the IPC payloads are fixed-size POD structs with static_assert size checks, and the swizzle is confined to the headless WebKit host subprocess which never handles real user input. No injection, auth, or data-exposure surface.

Level of scrutiny

High. This is a new user-facing API (Playwright-style page.mouse.* semantics) with backend-divergent behavior that's now documented (Chrome fires hover mousemove; WebKit intentionally no-ops hover and only updates internal cursor position). The ObjC method swizzle is process-global and affects the pre-existing click() path (now stamps s_trackedButtonsMask per the fix in c9237ab). The cross-backend event-count semantics, the parent-side state tracking (m_mouseX/Y/Buttons), and the fire-and-forget intermediate CDP events are all design choices a maintainer should ratify.

Other factors

The PR has been through several review rounds (CodeRabbit + claude inline comments) and every flagged issue — steps toInt32 wrap, sync-rejection state guard, multi-button drag selector priority, stale comments in three files, the doNativeClick cross-view mask leak, and JSDoc accuracy for the WebKit hover limitation — has been addressed in follow-up commits with all threads resolved. CI darwin shards pass per the author's build-48494 note; remaining failures are documented as unrelated flakes. Test coverage looks solid (drag sequence, hover, click synthesis, contextmenu+modifiers, NaN/Inf validation, prototype existence). The remaining reason to defer is purely scope: new public API + native input synthesis + ObjC swizzling is the kind of change that benefits from a human reviewer confirming the API contract and the documented WebKit limitation are acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: low-level pointer primitives (mouse.down/up/move) for WebView drag automation

1 participant