cuprite 0.19 → 0.20
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 +43 -41
- data/lib/capybara/cuprite/browser/deprecations.rb +26 -0
- data/lib/capybara/cuprite/browser/extensions.rb +59 -0
- data/lib/capybara/cuprite/browser.rb +18 -232
- data/lib/capybara/cuprite/deprecation.rb +13 -0
- data/lib/capybara/cuprite/driver/deprecations.rb +198 -0
- data/lib/capybara/cuprite/driver/extensions.rb +13 -0
- data/lib/capybara/cuprite/driver.rb +66 -201
- data/lib/capybara/cuprite/finder.rb +22 -0
- data/lib/capybara/cuprite/javascripts/drag.js +96 -101
- data/lib/capybara/cuprite/javascripts/index.js +507 -508
- data/lib/capybara/cuprite/node/deprecations.rb +43 -0
- data/lib/capybara/cuprite/node/extensions.rb +27 -0
- data/lib/capybara/cuprite/node.rb +130 -116
- data/lib/capybara/cuprite/options.rb +1 -1
- data/lib/capybara/cuprite/page/frames.rb +47 -0
- data/lib/capybara/cuprite/page/modals.rb +118 -0
- data/lib/capybara/cuprite/page.rb +6 -209
- data/lib/capybara/cuprite/version.rb +1 -1
- metadata +15 -6
- data/lib/capybara/cuprite/cookie.rb +0 -47
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "capybara/cuprite/deprecation"
|
|
4
|
+
|
|
5
|
+
module Capybara
|
|
6
|
+
module Cuprite
|
|
7
|
+
class Node < Capybara::Driver::Node
|
|
8
|
+
module Deprecations
|
|
9
|
+
def browser
|
|
10
|
+
Cuprite.deprecation_warning("#{self.class}#browser", "use page.driver.browser")
|
|
11
|
+
driver.browser
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parents
|
|
15
|
+
Cuprite.deprecation_warning("#{self.class}#parents", "use Capybara's ancestor")
|
|
16
|
+
command { driver.browser.evaluate("_cuprite.parents(node)", node: node) }.map do |parent|
|
|
17
|
+
self.class.new(driver, parent)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def property(name)
|
|
22
|
+
Cuprite.deprecation_warning("#{self.class}#property", "use node.native.node.property")
|
|
23
|
+
command { node.property(name) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def attributes
|
|
27
|
+
Cuprite.deprecation_warning("#{self.class}#attributes")
|
|
28
|
+
JSON.parse(command(:getAttributes))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def description
|
|
32
|
+
Cuprite.deprecation_warning("#{self.class}#description", "use node.native.node.description")
|
|
33
|
+
node.description
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def send_key(*keys)
|
|
37
|
+
Cuprite.deprecation_warning("#{self.class}#send_key", "use #send_keys")
|
|
38
|
+
send_keys(*keys)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Cuprite
|
|
5
|
+
class Node < Capybara::Driver::Node
|
|
6
|
+
module Extensions
|
|
7
|
+
def drag_by(x, y, steps: 1, delay: nil, scroll: true)
|
|
8
|
+
mouse = driver.browser.mouse
|
|
9
|
+
|
|
10
|
+
command do
|
|
11
|
+
x1, y1 = node.find_position
|
|
12
|
+
mouse.move(x: x1, y: y1)
|
|
13
|
+
mouse.down
|
|
14
|
+
|
|
15
|
+
sleep delay if delay
|
|
16
|
+
|
|
17
|
+
mouse.scroll_by(x, y) if scroll
|
|
18
|
+
|
|
19
|
+
x2, y2 = node.find_position
|
|
20
|
+
mouse.move(x: x2 + x, y: y2 + y, steps: steps)
|
|
21
|
+
mouse.up
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,45 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "capybara/cuprite/finder"
|
|
4
|
+
require "capybara/cuprite/node/deprecations"
|
|
5
|
+
require "capybara/cuprite/node/extensions"
|
|
4
6
|
|
|
5
7
|
module Capybara
|
|
6
8
|
module Cuprite
|
|
7
9
|
class Node < Capybara::Driver::Node
|
|
8
10
|
attr_reader :node
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
delegate %i[description] => :node
|
|
13
|
-
delegate %i[browser] => :driver
|
|
12
|
+
include Deprecations
|
|
13
|
+
include Extensions
|
|
14
14
|
|
|
15
15
|
DRAG_MODIFIER_ALIASES = { control: :ctrl, command: :meta, cmd: :meta }.freeze
|
|
16
|
+
DRAG_HTML5_JS = File.read(File.expand_path("javascripts/drag.js", __dir__))
|
|
17
|
+
TRIGGER_CLICK_WAIT = ENV.fetch("CUPRITE_TRIGGER_CLICK_WAIT", 0.1).to_f
|
|
16
18
|
|
|
17
19
|
def initialize(driver, node)
|
|
18
20
|
super(driver, self)
|
|
19
21
|
@node = node
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def command(name, *args)
|
|
23
|
-
raise ObsoleteNode.new(self, nil) unless node.evaluate("this.isConnected")
|
|
24
|
-
|
|
25
|
-
browser.send(name, node, *args)
|
|
26
|
-
rescue Ferrum::NodeNotFoundError => e
|
|
27
|
-
raise ObsoleteNode.new(self, e.response)
|
|
28
|
-
rescue Ferrum::BrowserError => e
|
|
29
|
-
case e.message
|
|
30
|
-
when "Cuprite.MouseEventFailed"
|
|
31
|
-
raise MouseEventFailed.new(self, e.response)
|
|
32
|
-
else
|
|
33
|
-
raise
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def parents
|
|
38
|
-
command(:parents).map do |parent|
|
|
39
|
-
self.class.new(driver, parent)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
24
|
def find_xpath(selector)
|
|
44
25
|
find(:xpath, selector)
|
|
45
26
|
end
|
|
@@ -48,26 +29,16 @@ module Capybara
|
|
|
48
29
|
find(:css, selector)
|
|
49
30
|
end
|
|
50
31
|
|
|
51
|
-
def find(method, selector)
|
|
52
|
-
command(:find_within, method, selector).map do |node|
|
|
53
|
-
self.class.new(driver, node)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
32
|
def all_text
|
|
58
|
-
filter_text(command
|
|
33
|
+
filter_text(command { node.text })
|
|
59
34
|
end
|
|
60
35
|
|
|
61
36
|
def visible_text
|
|
62
|
-
command(:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def property(name)
|
|
70
|
-
command(:property, name)
|
|
37
|
+
command(:visibleText).to_s
|
|
38
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
|
39
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
|
40
|
+
.gsub(/\n+/, "\n")
|
|
41
|
+
.tr("\u00a0", " ")
|
|
71
42
|
end
|
|
72
43
|
|
|
73
44
|
def [](name)
|
|
@@ -76,17 +47,13 @@ module Capybara
|
|
|
76
47
|
if (tag_name == "img" && name == "src") ||
|
|
77
48
|
(tag_name == "a" && name == "href")
|
|
78
49
|
# if attribute exists get the property
|
|
79
|
-
return command(:
|
|
50
|
+
return command(:getAttribute, name) && command { node.property(name) }
|
|
80
51
|
end
|
|
81
52
|
|
|
82
|
-
value = property(name)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def attributes
|
|
89
|
-
command(:attributes)
|
|
53
|
+
case (value = command { node.property(name) })
|
|
54
|
+
when String, Numeric, true, false then value
|
|
55
|
+
else command(:getAttribute, name)
|
|
56
|
+
end
|
|
90
57
|
end
|
|
91
58
|
|
|
92
59
|
def value
|
|
@@ -104,9 +71,9 @@ module Capybara
|
|
|
104
71
|
click if value != checked?
|
|
105
72
|
when "file"
|
|
106
73
|
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
|
|
107
|
-
command(
|
|
74
|
+
command { node.select_file(files) }
|
|
108
75
|
when "color"
|
|
109
|
-
node.evaluate("this.setAttribute('value',
|
|
76
|
+
node.evaluate("this.setAttribute('value', value)", value: value)
|
|
110
77
|
node.evaluate("this.dispatchEvent(new InputEvent('input'))")
|
|
111
78
|
node.evaluate("this.dispatchEvent(new Event('change', { bubbles: true }))")
|
|
112
79
|
when "date"
|
|
@@ -130,7 +97,7 @@ module Capybara
|
|
|
130
97
|
elsif tag_name == "textarea"
|
|
131
98
|
command(:set, value.to_s)
|
|
132
99
|
elsif self[:isContentEditable]
|
|
133
|
-
command(:
|
|
100
|
+
command(:deleteText)
|
|
134
101
|
click.type(value.to_s)
|
|
135
102
|
end
|
|
136
103
|
end
|
|
@@ -145,11 +112,11 @@ module Capybara
|
|
|
145
112
|
end
|
|
146
113
|
|
|
147
114
|
def tag_name
|
|
148
|
-
@tag_name ||= description["shadowRootType"] ? "ShadowRoot" : description["nodeName"].downcase
|
|
115
|
+
@tag_name ||= node.description["shadowRootType"] ? "ShadowRoot" : node.description["nodeName"].downcase
|
|
149
116
|
end
|
|
150
117
|
|
|
151
118
|
def visible?
|
|
152
|
-
command(:
|
|
119
|
+
command(:isVisible)
|
|
153
120
|
end
|
|
154
121
|
|
|
155
122
|
def checked?
|
|
@@ -161,7 +128,7 @@ module Capybara
|
|
|
161
128
|
end
|
|
162
129
|
|
|
163
130
|
def disabled?
|
|
164
|
-
command(:
|
|
131
|
+
command(:isDisabled)
|
|
165
132
|
end
|
|
166
133
|
|
|
167
134
|
def click(keys = [], **options)
|
|
@@ -177,7 +144,11 @@ module Capybara
|
|
|
177
144
|
end
|
|
178
145
|
|
|
179
146
|
def hover
|
|
180
|
-
command
|
|
147
|
+
command do
|
|
148
|
+
js(:scrollIntoViewport)
|
|
149
|
+
x, y = find_position
|
|
150
|
+
driver.browser.page.command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
|
151
|
+
end
|
|
181
152
|
end
|
|
182
153
|
|
|
183
154
|
def drag_to(other, **options)
|
|
@@ -188,20 +159,28 @@ module Capybara
|
|
|
188
159
|
options[:drop_offset] = drop_offset(*options.values_at(:x, :y, :offset))
|
|
189
160
|
modifiers = Array(options[:drop_modifiers]).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym) }
|
|
190
161
|
|
|
191
|
-
command(
|
|
162
|
+
command { drag(other.node, modifiers, options) }
|
|
192
163
|
end
|
|
193
164
|
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
165
|
+
def drop(*args)
|
|
166
|
+
command do
|
|
167
|
+
if args[0].is_a?(String)
|
|
168
|
+
driver.browser.execute("_cuprite.attachDropInput()")
|
|
169
|
+
driver.browser.at_css("#_cuprite_drop_file").select_file(args)
|
|
170
|
+
js(:dropFile)
|
|
171
|
+
else
|
|
172
|
+
strings = args.flat_map { |arg| arg.map { |type, data| { "type" => type, "data" => data } } }
|
|
173
|
+
node.evaluate("_cuprite.dropString(strings, this)", strings: strings)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
199
176
|
end
|
|
200
177
|
|
|
201
|
-
def drop(...) = command(:drop, ...)
|
|
202
|
-
|
|
203
178
|
def trigger(event)
|
|
204
|
-
command
|
|
179
|
+
command do
|
|
180
|
+
next js(:trigger, event.to_s) unless event.to_s == "click" && TRIGGER_CLICK_WAIT.positive?
|
|
181
|
+
|
|
182
|
+
driver.browser.wait_for_navigation!(TRIGGER_CLICK_WAIT) { js(:trigger, "click") }
|
|
183
|
+
end
|
|
205
184
|
end
|
|
206
185
|
|
|
207
186
|
def scroll_to(element, location, position = nil)
|
|
@@ -216,15 +195,7 @@ module Capybara
|
|
|
216
195
|
end
|
|
217
196
|
|
|
218
197
|
def scroll_by(x, y)
|
|
219
|
-
|
|
220
|
-
var el = arguments[0];
|
|
221
|
-
if (el.scrollBy){
|
|
222
|
-
el.scrollBy(arguments[1], arguments[2]);
|
|
223
|
-
} else {
|
|
224
|
-
el.scrollTop = el.scrollTop + arguments[2];
|
|
225
|
-
el.scrollLeft = el.scrollLeft + arguments[1];
|
|
226
|
-
}
|
|
227
|
-
JS
|
|
198
|
+
node.evaluate("this.scrollBy(x, y)", x: x, y: y)
|
|
228
199
|
end
|
|
229
200
|
|
|
230
201
|
def ==(other)
|
|
@@ -235,56 +206,114 @@ module Capybara
|
|
|
235
206
|
keys = keys.reject { |key| key.nil? || key == "" }
|
|
236
207
|
return if keys.empty?
|
|
237
208
|
|
|
238
|
-
command
|
|
209
|
+
command do
|
|
210
|
+
unless node.evaluate("_cuprite.containsSelection(this) || _cuprite.hasFocus(this)")
|
|
211
|
+
before_click("click")
|
|
212
|
+
node.click(mode: :left, keys: keys)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
driver.browser.keyboard.type(keys)
|
|
216
|
+
end
|
|
239
217
|
end
|
|
240
|
-
alias send_key send_keys
|
|
241
218
|
|
|
242
219
|
def path
|
|
243
220
|
command(:path)
|
|
244
221
|
end
|
|
245
222
|
|
|
246
223
|
def obscured?
|
|
247
|
-
command(:
|
|
224
|
+
command(:isObscured)
|
|
248
225
|
end
|
|
249
226
|
|
|
250
227
|
def shadow_root
|
|
251
|
-
root = driver.
|
|
252
|
-
|
|
253
|
-
JS
|
|
254
|
-
root && self.class.new(driver, root.node)
|
|
228
|
+
root = driver.browser.evaluate("node.shadowRoot", node: node)
|
|
229
|
+
root && self.class.new(driver, root)
|
|
255
230
|
end
|
|
256
231
|
|
|
257
232
|
def rect
|
|
258
|
-
|
|
259
|
-
arguments[0].getBoundingClientRect().toJSON()
|
|
260
|
-
JS
|
|
233
|
+
node.evaluate("this.getBoundingClientRect().toJSON()")
|
|
261
234
|
end
|
|
262
235
|
|
|
263
236
|
def inspect
|
|
264
237
|
%(#<#{self.class} @node=#{@node.inspect}>)
|
|
265
238
|
end
|
|
266
239
|
|
|
267
|
-
|
|
268
|
-
def to_json(*)
|
|
269
|
-
JSON.generate(as_json)
|
|
270
|
-
end
|
|
240
|
+
private
|
|
271
241
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
242
|
+
def find(method, selector)
|
|
243
|
+
command { Finder.find(driver.browser, method, selector, node) }.map do |node|
|
|
244
|
+
self.class.new(driver, node)
|
|
245
|
+
end
|
|
276
246
|
end
|
|
277
247
|
|
|
278
|
-
|
|
248
|
+
def command(function = nil, *args)
|
|
249
|
+
raise ObsoleteNode.new(self, nil) unless node.evaluate("this.isConnected")
|
|
250
|
+
|
|
251
|
+
block_given? ? yield : js(function, *args)
|
|
252
|
+
rescue Ferrum::NodeNotFoundError => e
|
|
253
|
+
raise ObsoleteNode.new(self, e.response)
|
|
254
|
+
end
|
|
279
255
|
|
|
280
256
|
def prepare_and_click(mode, name, keys, options)
|
|
281
257
|
delay = options[:delay].to_i
|
|
282
258
|
x, y = options.values_at(:x, :y)
|
|
283
259
|
offset = { x: x, y: y, position: options[:offset] || :top }
|
|
284
|
-
command(
|
|
260
|
+
command { before_click(name, offset) }
|
|
285
261
|
node.click(mode: mode, keys: keys, offset: offset, delay: delay)
|
|
286
262
|
end
|
|
287
263
|
|
|
264
|
+
def js(function, *args)
|
|
265
|
+
node.evaluate("_cuprite.#{function}(this, ...arguments)", *args)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def before_click(name, offset = {})
|
|
269
|
+
js(:scrollIntoViewport)
|
|
270
|
+
|
|
271
|
+
# If offset is given it may go outside of the element and likely error
|
|
272
|
+
# will be raised that we detected another element at this position.
|
|
273
|
+
return if offset[:x] || offset[:y]
|
|
274
|
+
|
|
275
|
+
x, y = find_position(**offset)
|
|
276
|
+
js(:mouseEventTest, name.to_s, x, y)
|
|
277
|
+
rescue Ferrum::JavaScriptError => e
|
|
278
|
+
raise MouseEventFailed, e.message if e.class_name == "MouseEventFailed"
|
|
279
|
+
|
|
280
|
+
raise
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def find_position(**options)
|
|
284
|
+
node.find_position(**options)
|
|
285
|
+
rescue Ferrum::BrowserError => e
|
|
286
|
+
raise MouseEventFailed, "MouseEventFailed: click, none, 0, 0" if e.message == "Could not compute content quads."
|
|
287
|
+
|
|
288
|
+
raise
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def drag(other, modifiers, options)
|
|
292
|
+
browser = driver.browser
|
|
293
|
+
delay, scroll, html5, drop_offset = options.values_at(:delay, :scroll, :html5, :drop_offset)
|
|
294
|
+
browser.execute("_cuprite.dragMousedownTracker()")
|
|
295
|
+
|
|
296
|
+
node.scroll_into_view if scroll
|
|
297
|
+
x1, y1 = node.find_position
|
|
298
|
+
|
|
299
|
+
browser.mouse.move(x: x1, y: y1)
|
|
300
|
+
browser.mouse.down
|
|
301
|
+
sleep delay if delay
|
|
302
|
+
|
|
303
|
+
html5 = !js(:legacyDragCheck) if html5.nil?
|
|
304
|
+
|
|
305
|
+
if html5
|
|
306
|
+
keys = modifiers.map(&:to_s)
|
|
307
|
+
driver.evaluate_async_script(DRAG_HTML5_JS, node, other, (delay || 0.05) * 1000, keys, drop_offset)
|
|
308
|
+
browser.mouse.up
|
|
309
|
+
else
|
|
310
|
+
other.scroll_into_view if scroll
|
|
311
|
+
x2, y2 = other.find_position(**drop_offset.to_h)
|
|
312
|
+
|
|
313
|
+
browser.keyboard.hold(*modifiers) { browser.mouse.move(x: x2, y: y2, steps: options[:steps]).up }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
288
317
|
def drop_offset(x, y, position)
|
|
289
318
|
return unless x
|
|
290
319
|
|
|
@@ -311,34 +340,19 @@ module Capybara
|
|
|
311
340
|
else
|
|
312
341
|
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
|
313
342
|
end
|
|
314
|
-
|
|
315
|
-
arguments[0].scrollIntoView(#{scroll_opts})
|
|
316
|
-
JS
|
|
343
|
+
element.node.evaluate("this.scrollIntoView(#{scroll_opts})")
|
|
317
344
|
end
|
|
318
345
|
|
|
319
346
|
def scroll_to_location(location)
|
|
320
347
|
height = { top: "0",
|
|
321
|
-
bottom: "
|
|
322
|
-
center: "(
|
|
348
|
+
bottom: "this.scrollHeight",
|
|
349
|
+
center: "(this.scrollHeight - this.clientHeight) / 2" }
|
|
323
350
|
|
|
324
|
-
|
|
325
|
-
if (arguments[0].scrollTo){
|
|
326
|
-
arguments[0].scrollTo(0, #{height[location]});
|
|
327
|
-
} else {
|
|
328
|
-
arguments[0].scrollTop = #{height[location]};
|
|
329
|
-
}
|
|
330
|
-
JS
|
|
351
|
+
node.evaluate("this.scrollTo(0, #{height[location]})")
|
|
331
352
|
end
|
|
332
353
|
|
|
333
354
|
def scroll_to_coords(x, y)
|
|
334
|
-
|
|
335
|
-
if (arguments[0].scrollTo){
|
|
336
|
-
arguments[0].scrollTo(arguments[1], arguments[2]);
|
|
337
|
-
} else {
|
|
338
|
-
arguments[0].scrollTop = arguments[2];
|
|
339
|
-
arguments[0].scrollLeft = arguments[1];
|
|
340
|
-
}
|
|
341
|
-
JS
|
|
355
|
+
node.evaluate("this.scrollTo(x, y)", x: x, y: y)
|
|
342
356
|
end
|
|
343
357
|
end
|
|
344
358
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Capybara
|
|
6
|
+
module Cuprite
|
|
7
|
+
class Page < Ferrum::Page
|
|
8
|
+
module Frames
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
delegate %i[at_css at_xpath css xpath
|
|
12
|
+
current_url current_title body execution_id execution_id!
|
|
13
|
+
evaluate execute] => :active_frame
|
|
14
|
+
|
|
15
|
+
def initialize(...)
|
|
16
|
+
@frame_stack = []
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def switch_to_frame(handle)
|
|
21
|
+
case handle
|
|
22
|
+
when :parent
|
|
23
|
+
@frame_stack.pop
|
|
24
|
+
when :top
|
|
25
|
+
@frame_stack = []
|
|
26
|
+
else
|
|
27
|
+
@frame_stack << handle
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def title
|
|
32
|
+
active_frame.current_title
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def active_frame
|
|
38
|
+
if @frame_stack.empty?
|
|
39
|
+
main_frame
|
|
40
|
+
else
|
|
41
|
+
@frames[@frame_stack.last]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Cuprite
|
|
5
|
+
class Page < Ferrum::Page
|
|
6
|
+
module Modals
|
|
7
|
+
MODAL_WAIT = ENV.fetch("CUPRITE_MODAL_WAIT", 0.05).to_f
|
|
8
|
+
|
|
9
|
+
def initialize(...)
|
|
10
|
+
@accept_modal = []
|
|
11
|
+
@modal_messages = []
|
|
12
|
+
@modal_response = nil
|
|
13
|
+
@unhandled_modal_error = nil
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The `Page.javascriptDialogOpening` event is handled on Ferrum's
|
|
18
|
+
# background CDP dispatcher thread, so raising there wouldn't reach
|
|
19
|
+
# the caller and would permanently kill that thread instead. The
|
|
20
|
+
# dialog is always accepted immediately from that thread, and the
|
|
21
|
+
# error (if any) is stashed here to be raised from the main thread
|
|
22
|
+
# the next time it makes a command round trip.
|
|
23
|
+
def command(...)
|
|
24
|
+
raise_pending_unhandled_modal_error! { super }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def accept_confirm
|
|
28
|
+
@accept_modal << true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def dismiss_confirm
|
|
32
|
+
@accept_modal << false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def accept_prompt(modal_response)
|
|
36
|
+
@accept_modal << true
|
|
37
|
+
@modal_response = modal_response
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def dismiss_prompt
|
|
41
|
+
@accept_modal << false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_modal(options)
|
|
45
|
+
start = Ferrum::Utils::ElapsedTime.monotonic_time
|
|
46
|
+
expect_text = options[:text]
|
|
47
|
+
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
|
|
48
|
+
not_found_msg = "Unable to find modal dialog"
|
|
49
|
+
not_found_msg += " with #{expect_text}" if expect_text
|
|
50
|
+
wait = options.fetch(:wait, timeout)
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
modal_text = @modal_messages.shift
|
|
54
|
+
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
|
|
55
|
+
rescue Capybara::ModalNotFound => e
|
|
56
|
+
raise e, not_found_msg if Ferrum::Utils::ElapsedTime.timeout?(start, wait)
|
|
57
|
+
|
|
58
|
+
sleep(MODAL_WAIT)
|
|
59
|
+
retry
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
modal_text
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def raise_pending_unhandled_modal_error!
|
|
68
|
+
error = @unhandled_modal_error
|
|
69
|
+
@unhandled_modal_error = nil
|
|
70
|
+
raise error if error
|
|
71
|
+
|
|
72
|
+
yield
|
|
73
|
+
ensure
|
|
74
|
+
error = @unhandled_modal_error
|
|
75
|
+
@unhandled_modal_error = nil
|
|
76
|
+
raise error if error
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def prepare_page
|
|
80
|
+
super
|
|
81
|
+
on("Page.javascriptDialogOpening") { |params| handle_javascript_dialog(params) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_javascript_dialog(params)
|
|
85
|
+
accept_modal = @accept_modal.last
|
|
86
|
+
unhandled_modal_error = nil
|
|
87
|
+
|
|
88
|
+
if [true, false].include?(accept_modal)
|
|
89
|
+
@accept_modal.pop
|
|
90
|
+
@modal_messages << params["message"]
|
|
91
|
+
options = { accept: accept_modal }
|
|
92
|
+
response = @modal_response || params["defaultPrompt"]
|
|
93
|
+
else
|
|
94
|
+
with_text = params["message"] ? "with text `#{params['message']}` " : ""
|
|
95
|
+
message = "Modal window #{with_text}has been opened, but you didn't wrap " \
|
|
96
|
+
"your code into (`accept_prompt` | `dismiss_prompt` | " \
|
|
97
|
+
"`accept_confirm` | `dismiss_confirm` | `accept_alert`), " \
|
|
98
|
+
"accepting by default"
|
|
99
|
+
|
|
100
|
+
if @options.raise_on_unhandled_modal
|
|
101
|
+
unhandled_modal_error = UnhandledModalError.new(message)
|
|
102
|
+
else
|
|
103
|
+
warn message
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
options = { accept: true }
|
|
107
|
+
response = params["defaultPrompt"]
|
|
108
|
+
end
|
|
109
|
+
options.merge!(promptText: response) if response
|
|
110
|
+
# Skips #command so a pending error isn't raised on the dispatcher thread.
|
|
111
|
+
client.command("Page.handleJavaScriptDialog", timeout: timeout, **options)
|
|
112
|
+
|
|
113
|
+
@unhandled_modal_error = unhandled_modal_error if unhandled_modal_error
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|