From ab8a1b2e69a5b790da8d09a89089463b054a5a15 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 24 Aug 2026 20:56:23 +0300 Subject: [PATCH 1/3] feat: add touch emulation to `Page#set_viewport`'s `mobile:` option `set_viewport(mobile: true)` already overrode device metrics via `Emulation.setDeviceMetricsOverride`; this adds the matching `Emulation.setTouchEmulationEnabled` call, so `mobile: true` fully covers mobile-viewport interpretation and touch emulation together. `width`/`height` now default to `0`, matching their documented "0 disables the override" semantics, so mobile/touch can be enabled on its own at the page's current size: page.set_viewport(mobile: true) Deliberately does not add a `Browser.new(mobile:)` option, a device size/scale preset, or a user agent: callers can already instantiate the browser and call `set_viewport` whenever they want, and device presets/UA are orthogonal concerns better left to the caller. Does not touch `Page#resize` either: it's Capybara's generic window-resize hook, unrelated to device emulation. Co-authored-by: Duncan Bayne --- docs/14-emulation.md | 16 ++++++++++++++-- lib/ferrum/page.rb | 10 ++++++++-- sig/ferrum/page.rbs | 2 ++ spec/page_spec.rb | 26 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/14-emulation.md b/docs/14-emulation.md index 3baaea88..c6d9d5b7 100644 --- a/docs/14-emulation.md +++ b/docs/14-emulation.md @@ -6,14 +6,26 @@ sidebar_position: 14 #### set_viewport -Overrides device screen dimensions and emulates viewport. +Overrides device screen dimensions and emulates viewport. When `:mobile` is +`true`, this also enables touch emulation (`navigator.maxTouchPoints`, +`ontouchstart`) alongside the mobile viewport metrics. * options `Hash` * :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. `false` by default + * :mobile `Boolean`, whether to emulate mobile device and enable touch. `false` by default ```ruby page.set_viewport(width: 1000, height: 600, scale_factor: 3) ``` + +`:width`, `:height`, and `:scale_factor` all default to `0`, which per the +[DevTools protocol](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride) +disables overriding that particular value. This means `mobile:` can be +enabled on its own, at the page's current size, without specifying a device +size preset: + +```ruby +page.set_viewport(mobile: true) +``` diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 2b05e7b3..f813114a 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -176,9 +176,15 @@ def close_connection # # @param [Float] scale_factor device scale factor value. 0 disables the override # - # @param [Boolean] mobile whether to emulate mobile device + # @param [Boolean] mobile whether to emulate mobile device and enable touch # - def set_viewport(width:, height:, scale_factor: 0, mobile: false) + def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false) + command( + "Emulation.setTouchEmulationEnabled", + enabled: mobile, + maxTouchPoints: 1 + ) + command( "Emulation.setDeviceMetricsOverride", slowmoable: true, diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index a7e00efb..6c32ae65 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -53,6 +53,8 @@ module Ferrum def close_connection: () -> void + 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] def position: () -> Hash[String, ::Integer] diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 2e88867d..4e113164 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -198,6 +198,32 @@ expect(page.viewport_size).to eq([500, 300]) expect(page.device_pixel_ratio).to eq(2) end + + def is_mobile? + page.evaluate("'ontouchstart' in window || navigator.maxTouchPoints > 0") + end + + context "when mobile is true" do + it "enables touch/mobile emulation without changing the window size" do + page.go_to("/") + + expect do + page.set_viewport(mobile: true) + page.reload + end.to change { is_mobile? }.to(true) + end + end + + context "when mobile is false" do + it "does not enable touch/mobile emulation" do + page.go_to("/") + + expect do + page.set_viewport(mobile: false) + page.reload + end.not_to(change { is_mobile? }) + end + end end describe "#on" do From b9b7069f9eb2850e544b9d5fe7991aa7fe1b452a Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 24 Aug 2026 23:27:50 +0300 Subject: [PATCH 2/3] fix: lint --- lib/ferrum/browser/binary.rb | 3 +-- lib/ferrum/page/screenshot.rb | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/ferrum/browser/binary.rb b/lib/ferrum/browser/binary.rb index f76eaaff..95a2d9e1 100644 --- a/lib/ferrum/browser/binary.rb +++ b/lib/ferrum/browser/binary.rb @@ -68,7 +68,7 @@ def prepare_paths [paths, exts] end - # rubocop:disable Style/CollectionCompact + # rubocop:disable-next Style/CollectionCompact def lazy_find(cmds) cmds.lazy.map do |cmd, path, ext| absolute_path = File.absolute_path(cmd) @@ -81,7 +81,6 @@ def lazy_find(cmds) cmd end.reject(&:nil?) # .compact isn't defined on Enumerator::Lazy end - # rubocop:enable Style/CollectionCompact end end end diff --git a/lib/ferrum/page/screenshot.rb b/lib/ferrum/page/screenshot.rb index cd64a2c8..00544210 100644 --- a/lib/ferrum/page/screenshot.rb +++ b/lib/ferrum/page/screenshot.rb @@ -32,9 +32,9 @@ module Screenshot A1: { width: 23.40, height: 33.10 }, A2: { width: 16.54, height: 23.40 }, A3: { width: 11.70, height: 16.54 }, - A4: { width: 8.27, height: 11.70 }, - A5: { width: 5.83, height: 8.27 }, - A6: { width: 4.13, height: 5.83 } + A4: { width: 8.27, height: 11.70 }, + A5: { width: 5.83, height: 8.27 }, + A6: { width: 4.13, height: 5.83 } }.freeze # From 36ea2624579bca3126d4d8596c4f123dc38bd1ee Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 24 Aug 2026 23:34:05 +0300 Subject: [PATCH 3/3] chore: add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c54443a..6f8dc56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ so callers can `#join` it if they need cleanup to have finished, e.g. before process exit or before reusing a fixed port. Default (`wait: true`) keeps the previous synchronous behavior; `#restart` always waits. - `Ferrum::Page::Stream#close_stream`, a public method that releases a CDP `IO` stream handle (`IO.close`). +- `Ferrum::Page#set_viewport`'s `mobile:` option now also enables touch emulation + (`Emulation.setTouchEmulationEnabled`), alongside the existing device metrics override, so it fully emulates a + mobile device's viewport and touch support together. [#94] ### Changed - `Ferrum::Page::Stream#stream` now closes the CDP stream handle (`IO.close`) once it's been fully read, so streams