cuprite 0.18 → 0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -1
- data/lib/capybara/cuprite/browser.rb +22 -11
- data/lib/capybara/cuprite/devices.rb +188 -0
- data/lib/capybara/cuprite/driver.rb +2 -0
- data/lib/capybara/cuprite/javascripts/drag.js +23 -21
- data/lib/capybara/cuprite/javascripts/index.js +30 -7
- data/lib/capybara/cuprite/node.rb +11 -1
- data/lib/capybara/cuprite/options.rb +19 -0
- data/lib/capybara/cuprite/page.rb +7 -2
- data/lib/capybara/cuprite/version.rb +1 -1
- data/lib/capybara/cuprite.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fa19bf03d8a264421374bf813afb0f6c062e15e8aa92ca7c046d3a659122319
|
|
4
|
+
data.tar.gz: d12031c15dda9ad7e777f7b66a887c6979af5b4c3e27010083378af512f979d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c85ee6afd497cfdc03375c111cc58dbcd6463f41c63bd745a6229a31a4d4e6bb6dc03fb8eddc0f8a7f93079c2cdf81524a3febca3494031e002eb869e9943188
|
|
7
|
+
data.tar.gz: 0d066fa1a7fb851a9ce0d3d9a9e4891a6c76bd3c7e558a74fa021035b8e2316d860fce4674f3054134fe03dc05c7c622124d8b88ded27d81bfeede3d75e28e7f
|
data/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Cuprite - Headless Chrome driver for Capybara
|
|
2
2
|
|
|
3
|
+
## [Documentation](https://docs.rubycdp.com/docs/cuprite/introduction)
|
|
4
|
+
|
|
5
|
+
<img align="right"
|
|
6
|
+
width="160" height="160"
|
|
7
|
+
alt="Cuprite logo"
|
|
8
|
+
src="https://raw.githubusercontent.com/rubycdp/cuprite/main/logo.svg?sanitize=true">
|
|
9
|
+
|
|
3
10
|
Cuprite is a pure Ruby driver (read as _no_ Selenium/WebDriver/ChromeDriver
|
|
4
11
|
dependency) for [Capybara](https://github.com/teamcapybara/capybara). It allows
|
|
5
12
|
you to run Capybara tests on a headless Chrome or Chromium. Under the hood it
|
|
@@ -7,6 +14,8 @@ uses [Ferrum](https://github.com/rubycdp/ferrum#index) which is high-level API
|
|
|
7
14
|
to the browser by CDP protocol.
|
|
8
15
|
|
|
9
16
|
|
|
17
|
+
<br clear="right">
|
|
18
|
+
|
|
10
19
|
## Install
|
|
11
20
|
|
|
12
21
|
Add this to your `Gemfile` and run `bundle install`.
|
|
@@ -84,7 +93,18 @@ end
|
|
|
84
93
|
timer) only surfaces on whatever command runs next.
|
|
85
94
|
* `:url_blacklist` (Array) - array of regexes to match against requested URLs
|
|
86
95
|
* `:url_whitelist` (Array) - array of regexes to match against requested URLs
|
|
96
|
+
* `:emulate` (Symbol) - device from Chrome DevTools' list to emulate, e.g. `:iphone_16`, `:pixel_9`, `:ipad_mini`
|
|
97
|
+
* `:mobile` (Boolean) - emulate a mobile device: viewport meta tag and touch events
|
|
98
|
+
* `:scale_factor` (Numeric) - device scale factor to emulate
|
|
87
99
|
|
|
100
|
+
`emulate:` sets the device's `:window_size`, `:scale_factor`, `:mobile` and user agent, options passed explicitly
|
|
101
|
+
take precedence:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Capybara.register_driver(:cuprite) do |app|
|
|
105
|
+
Capybara::Cuprite::Driver.new(app, emulate: :iphone_14)
|
|
106
|
+
end
|
|
107
|
+
```
|
|
88
108
|
|
|
89
109
|
## Debugging
|
|
90
110
|
|
|
@@ -198,7 +218,17 @@ Besides capybara screenshot method you can get image as Base64:
|
|
|
198
218
|
|
|
199
219
|
## Proxy
|
|
200
220
|
|
|
201
|
-
* `page.driver.set_proxy(ip, port, user, password)`
|
|
221
|
+
* `page.driver.set_proxy(ip, port, user, password, bypass)`
|
|
222
|
+
|
|
223
|
+
The proxy is passed to Chrome as a command-line flag, so it has to be set before the browser starts: either with the
|
|
224
|
+
`:proxy` driver option or by calling `set_proxy` before the browser is first used. Calling it once the browser is
|
|
225
|
+
running has no effect.
|
|
226
|
+
|
|
227
|
+
When connecting to a remote browser with the `:url` option (browserless, Selenoid, etc.), Cuprite doesn't launch
|
|
228
|
+
Chrome and the proxy flag is never applied, so configure the proxy on the remote browser itself. The `user` and
|
|
229
|
+
`password` are still used to answer the proxy's authentication challenge.
|
|
230
|
+
|
|
231
|
+
The `bypass` argument is a list of hosts that skip the proxy, so don't pass the host you want to go through it.
|
|
202
232
|
|
|
203
233
|
|
|
204
234
|
## URL Blocklisting & Allowlisting
|
|
@@ -14,6 +14,8 @@ module Capybara
|
|
|
14
14
|
dismiss_prompt reset_modals] => :page
|
|
15
15
|
|
|
16
16
|
def initialize(options = nil)
|
|
17
|
+
device = options&.dig(:emulate)
|
|
18
|
+
options = Devices.fetch(device).except(:user_agent).merge(options) if device
|
|
17
19
|
super
|
|
18
20
|
|
|
19
21
|
@options.raise_on_unhandled_modal = options&.dig(:raise_on_unhandled_modal)
|
|
@@ -38,6 +40,7 @@ module Capybara
|
|
|
38
40
|
|
|
39
41
|
def reset
|
|
40
42
|
super
|
|
43
|
+
ensure
|
|
41
44
|
@options.reset_window_size
|
|
42
45
|
@page = nil
|
|
43
46
|
end
|
|
@@ -109,7 +112,7 @@ module Capybara
|
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
def window_handles
|
|
112
|
-
targets.
|
|
115
|
+
targets.values.select(&:page?).map(&:id)
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def within_window(locator = nil)
|
|
@@ -123,17 +126,13 @@ module Capybara
|
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
def switch_to_window(target_id)
|
|
126
|
-
target =
|
|
127
|
-
raise Ferrum::NoSuchPageError unless target
|
|
128
|
-
|
|
129
|
+
target = window_target(target_id)
|
|
129
130
|
@page = attach_page(target.id)
|
|
130
131
|
@page.activate
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
def close_window(target_id)
|
|
134
|
-
target =
|
|
135
|
-
raise Ferrum::NoSuchPageError unless target
|
|
136
|
-
|
|
135
|
+
target = window_target(target_id)
|
|
137
136
|
@page = ClosedPage.new if @page.target_id == target.id
|
|
138
137
|
target.page.close
|
|
139
138
|
targets.delete(target_id) # page.close is async, delete target asap
|
|
@@ -156,6 +155,7 @@ module Capybara
|
|
|
156
155
|
delay = options[:delay]
|
|
157
156
|
scroll = options.fetch(:scroll, true)
|
|
158
157
|
html5 = options[:html5]
|
|
158
|
+
drop_offset = options[:drop_offset]
|
|
159
159
|
|
|
160
160
|
execute("_cuprite.dragMousedownTracker()")
|
|
161
161
|
|
|
@@ -169,21 +169,21 @@ module Capybara
|
|
|
169
169
|
html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil?
|
|
170
170
|
|
|
171
171
|
if html5
|
|
172
|
-
drag_html5(node, other, drop_modifiers, delay)
|
|
172
|
+
drag_html5(node, other, drop_modifiers, delay, drop_offset)
|
|
173
173
|
else
|
|
174
174
|
modifiers = keyboard.modifiers(drop_modifiers)
|
|
175
175
|
|
|
176
176
|
other.scroll_into_view if scroll
|
|
177
|
-
x2, y2 = other.find_position
|
|
177
|
+
x2, y2 = other.find_position(**drop_offset.to_h)
|
|
178
178
|
mouse.move(x: x2, y: y2, steps: steps)
|
|
179
179
|
|
|
180
180
|
mouse.up(modifiers: modifiers)
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
def drag_html5(node, other, drop_modifiers, delay)
|
|
184
|
+
def drag_html5(node, other, drop_modifiers, delay, drop_offset = nil)
|
|
185
185
|
keys = drop_modifiers.map(&:to_s)
|
|
186
|
-
evaluate_async(DRAG_HTML5_JS, timeout, node, other, (delay || 0.05) * 1000, keys)
|
|
186
|
+
evaluate_async(DRAG_HTML5_JS, timeout, node, other, (delay || 0.05) * 1000, keys, drop_offset)
|
|
187
187
|
mouse.up
|
|
188
188
|
end
|
|
189
189
|
|
|
@@ -266,6 +266,10 @@ module Capybara
|
|
|
266
266
|
private
|
|
267
267
|
|
|
268
268
|
def find_all(method, selector, within = nil)
|
|
269
|
+
# Capybara appends its text-match hint as `(expr)[...]`, but with `exact_text: true` the hint
|
|
270
|
+
# is empty and the result `(expr)[]` is invalid XPath. An empty predicate is never valid, so
|
|
271
|
+
# dropping it can't change a valid selector. Remove once teamcapybara/capybara#2860 is released.
|
|
272
|
+
selector = selector.delete_suffix("[]") if method == :xpath
|
|
269
273
|
nodes = if within
|
|
270
274
|
evaluate("_cuprite.find(arguments[0], arguments[1], arguments[2])", method, selector, within)
|
|
271
275
|
else
|
|
@@ -297,6 +301,13 @@ module Capybara
|
|
|
297
301
|
end
|
|
298
302
|
end
|
|
299
303
|
|
|
304
|
+
def window_target(target_id)
|
|
305
|
+
target = targets[target_id]
|
|
306
|
+
raise Ferrum::NoSuchPageError unless target&.page?
|
|
307
|
+
|
|
308
|
+
target
|
|
309
|
+
end
|
|
310
|
+
|
|
300
311
|
def attach_page(target_id = nil)
|
|
301
312
|
target = targets[target_id] if target_id
|
|
302
313
|
target ||= default_context.default_target
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Cuprite
|
|
5
|
+
module Devices
|
|
6
|
+
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"
|
|
7
|
+
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"
|
|
8
|
+
WINDOWS_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36"
|
|
9
|
+
|
|
10
|
+
LIST = {
|
|
11
|
+
iphone_se: {
|
|
12
|
+
window_size: [375, 667], scale_factor: 2, mobile: true,
|
|
13
|
+
user_agent: IPHONE_USER_AGENT
|
|
14
|
+
},
|
|
15
|
+
iphone_xr: {
|
|
16
|
+
window_size: [414, 896], scale_factor: 2, mobile: true,
|
|
17
|
+
user_agent: IPHONE_USER_AGENT
|
|
18
|
+
},
|
|
19
|
+
iphone_12_pro: {
|
|
20
|
+
window_size: [390, 844], scale_factor: 3, mobile: true,
|
|
21
|
+
user_agent: IPHONE_USER_AGENT
|
|
22
|
+
},
|
|
23
|
+
iphone_14: {
|
|
24
|
+
window_size: [390, 844], scale_factor: 3, mobile: true,
|
|
25
|
+
user_agent: IPHONE_USER_AGENT
|
|
26
|
+
},
|
|
27
|
+
iphone_14_plus: {
|
|
28
|
+
window_size: [428, 926], scale_factor: 3, mobile: true,
|
|
29
|
+
user_agent: IPHONE_USER_AGENT
|
|
30
|
+
},
|
|
31
|
+
iphone_14_pro: {
|
|
32
|
+
window_size: [393, 852], scale_factor: 3, mobile: true,
|
|
33
|
+
user_agent: IPHONE_USER_AGENT
|
|
34
|
+
},
|
|
35
|
+
iphone_14_pro_max: {
|
|
36
|
+
window_size: [430, 932], scale_factor: 3, mobile: true,
|
|
37
|
+
user_agent: IPHONE_USER_AGENT
|
|
38
|
+
},
|
|
39
|
+
iphone_15: {
|
|
40
|
+
window_size: [393, 852], scale_factor: 3, mobile: true,
|
|
41
|
+
user_agent: IPHONE_USER_AGENT
|
|
42
|
+
},
|
|
43
|
+
iphone_15_plus: {
|
|
44
|
+
window_size: [430, 932], scale_factor: 3, mobile: true,
|
|
45
|
+
user_agent: IPHONE_USER_AGENT
|
|
46
|
+
},
|
|
47
|
+
iphone_15_pro: {
|
|
48
|
+
window_size: [393, 852], scale_factor: 3, mobile: true,
|
|
49
|
+
user_agent: IPHONE_USER_AGENT
|
|
50
|
+
},
|
|
51
|
+
iphone_15_pro_max: {
|
|
52
|
+
window_size: [430, 932], scale_factor: 3, mobile: true,
|
|
53
|
+
user_agent: IPHONE_USER_AGENT
|
|
54
|
+
},
|
|
55
|
+
iphone_16e: {
|
|
56
|
+
window_size: [390, 844], scale_factor: 3, mobile: true,
|
|
57
|
+
user_agent: IPHONE_USER_AGENT
|
|
58
|
+
},
|
|
59
|
+
iphone_16: {
|
|
60
|
+
window_size: [393, 852], scale_factor: 3, mobile: true,
|
|
61
|
+
user_agent: IPHONE_USER_AGENT
|
|
62
|
+
},
|
|
63
|
+
iphone_16_plus: {
|
|
64
|
+
window_size: [430, 932], scale_factor: 3, mobile: true,
|
|
65
|
+
user_agent: IPHONE_USER_AGENT
|
|
66
|
+
},
|
|
67
|
+
iphone_16_pro: {
|
|
68
|
+
window_size: [402, 874], scale_factor: 3, mobile: true,
|
|
69
|
+
user_agent: IPHONE_USER_AGENT
|
|
70
|
+
},
|
|
71
|
+
iphone_16_pro_max: {
|
|
72
|
+
window_size: [440, 956], scale_factor: 3, mobile: true,
|
|
73
|
+
user_agent: IPHONE_USER_AGENT
|
|
74
|
+
},
|
|
75
|
+
ipad_mini: {
|
|
76
|
+
window_size: [768, 1024], scale_factor: 2, mobile: true,
|
|
77
|
+
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"
|
|
78
|
+
},
|
|
79
|
+
ipad_pro_13: {
|
|
80
|
+
window_size: [1032, 1376], scale_factor: 2, mobile: true,
|
|
81
|
+
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"
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
pixel_7: {
|
|
85
|
+
window_size: [412, 915], scale_factor: 2.625, mobile: true,
|
|
86
|
+
user_agent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
87
|
+
},
|
|
88
|
+
pixel_8: {
|
|
89
|
+
window_size: [412, 915], scale_factor: 2.625, mobile: true,
|
|
90
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
91
|
+
},
|
|
92
|
+
pixel_8_pro: {
|
|
93
|
+
window_size: [448, 997], scale_factor: 3, mobile: true,
|
|
94
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
95
|
+
},
|
|
96
|
+
pixel_8a: {
|
|
97
|
+
window_size: [412, 915], scale_factor: 2.625, mobile: true,
|
|
98
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
99
|
+
},
|
|
100
|
+
pixel_9: {
|
|
101
|
+
window_size: [412, 924], scale_factor: 2.625, mobile: true,
|
|
102
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
103
|
+
},
|
|
104
|
+
pixel_9_pro: {
|
|
105
|
+
window_size: [427, 952], scale_factor: 3, mobile: true,
|
|
106
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
107
|
+
},
|
|
108
|
+
pixel_9_pro_xl: {
|
|
109
|
+
window_size: [448, 997], scale_factor: 3, mobile: true,
|
|
110
|
+
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"
|
|
111
|
+
},
|
|
112
|
+
pixel_10: {
|
|
113
|
+
window_size: [412, 924], scale_factor: 2.625, mobile: true,
|
|
114
|
+
user_agent: "Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
115
|
+
},
|
|
116
|
+
pixel_9_pro_fold: {
|
|
117
|
+
window_size: [412, 922], scale_factor: 2.625, mobile: true,
|
|
118
|
+
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"
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
samsung_galaxy_s8_plus: {
|
|
122
|
+
window_size: [360, 740], scale_factor: 4, mobile: true,
|
|
123
|
+
user_agent: SAMSUNG_GALAXY_S8_PLUS_USER_AGENT
|
|
124
|
+
},
|
|
125
|
+
samsung_galaxy_s20_ultra: {
|
|
126
|
+
window_size: [412, 915], scale_factor: 3.5, mobile: true,
|
|
127
|
+
user_agent: "Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
128
|
+
},
|
|
129
|
+
samsung_galaxy_a55: {
|
|
130
|
+
window_size: [360, 800], scale_factor: 2.25, mobile: true,
|
|
131
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
132
|
+
},
|
|
133
|
+
galaxy_z_fold_6: {
|
|
134
|
+
window_size: [412, 968], scale_factor: 2.625, mobile: true,
|
|
135
|
+
user_agent: "Mozilla/5.0 (Linux; Android 14; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
136
|
+
},
|
|
137
|
+
galaxy_z_fold_5: {
|
|
138
|
+
window_size: [344, 882], scale_factor: 2.625, mobile: true,
|
|
139
|
+
user_agent: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
140
|
+
},
|
|
141
|
+
samsung_galaxy_a51_71: {
|
|
142
|
+
window_size: [412, 914], scale_factor: 2.625, mobile: true,
|
|
143
|
+
user_agent: SAMSUNG_GALAXY_S8_PLUS_USER_AGENT
|
|
144
|
+
},
|
|
145
|
+
galaxy_tab_s4: {
|
|
146
|
+
window_size: [712, 1138], scale_factor: 2.25, mobile: true,
|
|
147
|
+
user_agent: "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36"
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
surface_pro_7: {
|
|
151
|
+
window_size: [912, 1368], scale_factor: 2, mobile: true,
|
|
152
|
+
user_agent: WINDOWS_USER_AGENT
|
|
153
|
+
},
|
|
154
|
+
surface_pro_10: {
|
|
155
|
+
window_size: [960, 1440], scale_factor: 2, mobile: true,
|
|
156
|
+
user_agent: WINDOWS_USER_AGENT
|
|
157
|
+
},
|
|
158
|
+
surface_duo: {
|
|
159
|
+
window_size: [540, 720], scale_factor: 2.5, mobile: true,
|
|
160
|
+
user_agent: "Mozilla/5.0 (Linux; Android 11.0; Surface Duo) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Mobile Safari/537.36"
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
nest_hub_max: {
|
|
164
|
+
window_size: [1280, 800], scale_factor: 2, mobile: true,
|
|
165
|
+
user_agent: "Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%<version>s Safari/537.36 CrKey/1.54.250320"
|
|
166
|
+
},
|
|
167
|
+
jiophone_2: {
|
|
168
|
+
window_size: [240, 320], scale_factor: 1, mobile: true,
|
|
169
|
+
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"
|
|
170
|
+
},
|
|
171
|
+
moto_g4: {
|
|
172
|
+
window_size: [360, 640], scale_factor: 3, mobile: true,
|
|
173
|
+
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"
|
|
174
|
+
},
|
|
175
|
+
moto_g_power: {
|
|
176
|
+
window_size: [412, 823], scale_factor: 1.75, mobile: true,
|
|
177
|
+
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"
|
|
178
|
+
}
|
|
179
|
+
}.freeze
|
|
180
|
+
|
|
181
|
+
def self.fetch(name)
|
|
182
|
+
LIST.fetch(name.to_sym) do
|
|
183
|
+
raise ArgumentError, "Unknown device to emulate: #{name.inspect}, use one of #{LIST.keys.join(', ')}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -42,6 +42,7 @@ module Capybara
|
|
|
42
42
|
unless @options[:browser_options][:"remote-allow-origins"]
|
|
43
43
|
@options[:browser_options].merge!("remote-allow-origins": "*")
|
|
44
44
|
end
|
|
45
|
+
@options[:browser_options] = { "disable-web-security": nil }.merge(@options[:browser_options])
|
|
45
46
|
|
|
46
47
|
super()
|
|
47
48
|
end
|
|
@@ -139,6 +140,7 @@ module Capybara
|
|
|
139
140
|
browser.url_whitelist = @options[:url_whitelist]
|
|
140
141
|
browser.raise_on_unhandled_modal = @options.fetch(:raise_on_unhandled_modal, false)
|
|
141
142
|
browser.reset
|
|
143
|
+
ensure
|
|
142
144
|
@started = false
|
|
143
145
|
end
|
|
144
146
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// HTML5 drag-and-drop emulation.
|
|
2
2
|
//
|
|
3
|
-
// Ported
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// in pointOnRect, undeclared `key`, callback.call(true)) are preserved
|
|
8
|
-
// deliberately. See https://github.com/rubycdp/cuprite/issues/314.
|
|
3
|
+
// Ported from Capybara's Selenium driver (capybara/selenium/extensions/html5_drag.rb,
|
|
4
|
+
// HTML5_DRAG_DROP_SCRIPT), see https://github.com/rubycdp/cuprite/issues/314.
|
|
5
|
+
// Unlike the original, a drag ends the way it does in a browser: with `drop` if
|
|
6
|
+
// the target accepted it, otherwise with `dragleave`, never both [#325].
|
|
9
7
|
|
|
10
8
|
function rectCenter(rect){
|
|
11
9
|
return new DOMPoint(
|
|
@@ -31,7 +29,7 @@ function pointOnRect(pt, rect) {
|
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
if (pt.y <= rectPt.y) { // top side
|
|
34
|
-
var minYx = (
|
|
32
|
+
var minYx = (rect.top - pt.y) / slope + pt.x;
|
|
35
33
|
if (rect.left <= minYx && minYx <= rect.right)
|
|
36
34
|
return new DOMPoint(minYx, rect.top);
|
|
37
35
|
}
|
|
@@ -51,7 +49,7 @@ function dragEnterTarget() {
|
|
|
51
49
|
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
|
52
50
|
|
|
53
51
|
for (var i = 0; i < drop_modifier_keys.length; i++) {
|
|
54
|
-
key = drop_modifier_keys[i];
|
|
52
|
+
var key = drop_modifier_keys[i];
|
|
55
53
|
if (key == "control"){
|
|
56
54
|
key = "ctrl"
|
|
57
55
|
}
|
|
@@ -69,23 +67,26 @@ function dragEnterTarget() {
|
|
|
69
67
|
window.setTimeout(dragOnTarget, step_delay);
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
function dropPoint(rect) {
|
|
71
|
+
if (!drop_offset) return rectCenter(rect);
|
|
72
|
+
|
|
73
|
+
var origin = drop_offset.position == 'center' ? rectCenter(rect) : new DOMPoint(rect.left, rect.top);
|
|
74
|
+
return new DOMPoint(origin.x + drop_offset.x, origin.y + drop_offset.y);
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
function dragOnTarget() {
|
|
73
|
-
var
|
|
74
|
-
var dragOverOpts = Object.assign({clientX:
|
|
78
|
+
var point = dropPoint(target.getBoundingClientRect());
|
|
79
|
+
var dragOverOpts = Object.assign({clientX: point.x, clientY: point.y}, opts);
|
|
75
80
|
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
76
81
|
target.dispatchEvent(dragOverEvent);
|
|
77
|
-
window.setTimeout(
|
|
82
|
+
window.setTimeout(dragEnd, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
function
|
|
81
|
-
var
|
|
82
|
-
var
|
|
83
|
-
target.dispatchEvent(
|
|
84
|
-
|
|
85
|
-
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
|
86
|
-
target.dispatchEvent(dropEvent);
|
|
87
|
-
}
|
|
88
|
-
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
|
85
|
+
function dragEnd(drop, dragOverOpts) {
|
|
86
|
+
var dragEndOptions = Object.assign({}, opts, dragOverOpts);
|
|
87
|
+
var dropOrLeaveEvent = new DragEvent(drop ? 'drop' : 'dragleave', dragEndOptions);
|
|
88
|
+
target.dispatchEvent(dropOrLeaveEvent);
|
|
89
|
+
var dragEndEvent = new DragEvent('dragend', dragEndOptions);
|
|
89
90
|
source.dispatchEvent(dragEndEvent);
|
|
90
91
|
callback.call(true);
|
|
91
92
|
}
|
|
@@ -94,7 +95,8 @@ var source = arguments[0],
|
|
|
94
95
|
target = arguments[1],
|
|
95
96
|
step_delay = arguments[2],
|
|
96
97
|
drop_modifier_keys = arguments[3],
|
|
97
|
-
|
|
98
|
+
drop_offset = arguments[4],
|
|
99
|
+
callback = arguments[5];
|
|
98
100
|
|
|
99
101
|
var dt = new DataTransfer();
|
|
100
102
|
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
@@ -77,15 +77,27 @@ class Cuprite {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
let ancestorOpacityApplies = true;
|
|
81
|
+
|
|
80
82
|
while (node) {
|
|
81
83
|
if (node instanceof ShadowRoot) {
|
|
82
84
|
node = node.host;
|
|
83
85
|
} else {
|
|
84
86
|
style = window.getComputedStyle(node);
|
|
85
|
-
if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) {
|
|
87
|
+
if (style.display === "none" || style.visibility === "hidden" || (ancestorOpacityApplies && parseFloat(style.opacity) === 0)) {
|
|
86
88
|
return false;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// A node matching :modal or :popover-open is a top-layer element, so
|
|
92
|
+
// an ancestor's opacity no longer applies to it once shown.
|
|
93
|
+
if (ancestorOpacityApplies) {
|
|
94
|
+
try {
|
|
95
|
+
if (node.matches(":modal, :popover-open")) {
|
|
96
|
+
ancestorOpacityApplies = false;
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
let parent = node.parentElement;
|
|
90
102
|
if (parent && parent.tagName === "DETAILS" && !parent.open) {
|
|
91
103
|
// In a closed <details> only the first <summary> (and its subtree) is rendered.
|
|
@@ -208,6 +220,10 @@ class Cuprite {
|
|
|
208
220
|
|
|
209
221
|
this.keyupdowned(node, "keyup", char, keyCode);
|
|
210
222
|
}
|
|
223
|
+
|
|
224
|
+
if (valueBefore !== "" && node.value === "") {
|
|
225
|
+
this.input(node, "deleteContentBackward");
|
|
226
|
+
}
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
if (valueBefore !== node.value) {
|
|
@@ -226,8 +242,8 @@ class Cuprite {
|
|
|
226
242
|
return nativeTextareaValueSetter.call(node, value);
|
|
227
243
|
}
|
|
228
244
|
|
|
229
|
-
input(node) {
|
|
230
|
-
let event = new InputEvent("input", { inputType:
|
|
245
|
+
input(node, inputType = "insertText") {
|
|
246
|
+
let event = new InputEvent("input", { inputType: inputType, bubbles: true, cancelable: false });
|
|
231
247
|
node.dispatchEvent(event);
|
|
232
248
|
}
|
|
233
249
|
|
|
@@ -417,13 +433,14 @@ class Cuprite {
|
|
|
417
433
|
} else if (value == false && !node.parentNode.multiple) {
|
|
418
434
|
return false;
|
|
419
435
|
} else {
|
|
420
|
-
let
|
|
421
|
-
this.trigger(
|
|
436
|
+
let selectNode = node.closest("select");
|
|
437
|
+
this.trigger(selectNode, "focus");
|
|
422
438
|
|
|
423
439
|
node.selected = value;
|
|
440
|
+
selectNode.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
424
441
|
this.changed(node);
|
|
425
442
|
|
|
426
|
-
this.trigger(
|
|
443
|
+
this.trigger(selectNode, "blur");
|
|
427
444
|
return true;
|
|
428
445
|
}
|
|
429
446
|
}
|
|
@@ -467,8 +484,10 @@ class Cuprite {
|
|
|
467
484
|
event = new FocusEvent(name, { bubbles: true, cancelable: true });
|
|
468
485
|
} else if (EVENTS.FORM.indexOf(name) != -1) {
|
|
469
486
|
event = this.obtainEvent(name);
|
|
487
|
+
} else if (name == "input") {
|
|
488
|
+
event = new InputEvent(name, { inputType: "insertText", bubbles: true, composed: true });
|
|
470
489
|
} else {
|
|
471
|
-
|
|
490
|
+
event = new Event(name, { bubbles: true, cancelable: true });
|
|
472
491
|
}
|
|
473
492
|
|
|
474
493
|
node.dispatchEvent(event);
|
|
@@ -523,6 +542,10 @@ class Cuprite {
|
|
|
523
542
|
window.getSelection().deleteFromDocument();
|
|
524
543
|
}
|
|
525
544
|
|
|
545
|
+
hasFocus(node) {
|
|
546
|
+
return document.activeElement === node;
|
|
547
|
+
}
|
|
548
|
+
|
|
526
549
|
containsSelection(node) {
|
|
527
550
|
let selectedNode = document.getSelection().focusNode;
|
|
528
551
|
|
|
@@ -181,11 +181,14 @@ module Capybara
|
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def drag_to(other, **options)
|
|
184
|
+
raise ArgumentError, "You must specify both x: and y: for a drop offset" if options[:x].nil? ^ options[:y].nil?
|
|
185
|
+
|
|
184
186
|
options[:steps] ||= 1
|
|
185
187
|
options[:scroll] = true unless options.key?(:scroll)
|
|
188
|
+
options[:drop_offset] = drop_offset(*options.values_at(:x, :y, :offset))
|
|
186
189
|
modifiers = Array(options[:drop_modifiers]).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym) }
|
|
187
190
|
|
|
188
|
-
command(:drag, other.node, modifiers, options.slice(:steps, :delay, :scroll, :html5))
|
|
191
|
+
command(:drag, other.node, modifiers, options.slice(:steps, :delay, :scroll, :html5, :drop_offset))
|
|
189
192
|
end
|
|
190
193
|
|
|
191
194
|
def drag_by(x, y, **options)
|
|
@@ -282,6 +285,13 @@ module Capybara
|
|
|
282
285
|
node.click(mode: mode, keys: keys, offset: offset, delay: delay)
|
|
283
286
|
end
|
|
284
287
|
|
|
288
|
+
def drop_offset(x, y, position)
|
|
289
|
+
return unless x
|
|
290
|
+
|
|
291
|
+
position ||= driver.session_options.w3c_click_offset ? :center : :top
|
|
292
|
+
{ x: x, y: y, position: position.to_sym }
|
|
293
|
+
end
|
|
294
|
+
|
|
285
295
|
def filter_text(text)
|
|
286
296
|
text.gsub(/[\u200b\u200e\u200f]/, "")
|
|
287
297
|
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, " ")
|
|
@@ -6,6 +6,25 @@ module Ferrum
|
|
|
6
6
|
attr_writer :window_size
|
|
7
7
|
attr_accessor :url_blacklist, :url_whitelist, :raise_on_unhandled_modal
|
|
8
8
|
|
|
9
|
+
def mobile
|
|
10
|
+
@options.fetch(:mobile, false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def scale_factor
|
|
14
|
+
@options.fetch(:scale_factor, 0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def device_user_agent
|
|
18
|
+
Capybara::Cuprite::Devices.fetch(@options[:emulate])[:user_agent] if @options[:emulate]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_user_agent
|
|
22
|
+
return @default_user_agent unless device_user_agent
|
|
23
|
+
|
|
24
|
+
chrome_version = @default_user_agent.to_s[%r{Chrome/(\S+)}, 1].to_s
|
|
25
|
+
format(device_user_agent, version: chrome_version)
|
|
26
|
+
end
|
|
27
|
+
|
|
9
28
|
def reset_window_size
|
|
10
29
|
@window_size = @options[:window_size]
|
|
11
30
|
end
|
|
@@ -44,6 +44,10 @@ module Capybara
|
|
|
44
44
|
raise_pending_unhandled_modal_error! { super }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def set_viewport(width: 0, height: 0, scale_factor: @options.scale_factor, mobile: @options.mobile)
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
47
51
|
def set(node, value)
|
|
48
52
|
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
|
|
49
53
|
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
|
|
@@ -66,7 +70,7 @@ module Capybara
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def send_keys(node, keys)
|
|
69
|
-
unless evaluate_on(node: node, expression: %(_cuprite.containsSelection(this)))
|
|
73
|
+
unless evaluate_on(node: node, expression: %(_cuprite.containsSelection(this) || _cuprite.hasFocus(this)))
|
|
70
74
|
before_click(node, "click")
|
|
71
75
|
node.click(mode: :left, keys: keys)
|
|
72
76
|
end
|
|
@@ -174,7 +178,8 @@ module Capybara
|
|
|
174
178
|
super
|
|
175
179
|
|
|
176
180
|
width, height = @options.window_size
|
|
177
|
-
resize(width: width, height: height)
|
|
181
|
+
width && height ? resize(width: width, height: height) : set_viewport
|
|
182
|
+
command("Network.setUserAgentOverride", userAgent: default_user_agent) if @options.device_user_agent
|
|
178
183
|
|
|
179
184
|
if @options.url_blacklist.any?
|
|
180
185
|
network.blacklist = @options.url_blacklist
|
data/lib/capybara/cuprite.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cuprite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '0.
|
|
4
|
+
version: '0.19'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Vorotilin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: capybara
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.19.1
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.19.1
|
|
41
41
|
description: Cuprite is a driver for Capybara that allows you to run your tests on
|
|
42
42
|
a headless Chrome browser
|
|
43
43
|
email:
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/capybara/cuprite.rb
|
|
52
52
|
- lib/capybara/cuprite/browser.rb
|
|
53
53
|
- lib/capybara/cuprite/cookie.rb
|
|
54
|
+
- lib/capybara/cuprite/devices.rb
|
|
54
55
|
- lib/capybara/cuprite/driver.rb
|
|
55
56
|
- lib/capybara/cuprite/errors.rb
|
|
56
57
|
- lib/capybara/cuprite/javascripts/drag.js
|