capybara 3.19.0 → 3.20.0
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/History.md +27 -2
- data/README.md +1 -1
- data/lib/capybara/driver/node.rb +4 -0
- data/lib/capybara/node/actions.rb +3 -2
- data/lib/capybara/node/element.rb +11 -0
- data/lib/capybara/node/finders.rb +4 -1
- data/lib/capybara/queries/selector_query.rb +19 -5
- data/lib/capybara/selector/definition/label.rb +27 -10
- data/lib/capybara/selector/definition/link.rb +3 -2
- data/lib/capybara/selector.rb +2 -2
- data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
- data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
- data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
- data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
- data/lib/capybara/selenium/driver.rb +15 -2
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -1
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +1 -1
- data/lib/capybara/selenium/driver_specializations/safari_driver.rb +1 -1
- data/lib/capybara/selenium/node.rb +25 -1
- data/lib/capybara/selenium/nodes/safari_node.rb +19 -6
- data/lib/capybara/selenium/patches/atoms.rb +18 -0
- data/lib/capybara/spec/session/all_spec.rb +23 -0
- data/lib/capybara/spec/session/click_link_spec.rb +11 -0
- data/lib/capybara/spec/session/node_spec.rb +78 -5
- data/lib/capybara/spec/session/selectors_spec.rb +8 -0
- data/lib/capybara/spec/views/animated.erb +49 -0
- data/lib/capybara/spec/views/frame_one.erb +1 -0
- data/lib/capybara/spec/views/obscured.erb +9 -9
- data/lib/capybara/version.rb +1 -1
- data/spec/sauce_spec_chrome.rb +1 -0
- data/spec/selenium_spec_chrome.rb +17 -2
- data/spec/selenium_spec_chrome_remote.rb +4 -2
- data/spec/selenium_spec_edge.rb +4 -2
- data/spec/selenium_spec_firefox.rb +4 -11
- data/spec/selenium_spec_firefox_remote.rb +4 -2
- data/spec/selenium_spec_ie.rb +5 -6
- data/spec/selenium_spec_safari.rb +7 -12
- data/spec/server_spec.rb +4 -2
- data/spec/shared_selenium_node.rb +29 -0
- metadata +23 -2
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
(function(){
|
|
2
|
+
var OverflowState = {
|
|
3
|
+
NONE: "none",
|
|
4
|
+
HIDDEN: "hidden",
|
|
5
|
+
SCROLL: "scroll"
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function isShown_(elem, ignoreOpacity, parentsDisplayedFn) {
|
|
9
|
+
// By convention, BODY element is always shown: BODY represents the document
|
|
10
|
+
// and even if there's nothing rendered in there, user can always see there's
|
|
11
|
+
// the document.
|
|
12
|
+
var elemTagName = elem.tagName.toUpperCase();
|
|
13
|
+
if (elemTagName == "BODY") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Child of DETAILS element is not shown unless the DETAILS element is open
|
|
18
|
+
// or the child is a SUMMARY element.
|
|
19
|
+
|
|
20
|
+
var parent = getParentElement(elem);
|
|
21
|
+
if (parent && parent.tagName && (parent.tagName.toUpperCase() == "DETAILS") &&
|
|
22
|
+
!parent.open && !(elemTagName == "SUMMARY")) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Option or optgroup is shown if enclosing select is shown (ignoring the
|
|
27
|
+
// select's opacity).
|
|
28
|
+
if ((elemTagName == "OPTION") ||
|
|
29
|
+
(elemTagName == "OPTGROUP")) {
|
|
30
|
+
var select = getAncestor(elem, function(e) {
|
|
31
|
+
return e.tagName.toUpperCase() == "SELECT";
|
|
32
|
+
});
|
|
33
|
+
return !!select && isShown_(select, true, parentsDisplayedFn);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Image map elements are shown if image that uses it is shown, and
|
|
37
|
+
// the area of the element is positive.
|
|
38
|
+
var imageMap = maybeFindImageMap_(elem);
|
|
39
|
+
if (imageMap) {
|
|
40
|
+
return !!imageMap.image &&
|
|
41
|
+
imageMap.rect.width > 0 && imageMap.rect.height > 0 &&
|
|
42
|
+
isShown_(imageMap.image, ignoreOpacity, parentsDisplayedFn);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Any hidden input is not shown.
|
|
46
|
+
if ((elemTagName == "INPUT") && (elem.type.toLowerCase() == "hidden")) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Any NOSCRIPT element is not shown.
|
|
51
|
+
if (elemTagName == "NOSCRIPT") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Any element with hidden/collapsed visibility is not shown.
|
|
56
|
+
var visibility = window.getComputedStyle(elem)["visibility"];
|
|
57
|
+
if (visibility == "collapse" || visibility == "hidden") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!parentsDisplayedFn(elem)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Any transparent element is not shown.
|
|
66
|
+
if (!ignoreOpacity && getOpacity(elem) == 0) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Any element without positive size dimensions is not shown.
|
|
71
|
+
function positiveSize(e) {
|
|
72
|
+
var rect = getClientRect(e);
|
|
73
|
+
if (rect.height > 0 && rect.width > 0) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// A vertical or horizontal SVG Path element will report zero width or
|
|
77
|
+
// height but is "shown" if it has a positive stroke-width.
|
|
78
|
+
if ((e.tagName.toUpperCase() == "PATH") && (rect.height > 0 || rect.width > 0)) {
|
|
79
|
+
var strokeWidth = window.getComputedStyle(e)["stroke-width"];
|
|
80
|
+
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
|
|
81
|
+
}
|
|
82
|
+
// Zero-sized elements should still be considered to have positive size
|
|
83
|
+
// if they have a child element or text node with positive size, unless
|
|
84
|
+
// the element has an 'overflow' style of "hidden".
|
|
85
|
+
return window.getComputedStyle(e)["overflow"] != "hidden" &&
|
|
86
|
+
Array.prototype.slice.call(e.childNodes).some(function(n) {
|
|
87
|
+
return (n.nodeType == Node.TEXT_NODE) ||
|
|
88
|
+
((n.nodeType == Node.ELEMENT_NODE) && positiveSize(n));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!positiveSize(elem)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Elements that are hidden by overflow are not shown.
|
|
97
|
+
function hiddenByOverflow(e) {
|
|
98
|
+
return getOverflowState(e) == OverflowState.HIDDEN &&
|
|
99
|
+
Array.prototype.slice.call(e.childNodes).every(function(n) {
|
|
100
|
+
return (n.nodeType != Node.ELEMENT_NODE) || hiddenByOverflow(n) ||
|
|
101
|
+
!positiveSize(n);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return !hiddenByOverflow(elem);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getClientRegion(elem) {
|
|
108
|
+
var region = getClientRect(elem);
|
|
109
|
+
return { left: region.left,
|
|
110
|
+
right: region.left + region.width,
|
|
111
|
+
top: region.top,
|
|
112
|
+
bottom: region.top + region.height };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getParentElement(node) {
|
|
116
|
+
return node.parentElement
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getOverflowState(elem) {
|
|
120
|
+
var region = getClientRegion(elem);
|
|
121
|
+
var ownerDoc = elem.ownerDocument;
|
|
122
|
+
var htmlElem = ownerDoc.documentElement;
|
|
123
|
+
var bodyElem = ownerDoc.body;
|
|
124
|
+
var htmlOverflowStyle = window.getComputedStyle(htmlElem)["overflow"];
|
|
125
|
+
var treatAsFixedPosition;
|
|
126
|
+
|
|
127
|
+
// Return the closest ancestor that the given element may overflow.
|
|
128
|
+
function getOverflowParent(e) {
|
|
129
|
+
function canBeOverflowed(container) {
|
|
130
|
+
// The HTML element can always be overflowed.
|
|
131
|
+
if (container == htmlElem) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
var containerStyle = window.getComputedStyle(container);
|
|
135
|
+
// An element cannot overflow an element with an inline or contents display style.
|
|
136
|
+
var containerDisplay = containerStyle["display"];
|
|
137
|
+
if ((containerDisplay.indexOf("inline") == 0) ||
|
|
138
|
+
(containerDisplay == "contents")) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// An absolute-positioned element cannot overflow a static-positioned one.
|
|
142
|
+
if ((position == "absolute") && (containerStyle["position"] == "static")) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var position = window.getComputedStyle(e)["position"];
|
|
149
|
+
if (position == "fixed") {
|
|
150
|
+
treatAsFixedPosition = true;
|
|
151
|
+
// Fixed-position element may only overflow the viewport.
|
|
152
|
+
return e == htmlElem ? null : htmlElem;
|
|
153
|
+
} else {
|
|
154
|
+
var parent = getParentElement(e);
|
|
155
|
+
while (parent && !canBeOverflowed(parent)) {
|
|
156
|
+
parent = getParentElement(parent);
|
|
157
|
+
}
|
|
158
|
+
return parent;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Return the x and y overflow styles for the given element.
|
|
163
|
+
function getOverflowStyles(e) {
|
|
164
|
+
// When the <html> element has an overflow style of 'visible', it assumes
|
|
165
|
+
// the overflow style of the body, and the body is really overflow:visible.
|
|
166
|
+
var overflowElem = e;
|
|
167
|
+
if (htmlOverflowStyle == "visible") {
|
|
168
|
+
// Note: bodyElem will be null/undefined in SVG documents.
|
|
169
|
+
if (e == htmlElem && bodyElem) {
|
|
170
|
+
overflowElem = bodyElem;
|
|
171
|
+
} else if (e == bodyElem) {
|
|
172
|
+
return {x: "visible", y: "visible"};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var overflowElemStyle = window.getComputedStyle(overflowElem);
|
|
176
|
+
var overflow = {
|
|
177
|
+
x: overflowElemStyle["overflow-x"],
|
|
178
|
+
y: overflowElemStyle["overflow-y"]
|
|
179
|
+
};
|
|
180
|
+
// The <html> element cannot have a genuine 'visible' overflow style,
|
|
181
|
+
// because the viewport can't expand; 'visible' is really 'auto'.
|
|
182
|
+
if (e == htmlElem) {
|
|
183
|
+
overflow.x = overflow.x == "visible" ? "auto" : overflow.x;
|
|
184
|
+
overflow.y = overflow.y == "visible" ? "auto" : overflow.y;
|
|
185
|
+
}
|
|
186
|
+
return overflow;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Returns the scroll offset of the given element.
|
|
190
|
+
function getScroll(e) {
|
|
191
|
+
if (e == htmlElem) {
|
|
192
|
+
return { x: window.scrollX, y: window.scrollY }
|
|
193
|
+
}
|
|
194
|
+
return { x: e.scrollLeft, y: e.scrollTop }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if the element overflows any ancestor element.
|
|
198
|
+
for (var container = getOverflowParent(elem);
|
|
199
|
+
!!container;
|
|
200
|
+
container = getOverflowParent(container)) {
|
|
201
|
+
var containerOverflow = getOverflowStyles(container);
|
|
202
|
+
|
|
203
|
+
// If the container has overflow:visible, the element cannot overflow it.
|
|
204
|
+
if (containerOverflow.x == "visible" && containerOverflow.y == "visible") {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var containerRect = getClientRect(container);
|
|
209
|
+
|
|
210
|
+
// Zero-sized containers without overflow:visible hide all descendants.
|
|
211
|
+
if (containerRect.width == 0 || containerRect.height == 0) {
|
|
212
|
+
return OverflowState.HIDDEN;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check "underflow": if an element is to the left or above the container
|
|
216
|
+
var underflowsX = region.right < containerRect.left;
|
|
217
|
+
var underflowsY = region.bottom < containerRect.top;
|
|
218
|
+
if ((underflowsX && containerOverflow.x == "hidden") ||
|
|
219
|
+
(underflowsY && containerOverflow.y == "hidden")) {
|
|
220
|
+
return OverflowState.HIDDEN;
|
|
221
|
+
} else if ((underflowsX && containerOverflow.x != "visible") ||
|
|
222
|
+
(underflowsY && containerOverflow.y != "visible")) {
|
|
223
|
+
// When the element is positioned to the left or above a container, we
|
|
224
|
+
// have to distinguish between the element being completely outside the
|
|
225
|
+
// container and merely scrolled out of view within the container.
|
|
226
|
+
var containerScroll = getScroll(container);
|
|
227
|
+
var unscrollableX = region.right < containerRect.left - containerScroll.x;
|
|
228
|
+
var unscrollableY = region.bottom < containerRect.top - containerScroll.y;
|
|
229
|
+
if ((unscrollableX && containerOverflow.x != "visible") ||
|
|
230
|
+
(unscrollableY && containerOverflow.x != "visible")) {
|
|
231
|
+
return OverflowState.HIDDEN;
|
|
232
|
+
}
|
|
233
|
+
var containerState = getOverflowState(container);
|
|
234
|
+
return containerState == OverflowState.HIDDEN ?
|
|
235
|
+
OverflowState.HIDDEN : OverflowState.SCROLL;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check "overflow": if an element is to the right or below a container
|
|
239
|
+
var overflowsX = region.left >= containerRect.left + containerRect.width;
|
|
240
|
+
var overflowsY = region.top >= containerRect.top + containerRect.height;
|
|
241
|
+
if ((overflowsX && containerOverflow.x == "hidden") ||
|
|
242
|
+
(overflowsY && containerOverflow.y == "hidden")) {
|
|
243
|
+
return OverflowState.HIDDEN;
|
|
244
|
+
} else if ((overflowsX && containerOverflow.x != "visible") ||
|
|
245
|
+
(overflowsY && containerOverflow.y != "visible")) {
|
|
246
|
+
// If the element has fixed position and falls outside the scrollable area
|
|
247
|
+
// of the document, then it is hidden.
|
|
248
|
+
if (treatAsFixedPosition) {
|
|
249
|
+
var docScroll = getScroll(container);
|
|
250
|
+
if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||
|
|
251
|
+
(region.right >= htmlElem.scrollHeight - docScroll.y)) {
|
|
252
|
+
return OverflowState.HIDDEN;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// If the element can be scrolled into view of the parent, it has a scroll
|
|
256
|
+
// state; unless the parent itself is entirely hidden by overflow, in
|
|
257
|
+
// which it is also hidden by overflow.
|
|
258
|
+
var containerState = getOverflowState(container);
|
|
259
|
+
return containerState == OverflowState.HIDDEN ?
|
|
260
|
+
OverflowState.HIDDEN : OverflowState.SCROLL;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Does not overflow any ancestor.
|
|
265
|
+
return OverflowState.NONE;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getViewportSize(win) {
|
|
269
|
+
var el = win.document.documentElement;
|
|
270
|
+
return { width: el.clientWidth, height: el.clientHeight };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function rect_(x, y, w, h){
|
|
274
|
+
return { left: x, top: y, width: w, height: h };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getClientRect(elem) {
|
|
278
|
+
var imageMap = maybeFindImageMap_(elem);
|
|
279
|
+
if (imageMap) {
|
|
280
|
+
return imageMap.rect;
|
|
281
|
+
} else if (elem.tagName.toUpperCase() == "HTML") {
|
|
282
|
+
// Define the client rect of the <html> element to be the viewport.
|
|
283
|
+
var doc = elem.ownerDocument;
|
|
284
|
+
// TODO: Is this too simplified???
|
|
285
|
+
var viewportSize = getViewportSize(window);
|
|
286
|
+
return rect_(0, 0, viewportSize.width, viewportSize.height);
|
|
287
|
+
} else {
|
|
288
|
+
var nativeRect;
|
|
289
|
+
try {
|
|
290
|
+
nativeRect = elem.getBoundingClientRect();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return rect_(0, 0, 0, 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return rect_(nativeRect.left, nativeRect.top,
|
|
296
|
+
nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getOpacity(elem) {
|
|
301
|
+
// By default the element is opaque.
|
|
302
|
+
var elemOpacity = 1;
|
|
303
|
+
|
|
304
|
+
var opacityStyle = window.getComputedStyle(elem)["opacity"];
|
|
305
|
+
if (opacityStyle) {
|
|
306
|
+
elemOpacity = Number(opacityStyle);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Let's apply the parent opacity to the element.
|
|
310
|
+
var parentElement = getParentElement(elem);
|
|
311
|
+
if (parentElement && parentElement.nodeType == Node.ELEMENT_NODE) {
|
|
312
|
+
elemOpacity = elemOpacity * getOpacity(parentElement);
|
|
313
|
+
}
|
|
314
|
+
return elemOpacity;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getAreaRelativeRect_(area) {
|
|
318
|
+
var shape = area.shape.toLowerCase();
|
|
319
|
+
var coords = area.coords.split(",");
|
|
320
|
+
if (shape == "rect" && coords.length == 4) {
|
|
321
|
+
var x = coords[0], y = coords[1];
|
|
322
|
+
return rect_(x, y, coords[2] - x, coords[3] - y);
|
|
323
|
+
} else if (shape == "circle" && coords.length == 3) {
|
|
324
|
+
var centerX = coords[0], centerY = coords[1], radius = coords[2];
|
|
325
|
+
return rect_(centerX - radius, centerY - radius, 2 * radius, 2 * radius);
|
|
326
|
+
} else if (shape == "poly" && coords.length > 2) {
|
|
327
|
+
var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;
|
|
328
|
+
for (var i = 2; i + 1 < coords.length; i += 2) {
|
|
329
|
+
minX = Math.min(minX, coords[i]);
|
|
330
|
+
maxX = Math.max(maxX, coords[i]);
|
|
331
|
+
minY = Math.min(minY, coords[i + 1]);
|
|
332
|
+
maxY = Math.max(maxY, coords[i + 1]);
|
|
333
|
+
}
|
|
334
|
+
return rect_(minX, minY, maxX - minX, maxY - minY);
|
|
335
|
+
}
|
|
336
|
+
return rect_(0, 0, 0, 0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function maybeFindImageMap_(elem) {
|
|
340
|
+
// If not a <map> or <area>, return null indicating so.
|
|
341
|
+
var elemTagName = elem.tagName.toUpperCase();
|
|
342
|
+
var isMap = elemTagName == "MAP";
|
|
343
|
+
if (!isMap && (elemTagName != "AREA")) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Get the <map> associated with this element, or null if none.
|
|
348
|
+
var map = isMap ? elem :
|
|
349
|
+
((getParentElement(elem).tagName.toUpperCase() == "MAP") ?
|
|
350
|
+
getParentElement(elem) : null);
|
|
351
|
+
|
|
352
|
+
var image = null, rect = null;
|
|
353
|
+
if (map && map.name) {
|
|
354
|
+
var mapDoc = map.ownerDocument;
|
|
355
|
+
|
|
356
|
+
image = mapDoc.querySelector("*[usemap='#" + map.name + "']");
|
|
357
|
+
|
|
358
|
+
if (image) {
|
|
359
|
+
rect = getClientRect(image);
|
|
360
|
+
if (!isMap && elem.shape.toLowerCase() != "default") {
|
|
361
|
+
// Shift and crop the relative area rectangle to the map.
|
|
362
|
+
var relRect = getAreaRelativeRect_(elem);
|
|
363
|
+
var relX = Math.min(Math.max(relRect.left, 0), rect.width);
|
|
364
|
+
var relY = Math.min(Math.max(relRect.top, 0), rect.height);
|
|
365
|
+
var w = Math.min(relRect.width, rect.width - relX);
|
|
366
|
+
var h = Math.min(relRect.height, rect.height - relY);
|
|
367
|
+
rect = rect_(relX + rect.left, relY + rect.top, w, h);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {image: image, rect: rect || rect_(0, 0, 0, 0)};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getAncestor(element, matcher) {
|
|
376
|
+
if (element) {
|
|
377
|
+
element = getParentElement(element);
|
|
378
|
+
}
|
|
379
|
+
while (element) {
|
|
380
|
+
if (matcher(element)) {
|
|
381
|
+
return element;
|
|
382
|
+
}
|
|
383
|
+
element = getParentElement(element);
|
|
384
|
+
}
|
|
385
|
+
// Reached the root of the DOM without a match
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
function isElement(node, opt_tagName) {
|
|
391
|
+
// because we call this with deprecated tags such as SHADOW
|
|
392
|
+
if (opt_tagName && (typeof opt_tagName !== "string")) {
|
|
393
|
+
opt_tagName = opt_tagName.toString();
|
|
394
|
+
}
|
|
395
|
+
return !!node && node.nodeType == Node.ELEMENT_NODE &&
|
|
396
|
+
(!opt_tagName || node.tagName.toUpperCase() == opt_tagName);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function getParentNodeInComposedDom(node) {
|
|
400
|
+
var /**@type {Node}*/ parent = node.parentNode;
|
|
401
|
+
|
|
402
|
+
// Shadow DOM v1
|
|
403
|
+
if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {
|
|
404
|
+
// Can be null on purpose, meaning it has no parent as
|
|
405
|
+
// it hasn't yet been slotted
|
|
406
|
+
return node.assignedSlot ? node.assignedSlot.parentNode : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Shadow DOM V0 (deprecated)
|
|
410
|
+
if (node.getDestinationInsertionPoints) {
|
|
411
|
+
var destinations = node.getDestinationInsertionPoints();
|
|
412
|
+
if (destinations.length > 0) {
|
|
413
|
+
return destinations[destinations.length - 1];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return parent;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return function isShown(elem, opt_ignoreOpacity) {
|
|
421
|
+
/**
|
|
422
|
+
* Determines whether an element or its parents have `display: none` set
|
|
423
|
+
* @param {!Node} e the element
|
|
424
|
+
* @return {boolean}
|
|
425
|
+
*/
|
|
426
|
+
function displayed(e) {
|
|
427
|
+
if (window.getComputedStyle(e)["display"] == "none"){
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var parent = getParentNodeInComposedDom(e);
|
|
432
|
+
|
|
433
|
+
if ((typeof ShadowRoot === "function") && (parent instanceof ShadowRoot)) {
|
|
434
|
+
if (parent.host.shadowRoot !== parent) {
|
|
435
|
+
// There is a younger shadow root, which will take precedence over
|
|
436
|
+
// the shadow this element is in, thus this element won't be
|
|
437
|
+
// displayed.
|
|
438
|
+
return false;
|
|
439
|
+
} else {
|
|
440
|
+
parent = parent.host;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (parent && (parent.nodeType == Node.DOCUMENT_NODE ||
|
|
445
|
+
parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return parent && displayed(parent);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return isShown_(elem, !!opt_ignoreOpacity, displayed);
|
|
453
|
+
};
|
|
454
|
+
})()
|
|
@@ -18,6 +18,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
18
18
|
def load_selenium
|
|
19
19
|
require 'selenium-webdriver'
|
|
20
20
|
require 'capybara/selenium/logger_suppressor'
|
|
21
|
+
require 'capybara/selenium/patches/atoms'
|
|
21
22
|
warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
|
|
22
23
|
rescue LoadError => e
|
|
23
24
|
raise e unless e.message.match?(/selenium-webdriver/)
|
|
@@ -135,6 +136,18 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
|
|
139
|
+
def frame_obscured_at?(x:, y:)
|
|
140
|
+
frame = @frame_handles[current_window_handle].last
|
|
141
|
+
return false unless frame
|
|
142
|
+
|
|
143
|
+
switch_to_frame(:parent)
|
|
144
|
+
begin
|
|
145
|
+
return frame.base.obscured?(x: x, y: y)
|
|
146
|
+
ensure
|
|
147
|
+
switch_to_frame(frame)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
138
151
|
def switch_to_frame(frame)
|
|
139
152
|
handles = @frame_handles[current_window_handle]
|
|
140
153
|
case frame
|
|
@@ -145,7 +158,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
145
158
|
handles.pop
|
|
146
159
|
browser.switch_to.parent_frame
|
|
147
160
|
else
|
|
148
|
-
handles << frame
|
|
161
|
+
handles << frame
|
|
149
162
|
browser.switch_to.frame(frame.native)
|
|
150
163
|
end
|
|
151
164
|
end
|
|
@@ -414,7 +427,7 @@ private
|
|
|
414
427
|
|
|
415
428
|
def specialize_driver
|
|
416
429
|
browser_type = browser.browser
|
|
417
|
-
|
|
430
|
+
Capybara::Selenium::Driver.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
|
|
418
431
|
extend specialization
|
|
419
432
|
end
|
|
420
433
|
end
|
|
@@ -60,7 +60,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
|
|
|
60
60
|
# so we have to move to the default_content and iterate back through the frames
|
|
61
61
|
handles = @frame_handles[current_window_handle]
|
|
62
62
|
browser.switch_to.default_content
|
|
63
|
-
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
|
63
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
private
|
|
@@ -10,7 +10,7 @@ module Capybara::Selenium::Driver::InternetExplorerDriver
|
|
|
10
10
|
# so we have to move to the default_content and iterate back through the frames
|
|
11
11
|
handles = @frame_handles[current_window_handle]
|
|
12
12
|
browser.switch_to.default_content
|
|
13
|
-
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
|
13
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -10,7 +10,7 @@ module Capybara::Selenium::Driver::SafariDriver
|
|
|
10
10
|
# behaves like switch_to_frame(:top)
|
|
11
11
|
handles = @frame_handles[current_window_handle]
|
|
12
12
|
browser.switch_to.default_content
|
|
13
|
-
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
|
13
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -155,7 +155,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
def content_editable?
|
|
158
|
-
native.attribute('isContentEditable')
|
|
158
|
+
native.attribute('isContentEditable') == 'true'
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def ==(other)
|
|
@@ -166,6 +166,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
|
166
166
|
driver.evaluate_script GET_XPATH_SCRIPT, self
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
def obscured?(x: nil, y: nil)
|
|
170
|
+
res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
|
|
171
|
+
return true if res == true
|
|
172
|
+
|
|
173
|
+
driver.frame_obscured_at?(x: res['x'], y: res['y'])
|
|
174
|
+
end
|
|
175
|
+
|
|
169
176
|
protected
|
|
170
177
|
|
|
171
178
|
def scroll_if_needed
|
|
@@ -401,6 +408,23 @@ private
|
|
|
401
408
|
})(arguments[0], document)
|
|
402
409
|
JS
|
|
403
410
|
|
|
411
|
+
OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
|
|
412
|
+
(function(el, x, y) {
|
|
413
|
+
var box = el.getBoundingClientRect();
|
|
414
|
+
if (x == null) x = box.width/2;
|
|
415
|
+
if (y == null) y = box.height/2 ;
|
|
416
|
+
|
|
417
|
+
var px = box.left + x,
|
|
418
|
+
py = box.top + y,
|
|
419
|
+
e = document.elementFromPoint(px, py);
|
|
420
|
+
|
|
421
|
+
if (!el.contains(e))
|
|
422
|
+
return true;
|
|
423
|
+
|
|
424
|
+
return { x: px, y: py };
|
|
425
|
+
})(arguments[0], arguments[1], arguments[2])
|
|
426
|
+
JS
|
|
427
|
+
|
|
404
428
|
# SettableValue encapsulates time/date field formatting
|
|
405
429
|
class SettableValue
|
|
406
430
|
attr_reader :value
|
|
@@ -16,6 +16,10 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
|
|
|
16
16
|
return find_css('th:first-child,td:first-child')[0].click(keys, options)
|
|
17
17
|
end
|
|
18
18
|
raise
|
|
19
|
+
rescue ::Selenium::WebDriver::Error::WebDriverError
|
|
20
|
+
# Safari doesn't return a specific error here - assume it's an ElementNotInteractableError
|
|
21
|
+
raise ::Selenium::WebDriver::Error::ElementNotInteractableError,
|
|
22
|
+
'Non distinct error raised in #click, translated to ElementNotInteractableError for retry'
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def select_option
|
|
@@ -54,7 +58,9 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
|
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
def send_keys(*args)
|
|
57
|
-
|
|
61
|
+
if args.none? { |arg| arg.is_a?(Array) || (arg.is_a?(Symbol) && MODIFIER_KEYS.include?(arg)) }
|
|
62
|
+
return super(*args.map { |arg| arg == :space ? ' ' : arg })
|
|
63
|
+
end
|
|
58
64
|
|
|
59
65
|
native.click
|
|
60
66
|
_send_keys(args).perform
|
|
@@ -74,6 +80,11 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
|
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
|
|
83
|
+
def hover
|
|
84
|
+
# Workaround issue where hover would sometimes fail - possibly due to mouse not having moved
|
|
85
|
+
scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
|
|
86
|
+
end
|
|
87
|
+
|
|
77
88
|
private
|
|
78
89
|
|
|
79
90
|
def bridge
|
|
@@ -95,11 +106,7 @@ private
|
|
|
95
106
|
|
|
96
107
|
def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
|
|
97
108
|
case keys
|
|
98
|
-
when
|
|
99
|
-
:alt, :left_alt, :right_alt,
|
|
100
|
-
:shift, :left_shift, :right_shift,
|
|
101
|
-
:meta, :left_meta, :right_meta,
|
|
102
|
-
:command
|
|
109
|
+
when *MODIFIER_KEYS
|
|
103
110
|
down_keys.press(keys)
|
|
104
111
|
actions.key_down(keys)
|
|
105
112
|
when String
|
|
@@ -117,6 +124,12 @@ private
|
|
|
117
124
|
actions
|
|
118
125
|
end
|
|
119
126
|
|
|
127
|
+
MODIFIER_KEYS = %i[control left_control right_control
|
|
128
|
+
alt left_alt right_alt
|
|
129
|
+
shift left_shift right_shift
|
|
130
|
+
meta left_meta right_meta
|
|
131
|
+
command].freeze
|
|
132
|
+
|
|
120
133
|
class ModifierKeysStack
|
|
121
134
|
def initialize
|
|
122
135
|
@stack = []
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CapybaraAtoms
|
|
4
|
+
private # rubocop:disable Layout/IndentationWidth
|
|
5
|
+
|
|
6
|
+
def read_atom(function)
|
|
7
|
+
@atoms ||= Hash.new do |hash, key|
|
|
8
|
+
hash[key] = begin
|
|
9
|
+
File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
|
|
10
|
+
rescue Errno::ENOENT
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
@atoms[function]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
::Selenium::WebDriver::Remote::Bridge.prepend CapybaraAtoms unless ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
|
|
@@ -100,6 +100,29 @@ Capybara::SpecHelper.spec '#all' do
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
context 'with obscured filter', requires: [:css] do
|
|
104
|
+
it 'should only find nodes on top in the viewport when false' do
|
|
105
|
+
expect(@session.all(:css, 'a.simple', obscured: false).size).to eq(1)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'should not find nodes on top outside the viewport when false' do
|
|
109
|
+
expect(@session.all(:link, 'Download Me', obscured: false).size).to eq(0)
|
|
110
|
+
@session.scroll_to(@session.find_link('Download Me'))
|
|
111
|
+
expect(@session.all(:link, 'Download Me', obscured: false).size).to eq(1)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'should find top nodes outside the viewport when true' do
|
|
115
|
+
expect(@session.all(:link, 'Download Me', obscured: true).size).to eq(1)
|
|
116
|
+
@session.scroll_to(@session.find_link('Download Me'))
|
|
117
|
+
expect(@session.all(:link, 'Download Me', obscured: true).size).to eq(0)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'should only find non-top nodes when true' do
|
|
121
|
+
# Also need visible: false so visibility is ignored
|
|
122
|
+
expect(@session.all(:css, 'a.simple', visible: false, obscured: true).size).to eq(1)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
103
126
|
context 'with element count filters' do
|
|
104
127
|
context ':count' do
|
|
105
128
|
it 'should succeed when the number of elements founds matches the expectation' do
|