Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/chrome-driver/atoms.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview Chrome specific atoms.
20
*
21
*/
22
23
goog.provide('webdriver.chrome');
24
25
goog.require('bot.dom');
26
goog.require('bot.locators');
27
goog.require('goog.dom');
28
goog.require('goog.math.Coordinate');
29
goog.require('goog.math.Rect');
30
goog.require('goog.math.Size');
31
goog.require('goog.style');
32
33
/**
34
* True if shadow dom is enabled.
35
* @const
36
* @type {boolean}
37
*/
38
var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';
39
40
/**
41
* Returns the minimum required offsets to scroll a given region into view.
42
* If the region is larger than the scrollable view, the region will be
43
* centered or aligned with the top-left of the scrollable view, depending
44
* on the value of "center".
45
*
46
* @param {!goog.math.Size} size The size of the scrollable view.
47
* @param {!goog.math.Rect} region The region of the scrollable to bring into
48
* view.
49
* @param {boolean} center If true, when the region is too big to view,
50
* center it instead of aligning with the top-left.
51
* @return {!goog.math.Coordinate} Offset by which to scroll.
52
* @private
53
*/
54
webdriver.chrome.computeScrollOffsets_ = function(size, region,
55
center) {
56
var scroll = [0, 0];
57
var scrollableSize = [size.width, size.height];
58
var regionLoc = [region.left, region.top];
59
var regionSize = [region.width, region.height];
60
61
for (var i = 0; i < 2; i++) {
62
if (regionSize[i] > scrollableSize[i]) {
63
if (center)
64
scroll[i] = regionLoc[i] + regionSize[i] / 2 - scrollableSize[i] / 2;
65
else
66
scroll[i] = regionLoc[i];
67
} else {
68
var alignRight = regionLoc[i] - scrollableSize[i] + regionSize[i];
69
if (alignRight > 0)
70
scroll[i] = alignRight;
71
else if (regionLoc[i] < 0)
72
scroll[i] = regionLoc[i];
73
}
74
}
75
76
return new goog.math.Coordinate(scroll[0], scroll[1]);
77
};
78
79
80
/**
81
* Return the offset of the given element from its container.
82
*
83
* @param {!Element} container The container.
84
* @param {!Element} elem The element.
85
* @return {!goog.math.Coordinate} The offset.
86
* @private
87
*/
88
webdriver.chrome.computeOffsetInContainer_ = function(container, elem) {
89
var offset = goog.math.Coordinate.difference(
90
goog.style.getPageOffset(elem), goog.style.getPageOffset(container));
91
var containerBorder = goog.style.getBorderBox(container);
92
offset.x -= containerBorder.left;
93
offset.y -= containerBorder.top;
94
return offset;
95
};
96
97
98
/**
99
* Scrolls the region of an element into view. If the region will not fit,
100
* it will be aligned at the top-left or centered, depending on
101
* "center".
102
*
103
* @param {!Element} elem The element with the region to scroll into view.
104
* @param {!goog.math.Rect} region The region, relative to the element's
105
* border box, to scroll into view.
106
* @param {boolean} center If true, when the region is too big to view,
107
* center it instead of aligning with the top-left.
108
* @private
109
*/
110
webdriver.chrome.scrollIntoView_ = function(elem, region, center) {
111
function scrollHelper(scrollable, size, offset, region, center) {
112
region = new goog.math.Rect(
113
offset.x + region.left, offset.y + region.top,
114
region.width, region.height);
115
116
var scroll = webdriver.chrome.computeScrollOffsets_(size, region, center);
117
scrollable.scrollLeft += scroll.x;
118
scrollable.scrollTop += scroll.y;
119
}
120
121
function getContainer(elem) {
122
var container = elem.parentNode;
123
if (SHADOW_DOM_ENABLED && (container instanceof ShadowRoot)) {
124
container = elem.host;
125
}
126
return container;
127
}
128
129
var doc = goog.dom.getOwnerDocument(elem);
130
var container = getContainer(elem);
131
var offset;
132
while (container &&
133
container != doc.documentElement &&
134
container != doc.body) {
135
offset = webdriver.chrome.computeOffsetInContainer_(
136
/** @type {!Element} */ (container), elem);
137
var containerSize = new goog.math.Size(container.clientWidth,
138
container.clientHeight);
139
scrollHelper(container, containerSize, offset, region, center);
140
container = getContainer(container);
141
}
142
143
offset = goog.style.getClientPosition(elem);
144
var windowSize = goog.dom.getDomHelper(elem).getViewportSize();
145
// Chrome uses either doc.documentElement or doc.body, depending on
146
// compatibility settings. For reliability, call scrollHelper on both.
147
// Calling scrollHelper on the wrong object is harmless.
148
scrollHelper(doc.documentElement, windowSize, offset, region, center);
149
if (doc.body)
150
scrollHelper(doc.body, windowSize, offset, region, center);
151
};
152
153
154
/**
155
* Scrolls a region of the given element into the client's view and returns
156
* its position relative to the client viewport. If the element or region is too
157
* large to fit in the view, it will be centered or aligned to the top-left,
158
* depending on the value of "center".
159
*
160
* scrollIntoView is not used because it does not work correctly in Chrome:
161
* http://crbug.com/73953.
162
*
163
* The element should be attached to the current document.
164
*
165
* @param {!Element} elem The element to use.
166
* @param {boolean} center If true, center the region when it is too big
167
* to fit in the view.
168
* @param {!goog.math.Rect} opt_region The region relative to the element's
169
* border box to be scrolled into view. If null, the border box will be
170
* used.
171
* @return {!goog.math.Coordinate} The top-left coordinate of the element's
172
* region in client space.
173
*/
174
webdriver.chrome.getLocationInView = function(elem, center, opt_region) {
175
var region = opt_region;
176
if (!region)
177
region = new goog.math.Rect(0, 0, elem.offsetWidth, elem.offsetHeight);
178
179
if (elem != elem.ownerDocument.documentElement)
180
webdriver.chrome.scrollIntoView_(elem, region, center);
181
182
var elemClientPos = goog.style.getClientPosition(elem);
183
return new goog.math.Coordinate(
184
elemClientPos.x + region.left, elemClientPos.y + region.top);
185
};
186
187
188
/**
189
* Returns the first client rect of the given element, relative to the
190
* element's border box. If the element does not have any client rects,
191
* throws an error.
192
*
193
* @param {!Element} elem The element to use.
194
* @return {!goog.math.Rect} The first client rect of the given element,
195
* relative to the element's border box.
196
*/
197
webdriver.chrome.getFirstClientRect = function(elem) {
198
var clientRects = elem.getClientRects();
199
if (clientRects.length == 0)
200
throw new Error('Element does not have any client rects');
201
var clientRect = clientRects[0];
202
var clientPos = goog.style.getClientPosition(elem);
203
return new goog.math.Rect(
204
clientRect.left - clientPos.x, clientRect.top - clientPos.y,
205
clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
206
};
207
208
209
/**
210
* Returns whether the element or any of its descendants would receive a click
211
* at the given location. Useful for debugging test clicking issues.
212
*
213
* @param {!Element} elem The element to use.
214
* @param {!goog.math.Coordinate} coord The coordinate to use.
215
* @return {{clickable:boolean, message: (string|undefined)}} Object containing
216
* a boolean "clickable" property, as to whether it can be clicked, and an
217
* optional "message" string property, which contains any warning/error
218
* message.
219
*/
220
webdriver.chrome.isElementClickable = function(elem, coord) {
221
/**
222
* @param {boolean} clickable .
223
* @param {string=} opt_msg .
224
* @return {{clickable: boolean, message: (string|undefined)}} .
225
*/
226
function makeResult(clickable, opt_msg) {
227
var dict = {'clickable': clickable};
228
if (opt_msg)
229
dict['message'] = opt_msg;
230
return dict;
231
}
232
233
// get the outermost ancestor of the element. This will be either the document
234
// or a shadow root.
235
var owner = elem;
236
while (owner.parentNode) {
237
owner = owner.parentNode;
238
}
239
240
var elemAtPoint = owner.elementFromPoint(coord.x, coord.y);
241
if (elemAtPoint == elem)
242
return makeResult(true);
243
244
var coordStr = '(' + coord.x + ', ' + coord.y + ')';
245
if (elemAtPoint == null) {
246
return makeResult(
247
false, 'Element is not clickable at point ' + coordStr);
248
}
249
var elemAtPointHTML = elemAtPoint.outerHTML.replace(elemAtPoint.innerHTML,
250
elemAtPoint.hasChildNodes()
251
? '...' : '');
252
var parentElemIter = elemAtPoint.parentNode;
253
while (parentElemIter) {
254
if (parentElemIter == elem) {
255
return makeResult(
256
true,
257
'Element\'s descendant would receive the click. Consider ' +
258
'clicking the descendant instead. Descendant: ' +
259
elemAtPointHTML);
260
}
261
parentElemIter = parentElemIter.parentNode;
262
}
263
var elemHTML = elem.outerHTML.replace(elem.innerHTML,
264
elem.hasChildNodes() ? '...' : '');
265
return makeResult(
266
false,
267
'Element ' + elemHTML + ' is not clickable at point '
268
+ coordStr + '. Other element ' +
269
'would receive the click: ' + elemAtPointHTML);
270
};
271
272
273
/**
274
* Returns the current page zoom ratio for the page with the given element.
275
*
276
* @param {!Element} elem The element to use.
277
* @return {number} Page zoom ratio.
278
*/
279
webdriver.chrome.getPageZoom = function(elem) {
280
// From http://stackoverflow.com/questions/1713771/
281
// how-to-detect-page-zoom-level-in-all-modern-browsers
282
var doc = goog.dom.getOwnerDocument(elem);
283
var docElem = doc.documentElement;
284
var width = Math.max(
285
docElem.clientWidth, docElem.offsetWidth, docElem.scrollWidth);
286
return doc.width / width;
287
};
288
289
/**
290
* Determines whether an element is what a user would call "shown". Mainly based
291
* on bot.dom.isShown, but with extra intelligence regarding shadow DOM.
292
*
293
* @param {!Element} elem The element to consider.
294
* @param {boolean=} opt_inComposedDom Whether to check if the element is shown
295
* within the composed DOM; defaults to false.
296
* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity
297
* when determining whether it is shown; defaults to false.
298
* @return {boolean} Whether or not the element is visible.
299
*/
300
webdriver.chrome.isElementDisplayed = function(elem,
301
opt_inComposedDom,
302
opt_ignoreOpacity) {
303
if (!bot.dom.isShown(elem, opt_ignoreOpacity)) {
304
return false;
305
}
306
// if it's not invisible then check if the element is within the shadow DOM
307
// of an invisible element, using recursive calls to this function
308
if (SHADOW_DOM_ENABLED) {
309
var topLevelNode = elem;
310
while (topLevelNode.parentNode) {
311
topLevelNode = topLevelNode.parentNode;
312
}
313
if (topLevelNode instanceof ShadowRoot) {
314
return webdriver.chrome.isElementDisplayed(topLevelNode.host,
315
opt_inComposedDom);
316
}
317
}
318
// if it's not invisible, or in a shadow DOM, then it's definitely visible
319
return true;
320
};
321
322
/**
323
* Same as bot.locators.findElement (description copied below), but
324
* with workarounds for shadow DOM.
325
*
326
* Find the first element in the DOM matching the target. The target
327
* object should have a single key, the name of which determines the
328
* locator strategy and the value of which gives the value to be
329
* searched for. For example {id: 'foo'} indicates that the first
330
* element on the DOM with the ID 'foo' should be returned.
331
*
332
* @param {!Object} target The selector to search for.
333
* @param {(Document|Element)=} opt_root The node from which to start the
334
* search. If not specified, will use {@code document} as the root.
335
* @return {Element} The first matching element found in the DOM, or null if no
336
* such element could be found.
337
*/
338
webdriver.chrome.findElement = function(target, opt_root) {
339
// This works fine if opt_root is outside of a shadow DOM, but for various
340
// (presumably performance-based) reasons, it works by getting opt_root's
341
// owning document, searching that, and then checking if the result is owned
342
// by opt_root. Searching the owning document for a child of a shadow root
343
// obviously doesn't work. However we try the performance-optimised version
344
// first...
345
var elem = bot.locators.findElement(target, opt_root);
346
if (elem) {
347
return elem;
348
}
349
// If we didn't find anything using that method, check to see if opt_root
350
// is within a shadow DOM...
351
if (SHADOW_DOM_ENABLED && opt_root) {
352
var topLevelNode = opt_root;
353
while (topLevelNode.parentNode) {
354
topLevelNode = topLevelNode.parentNode;
355
}
356
if (topLevelNode instanceof ShadowRoot) {
357
// findElement_s_ works fine if passed an root that's in a shadow root.
358
elem = bot.locators.findElements(target, opt_root)[0];
359
if (elem) {
360
return elem;
361
}
362
}
363
}
364
return null;
365
};
366
367