From 9d1c89155ba0d94568670209dad7950733da3789 Mon Sep 17 00:00:00 2001 From: sean0407 <22557931+sean0407@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:06:29 +0800 Subject: [PATCH 1/4] fix(mouse): preserve held button during movement --- CHANGELOG.md | 2 ++ lib/ferrum/mouse.rb | 2 ++ spec/mouse_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a97178d..3d0841d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ the browser; a failed `IO.close` is raised to the caller. ### Fixed +- Mouse moves sent while a button is held now include the active `button` value, allowing Chrome to start drag sessions + and emit `Input.dragIntercepted` [#635] - An out-of-process iframe, e.g. Chrome's PDF viewer for an embedded PDF, appearing while a web worker was connected raised `'Page.enable' wasn't found` in the event thread. Only pages and iframes are now searched for its parent [#640] - A browser that stopped reading its websocket froze the whole client: a large command blocked in a socket write diff --git a/lib/ferrum/mouse.rb b/lib/ferrum/mouse.rb index 6e5a8f9e..b4f2042c 100644 --- a/lib/ferrum/mouse.rb +++ b/lib/ferrum/mouse.rb @@ -156,12 +156,14 @@ def move(x:, y:, steps: 1) steps.times do |i| new_x = from_x + ((@x - from_x) * ((i + 1) / steps.to_f)) new_y = from_y + ((@y - from_y) * ((i + 1) / steps.to_f)) + button = BUTTON_MASKS.find { |_name, mask| mask.positive? && @buttons.anybits?(mask) }&.first || "none" @page.command("Input.dispatchMouseEvent", slowmoable: true, type: "mouseMoved", x: new_x, y: new_y, + button: button, buttons: @buttons) end diff --git a/spec/mouse_spec.rb b/spec/mouse_spec.rb index e823bbbc..c1ad5452 100644 --- a/spec/mouse_spec.rb +++ b/spec/mouse_spec.rb @@ -57,6 +57,29 @@ end describe "#move" do + it "preserves the pressed button for drag interception" do + browser.go_to("/simple") + browser.execute(<<~JS) + const source = document.createElement("div"); + source.draggable = true; + source.style.cssText = "position:fixed;left:0;top:0;width:100px;height:100px"; + document.body.appendChild(source); + JS + + intercepted = Queue.new + browser.page.on("Input.dragIntercepted") { |params| intercepted << params } + browser.page.command("Input.setInterceptDrags", enabled: true) + + browser.mouse.move(x: 10, y: 10) + .down(button: :left) + .move(x: 80, y: 80, steps: 5) + + expect(intercepted.pop(timeout: 1)).not_to be_nil + ensure + browser&.mouse&.up(button: :left) + browser&.page&.command("Input.setInterceptDrags", enabled: false) + end + it "splits into steps" do browser.go_to("/simple") browser.mouse.move(x: 100, y: 100) From dbcd7b533c712527376918e220c37a5ddfecff59 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Fri, 16 Oct 2026 13:31:51 +0300 Subject: [PATCH 2/4] ref(mouse): resolve the held button once per move --- lib/ferrum/mouse.rb | 6 +++++- sig/ferrum/mouse.rbs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ferrum/mouse.rb b/lib/ferrum/mouse.rb index b4f2042c..f34985e5 100644 --- a/lib/ferrum/mouse.rb +++ b/lib/ferrum/mouse.rb @@ -152,11 +152,11 @@ def move(x:, y:, steps: 1) from_y = @y @x = x @y = y + button = pressed_button steps.times do |i| new_x = from_x + ((@x - from_x) * ((i + 1) / steps.to_f)) new_y = from_y + ((@y - from_y) * ((i + 1) / steps.to_f)) - button = BUTTON_MASKS.find { |_name, mask| mask.positive? && @buttons.anybits?(mask) }&.first || "none" @page.command("Input.dispatchMouseEvent", slowmoable: true, @@ -187,6 +187,10 @@ def validate_button(button) button end + def pressed_button + BUTTON_MASKS.find { |_name, mask| mask.positive? && @buttons.anybits?(mask) }&.first || "none" + end + def register_event_button(type, button) case type when "mousePressed" diff --git a/sig/ferrum/mouse.rbs b/sig/ferrum/mouse.rbs index 45d57253..7d87eeeb 100644 --- a/sig/ferrum/mouse.rbs +++ b/sig/ferrum/mouse.rbs @@ -29,6 +29,8 @@ module Ferrum def validate_button: (Symbol button) -> String + def pressed_button: () -> String + def register_event_button: (String type, String button) -> ::Integer end end From c8b07736ae6edeaa939d8946e64af9750b13e8f7 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Fri, 16 Oct 2026 13:31:51 +0300 Subject: [PATCH 3/4] test(mouse): assert native HTML5 drag and drop events instead of drag interception The previous spec relied on Queue#pop(timeout:), which Ruby 3.1 doesn't support. --- spec/mouse_spec.rb | 23 ++++----------------- spec/support/views/html5_drag.erb | 33 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 spec/support/views/html5_drag.erb diff --git a/spec/mouse_spec.rb b/spec/mouse_spec.rb index c1ad5452..df683516 100644 --- a/spec/mouse_spec.rb +++ b/spec/mouse_spec.rb @@ -57,27 +57,12 @@ end describe "#move" do - it "preserves the pressed button for drag interception" do - browser.go_to("/simple") - browser.execute(<<~JS) - const source = document.createElement("div"); - source.draggable = true; - source.style.cssText = "position:fixed;left:0;top:0;width:100px;height:100px"; - document.body.appendChild(source); - JS + it "drags and drops with a button held" do + browser.go_to("/html5_drag") - intercepted = Queue.new - browser.page.on("Input.dragIntercepted") { |params| intercepted << params } - browser.page.command("Input.setInterceptDrags", enabled: true) - - browser.mouse.move(x: 10, y: 10) - .down(button: :left) - .move(x: 80, y: 80, steps: 5) + browser.mouse.move(x: 10, y: 10).down.move(x: 250, y: 50, steps: 10).up - expect(intercepted.pop(timeout: 1)).not_to be_nil - ensure - browser&.mouse&.up(button: :left) - browser&.page&.command("Input.setInterceptDrags", enabled: false) + expect(browser.evaluate("window.events")).to eq(%w[dragstart dragover drop dragend]) end it "splits into steps" do diff --git a/spec/support/views/html5_drag.erb b/spec/support/views/html5_drag.erb new file mode 100644 index 00000000..34d7869f --- /dev/null +++ b/spec/support/views/html5_drag.erb @@ -0,0 +1,33 @@ + + + + + ferrum html5 drag and drop + + + + + +
+
+ + From e518b084bf607d5d2d225a8e55ddf68570ca2004 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Fri, 16 Oct 2026 13:31:51 +0300 Subject: [PATCH 4/4] docs(changelog): mention native HTML5 drag and drop for #635 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0841d0..b325992f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ the browser; a failed `IO.close` is raised to the caller. ### Fixed -- Mouse moves sent while a button is held now include the active `button` value, allowing Chrome to start drag sessions - and emit `Input.dragIntercepted` [#635] +- `Mouse#move` with a button held didn't send which button, so Chrome never started a drag. Native HTML5 drag and drop + now works with `down`, `move` and `up`, and `Input.dragIntercepted` fires when drags are intercepted [#635] - An out-of-process iframe, e.g. Chrome's PDF viewer for an embedded PDF, appearing while a web worker was connected raised `'Page.enable' wasn't found` in the event thread. Only pages and iframes are now searched for its parent [#640] - A browser that stopped reading its websocket froze the whole client: a large command blocked in a socket write