cuprite 0.17 → 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 +23 -2
- data/lib/capybara/cuprite/browser.rb +49 -4
- data/lib/capybara/cuprite/driver.rb +8 -2
- data/lib/capybara/cuprite/errors.rb +9 -0
- data/lib/capybara/cuprite/javascripts/drag.js +118 -0
- data/lib/capybara/cuprite/javascripts/index.js +95 -16
- data/lib/capybara/cuprite/node.rb +43 -3
- data/lib/capybara/cuprite/options.rb +1 -1
- data/lib/capybara/cuprite/page.rb +46 -5
- data/lib/capybara/cuprite/version.rb +1 -1
- metadata +6 -5
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,6 +76,12 @@ 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
|
|
|
@@ -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,18 +151,39 @@ 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
164
|
|
|
145
165
|
mouse.move(x: x1, y: y1)
|
|
146
166
|
mouse.down
|
|
147
167
|
sleep delay if delay
|
|
148
168
|
|
|
149
|
-
|
|
169
|
+
html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil?
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
if html5
|
|
172
|
+
drag_html5(node, other, drop_modifiers, delay)
|
|
173
|
+
else
|
|
174
|
+
modifiers = keyboard.modifiers(drop_modifiers)
|
|
153
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)
|
|
154
187
|
mouse.up
|
|
155
188
|
end
|
|
156
189
|
|
|
@@ -169,6 +202,18 @@ module Capybara
|
|
|
169
202
|
mouse.up
|
|
170
203
|
end
|
|
171
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
|
+
|
|
172
217
|
def select_file(node, value)
|
|
173
218
|
node.select_file(value)
|
|
174
219
|
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
|
|
@@ -406,4 +413,3 @@ module Capybara
|
|
|
406
413
|
end
|
|
407
414
|
end
|
|
408
415
|
end
|
|
409
|
-
# 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) {
|
|
@@ -153,15 +174,13 @@ class Cuprite {
|
|
|
153
174
|
|
|
154
175
|
let valueBefore = node.value;
|
|
155
176
|
|
|
177
|
+
node.focus();
|
|
156
178
|
this.trigger(node, "focus");
|
|
157
179
|
this.setValue(node, "");
|
|
158
180
|
|
|
159
|
-
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") {
|
|
160
182
|
this.setValue(node, value);
|
|
161
183
|
this.input(node);
|
|
162
|
-
} else if (node.type == "time") {
|
|
163
|
-
this.setValue(node, new Date(value).toTimeString().split(" ")[0]);
|
|
164
|
-
this.input(node);
|
|
165
184
|
} else if (node.type == "datetime-local") {
|
|
166
185
|
value = new Date(value);
|
|
167
186
|
let year = value.getFullYear();
|
|
@@ -309,7 +328,7 @@ class Cuprite {
|
|
|
309
328
|
x -= frameOffset.left;
|
|
310
329
|
y -= frameOffset.top;
|
|
311
330
|
|
|
312
|
-
let element =
|
|
331
|
+
let element = node.getRootNode().elementFromPoint(x, y);
|
|
313
332
|
|
|
314
333
|
let el = element;
|
|
315
334
|
while (el) {
|
|
@@ -445,7 +464,7 @@ class Cuprite {
|
|
|
445
464
|
options["button"] || 0, null
|
|
446
465
|
)
|
|
447
466
|
} else if (EVENTS.FOCUS.indexOf(name) != -1) {
|
|
448
|
-
event =
|
|
467
|
+
event = new FocusEvent(name, { bubbles: true, cancelable: true });
|
|
449
468
|
} else if (EVENTS.FORM.indexOf(name) != -1) {
|
|
450
469
|
event = this.obtainEvent(name);
|
|
451
470
|
} else {
|
|
@@ -518,6 +537,66 @@ class Cuprite {
|
|
|
518
537
|
return node.contains(selectedNode);
|
|
519
538
|
}
|
|
520
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
|
+
|
|
521
600
|
// This command is purely for testing error handling
|
|
522
601
|
browserError() {
|
|
523
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
|
|
@@ -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?
|
|
@@ -162,8 +183,9 @@ module Capybara
|
|
|
162
183
|
def drag_to(other, **options)
|
|
163
184
|
options[:steps] ||= 1
|
|
164
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) }
|
|
165
187
|
|
|
166
|
-
command(:drag, other.node, options
|
|
188
|
+
command(:drag, other.node, modifiers, options.slice(:steps, :delay, :scroll, :html5))
|
|
167
189
|
end
|
|
168
190
|
|
|
169
191
|
def drag_by(x, y, **options)
|
|
@@ -173,6 +195,8 @@ module Capybara
|
|
|
173
195
|
command(:drag_by, x, y, options[:steps], options[:delay], options[:scroll])
|
|
174
196
|
end
|
|
175
197
|
|
|
198
|
+
def drop(...) = command(:drop, ...)
|
|
199
|
+
|
|
176
200
|
def trigger(event)
|
|
177
201
|
command(:trigger, event)
|
|
178
202
|
end
|
|
@@ -205,6 +229,9 @@ module Capybara
|
|
|
205
229
|
end
|
|
206
230
|
|
|
207
231
|
def send_keys(*keys)
|
|
232
|
+
keys = keys.reject { |key| key.nil? || key == "" }
|
|
233
|
+
return if keys.empty?
|
|
234
|
+
|
|
208
235
|
command(:send_keys, keys)
|
|
209
236
|
end
|
|
210
237
|
alias send_key send_keys
|
|
@@ -217,6 +244,19 @@ module Capybara
|
|
|
217
244
|
command(:obscured?)
|
|
218
245
|
end
|
|
219
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
|
+
|
|
220
260
|
def inspect
|
|
221
261
|
%(#<#{self.class} @node=#{@node.inspect}>)
|
|
222
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 = {})
|
|
@@ -140,6 +158,18 @@ module Capybara
|
|
|
140
158
|
|
|
141
159
|
private
|
|
142
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
|
+
|
|
143
173
|
def prepare_page
|
|
144
174
|
super
|
|
145
175
|
|
|
@@ -154,6 +184,8 @@ module Capybara
|
|
|
154
184
|
|
|
155
185
|
on("Page.javascriptDialogOpening") do |params|
|
|
156
186
|
accept_modal = @accept_modal.last
|
|
187
|
+
unhandled_modal_error = nil
|
|
188
|
+
|
|
157
189
|
if [true, false].include?(accept_modal)
|
|
158
190
|
@accept_modal.pop
|
|
159
191
|
@modal_messages << params["message"]
|
|
@@ -161,15 +193,24 @@ module Capybara
|
|
|
161
193
|
response = @modal_response || params["defaultPrompt"]
|
|
162
194
|
else
|
|
163
195
|
with_text = params["message"] ? "with text `#{params['message']}` " : ""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
|
|
168
207
|
options = { accept: true }
|
|
169
208
|
response = params["defaultPrompt"]
|
|
170
209
|
end
|
|
171
210
|
options.merge!(promptText: response) if response
|
|
172
|
-
|
|
211
|
+
ferrum_command("Page.handleJavaScriptDialog", **options)
|
|
212
|
+
|
|
213
|
+
@unhandled_modal_error = unhandled_modal_error if unhandled_modal_error
|
|
173
214
|
end
|
|
174
215
|
end
|
|
175
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,7 +76,7 @@ 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
|
- - ">="
|