cuprite 0.18 → 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 +69 -37
- data/lib/capybara/cuprite/browser/deprecations.rb +26 -0
- data/lib/capybara/cuprite/browser/extensions.rb +59 -0
- data/lib/capybara/cuprite/browser.rb +28 -231
- data/lib/capybara/cuprite/deprecation.rb +13 -0
- data/lib/capybara/cuprite/devices.rb +188 -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 +68 -201
- data/lib/capybara/cuprite/finder.rb +22 -0
- data/lib/capybara/cuprite/javascripts/drag.js +100 -103
- data/lib/capybara/cuprite/javascripts/index.js +511 -489
- data/lib/capybara/cuprite/node/deprecations.rb +43 -0
- data/lib/capybara/cuprite/node/extensions.rb +27 -0
- data/lib/capybara/cuprite/node.rb +140 -116
- data/lib/capybara/cuprite/options.rb +19 -0
- data/lib/capybara/cuprite/page/frames.rb +47 -0
- data/lib/capybara/cuprite/page/modals.rb +118 -0
- data/lib/capybara/cuprite/page.rb +9 -207
- data/lib/capybara/cuprite/version.rb +1 -1
- data/lib/capybara/cuprite.rb +1 -0
- metadata +16 -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,28 +144,43 @@ 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)
|
|
155
|
+
raise ArgumentError, "You must specify both x: and y: for a drop offset" if options[:x].nil? ^ options[:y].nil?
|
|
156
|
+
|
|
184
157
|
options[:steps] ||= 1
|
|
185
158
|
options[:scroll] = true unless options.key?(:scroll)
|
|
159
|
+
options[:drop_offset] = drop_offset(*options.values_at(:x, :y, :offset))
|
|
186
160
|
modifiers = Array(options[:drop_modifiers]).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym) }
|
|
187
161
|
|
|
188
|
-
command(
|
|
162
|
+
command { drag(other.node, modifiers, options) }
|
|
189
163
|
end
|
|
190
164
|
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
196
176
|
end
|
|
197
177
|
|
|
198
|
-
def drop(...) = command(:drop, ...)
|
|
199
|
-
|
|
200
178
|
def trigger(event)
|
|
201
|
-
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
|
|
202
184
|
end
|
|
203
185
|
|
|
204
186
|
def scroll_to(element, location, position = nil)
|
|
@@ -213,15 +195,7 @@ module Capybara
|
|
|
213
195
|
end
|
|
214
196
|
|
|
215
197
|
def scroll_by(x, y)
|
|
216
|
-
|
|
217
|
-
var el = arguments[0];
|
|
218
|
-
if (el.scrollBy){
|
|
219
|
-
el.scrollBy(arguments[1], arguments[2]);
|
|
220
|
-
} else {
|
|
221
|
-
el.scrollTop = el.scrollTop + arguments[2];
|
|
222
|
-
el.scrollLeft = el.scrollLeft + arguments[1];
|
|
223
|
-
}
|
|
224
|
-
JS
|
|
198
|
+
node.evaluate("this.scrollBy(x, y)", x: x, y: y)
|
|
225
199
|
end
|
|
226
200
|
|
|
227
201
|
def ==(other)
|
|
@@ -232,56 +206,121 @@ module Capybara
|
|
|
232
206
|
keys = keys.reject { |key| key.nil? || key == "" }
|
|
233
207
|
return if keys.empty?
|
|
234
208
|
|
|
235
|
-
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
|
|
236
217
|
end
|
|
237
|
-
alias send_key send_keys
|
|
238
218
|
|
|
239
219
|
def path
|
|
240
220
|
command(:path)
|
|
241
221
|
end
|
|
242
222
|
|
|
243
223
|
def obscured?
|
|
244
|
-
command(:
|
|
224
|
+
command(:isObscured)
|
|
245
225
|
end
|
|
246
226
|
|
|
247
227
|
def shadow_root
|
|
248
|
-
root = driver.
|
|
249
|
-
|
|
250
|
-
JS
|
|
251
|
-
root && self.class.new(driver, root.node)
|
|
228
|
+
root = driver.browser.evaluate("node.shadowRoot", node: node)
|
|
229
|
+
root && self.class.new(driver, root)
|
|
252
230
|
end
|
|
253
231
|
|
|
254
232
|
def rect
|
|
255
|
-
|
|
256
|
-
arguments[0].getBoundingClientRect().toJSON()
|
|
257
|
-
JS
|
|
233
|
+
node.evaluate("this.getBoundingClientRect().toJSON()")
|
|
258
234
|
end
|
|
259
235
|
|
|
260
236
|
def inspect
|
|
261
237
|
%(#<#{self.class} @node=#{@node.inspect}>)
|
|
262
238
|
end
|
|
263
239
|
|
|
264
|
-
|
|
265
|
-
def to_json(*)
|
|
266
|
-
JSON.generate(as_json)
|
|
267
|
-
end
|
|
240
|
+
private
|
|
268
241
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
273
246
|
end
|
|
274
247
|
|
|
275
|
-
|
|
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
|
|
276
255
|
|
|
277
256
|
def prepare_and_click(mode, name, keys, options)
|
|
278
257
|
delay = options[:delay].to_i
|
|
279
258
|
x, y = options.values_at(:x, :y)
|
|
280
259
|
offset = { x: x, y: y, position: options[:offset] || :top }
|
|
281
|
-
command(
|
|
260
|
+
command { before_click(name, offset) }
|
|
282
261
|
node.click(mode: mode, keys: keys, offset: offset, delay: delay)
|
|
283
262
|
end
|
|
284
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
|
+
|
|
317
|
+
def drop_offset(x, y, position)
|
|
318
|
+
return unless x
|
|
319
|
+
|
|
320
|
+
position ||= driver.session_options.w3c_click_offset ? :center : :top
|
|
321
|
+
{ x: x, y: y, position: position.to_sym }
|
|
322
|
+
end
|
|
323
|
+
|
|
285
324
|
def filter_text(text)
|
|
286
325
|
text.gsub(/[\u200b\u200e\u200f]/, "")
|
|
287
326
|
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, " ")
|
|
@@ -301,34 +340,19 @@ module Capybara
|
|
|
301
340
|
else
|
|
302
341
|
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
|
303
342
|
end
|
|
304
|
-
|
|
305
|
-
arguments[0].scrollIntoView(#{scroll_opts})
|
|
306
|
-
JS
|
|
343
|
+
element.node.evaluate("this.scrollIntoView(#{scroll_opts})")
|
|
307
344
|
end
|
|
308
345
|
|
|
309
346
|
def scroll_to_location(location)
|
|
310
347
|
height = { top: "0",
|
|
311
|
-
bottom: "
|
|
312
|
-
center: "(
|
|
348
|
+
bottom: "this.scrollHeight",
|
|
349
|
+
center: "(this.scrollHeight - this.clientHeight) / 2" }
|
|
313
350
|
|
|
314
|
-
|
|
315
|
-
if (arguments[0].scrollTo){
|
|
316
|
-
arguments[0].scrollTo(0, #{height[location]});
|
|
317
|
-
} else {
|
|
318
|
-
arguments[0].scrollTop = #{height[location]};
|
|
319
|
-
}
|
|
320
|
-
JS
|
|
351
|
+
node.evaluate("this.scrollTo(0, #{height[location]})")
|
|
321
352
|
end
|
|
322
353
|
|
|
323
354
|
def scroll_to_coords(x, y)
|
|
324
|
-
|
|
325
|
-
if (arguments[0].scrollTo){
|
|
326
|
-
arguments[0].scrollTo(arguments[1], arguments[2]);
|
|
327
|
-
} else {
|
|
328
|
-
arguments[0].scrollTop = arguments[2];
|
|
329
|
-
arguments[0].scrollLeft = arguments[1];
|
|
330
|
-
}
|
|
331
|
-
JS
|
|
355
|
+
node.evaluate("this.scrollTo(x, y)", x: x, y: y)
|
|
332
356
|
end
|
|
333
357
|
end
|
|
334
358
|
end
|
|
@@ -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, nil)
|
|
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
|
|
@@ -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
|