Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,10 @@ Metrics/PerceivedComplexity:

plugins:
- rubocop-rake

Naming/VariableNumber:
CheckSymbols: false

Layout/LineLength:
Exclude:
- lib/capybara/cuprite/devices.rb
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
## [Unreleased](https://github.com/rubycdp/cuprite/compare/v0.18...main) ##

### Added
- `:emulate` driver option to emulate a device listed in Chrome DevTools with its user agent, e.g.
`emulate: :iphone_14`, along with `:mobile` and `:scale_factor` options it sets, kept across window resizes [#216]

### Changed
- Ferrum is bumped to `~> 0.19.0`

### Fixed
- `Driver#reset!` kept the page it had cached when the browser failed to reset, so every later example raised
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,18 @@ end
timer) only surfaces on whatever command runs next.
* `:url_blacklist` (Array) - array of regexes to match against requested URLs
* `:url_whitelist` (Array) - array of regexes to match against requested URLs
* `:emulate` (Symbol) - device from Chrome DevTools' list to emulate, e.g. `:iphone_16`, `:pixel_9`, `:ipad_mini`
* `:mobile` (Boolean) - emulate a mobile device: viewport meta tag and touch events
* `:scale_factor` (Numeric) - device scale factor to emulate

`emulate:` sets the device's `:window_size`, `:scale_factor`, `:mobile` and user agent, options passed explicitly
take precedence:

```ruby
Capybara.register_driver(:cuprite) do |app|
Capybara::Cuprite::Driver.new(app, emulate: :iphone_14)
end
```

## Debugging

Expand Down
2 changes: 1 addition & 1 deletion cuprite.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 3.1"

s.add_dependency "capybara", "~> 3.0"
s.add_dependency "ferrum", "~> 0.18.0"
s.add_dependency "ferrum", "~> 0.19.0"
end
14 changes: 14 additions & 0 deletions docs/2-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ Cuprite-specific options are:
[URL filtering](/docs/cuprite/url-filtering).
* `:url_whitelist` (Array) - Regexes to match against requested URLs that are allowed, see
[URL filtering](/docs/cuprite/url-filtering).
* `:emulate` (Symbol) - Device to emulate, one of those listed in Chrome DevTools' device toolbar, such as
`:iphone_16`, `:pixel_9`, `:samsung_galaxy_a55` or `:ipad_mini`, see `Capybara::Cuprite::Devices::LIST`. It sets
the device's `:window_size`, `:scale_factor` and `:mobile`, options passed explicitly take precedence, and its
user agent, unless a `User-Agent` is set with [headers](/docs/cuprite/headers).
* `:mobile` (Boolean) - Emulate a mobile device: honor `<meta name="viewport">` and enable touch events, `false`
by default. Kept across window resizes.
* `:scale_factor` (Numeric) - Device scale factor (`window.devicePixelRatio`) to emulate, `0` (browser default)
by default.

Cuprite changes a few Ferrum defaults:

Expand All @@ -49,6 +57,12 @@ Capybara::Cuprite::Driver.new(app, timeout: 15, process_timeout: 30)

# Connect to a remote browser
Capybara::Cuprite::Driver.new(app, url: "http://chrome:3333")

# Emulate an iPhone 14
Capybara::Cuprite::Driver.new(app, emulate: :iphone_14)

