From a60d423f6ef56aaace0e01721943fd44985a40f3 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Thu, 20 Mar 2025 16:42:32 +1100 Subject: [PATCH 01/11] Add mobile support to Page Note that when mobile is true, *both* mobile emulation and touch support are enabled. The spec checks whether mobile support has been enabled by checking for touch support with JavaScript. --- lib/ferrum/page.rb | 17 +++++++++++++++-- spec/page_spec.rb | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 9abcf918..842dc184 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -157,9 +157,22 @@ def set_viewport(width:, height:, scale_factor: 0, mobile: false) deviceScaleFactor: scale_factor, mobile: mobile ) + + options = if mobile + { + enabled: true, + maxTouchPoints: 1 + } + else + { + enabled: false + } + end + + command("Emulation.setTouchEmulationEnabled", **options) end - def resize(width: nil, height: nil, fullscreen: false) + def resize(width: nil, height: nil, fullscreen: false, mobile: false) if fullscreen width, height = document_size self.window_bounds = { window_state: "fullscreen" } @@ -168,7 +181,7 @@ def resize(width: nil, height: nil, fullscreen: false) self.window_bounds = { width: width, height: height } end - set_viewport(width: width, height: height) + set_viewport(width: width, height: height, mobile: mobile) end # diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 42b5c6f2..354d2faa 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -235,4 +235,48 @@ wait_for { message_b }.to eq("goodbye") end end + + describe "#resize" do + def body_size + { + height: page.evaluate("document.body.clientHeight"), + width: page.evaluate("document.body.clientWidth") + } + end + + def is_mobile? + page.evaluate("'ontouchstart' in window || navigator.maxTouchPoints > 0") + end + + before do + page.go_to("/") + end + + context "given a different size" do + it "resizes the page" do + expect { page.resize(width: 2000, height: 1000) }.to change { body_size }.to(width: 2000, height: 1000) + end + end + + context "given a zero height" do + it "does not change the height" do + expect { page.resize(width: 2000, height: 0) }.not_to change { body_size[:height] } + end + end + + context "given a zero width" do + it "does not change the width" do + expect { page.resize(width: 0, height: 1000) }.not_to change { body_size[:width] } + end + end + + context "when mobile is true" do + it "enables mobile emulation in the browser" do + expect do + page.resize(width: 0, height: 0, mobile: true) + page.reload + end.to change { is_mobile? }.to(true) + end + end + end end From d2a8b284bf61cf5fca50651d0d2f078395aaa2ba Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Fri, 21 Mar 2025 10:20:26 +1100 Subject: [PATCH 02/11] Add documentation for mobile option --- lib/ferrum/browser.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 12542f9d..4f6d7ed1 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -129,6 +129,8 @@ class Browser # @option options [Hash] :env # Environment variables you'd like to pass through to the process. # + # @option options [Boolean] :mobile + # Specify whether to enable mobile emulation and touch UI. def initialize(options = nil) @options = Options.new(options) @client = @process = @contexts = nil From 4d8d97b4a724bf16e9bc7cc9a1cbed895ef3f6e5 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Fri, 21 Mar 2025 10:35:59 +1100 Subject: [PATCH 03/11] Add mobile to browser options This way we can use Cuprite to register a driver that in turn enables mobile emulation in the browser. --- lib/ferrum/browser/options.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb index 000f826e..52c0874c 100644 --- a/lib/ferrum/browser/options.rb +++ b/lib/ferrum/browser/options.rb @@ -15,7 +15,7 @@ class Options :js_errors, :base_url, :slowmo, :pending_connection_errors, :url, :ws_url, :env, :process_timeout, :browser_name, :browser_path, :save_path, :proxy, :port, :host, :headless, :incognito, :browser_options, - :ignore_default_browser_options, :xvfb, :flatten + :ignore_default_browser_options, :xvfb, :flatten, :mobile attr_accessor :timeout, :default_user_agent def initialize(options = nil) @@ -32,6 +32,7 @@ def initialize(options = nil) @pending_connection_errors = @options.fetch(:pending_connection_errors, true) @process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT) @slowmo = @options[:slowmo].to_f + @mobile = @options.fetch(:mobile, false) @env = @options[:env] @xvfb = @options[:xvfb] From 0da98746fd9c48663fd78235a2dd8bfbda5437c2 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Fri, 21 Mar 2025 11:25:48 +1100 Subject: [PATCH 04/11] Override the viewport size when mobile: true This is a bit hacky; ideally, a caller wouldn't both specify a non-mobile size *and* mobile: true. --- lib/ferrum/page.rb | 57 ++++++++++++++++++++++++++++------------------ spec/page_spec.rb | 7 ++++++ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 842dc184..c3fe13e8 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -136,7 +136,11 @@ def close_connection end # - # Overrides device screen dimensions and emulates viewport according to parameters + # Overrides device screen dimensions and emulates viewport according to parameters. + # + # Note that passing mobile: true will cause set_viewport to ignore the passed + # height and width values, and instead use 390 x 844, which is the viewport size + # of an iPhone 14. # # Read more [here](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride). # @@ -149,27 +153,36 @@ def close_connection # @param [Boolean] mobile whether to emulate mobile device # def set_viewport(width:, height:, scale_factor: 0, mobile: false) - command( - "Emulation.setDeviceMetricsOverride", - slowmoable: true, - width: width, - height: height, - deviceScaleFactor: scale_factor, - mobile: mobile - ) - - options = if mobile - { - enabled: true, - maxTouchPoints: 1 - } - else - { - enabled: false - } - end - - command("Emulation.setTouchEmulationEnabled", **options) + if mobile + command( + "Emulation.setTouchEmulationEnabled", + enabled: true, + maxTouchPoints: 1 + ) + + command( + "Emulation.setDeviceMetricsOverride", + deviceScaleFactor: 3.0, + height: 844, + mobile: true, + slowmoable: true, + width: 390 + ) + else + command( + "Emulation.setTouchEmulationEnabled", + enabled: false + ) + + command( + "Emulation.setDeviceMetricsOverride", + deviceScaleFactor: scale_factor, + height: height, + mobile: false, + slowmoable: true, + width: width + ) + end end def resize(width: nil, height: nil, fullscreen: false, mobile: false) diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 354d2faa..15f0671e 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -277,6 +277,13 @@ def is_mobile? page.reload end.to change { is_mobile? }.to(true) end + + it "resizes to the size of an iPhone 14 viewport, taking into account scale factor" do + expect do + page.resize(width: 0, height: 0, mobile: true) + page.reload + end.to change { body_size }.to(width: 980, height: 2120) + end end end end From 03b814cd2f3cfae2b27b7ef788fe32f00a21d6cf Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Fri, 21 Mar 2025 11:52:58 +1100 Subject: [PATCH 05/11] Update README Add details of resize and mobile behavior to the README. Note that the behaviour on height or width of 0 was pre-existing; I just documented it and added a spec. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c7b77a0..4f9b8759 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ Ferrum::Browser.new(options) * `:proxy` (Hash) - Specify proxy settings, [read more](https://github.com/rubycdp/ferrum#proxy) * `:save_path` (String) - Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. * `:env` (Hash) - Environment variables you'd like to pass through to the process - + * `:mobile` (Boolean) - Specify whether to enable mobile emulation and touch UI. ## Navigation @@ -1089,6 +1089,13 @@ Overrides device screen dimensions and emulates viewport. * :scale_factor `Float`, device scale factor. `0` by default * :mobile `Boolean`, whether to emulate mobile device. `false` by default +Values of `0` for either `:width` or `:height` will be ignored; i.e., no viewport resize will take place. + +If `:mobile` is `true`: + +1. `:height` and `:width` will be ignored, and instead the viewport size of an iPhone 14 will be used (390 x 844). +2. Touch emulation will be enabled, with a maximum of 1 touch point. + ```ruby page.set_viewport(width: 1000, height: 600, scale_factor: 3) ``` From 552451e42ddb2b8d172f4328713e4cf8ec558528 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Mon, 24 Mar 2025 10:34:00 +1100 Subject: [PATCH 06/11] Fix Rubocop offenses --- spec/page_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 15f0671e..e22c7640 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -260,13 +260,13 @@ def is_mobile? context "given a zero height" do it "does not change the height" do - expect { page.resize(width: 2000, height: 0) }.not_to change { body_size[:height] } + expect { page.resize(width: 2000, height: 0) }.not_to(change { body_size[:height] }) end end context "given a zero width" do it "does not change the width" do - expect { page.resize(width: 0, height: 1000) }.not_to change { body_size[:width] } + expect { page.resize(width: 0, height: 1000) }.not_to(change { body_size[:width] }) end end From 9dc484e71e93a1b50e567fb1f33cdd17ab561385 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Mon, 24 Mar 2025 12:54:11 +1100 Subject: [PATCH 07/11] Tweak README to include mobile example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f9b8759..c0404bbe 100644 --- a/README.md +++ b/README.md @@ -1097,7 +1097,7 @@ If `:mobile` is `true`: 2. Touch emulation will be enabled, with a maximum of 1 touch point. ```ruby -page.set_viewport(width: 1000, height: 600, scale_factor: 3) +page.set_viewport(width: 1000, height: 600, scale_factor: 3, mobile: true) ``` From 0747ff7e5d95821c15fabc42a416a85ac9e7f375 Mon Sep 17 00:00:00 2001 From: Duncan Bayne Date: Thu, 27 Mar 2025 14:24:01 +1100 Subject: [PATCH 08/11] Remove automatic resizing from Ferrum This should really live at the Cuprite level, emulating specific devices. --- lib/ferrum/page.rb | 31 +++++++++---------------------- spec/page_spec.rb | 7 ------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index c3fe13e8..1960ede1 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -138,10 +138,6 @@ def close_connection # # Overrides device screen dimensions and emulates viewport according to parameters. # - # Note that passing mobile: true will cause set_viewport to ignore the passed - # height and width values, and instead use 390 x 844, which is the viewport size - # of an iPhone 14. - # # Read more [here](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride). # # @param [Integer] width width value in pixels. 0 disables the override @@ -159,30 +155,21 @@ def set_viewport(width:, height:, scale_factor: 0, mobile: false) enabled: true, maxTouchPoints: 1 ) - - command( - "Emulation.setDeviceMetricsOverride", - deviceScaleFactor: 3.0, - height: 844, - mobile: true, - slowmoable: true, - width: 390 - ) else command( "Emulation.setTouchEmulationEnabled", enabled: false ) - - command( - "Emulation.setDeviceMetricsOverride", - deviceScaleFactor: scale_factor, - height: height, - mobile: false, - slowmoable: true, - width: width - ) end + + command( + "Emulation.setDeviceMetricsOverride", + deviceScaleFactor: scale_factor, + height: height, + mobile: mobile, + slowmoable: true, + width: width + ) end def resize(width: nil, height: nil, fullscreen: false, mobile: false) diff --git a/spec/page_spec.rb b/spec/page_spec.rb index e22c7640..ab521a7a 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -277,13 +277,6 @@ def is_mobile? page.reload end.to change { is_mobile? }.to(true) end - - it "resizes to the size of an iPhone 14 viewport, taking into account scale factor" do - expect do - page.resize(width: 0, height: 0, mobile: true) - page.reload - end.to change { body_size }.to(width: 980, height: 2120) - end end end end From 6e97cb8186256847f5223b9f9ded45e22498cfb8 Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Tue, 13 May 2025 16:41:17 +1000 Subject: [PATCH 09/11] Expose hook to log CDP messages --- lib/ferrum/client.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index 1039a475..7a24d419 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -64,7 +64,7 @@ class Client delegate %i[timeout timeout=] => :options attr_reader :ws_url, :options, :subscriber - + attr_accessor :on_syncronous_message def initialize(ws_url, options) @command_id = 0 @ws_url = ws_url @@ -96,6 +96,9 @@ def send_message(message, async:) raise TimeoutError unless data error, response = data.values_at("error", "result") + if on_syncronous_message + on_syncronous_message.call(message:, error:, response:) + end raise_browser_error(error) if error response end From c20dcf06d52660395a13a369ead705686716907a Mon Sep 17 00:00:00 2001 From: Talya Connor Date: Wed, 14 May 2025 12:33:11 +1000 Subject: [PATCH 10/11] spelling fix. --- lib/ferrum/client.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index 7a24d419..f4c6b8a9 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -64,7 +64,7 @@ class Client delegate %i[timeout timeout=] => :options attr_reader :ws_url, :options, :subscriber - attr_accessor :on_syncronous_message + attr_accessor :on_synchronous_message def initialize(ws_url, options) @command_id = 0 @ws_url = ws_url @@ -96,8 +96,8 @@ def send_message(message, async:) raise TimeoutError unless data error, response = data.values_at("error", "result") - if on_syncronous_message - on_syncronous_message.call(message:, error:, response:) + if on_synchronous_message + on_synchronous_message.call(message:, error:, response:) end raise_browser_error(error) if error response From 52266966c8b2ff7595d738e996c012cd9c228248 Mon Sep 17 00:00:00 2001 From: Talya Connor Date: Wed, 14 May 2025 13:26:08 +1000 Subject: [PATCH 11/11] Ferrum::Browser::Process supports custom Xvfb class. --- lib/ferrum/browser/process.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/ferrum/browser/process.rb b/lib/ferrum/browser/process.rb index d68769fc..7b56dbd6 100644 --- a/lib/ferrum/browser/process.rb +++ b/lib/ferrum/browser/process.rb @@ -93,8 +93,12 @@ def start process_options[:out] = process_options[:err] = write_io if @command.xvfb? - @xvfb = Xvfb.start(@command.options) - ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid)) + if @command.options.xvfb.respond_to?(:start) + @xvfb = @command.options.xvfb.start(@command.options) + else + @xvfb = Xvfb.start(@command.options) + ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid)) + end end env = Hash(@xvfb&.to_env).merge(@env)