Skip to content

Enable mobile emulation #527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,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 @@ -1088,8 +1088,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 @@ -126,6 +126,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, :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 @@ -31,6 +31,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
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