# Emulate a Pixel 9 with a custom scale factor
Capybara::Cuprite::Driver.new(app, emulate: :pixel_9, scale_factor: 1)
```

## Environment variables
Expand Down
1 change: 1 addition & 0 deletions lib/capybara/cuprite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "capybara/cuprite/browser"
require "capybara/cuprite/page"
require "capybara/cuprite/options"
require "capybara/cuprite/devices"
require "capybara/cuprite/node"
require "capybara/cuprite/errors"

Expand Down
2 changes: 2 additions & 0 deletions lib/capybara/cuprite/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Browser < Ferrum::Browser
dismiss_prompt reset_modals] => :page

def initialize(options = nil)
device = options&.dig(:emulate)
options = Devices.fetch(device).except(:user_agent).merge(options) if device
super

@options.raise_on_unhandled_modal = options&.dig(:raise_on_unhandled_modal)
Expand Down
188 changes: 188 additions & 0 deletions lib/capybara/cuprite/devices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# frozen_string_literal: true

module Capybara
module Cuprite
module Devices
IPHONE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1"
SAMSUNG_GALAXY_S8_PLUS_USER_AGENT = "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
WINDOWS_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36"

LIST = {
iphone_se: {
window_size: [375, 667], scale_factor: 2, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_xr: {
window_size: [414, 896], scale_factor: 2, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_12_pro: {
window_size: [390, 844], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_14: {
window_size: [390, 844], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_14_plus: {
window_size: [428, 926], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_14_pro: {
window_size: [393, 852], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_14_pro_max: {
window_size: [430, 932], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_15: {
window_size: [393, 852], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_15_plus: {
window_size: [430, 932], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_15_pro: {
window_size: [393, 852], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_15_pro_max: {
window_size: [430, 932], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_16e: {
window_size: [390, 844], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_16: {
window_size: [393, 852], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_16_plus: {
window_size: [430, 932], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_16_pro: {
window_size: [402, 874], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
iphone_16_pro_max: {
window_size: [440, 956], scale_factor: 3, mobile: true,
user_agent: IPHONE_USER_AGENT
},
ipad_mini: {
window_size: [768, 1024], scale_factor: 2, mobile: true,
user_agent: "Mozilla/5.0 (iPad; CPU OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1"
},
ipad_pro_13: {
window_size: [1032, 1376], scale_factor: 2, mobile: true,
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"
},

pixel_7: {
window_size: [412, 915], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_8: {
window_size: [412, 915], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_8_pro: {
window_size: [448, 997], scale_factor: 3, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_8a: {
window_size: [412, 915], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_9: {
window_size: [412, 924], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_9_pro: {
window_size: [427, 952], scale_factor: 3, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_9_pro_xl: {
window_size: [448, 997], scale_factor: 3, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_10: {
window_size: [412, 924], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
pixel_9_pro_fold: {
window_size: [412, 922], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro Fold) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},

samsung_galaxy_s8_plus: {
window_size: [360, 740], scale_factor: 4, mobile: true,
user_agent: SAMSUNG_GALAXY_S8_PLUS_USER_AGENT
},
samsung_galaxy_s20_ultra: {
window_size: [412, 915], scale_factor: 3.5, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
samsung_galaxy_a55: {
window_size: [360, 800], scale_factor: 2.25, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
galaxy_z_fold_6: {
window_size: [412, 968], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
galaxy_z_fold_5: {
window_size: [344, 882], scale_factor: 2.625, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
samsung_galaxy_a51_71: {
window_size: [412, 914], scale_factor: 2.625, mobile: true,
user_agent: SAMSUNG_GALAXY_S8_PLUS_USER_AGENT
},
galaxy_tab_s4: {
window_size: [712, 1138], scale_factor: 2.25, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36"
},

surface_pro_7: {
window_size: [912, 1368], scale_factor: 2, mobile: true,
user_agent: WINDOWS_USER_AGENT
},
surface_pro_10: {
window_size: [960, 1440], scale_factor: 2, mobile: true,
user_agent: WINDOWS_USER_AGENT
},
surface_duo: {
window_size: [540, 720], scale_factor: 2.5, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 11.0; Surface Duo) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},

nest_hub_max: {
window_size: [1280, 800], scale_factor: 2, mobile: true,
user_agent: "Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36 CrKey/1.54.250320"
},
jiophone_2: {
window_size: [240, 320], scale_factor: 1, mobile: true,
user_agent: "Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5"
},
moto_g4: {
window_size: [360, 640], scale_factor: 3, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 6.0.1; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
},
moto_g_power: {
window_size: [412, 823], scale_factor: 1.75, mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
}
}.freeze

def self.fetch(name)
LIST.fetch(name.to_sym) do
raise ArgumentError, "Unknown device to emulate: #{name.inspect}, use one of #{LIST.keys.join(', ')}"
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/capybara/cuprite/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ class Options
attr_writer :window_size
attr_accessor :url_blacklist, :url_whitelist, :raise_on_unhandled_modal

def mobile
@options.fetch(:mobile, false)
end

def scale_factor
@options.fetch(:scale_factor, 0)
end

def device_user_agent
Capybara::Cuprite::Devices.fetch(@options[:emulate])[:user_agent] if @options[:emulate]
end

def default_user_agent
return @default_user_agent unless device_user_agent

chrome_version = @default_user_agent.to_s[%r{Chrome/(\S+)}, 1].to_s
format(device_user_agent, version: chrome_version)
end

def reset_window_size
@window_size = @options[:window_size]
end
Expand Down
5 changes: 5 additions & 0 deletions lib/capybara/cuprite/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def command(...)
raise_pending_unhandled_modal_error! { super }
end

def set_viewport(width: 0, height: 0, scale_factor: @options.scale_factor, mobile: @options.mobile)
super
end

def set(node, value)
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
Expand Down Expand Up @@ -175,6 +179,7 @@ def prepare_page

width, height = @options.window_size
resize(width: width, height: height)
command("Network.setUserAgentOverride", userAgent: default_user_agent) if @options.device_user_agent

if @options.url_blacklist.any?
network.blacklist = @options.url_blacklist
Expand Down
Loading
Loading