cuprite 0.15.1 → 0.18
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 +44 -27
- data/lib/capybara/cuprite/browser.rb +63 -7
- data/lib/capybara/cuprite/cookie.rb +1 -1
- data/lib/capybara/cuprite/driver.rb +12 -33
- data/lib/capybara/cuprite/errors.rb +9 -0
- data/lib/capybara/cuprite/javascripts/drag.js +118 -0
- data/lib/capybara/cuprite/javascripts/index.js +124 -16
- data/lib/capybara/cuprite/node.rb +51 -5
- data/lib/capybara/cuprite/options.rb +1 -1
- data/lib/capybara/cuprite/page.rb +46 -6
- data/lib/capybara/cuprite/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed2641aa002e810fd4dfedc8b3128f38e5f2df0f747d59bc1cf7a81aa1295076
|
|
4
|
+
data.tar.gz: 7e967cb1a6a11ad9f446290cd5035d2d5d3e9c6724ba6f4df4c31e761b29545d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcee409e8ee24caf139c3a0c29318ccea5ddefb0759f228f0f24db76aef28864c73d46e02ecc820160e82d6f366c61648ad1dc20dd930b5d9f300c8853d17003
|
|
7
|
+
data.tar.gz: 5168b1c18e81936795a02154829100dc3a616c2d2c3db71c134c8e64f5f13d93fd1098e8c26aea8d84a26840bf547a80b263b63b293420dc9f18cc18a5d29632
|
data/README.md
CHANGED
|
@@ -27,12 +27,27 @@ Capybara.register_driver(:cuprite) do |app|
|
|
|
27
27
|
end
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
If Chrome or Chromium cannot use its sandbox inside a container—for example,
|
|
31
|
+
because it runs as `root`—enable Ferrum's container mode:
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
|
-
Capybara::Cuprite::Driver.new(app,
|
|
34
|
+
Capybara::Cuprite::Driver.new(app, dockerize: true)
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
The `dockerize` option requires Ferrum 0.17.2 or newer. With an older Ferrum
|
|
38
|
+
version, pass the browser option explicitly:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Capybara::Cuprite::Driver.new(
|
|
42
|
+
app,
|
|
43
|
+
browser_options: { "no-sandbox" => nil }
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Both approaches disable the browser's sandbox, so do not use them merely
|
|
48
|
+
because Docker is involved. Prefer running the browser as a non-root user with
|
|
49
|
+
a working sandbox when possible.
|
|
50
|
+
|
|
36
51
|
Since Cuprite uses [Ferrum](https://github.com/rubycdp/ferrum#examples) there
|
|
37
52
|
are many useful methods you can call even using this driver:
|
|
38
53
|
|
|
@@ -61,24 +76,20 @@ end
|
|
|
61
76
|
`Cuprite`-specific options are:
|
|
62
77
|
|
|
63
78
|
* options `Hash`
|
|
79
|
+
* `:raise_on_unhandled_modal` (Boolean) - When set to `false`, output a warning. When set to `true`, raise
|
|
80
|
+
`Capybara::Cuprite::UnhandledModalError` instead. The dialog is always auto-accepted either way; the raise is
|
|
81
|
+
deferred and surfaces on the next command sent to the browser. In practice that's almost always the same action
|
|
82
|
+
that triggered the dialog (a click, `visit`, `evaluate_script`, ...), since a JS dialog blocks the page until
|
|
83
|
+
answered, so that action's own command is what was waiting. A dialog fired with nothing in flight (e.g. a bare JS
|
|
84
|
+
timer) only surfaces on whatever command runs next.
|
|
64
85
|
* `:url_blacklist` (Array) - array of regexes to match against requested URLs
|
|
65
86
|
* `:url_whitelist` (Array) - array of regexes to match against requested URLs
|
|
66
87
|
|
|
67
88
|
|
|
68
89
|
## Debugging
|
|
69
90
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`page.driver.debug(binding)` in your test to pause it. This will launch the
|
|
73
|
-
browser where you can inspect the content.
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
Capybara.register_driver :cuprite do |app|
|
|
77
|
-
Capybara::Cuprite::Driver.new(app, inspector: ENV['INSPECTOR'])
|
|
78
|
-
end
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
then somewhere in the test:
|
|
91
|
+
You can put `page.driver.debug` or `page.driver.debug(binding)` in your test to pause it.
|
|
92
|
+
This will launch the browser where you can inspect the content.
|
|
82
93
|
|
|
83
94
|
```ruby
|
|
84
95
|
it "does something useful" do
|
|
@@ -122,13 +133,17 @@ be automatically cleared at the end of the test.
|
|
|
122
133
|
|
|
123
134
|
## Network traffic
|
|
124
135
|
|
|
125
|
-
* `page.driver.network_traffic`
|
|
126
|
-
the current page. This returns an array of request objects.
|
|
136
|
+
* `page.driver.network_traffic` allows you to inspect network traffic (i.e., loaded resources) on the current page. It returns an array of `Ferrum::Network::Exchange` objects, each representing a network request/response exchange. You can query both the request and response details of each exchange.
|
|
127
137
|
|
|
128
138
|
```ruby
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
# Retrieve all network exchanges
|
|
140
|
+
network_traffic = page.driver.network_traffic
|
|
141
|
+
|
|
142
|
+
# Access the first exchange
|
|
143
|
+
first_exchange = network_traffic.first
|
|
144
|
+
|
|
145
|
+
# Inspect the response of the first request
|
|
146
|
+
response = first_exchange.response
|
|
132
147
|
```
|
|
133
148
|
|
|
134
149
|
* `page.driver.wait_for_network_idle` Natively waits for network idle and if
|
|
@@ -183,27 +198,29 @@ Besides capybara screenshot method you can get image as Base64:
|
|
|
183
198
|
|
|
184
199
|
## Proxy
|
|
185
200
|
|
|
186
|
-
* `page.driver.set_proxy(ip, port,
|
|
201
|
+
* `page.driver.set_proxy(ip, port, user, password)`
|
|
187
202
|
|
|
188
203
|
|
|
189
|
-
## URL
|
|
204
|
+
## URL Blocklisting & Allowlisting
|
|
190
205
|
|
|
191
|
-
Cuprite supports URL
|
|
206
|
+
Cuprite supports URL blocklisting, which allows you to prevent scripts from
|
|
192
207
|
running on designated domains:
|
|
193
208
|
|
|
194
209
|
```ruby
|
|
195
|
-
page.driver.browser.
|
|
210
|
+
page.driver.browser.url_blocklist = %r{http://www.example.com}
|
|
196
211
|
```
|
|
197
212
|
|
|
198
|
-
and also URL
|
|
213
|
+
and also URL allowlisting, which allows scripts to only run on designated
|
|
199
214
|
domains:
|
|
200
215
|
|
|
201
216
|
```ruby
|
|
202
|
-
page.driver.browser.
|
|
217
|
+
page.driver.browser.url_allowlist = %r{http://www.example.com}
|
|
203
218
|
```
|
|
204
219
|
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
For legacy support, `url_blacklist=` and `url_whitelist=` continue to work respectively.
|
|
221
|
+
|
|
222
|
+
If you are experiencing slower run times, consider creating a URL allowlist of
|
|
223
|
+
domains that are essential or a blocklist of domains that are not essential,
|
|
207
224
|
such as ad networks or analytics, to your testing environment.
|
|
208
225
|
|
|
209
226
|
## License
|
|
@@ -7,6 +7,8 @@ module Capybara
|
|
|
7
7
|
class Browser < Ferrum::Browser
|
|
8
8
|
extend Forwardable
|
|
9
9
|
|
|
10
|
+
DRAG_HTML5_JS = File.read(File.expand_path("javascripts/drag.js", __dir__))
|
|
11
|
+
|
|
10
12
|
delegate %i[send_keys select set hover trigger before_click switch_to_frame
|
|
11
13
|
find_modal accept_confirm dismiss_confirm accept_prompt
|
|
12
14
|
dismiss_prompt reset_modals] => :page
|
|
@@ -14,6 +16,7 @@ module Capybara
|
|
|
14
16
|
def initialize(options = nil)
|
|
15
17
|
super
|
|
16
18
|
|
|
19
|
+
@options.raise_on_unhandled_modal = options&.dig(:raise_on_unhandled_modal)
|
|
17
20
|
@options.url_blacklist = prepare_wildcards(options&.dig(:url_blacklist))
|
|
18
21
|
@options.url_whitelist = prepare_wildcards(options&.dig(:url_whitelist))
|
|
19
22
|
|
|
@@ -49,6 +52,14 @@ module Capybara
|
|
|
49
52
|
super
|
|
50
53
|
end
|
|
51
54
|
|
|
55
|
+
def raise_on_unhandled_modal
|
|
56
|
+
@options.raise_on_unhandled_modal
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def raise_on_unhandled_modal=(value)
|
|
60
|
+
@options.raise_on_unhandled_modal = value
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def url_whitelist
|
|
53
64
|
@options.url_whitelist
|
|
54
65
|
end
|
|
@@ -116,6 +127,7 @@ module Capybara
|
|
|
116
127
|
raise Ferrum::NoSuchPageError unless target
|
|
117
128
|
|
|
118
129
|
@page = attach_page(target.id)
|
|
130
|
+
@page.activate
|
|
119
131
|
end
|
|
120
132
|
|
|
121
133
|
def close_window(target_id)
|
|
@@ -139,29 +151,69 @@ module Capybara
|
|
|
139
151
|
raise NotImplementedError
|
|
140
152
|
end
|
|
141
153
|
|
|
142
|
-
def drag(node, other,
|
|
154
|
+
def drag(node, other, drop_modifiers, options = {})
|
|
155
|
+
steps = options.fetch(:steps, 1)
|
|
156
|
+
delay = options[:delay]
|
|
157
|
+
scroll = options.fetch(:scroll, true)
|
|
158
|
+
html5 = options[:html5]
|
|
159
|
+
|
|
160
|
+
execute("_cuprite.dragMousedownTracker()")
|
|
161
|
+
|
|
162
|
+
node.scroll_into_view if scroll
|
|
143
163
|
x1, y1 = node.find_position
|
|
144
|
-
x2, y2 = other.find_position
|
|
145
164
|
|
|
146
165
|
mouse.move(x: x1, y: y1)
|
|
147
166
|
mouse.down
|
|
148
167
|
sleep delay if delay
|
|
149
|
-
|
|
168
|
+
|
|
169
|
+
html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil?
|
|
170
|
+
|
|
171
|
+
if html5
|
|
172
|
+
drag_html5(node, other, drop_modifiers, delay)
|
|
173
|
+
else
|
|
174
|
+
modifiers = keyboard.modifiers(drop_modifiers)
|
|
175
|
+
|
|
176
|
+
other.scroll_into_view if scroll
|
|
177
|
+
x2, y2 = other.find_position
|
|
178
|
+
mouse.move(x: x2, y: y2, steps: steps)
|
|
179
|
+
|
|
180
|
+
mouse.up(modifiers: modifiers)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def drag_html5(node, other, drop_modifiers, delay)
|
|
185
|
+
keys = drop_modifiers.map(&:to_s)
|
|
186
|
+
evaluate_async(DRAG_HTML5_JS, timeout, node, other, (delay || 0.05) * 1000, keys)
|
|
150
187
|
mouse.up
|
|
151
188
|
end
|
|
152
189
|
|
|
153
|
-
def drag_by(node,
|
|
190
|
+
def drag_by(node, dx, dy, steps, delay = nil, scroll = true)
|
|
154
191
|
x1, y1 = node.find_position
|
|
155
|
-
x2 = x1 + x
|
|
156
|
-
y2 = y1 + y
|
|
157
192
|
|
|
158
193
|
mouse.move(x: x1, y: y1)
|
|
159
194
|
mouse.down
|
|
195
|
+
|
|
160
196
|
sleep delay if delay
|
|
161
|
-
|
|
197
|
+
|
|
198
|
+
evaluate("window.scrollBy(#{dx}, #{dy})") if scroll # should be extracted to Mouse#scroll_by in ferrum
|
|
199
|
+
|
|
200
|
+
x2, y2 = node.find_position
|
|
201
|
+
mouse.move(x: x2 + dx, y: y2 + dy, steps: steps)
|
|
162
202
|
mouse.up
|
|
163
203
|
end
|
|
164
204
|
|
|
205
|
+
def drop(node, *args)
|
|
206
|
+
if args[0].is_a?(String)
|
|
207
|
+
execute("_cuprite.attachDropInput()")
|
|
208
|
+
input = find(:css, "#_cuprite_drop_file").first
|
|
209
|
+
select_file(input, args)
|
|
210
|
+
evaluate_on(node: node, expression: "_cuprite.dropFile(this)")
|
|
211
|
+
else
|
|
212
|
+
strings = args.flat_map { |arg| arg.map { |type, data| { "type" => type, "data" => data } } }
|
|
213
|
+
evaluate_on(node: node, expression: "_cuprite.dropString(#{strings.to_json}, this)")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
165
217
|
def select_file(node, value)
|
|
166
218
|
node.select_file(value)
|
|
167
219
|
end
|
|
@@ -203,6 +255,10 @@ module Capybara
|
|
|
203
255
|
evaluate_on(node: node, expression: "_cuprite.path(this)")
|
|
204
256
|
end
|
|
205
257
|
|
|
258
|
+
def obscured?(node)
|
|
259
|
+
evaluate_on(node: node, expression: "_cuprite.isObscured(this)")
|
|
260
|
+
end
|
|
261
|
+
|
|
206
262
|
def all_text(node)
|
|
207
263
|
node.text
|
|
208
264
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "forwardable"
|
|
5
5
|
|
|
6
|
-
# rubocop:disable Metrics/ClassLength
|
|
6
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
7
7
|
module Capybara
|
|
8
8
|
module Cuprite
|
|
9
9
|
class Driver < Capybara::Driver::Base
|
|
@@ -35,6 +35,8 @@ module Capybara
|
|
|
35
35
|
@screen_size ||= DEFAULT_MAXIMIZE_SCREEN_SIZE
|
|
36
36
|
@options[:save_path] ||= File.expand_path(Capybara.save_path) if Capybara.save_path
|
|
37
37
|
|
|
38
|
+
@options[:pending_connection_errors] = true unless @options.key?(:pending_connection_errors)
|
|
39
|
+
|
|
38
40
|
# It's set for debug() to make devtools tab open correctly.
|
|
39
41
|
@options[:browser_options] ||= {}
|
|
40
42
|
unless @options[:browser_options][:"remote-allow-origins"]
|
|
@@ -108,10 +110,14 @@ module Capybara
|
|
|
108
110
|
handle = case locator
|
|
109
111
|
when Capybara::Node::Element
|
|
110
112
|
locator.native.description["frameId"]
|
|
113
|
+
when Capybara::Cuprite::Node
|
|
114
|
+
locator.description["frameId"]
|
|
111
115
|
when :parent, :top
|
|
112
116
|
locator
|
|
113
117
|
end
|
|
114
118
|
|
|
119
|
+
raise ArgumentError, "Unable to switch to frame from #{locator.class}" unless handle
|
|
120
|
+
|
|
115
121
|
browser.switch_to_frame(handle)
|
|
116
122
|
end
|
|
117
123
|
|
|
@@ -131,6 +137,7 @@ module Capybara
|
|
|
131
137
|
@paper_size = nil
|
|
132
138
|
browser.url_blacklist = @options[:url_blacklist]
|
|
133
139
|
browser.url_whitelist = @options[:url_whitelist]
|
|
140
|
+
browser.raise_on_unhandled_modal = @options.fetch(:raise_on_unhandled_modal, false)
|
|
134
141
|
browser.reset
|
|
135
142
|
@started = false
|
|
136
143
|
end
|
|
@@ -251,7 +258,8 @@ module Capybara
|
|
|
251
258
|
|
|
252
259
|
def remove_cookie(name, **options)
|
|
253
260
|
options[:domain] = default_domain if options.empty?
|
|
254
|
-
|
|
261
|
+
options = options.merge(name: name)
|
|
262
|
+
browser.cookies.remove(**options)
|
|
255
263
|
end
|
|
256
264
|
|
|
257
265
|
def clear_cookies
|
|
@@ -273,32 +281,8 @@ module Capybara
|
|
|
273
281
|
end
|
|
274
282
|
alias authorize basic_authorize
|
|
275
283
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
devtools_frontend_path = response[0]&.[]("devtoolsFrontendUrl")
|
|
280
|
-
raise "Could not generate debug url for remote debugging session" unless devtools_frontend_path
|
|
281
|
-
|
|
282
|
-
build_remote_debug_url(path: devtools_frontend_path)
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def debug(binding = nil)
|
|
286
|
-
if @options[:inspector]
|
|
287
|
-
Process.spawn(browser.process.path, debug_url)
|
|
288
|
-
|
|
289
|
-
if binding.respond_to?(:pry)
|
|
290
|
-
Pry.start(binding)
|
|
291
|
-
elsif binding.respond_to?(:irb)
|
|
292
|
-
binding.irb
|
|
293
|
-
else
|
|
294
|
-
pause
|
|
295
|
-
end
|
|
296
|
-
else
|
|
297
|
-
raise Error, "To use the remote debugging, you have to launch " \
|
|
298
|
-
"the driver with `inspector: ENV['INSPECTOR']` " \
|
|
299
|
-
"configuration option and run your test suite passing " \
|
|
300
|
-
"env variable"
|
|
301
|
-
end
|
|
284
|
+
def debug(...)
|
|
285
|
+
browser.debug(...)
|
|
302
286
|
end
|
|
303
287
|
|
|
304
288
|
def pause
|
|
@@ -377,10 +361,6 @@ module Capybara
|
|
|
377
361
|
|
|
378
362
|
private
|
|
379
363
|
|
|
380
|
-
def build_remote_debug_url(path:)
|
|
381
|
-
"http://#{browser.process.host}:#{browser.process.port}#{path}"
|
|
382
|
-
end
|
|
383
|
-
|
|
384
364
|
def default_domain
|
|
385
365
|
if @started
|
|
386
366
|
URI.parse(browser.current_url).host
|
|
@@ -433,4 +413,3 @@ module Capybara
|
|
|
433
413
|
end
|
|
434
414
|
end
|
|
435
415
|
end
|
|
436
|
-
# rubocop:enable Metrics/ClassLength
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// HTML5 drag-and-drop emulation.
|
|
2
|
+
//
|
|
3
|
+
// Ported near-verbatim from Capybara's Selenium driver
|
|
4
|
+
// (capybara/selenium/extensions/html5_drag.rb, HTML5_DRAG_DROP_SCRIPT) so
|
|
5
|
+
// Cuprite matches Capybara's HTML5 drag behaviour and its shared specs. Kept
|
|
6
|
+
// close to the source to ease future syncs; known upstream quirks (rectPt.top
|
|
7
|
+
// in pointOnRect, undeclared `key`, callback.call(true)) are preserved
|
|
8
|
+
// deliberately. See https://github.com/rubycdp/cuprite/issues/314.
|
|
9
|
+
|
|
10
|
+
function rectCenter(rect){
|
|
11
|
+
return new DOMPoint(
|
|
12
|
+
(rect.left + rect.right)/2,
|
|
13
|
+
(rect.top + rect.bottom)/2
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pointOnRect(pt, rect) {
|
|
18
|
+
var rectPt = rectCenter(rect);
|
|
19
|
+
var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
|
|
20
|
+
|
|
21
|
+
if (pt.x <= rectPt.x) { // left side
|
|
22
|
+
var minXy = slope * (rect.left - pt.x) + pt.y;
|
|
23
|
+
if (rect.top <= minXy && minXy <= rect.bottom)
|
|
24
|
+
return new DOMPoint(rect.left, minXy);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (pt.x >= rectPt.x) { // right side
|
|
28
|
+
var maxXy = slope * (rect.right - pt.x) + pt.y;
|
|
29
|
+
if (rect.top <= maxXy && maxXy <= rect.bottom)
|
|
30
|
+
return new DOMPoint(rect.right, maxXy);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (pt.y <= rectPt.y) { // top side
|
|
34
|
+
var minYx = (rectPt.top - pt.y) / slope + pt.x;
|
|
35
|
+
if (rect.left <= minYx && minYx <= rect.right)
|
|
36
|
+
return new DOMPoint(minYx, rect.top);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (pt.y >= rectPt.y) { // bottom side
|
|
40
|
+
var maxYx = (rect.bottom - pt.y) / slope + pt.x;
|
|
41
|
+
if (rect.left <= maxYx && maxYx <= rect.right)
|
|
42
|
+
return new DOMPoint(maxYx, rect.bottom);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new DOMPoint(pt.x,pt.y);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dragEnterTarget() {
|
|
49
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
|
50
|
+
var targetRect = target.getBoundingClientRect();
|
|
51
|
+
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
|
52
|
+
|
|
53
|
+
for (var i = 0; i < drop_modifier_keys.length; i++) {
|
|
54
|
+
key = drop_modifier_keys[i];
|
|
55
|
+
if (key == "control"){
|
|
56
|
+
key = "ctrl"
|
|
57
|
+
}
|
|
58
|
+
opts[key + 'Key'] = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var dragEnterEvent = new DragEvent('dragenter', opts);
|
|
62
|
+
target.dispatchEvent(dragEnterEvent);
|
|
63
|
+
|
|
64
|
+
// fire 2 dragover events to simulate dragging with a direction
|
|
65
|
+
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
|
66
|
+
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
|
67
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
68
|
+
target.dispatchEvent(dragOverEvent);
|
|
69
|
+
window.setTimeout(dragOnTarget, step_delay);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function dragOnTarget() {
|
|
73
|
+
var targetCenter = rectCenter(target.getBoundingClientRect());
|
|
74
|
+
var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
|
75
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
76
|
+
target.dispatchEvent(dragOverEvent);
|
|
77
|
+
window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function dragLeave(drop, dragOverOpts) {
|
|
81
|
+
var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
|
|
82
|
+
var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
|
|
83
|
+
target.dispatchEvent(dragLeaveEvent);
|
|
84
|
+
if (drop) {
|
|
85
|
+
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
|
86
|
+
target.dispatchEvent(dropEvent);
|
|
87
|
+
}
|
|
88
|
+
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
|
89
|
+
source.dispatchEvent(dragEndEvent);
|
|
90
|
+
callback.call(true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var source = arguments[0],
|
|
94
|
+
target = arguments[1],
|
|
95
|
+
step_delay = arguments[2],
|
|
96
|
+
drop_modifier_keys = arguments[3],
|
|
97
|
+
callback = arguments[4];
|
|
98
|
+
|
|
99
|
+
var dt = new DataTransfer();
|
|
100
|
+
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
101
|
+
|
|
102
|
+
while (source && !source.draggable) {
|
|
103
|
+
source = source.parentElement;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (source.tagName == 'A'){
|
|
107
|
+
dt.setData('text/uri-list', source.href);
|
|
108
|
+
dt.setData('text', source.href);
|
|
109
|
+
}
|
|
110
|
+
if (source.tagName == 'IMG'){
|
|
111
|
+
dt.setData('text/uri-list', source.src);
|
|
112
|
+
dt.setData('text', source.src);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var dragEvent = new DragEvent('dragstart', opts);
|
|
116
|
+
source.dispatchEvent(dragEvent);
|
|
117
|
+
|
|
118
|
+
window.setTimeout(dragEnterTarget, step_delay);
|
|
@@ -52,13 +52,17 @@ class Cuprite {
|
|
|
52
52
|
if (this.isVisible(node)) {
|
|
53
53
|
if (node.nodeName == "TEXTAREA") {
|
|
54
54
|
return node.textContent;
|
|
55
|
-
} else {
|
|
56
|
-
if (node instanceof SVGElement) {
|
|
57
|
-
return node.textContent;
|
|
58
|
-
} else {
|
|
59
|
-
return node.innerText;
|
|
60
|
-
}
|
|
61
55
|
}
|
|
56
|
+
if (node instanceof SVGElement) {
|
|
57
|
+
return node.textContent;
|
|
58
|
+
}
|
|
59
|
+
if (node instanceof ShadowRoot) {
|
|
60
|
+
return Array.from(node.children)
|
|
61
|
+
.map(child => this.visibleText(child))
|
|
62
|
+
.filter(text => text)
|
|
63
|
+
.join(" ");
|
|
64
|
+
}
|
|
65
|
+
return node.innerText;
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -74,11 +78,24 @@ class Cuprite {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
while (node) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
if (node instanceof ShadowRoot) {
|
|
82
|
+
node = node.host;
|
|
83
|
+
} else {
|
|
84
|
+
style = window.getComputedStyle(node);
|
|
85
|
+
if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let parent = node.parentElement;
|
|
90
|
+
if (parent && parent.tagName === "DETAILS" && !parent.open) {
|
|
91
|
+
// In a closed <details> only the first <summary> (and its subtree) is rendered.
|
|
92
|
+
if (node !== parent.querySelector(":scope > summary")) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
node = parent ?? (node.getRootNode() instanceof ShadowRoot && node.getRootNode());
|
|
80
98
|
}
|
|
81
|
-
node = node.parentElement;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
return true;
|
|
@@ -94,6 +111,10 @@ class Cuprite {
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
path(node) {
|
|
114
|
+
if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
|
|
115
|
+
return "(: Shadow DOM element - no XPath :)";
|
|
116
|
+
};
|
|
117
|
+
|
|
97
118
|
let nodes = [node];
|
|
98
119
|
let parent = node.parentNode;
|
|
99
120
|
while (parent !== document && parent !== null) {
|
|
@@ -115,6 +136,35 @@ class Cuprite {
|
|
|
115
136
|
return `//${selectors.join("/")}`;
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Returns true if the node is obscured in the viewport.
|
|
141
|
+
*
|
|
142
|
+
* @param {Element} node
|
|
143
|
+
* @return {boolean} true if the node is obscured, false otherwise
|
|
144
|
+
*/
|
|
145
|
+
isObscured(node) {
|
|
146
|
+
let win = window;
|
|
147
|
+
let rect = node.getBoundingClientRect();
|
|
148
|
+
let px = rect.left + rect.width / 2;
|
|
149
|
+
let py = rect.top + rect.height / 2;
|
|
150
|
+
|
|
151
|
+
while (win) {
|
|
152
|
+
let topNode = win.document.elementFromPoint(px, py);
|
|
153
|
+
|
|
154
|
+
if (node !== topNode && !node.contains(topNode)) return true;
|
|
155
|
+
|
|
156
|
+
node = win.frameElement;
|
|
157
|
+
if (!node) return false;
|
|
158
|
+
|
|
159
|
+
rect = node.getBoundingClientRect();
|
|
160
|
+
px = rect.left + px;
|
|
161
|
+
py = rect.top + py;
|
|
162
|
+
win = win.parent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
118
168
|
set(node, value) {
|
|
119
169
|
if (node.readOnly) return;
|
|
120
170
|
|
|
@@ -124,15 +174,13 @@ class Cuprite {
|
|
|
124
174
|
|
|
125
175
|
let valueBefore = node.value;
|
|
126
176
|
|
|
177
|
+
node.focus();
|
|
127
178
|
this.trigger(node, "focus");
|
|
128
179
|
this.setValue(node, "");
|
|
129
180
|
|
|
130
|
-
if (node.type == "number" || node.type == "date" || node.type == "range") {
|
|
181
|
+
if (node.type == "number" || node.type == "date" || node.type == "range" || node.type == "time" || node.type == "month" || node.type == "week") {
|
|
131
182
|
this.setValue(node, value);
|
|
132
183
|
this.input(node);
|
|
133
|
-
} else if (node.type == "time") {
|
|
134
|
-
this.setValue(node, new Date(value).toTimeString().split(" ")[0]);
|
|
135
|
-
this.input(node);
|
|
136
184
|
} else if (node.type == "datetime-local") {
|
|
137
185
|
value = new Date(value);
|
|
138
186
|
let year = value.getFullYear();
|
|
@@ -280,7 +328,7 @@ class Cuprite {
|
|
|
280
328
|
x -= frameOffset.left;
|
|
281
329
|
y -= frameOffset.top;
|
|
282
330
|
|
|
283
|
-
let element =
|
|
331
|
+
let element = node.getRootNode().elementFromPoint(x, y);
|
|
284
332
|
|
|
285
333
|
let el = element;
|
|
286
334
|
while (el) {
|
|
@@ -416,7 +464,7 @@ class Cuprite {
|
|
|
416
464
|
options["button"] || 0, null
|
|
417
465
|
)
|
|
418
466
|
} else if (EVENTS.FOCUS.indexOf(name) != -1) {
|
|
419
|
-
event =
|
|
467
|
+
event = new FocusEvent(name, { bubbles: true, cancelable: true });
|
|
420
468
|
} else if (EVENTS.FORM.indexOf(name) != -1) {
|
|
421
469
|
event = this.obtainEvent(name);
|
|
422
470
|
} else {
|
|
@@ -489,6 +537,66 @@ class Cuprite {
|
|
|
489
537
|
return node.contains(selectedNode);
|
|
490
538
|
}
|
|
491
539
|
|
|
540
|
+
dragMousedownTracker() {
|
|
541
|
+
window._cupriteMousedownPrevented = null;
|
|
542
|
+
document.addEventListener('mousedown', ev => {
|
|
543
|
+
window._cupriteMousedownPrevented = ev.defaultPrevented;
|
|
544
|
+
}, { once: true, passive: true });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
legacyDragCheck(node) {
|
|
548
|
+
if ([true, null].indexOf(window._cupriteMousedownPrevented) >= 0) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let el = node;
|
|
553
|
+
do {
|
|
554
|
+
if (el.draggable) return false;
|
|
555
|
+
} while (el = el.parentElement);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Ported from Capybara's Selenium html5_drag.rb (DROP_STRING).
|
|
560
|
+
dropString(strings, target) {
|
|
561
|
+
let dt = new DataTransfer();
|
|
562
|
+
let opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
563
|
+
for (let i = 0; i < strings.length; i++) {
|
|
564
|
+
if (dt.items) {
|
|
565
|
+
dt.items.add(strings[i]["data"], strings[i]["type"]);
|
|
566
|
+
} else {
|
|
567
|
+
dt.setData(strings[i]["type"], strings[i]["data"]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
target.dispatchEvent(new DragEvent("drop", opts));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Ported from Capybara's Selenium html5_drag.rb (ATTACH_FILE).
|
|
574
|
+
attachDropInput() {
|
|
575
|
+
document.getElementById("_cuprite_drop_file")?.remove();
|
|
576
|
+
let input = document.createElement("INPUT");
|
|
577
|
+
input.type = "file";
|
|
578
|
+
input.id = "_cuprite_drop_file";
|
|
579
|
+
input.multiple = true;
|
|
580
|
+
document.body.appendChild(input);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Ported from Capybara's Selenium html5_drag.rb (DROP_FILE).
|
|
584
|
+
dropFile(target) {
|
|
585
|
+
let input = document.getElementById("_cuprite_drop_file");
|
|
586
|
+
let files = input.files;
|
|
587
|
+
let dt = new DataTransfer();
|
|
588
|
+
let opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
589
|
+
input.parentElement.removeChild(input);
|
|
590
|
+
if (dt.items) {
|
|
591
|
+
for (let i = 0; i < files.length; i++) {
|
|
592
|
+
dt.items.add(files[i]);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
Object.defineProperty(dt, "files", { value: files, writable: false });
|
|
596
|
+
}
|
|
597
|
+
target.dispatchEvent(new DragEvent("drop", opts));
|
|
598
|
+
}
|
|
599
|
+
|
|
492
600
|
// This command is purely for testing error handling
|
|
493
601
|
browserError() {
|
|
494
602
|
throw new Error("zomg");
|
|
@@ -12,12 +12,16 @@ module Capybara
|
|
|
12
12
|
delegate %i[description] => :node
|
|
13
13
|
delegate %i[browser] => :driver
|
|
14
14
|
|
|
15
|
+
DRAG_MODIFIER_ALIASES = { control: :ctrl, command: :meta, cmd: :meta }.freeze
|
|
16
|
+
|
|
15
17
|
def initialize(driver, node)
|
|
16
18
|
super(driver, self)
|
|
17
19
|
@node = node
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def command(name, *args)
|
|
23
|
+
raise ObsoleteNode.new(self, nil) unless node.evaluate("this.isConnected")
|
|
24
|
+
|
|
21
25
|
browser.send(name, node, *args)
|
|
22
26
|
rescue Ferrum::NodeNotFoundError => e
|
|
23
27
|
raise ObsoleteNode.new(self, e.response)
|
|
@@ -89,7 +93,7 @@ module Capybara
|
|
|
89
93
|
command(:value)
|
|
90
94
|
end
|
|
91
95
|
|
|
92
|
-
def set(value, options = {})
|
|
96
|
+
def set(value, options = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
93
97
|
warn "Options passed to Node#set but Cuprite doesn't currently support any - ignoring" unless options.empty?
|
|
94
98
|
|
|
95
99
|
if tag_name == "input"
|
|
@@ -103,6 +107,23 @@ module Capybara
|
|
|
103
107
|
command(:select_file, files)
|
|
104
108
|
when "color"
|
|
105
109
|
node.evaluate("this.setAttribute('value', '#{value}')")
|
|
110
|
+
node.evaluate("this.dispatchEvent(new InputEvent('input'))")
|
|
111
|
+
node.evaluate("this.dispatchEvent(new Event('change', { bubbles: true }))")
|
|
112
|
+
when "date"
|
|
113
|
+
value = value.to_date.iso8601 if !value.is_a?(String) && value.respond_to?(:to_date)
|
|
114
|
+
command(:set, value.to_s)
|
|
115
|
+
when "time"
|
|
116
|
+
value = value.to_time.strftime("%H:%M") if !value.is_a?(String) && value.respond_to?(:to_time)
|
|
117
|
+
command(:set, value.to_s)
|
|
118
|
+
when "month"
|
|
119
|
+
value = value.to_date.strftime("%Y-%m") if !value.is_a?(String) && value.respond_to?(:to_date)
|
|
120
|
+
command(:set, value.to_s)
|
|
121
|
+
when "week"
|
|
122
|
+
value = value.to_date.strftime("%G-W%V") if !value.is_a?(String) && value.respond_to?(:to_date)
|
|
123
|
+
command(:set, value.to_s)
|
|
124
|
+
when "datetime-local"
|
|
125
|
+
value = value.to_time.strftime("%Y-%m-%dT%H:%M") if !value.is_a?(String) && value.respond_to?(:to_time)
|
|
126
|
+
command(:set, value.to_s)
|
|
106
127
|
else
|
|
107
128
|
command(:set, value.to_s)
|
|
108
129
|
end
|
|
@@ -110,7 +131,7 @@ module Capybara
|
|
|
110
131
|
command(:set, value.to_s)
|
|
111
132
|
elsif self[:isContentEditable]
|
|
112
133
|
command(:delete_text)
|
|
113
|
-
|
|
134
|
+
click.type(value.to_s)
|
|
114
135
|
end
|
|
115
136
|
end
|
|
116
137
|
|
|
@@ -124,7 +145,7 @@ module Capybara
|
|
|
124
145
|
end
|
|
125
146
|
|
|
126
147
|
def tag_name
|
|
127
|
-
@tag_name ||= description["nodeName"].downcase
|
|
148
|
+
@tag_name ||= description["shadowRootType"] ? "ShadowRoot" : description["nodeName"].downcase
|
|
128
149
|
end
|
|
129
150
|
|
|
130
151
|
def visible?
|
|
@@ -161,16 +182,21 @@ module Capybara
|
|
|
161
182
|
|
|
162
183
|
def drag_to(other, **options)
|
|
163
184
|
options[:steps] ||= 1
|
|
185
|
+
options[:scroll] = true unless options.key?(:scroll)
|
|
186
|
+
modifiers = Array(options[:drop_modifiers]).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym) }
|
|
164
187
|
|
|
165
|
-
command(:drag, other.node, options
|
|
188
|
+
command(:drag, other.node, modifiers, options.slice(:steps, :delay, :scroll, :html5))
|
|
166
189
|
end
|
|
167
190
|
|
|
168
191
|
def drag_by(x, y, **options)
|
|
169
192
|
options[:steps] ||= 1
|
|
193
|
+
options[:scroll] = true unless options.key?(:scroll)
|
|
170
194
|
|
|
171
|
-
command(:drag_by, x, y, options[:steps], options[:delay])
|
|
195
|
+
command(:drag_by, x, y, options[:steps], options[:delay], options[:scroll])
|
|
172
196
|
end
|
|
173
197
|
|
|
198
|
+
def drop(...) = command(:drop, ...)
|
|
199
|
+
|
|
174
200
|
def trigger(event)
|
|
175
201
|
command(:trigger, event)
|
|
176
202
|
end
|
|
@@ -203,6 +229,9 @@ module Capybara
|
|
|
203
229
|
end
|
|
204
230
|
|
|
205
231
|
def send_keys(*keys)
|
|
232
|
+
keys = keys.reject { |key| key.nil? || key == "" }
|
|
233
|
+
return if keys.empty?
|
|
234
|
+
|
|
206
235
|
command(:send_keys, keys)
|
|
207
236
|
end
|
|
208
237
|
alias send_key send_keys
|
|
@@ -211,6 +240,23 @@ module Capybara
|
|
|
211
240
|
command(:path)
|
|
212
241
|
end
|
|
213
242
|
|
|
243
|
+
def obscured?
|
|
244
|
+
command(:obscured?)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def shadow_root
|
|
248
|
+
root = driver.evaluate_script <<~JS, self
|
|
249
|
+
arguments[0].shadowRoot
|
|
250
|
+
JS
|
|
251
|
+
root && self.class.new(driver, root.node)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def rect
|
|
255
|
+
driver.evaluate_script <<~JS, self
|
|
256
|
+
arguments[0].getBoundingClientRect().toJSON()
|
|
257
|
+
JS
|
|
258
|
+
end
|
|
259
|
+
|
|
214
260
|
def inspect
|
|
215
261
|
%(#<#{self.class} @node=#{@node.inspect}>)
|
|
216
262
|
end
|
|
@@ -15,6 +15,7 @@ module Capybara
|
|
|
15
15
|
TRIGGER_CLICK_WAIT = ENV.fetch("CUPRITE_TRIGGER_CLICK_WAIT", 0.1).to_f
|
|
16
16
|
|
|
17
17
|
extend Forwardable
|
|
18
|
+
|
|
18
19
|
delegate %i[at_css at_xpath css xpath
|
|
19
20
|
current_url current_title body execution_id execution_id!
|
|
20
21
|
evaluate evaluate_on evaluate_async execute] => :active_frame
|
|
@@ -24,9 +25,25 @@ module Capybara
|
|
|
24
25
|
@accept_modal = []
|
|
25
26
|
@modal_messages = []
|
|
26
27
|
@modal_response = nil
|
|
28
|
+
@unhandled_modal_error = nil
|
|
27
29
|
super
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Keep a handle to Ferrum's own implementation before overriding it
|
|
33
|
+
# below, so answering a dialog (see `handle_javascript_dialog`) can
|
|
34
|
+
# bypass our override.
|
|
35
|
+
alias ferrum_command command
|
|
36
|
+
|
|
37
|
+
# The `Page.javascriptDialogOpening` event is handled on Ferrum's
|
|
38
|
+
# background CDP dispatcher thread, so raising there wouldn't reach
|
|
39
|
+
# the caller and would permanently kill that thread instead. The
|
|
40
|
+
# dialog is always accepted immediately from that thread, and the
|
|
41
|
+
# error (if any) is stashed here to be raised from the main thread
|
|
42
|
+
# the next time it makes a command round trip.
|
|
43
|
+
def command(...)
|
|
44
|
+
raise_pending_unhandled_modal_error! { super }
|
|
45
|
+
end
|
|
46
|
+
|
|
30
47
|
def set(node, value)
|
|
31
48
|
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
|
|
32
49
|
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
|
|
@@ -99,6 +116,7 @@ module Capybara
|
|
|
99
116
|
@accept_modal = []
|
|
100
117
|
@modal_response = nil
|
|
101
118
|
@modal_messages = []
|
|
119
|
+
@unhandled_modal_error = nil
|
|
102
120
|
end
|
|
103
121
|
|
|
104
122
|
def before_click(node, name, _keys = [], offset = {})
|
|
@@ -123,7 +141,6 @@ module Capybara
|
|
|
123
141
|
@frame_stack = []
|
|
124
142
|
else
|
|
125
143
|
@frame_stack << handle
|
|
126
|
-
inject_extensions
|
|
127
144
|
end
|
|
128
145
|
end
|
|
129
146
|
|
|
@@ -141,6 +158,18 @@ module Capybara
|
|
|
141
158
|
|
|
142
159
|
private
|
|
143
160
|
|
|
161
|
+
def raise_pending_unhandled_modal_error!
|
|
162
|
+
error = @unhandled_modal_error
|
|
163
|
+
@unhandled_modal_error = nil
|
|
164
|
+
raise error if error
|
|
165
|
+
|
|
166
|
+
yield
|
|
167
|
+
ensure
|
|
168
|
+
error = @unhandled_modal_error
|
|
169
|
+
@unhandled_modal_error = nil
|
|
170
|
+
raise error if error
|
|
171
|
+
end
|
|
172
|
+
|
|
144
173
|
def prepare_page
|
|
145
174
|
super
|
|
146
175
|
|
|
@@ -155,6 +184,8 @@ module Capybara
|
|
|
155
184
|
|
|
156
185
|
on("Page.javascriptDialogOpening") do |params|
|
|
157
186
|
accept_modal = @accept_modal.last
|
|
187
|
+
unhandled_modal_error = nil
|
|
188
|
+
|
|
158
189
|
if [true, false].include?(accept_modal)
|
|
159
190
|
@accept_modal.pop
|
|
160
191
|
@modal_messages << params["message"]
|
|
@@ -162,15 +193,24 @@ module Capybara
|
|
|
162
193
|
response = @modal_response || params["defaultPrompt"]
|
|
163
194
|
else
|
|
164
195
|
with_text = params["message"] ? "with text `#{params['message']}` " : ""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
196
|
+
message = "Modal window #{with_text}has been opened, but you didn't wrap " \
|
|
197
|
+
"your code into (`accept_prompt` | `dismiss_prompt` | " \
|
|
198
|
+
"`accept_confirm` | `dismiss_confirm` | `accept_alert`), " \
|
|
199
|
+
"accepting by default"
|
|
200
|
+
|
|
201
|
+
if @options.raise_on_unhandled_modal
|
|
202
|
+
unhandled_modal_error = UnhandledModalError.new(message)
|
|
203
|
+
else
|
|
204
|
+
warn message
|
|
205
|
+
end
|
|
206
|
+
|
|
169
207
|
options = { accept: true }
|
|
170
208
|
response = params["defaultPrompt"]
|
|
171
209
|
end
|
|
172
210
|
options.merge!(promptText: response) if response
|
|
173
|
-
|
|
211
|
+
ferrum_command("Page.handleJavaScriptDialog", **options)
|
|
212
|
+
|
|
213
|
+
@unhandled_modal_error = unhandled_modal_error if unhandled_modal_error
|
|
174
214
|
end
|
|
175
215
|
end
|
|
176
216
|
|
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.18'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Vorotilin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-03 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.18.0
|
|
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.18.0
|
|
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:
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/capybara/cuprite/cookie.rb
|
|
54
54
|
- lib/capybara/cuprite/driver.rb
|
|
55
55
|
- lib/capybara/cuprite/errors.rb
|
|
56
|
+
- lib/capybara/cuprite/javascripts/drag.js
|
|
56
57
|
- lib/capybara/cuprite/javascripts/index.js
|
|
57
58
|
- lib/capybara/cuprite/node.rb
|
|
58
59
|
- lib/capybara/cuprite/options.rb
|
|
@@ -75,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
75
76
|
requirements:
|
|
76
77
|
- - ">="
|
|
77
78
|
- !ruby/object:Gem::Version
|
|
78
|
-
version:
|
|
79
|
+
version: '3.1'
|
|
79
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
81
|
requirements:
|
|
81
82
|
- - ">="
|
|
82
83
|
- !ruby/object:Gem::Version
|
|
83
84
|
version: '0'
|
|
84
85
|
requirements: []
|
|
85
|
-
rubygems_version: 3.5.
|
|
86
|
+
rubygems_version: 3.5.22
|
|
86
87
|
signing_key:
|
|
87
88
|
specification_version: 4
|
|
88
89
|
summary: Headless Chrome driver for Capybara
|