diff --git a/docs/2-customization.md b/docs/2-customization.md index fa262c94..15fe18bb 100644 --- a/docs/2-customization.md +++ b/docs/2-customization.md @@ -56,6 +56,15 @@ Ferrum::Browser.new(options) * `: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 +You can emulate specific devices at the time you register a driver: + +```ruby +require "capybara/cuprite/devices" +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new(app, Capybara::Cuprite::Devices::IPHONE_14) +end +``` + ## Examples ```ruby diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index c7f429cb..5f036d26 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -133,6 +133,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 diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb index a638369c..bdb61318 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, :dockerize, :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) @@ -33,6 +33,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] diff --git a/lib/ferrum/browser/process.rb b/lib/ferrum/browser/process.rb index 0c612c5a..54b444e8 100644 --- a/lib/ferrum/browser/process.rb +++ b/lib/ferrum/browser/process.rb @@ -94,8 +94,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) diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index 7b1341b3..6b383fed 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -65,6 +65,7 @@ class Client delegate %i[timeout timeout=] => :options attr_reader :ws_url, :options, :subscriber + attr_accessor :on_synchronous_message def initialize(ws_url, options) @command_id = 0 @@ -97,6 +98,7 @@ def send_message(message, async:) raise TimeoutError unless data error, response = data.values_at("error", "result") + on_synchronous_message&.call(message:, error:, response:) raise_browser_error(error) if error response end diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 4e641135..7d8a628e 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -137,7 +137,7 @@ def close_connection end # - # Overrides device screen dimensions and emulates viewport according to parameters + # Overrides device screen dimensions and emulates viewport according to parameters. # # Read more [here](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride). # @@ -150,17 +150,30 @@ def close_connection # @param [Boolean] mobile whether to emulate mobile device # def set_viewport(width:, height:, scale_factor: 0, mobile: false) + if mobile + command( + "Emulation.setTouchEmulationEnabled", + enabled: true, + maxTouchPoints: 1 + ) + else + command( + "Emulation.setTouchEmulationEnabled", + enabled: false + ) + end + command( "Emulation.setDeviceMetricsOverride", - slowmoable: true, - width: width, - height: height, deviceScaleFactor: scale_factor, - mobile: mobile + height: height, + mobile: mobile, + slowmoable: true, + width: width ) 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" } @@ -169,7 +182,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 ddf56c1c..5dce2c51 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