From 51f9606b9fe3828170409868de3b8707600e7f0f Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:19:05 +0200 Subject: [PATCH 1/2] Keep hover and pointer overrides on desktop set_viewport Since #618, set_viewport sent Emulation.setTouchEmulationEnabled on every call, so every desktop viewport and every resize sent enabled: false. Chrome answers that by resetting hover and pointer to the detected input devices, even if touch emulation was never on, which undoes --blink-settings overrides. On headless Linux, which has no input devices, (hover: hover) and (pointer: fine) stopped matching after set_viewport or resize. set_viewport now sends enabled: false only when the page goes back to a desktop viewport after set_viewport(mobile: true), and only once. Chromium follows the same rule for its own resets since Chrome 141 (Chromium commit a65c088). Fixes #664 --- CHANGELOG.md | 3 +++ lib/ferrum/page.rb | 16 +++++++++++----- sig/ferrum/page.rbs | 1 + spec/page_spec.rb | 24 ++++++++++++++++++++++++ spec/unit/page_spec.rb | 15 +++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 spec/unit/page_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index ef12d3b0..859bb1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,9 @@ - When Chrome can't compute an element's content quads, `node.click` falls back to its bounding rect, but it added the page scroll offset and missed the element on a scrolled page. It clicks in viewport coordinates now - `Proxy#rotate` to an upstream proxy failed every HTTPS request with `ERR_TUNNEL_CONNECTION_FAILED` +- `set_viewport` without `mobile: true` and `resize` turned touch emulation off, even on a page that never had it on. + Chrome then resets hover and pointer to the detected input devices, which undid `--blink-settings` overrides such + as a mouse on headless Linux. Touch emulation is only turned off now after a `mobile: true` viewport [#664] ### Removed - `Browser::Process#default_user_agent`, `#browser_version`, `#protocol_version`, `#v8_version` and `#webkit_version`, diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index cad99476..419aeb97 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -109,6 +109,7 @@ def initialize(client, context_id:, target_id:, proxy: nil) @main_frame = Frame.new(nil, self) @event = Utils::Event.new.tap(&:set) @document_version = 0 + @touch_emulation_enabled = false self.proxy = proxy @mouse = Mouse.new(self) @@ -193,11 +194,16 @@ def close_connection # @param [Boolean] mobile whether to emulate mobile device and enable touch # def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false) - command( - "Emulation.setTouchEmulationEnabled", - enabled: mobile, - maxTouchPoints: 1 - ) + # Turning touch emulation off makes Chrome reset hover and pointer to the detected input devices, which undoes + # --blink-settings overrides, so only turn it off after a mobile viewport turned it on. + if mobile || @touch_emulation_enabled + command( + "Emulation.setTouchEmulationEnabled", + enabled: mobile, + maxTouchPoints: 1 + ) + @touch_emulation_enabled = mobile + end command( "Emulation.setDeviceMetricsOverride", diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index 643b0ace..acb0ef2c 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -44,6 +44,7 @@ module Ferrum @downloads: Downloads @referrer: String? @document_version: ::Integer + @touch_emulation_enabled: bool def initialize: ((Client | SessionClient) client, context_id: String?, target_id: String, ?proxy: { host: String, port: ::Integer, user: String?, password: String? }?) -> void diff --git a/spec/page_spec.rb b/spec/page_spec.rb index bc3d2861..4ffb9b6c 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -256,6 +256,30 @@ def is_mobile? page.reload end.not_to(change { is_mobile? }) end + + it "keeps the pointer type set with --blink-settings", :launch do + browser = Ferrum::Browser.new(browser_options: { "blink-settings" => "primaryPointerType=2,availablePointerTypes=2" }) + browser.go_to(base_url) + + expect do + browser.set_viewport(width: 500, height: 300) + end.not_to(change { browser.evaluate("matchMedia('(pointer: coarse)').matches") }.from(true)) + ensure + browser&.quit + end + end + + context "when mobile was true before" do + it "disables touch/mobile emulation again" do + page.go_to("/") + page.set_viewport(mobile: true) + page.reload + + expect do + page.set_viewport(mobile: false) + page.reload + end.to change { is_mobile? }.from(true).to(false) + end end end diff --git a/spec/unit/page_spec.rb b/spec/unit/page_spec.rb new file mode 100644 index 00000000..041ca370 --- /dev/null +++ b/spec/unit/page_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +describe Ferrum::Page do + describe "#set_viewport" do + it "turns touch emulation off only once after a mobile viewport" do + page.set_viewport(mobile: true) + allow(page).to receive(:command).and_call_original + + 2.times { page.set_viewport(width: 500, height: 300) } + + expect(page).to have_received(:command) + .with("Emulation.setTouchEmulationEnabled", enabled: false, maxTouchPoints: 1).once + end + end +end From 34d595e7ef97a171c3358ceb27ff4a8253a89a8c Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Tue, 27 Oct 2026 16:09:57 +0300 Subject: [PATCH 2/2] fix(page): default set_viewport mobile: to nil, leaving touch emulation as is A desktop set_viewport or resize no longer sends setTouchEmulationEnabled, so --blink-settings hover and pointer stay. mobile: true still turns touch on and an explicit mobile: false turns it off. Replaces the per-page touch emulation state, so the unit spec for it is removed. Refs #664 --- CHANGELOG.md | 5 ++--- docs/14-emulation.md | 7 ++++++- lib/ferrum/page.rb | 22 +++++++--------------- sig/ferrum/page.rbs | 3 +-- spec/page_spec.rb | 2 ++ spec/unit/page_spec.rb | 15 --------------- 6 files changed, 18 insertions(+), 36 deletions(-) delete mode 100644 spec/unit/page_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0548d20b..7fb4d491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,8 @@ ### Changed ### Fixed -- `set_viewport` without `mobile: true` and `resize` turned touch emulation off, even on a page that never had it on. - Chrome then resets hover and pointer to the detected input devices, which undid `--blink-settings` overrides such - as a mouse on headless Linux. Touch emulation is only turned off now after a `mobile: true` viewport [#664] +- `set_viewport` and `resize` turned touch emulation off on every desktop viewport, which reset hover and pointer and + undid `--blink-settings` overrides. `mobile:` defaults to `nil` and leaves touch as is, `false` turns it off [#664] ### Removed diff --git a/docs/14-emulation.md b/docs/14-emulation.md index c6d9d5b7..f796382b 100644 --- a/docs/14-emulation.md +++ b/docs/14-emulation.md @@ -14,7 +14,8 @@ Overrides device screen dimensions and emulates viewport. When `:mobile` is * :width `Integer`, viewport width. `0` by default * :height `Integer`, viewport height. `0` by default * :scale_factor `Float`, device scale factor. `0` by default - * :mobile `Boolean`, whether to emulate mobile device and enable touch. `false` by default + * :mobile `Boolean | nil`, `true` emulates a mobile device with touch, `false` a desktop without touch, `nil` a + desktop and leaves touch emulation as is. `nil` by default ```ruby page.set_viewport(width: 1000, height: 600, scale_factor: 3) @@ -29,3 +30,7 @@ size preset: ```ruby page.set_viewport(mobile: true) ``` + +Turning touch emulation off with `mobile: false` makes Chrome reset hover and pointer media features to the +detected input devices, which undoes `--blink-settings` overrides such as `primaryHoverType` until the next +navigation. Leave `:mobile` out unless the page was switched to mobile before. diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 52f4a1f3..3597d5b0 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -110,7 +110,6 @@ def initialize(client, context_id:, target_id:, proxy: nil) @main_frame = Frame.new(nil, self) @event = Utils::Event.new.tap(&:set) @document_version = 0 - @touch_emulation_enabled = false self.proxy = proxy @mouse = Mouse.new(self) @@ -192,19 +191,12 @@ def close_connection # # @param [Float] scale_factor device scale factor value. 0 disables the override # - # @param [Boolean] mobile whether to emulate mobile device and enable touch - # - def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false) - # Turning touch emulation off makes Chrome reset hover and pointer to the detected input devices, which undoes - # --blink-settings overrides, so only turn it off after a mobile viewport turned it on. - if mobile || @touch_emulation_enabled - command( - "Emulation.setTouchEmulationEnabled", - enabled: mobile, - maxTouchPoints: 1 - ) - @touch_emulation_enabled = mobile - end + # @param [Boolean, nil] mobile `true` emulates a mobile device with touch, `false` a desktop without touch, `nil` + # a desktop and leaves touch emulation as is. Turning touch off resets hover and pointer to the detected input + # devices, which undoes `--blink-settings` overrides until the next navigation + # + def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: nil) + command("Emulation.setTouchEmulationEnabled", enabled: mobile, maxTouchPoints: 1) unless mobile.nil? command( "Emulation.setDeviceMetricsOverride", @@ -212,7 +204,7 @@ def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false) width: width, height: height, deviceScaleFactor: scale_factor, - mobile: mobile + mobile: mobile || false ) end diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index d7e276e1..b73620fe 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -44,7 +44,6 @@ module Ferrum @downloads: Downloads @referrer: String? @document_version: ::Integer - @touch_emulation_enabled: bool def initialize: ((Client | SessionClient) client, context_id: String?, target_id: String, ?proxy: { host: String, port: ::Integer, user: String?, password: String? }?) -> void @@ -58,7 +57,7 @@ module Ferrum def close_connection: () -> void - def set_viewport: (?width: ::Integer, ?height: ::Integer, ?scale_factor: ::Float, ?mobile: bool) -> Hash[String, untyped] + def set_viewport: (?width: ::Integer, ?height: ::Integer, ?scale_factor: ::Float, ?mobile: bool?) -> Hash[String, untyped] def resize: (?width: ::Numeric?, ?height: ::Numeric?, ?fullscreen: bool) -> Hash[String, untyped] diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 9160aa90..272a6d57 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -304,7 +304,9 @@ def is_mobile? page.reload end.not_to(change { is_mobile? }) end + end + context "when mobile is not given" do it "keeps the pointer type set with --blink-settings", :launch do browser = Ferrum::Browser.new(browser_options: { "blink-settings" => "primaryPointerType=2,availablePointerTypes=2" }) browser.go_to(base_url) diff --git a/spec/unit/page_spec.rb b/spec/unit/page_spec.rb deleted file mode 100644 index 041ca370..00000000 --- a/spec/unit/page_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -describe Ferrum::Page do - describe "#set_viewport" do - it "turns touch emulation off only once after a mobile viewport" do - page.set_viewport(mobile: true) - allow(page).to receive(:command).and_call_original - - 2.times { page.set_viewport(width: 500, height: 300) } - - expect(page).to have_received(:command) - .with("Emulation.setTouchEmulationEnabled", enabled: false, maxTouchPoints: 1).once - end - end -end