Skip to content
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need flag for browser instantiation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent was to expose this via driver registration, as per Selenium.

I have a draft PR open for Cuprite that uses this; see e.g.:

https://github.com/rubycdp/cuprite/pull/292/files#diff-89eebfcbc0f14b6d989517837ca1e94fce4e2ce9a03233641cd936f2b8d2ed94

Capybara.register_driver(:cuprite_mobile) do |app|
  options = { mobile: true }
  options.merge!(inspector: true) if ENV["INSPECTOR"]
  options.merge!(logger: StringIO.new) if ENV["CI"]
  options.merge!(headless: false) if ENV["HEADLESS"] == "false"
  Capybara::Cuprite::Driver.new(app, options)
end


## Navigation

Expand Down Expand Up @@ -1089,8 +1089,15 @@ 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)
page.set_viewport(width: 1000, height: 600, scale_factor: 3, mobile: true)
```


Expand Down
2 changes: 2 additions & 0 deletions lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/ferrum/browser/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]
Expand Down
8 changes: 6 additions & 2 deletions lib/ferrum/browser/process.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion lib/ferrum/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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
@ws_url = ws_url
Expand Down Expand Up @@ -96,6 +96,9 @@ def send_message(message, async:)
raise TimeoutError unless data

error, response = data.values_at("error", "result")
if on_synchronous_message
on_synchronous_message.call(message:, error:, response:)
end
raise_browser_error(error) if error
response
end
Expand Down
27 changes: 20 additions & 7 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,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).
#
Expand All @@ -149,17 +149,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" }
Expand All @@ -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

#
Expand Down
44 changes: 44 additions & 0 deletions spec/page_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really test whether mobile emulation is enabled, but I couldn't find a good way of doing that directly via JavaScript evaluation. So we use touch emulation as a proxy.

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
Loading