blob: 5185e010fbc0b410fdaece082f4c249b6fe2c738 [file] [log] [blame] [edit]
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Chrome specific atoms.
*
*/
goog.provide('webdriver.chrome');
goog.require('bot.dom');
goog.require('bot.locators');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Rect');
goog.require('goog.math.Size');
goog.require('goog.style');
/**
* True if shadow dom is enabled.
* @const
* @type {boolean}
*/
var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';
/**
* Returns the minimum required offsets to scroll a given region into view.
* If the region is larger than the scrollable view, the region will be
* centered or aligned with the top-left of the scrollable view, depending
* on the value of "center".
*
* @param {!goog.math.Size} size The size of the scrollable view.
* @param {!goog.math.Rect} region The region of the scrollable to bring into
* view.
* @param {boolean} center If true, when the region is too big to view,
* center it instead of aligning with the top-left.
* @return {!goog.math.Coordinate} Offset by which to scroll.
* @private
*/
webdriver.chrome.computeScrollOffsets_ = function(size, region,
center) {
var scroll = [0, 0];
var scrollableSize = [size.width, size.height];
var regionLoc = [region.left, region.top];
var regionSize = [region.width, region.height];
for (var i = 0; i < 2; i++) {
if (regionSize[i] > scrollableSize[i]) {
if (center)
scroll[i] = regionLoc[i] + regionSize[i] / 2 - scrollableSize[i] / 2;
else
scroll[i] = regionLoc[i];
} else {
var alignRight = regionLoc[i] - scrollableSize[i] + regionSize[i];
if (alignRight > 0)
scroll[i] = alignRight;
else if (regionLoc[i] < 0)
scroll[i] = regionLoc[i];
}
}
return new goog.math.Coordinate(scroll[0], scroll[1]);
};
/**
* Return the offset of the given element from its container.
*
* @param {!Element} container The container.
* @param {!Element} elem The element.
* @return {!goog.math.Coordinate} The offset.
* @private
*/
webdriver.chrome.computeOffsetInContainer_ = function(container, elem) {
var offset = goog.math.Coordinate.difference(
goog.style.getPageOffset(elem), goog.style.getPageOffset(container));
var containerBorder = goog.style.getBorderBox(container);
offset.x -= containerBorder.left;
offset.y -= containerBorder.top;
return offset;
};
/**
* Scrolls the region of an element into view. If the region will not fit,
* it will be aligned at the top-left or centered, depending on
* "center".
*
* @param {!Element} elem The element with the region to scroll into view.
* @param {!goog.math.Rect} region The region, relative to the element's
* border box, to scroll into view.
* @param {boolean} center If true, when the region is too big to view,
* center it instead of aligning with the top-left.
* @private
*/
webdriver.chrome.scrollIntoView_ = function(elem, region, center) {
function scrollHelper(scrollable, size, offset, region, center) {
region = new goog.math.Rect(
offset.x + region.left, offset.y + region.top,
region.width, region.height);
var scroll = webdriver.chrome.computeScrollOffsets_(size, region, center);
scrollable.scrollLeft += scroll.x;
scrollable.scrollTop += scroll.y;
}
function getContainer(elem) {
var container = elem.parentNode;
if (SHADOW_DOM_ENABLED && (container instanceof ShadowRoot)) {
container = elem.host;
}
return container;
}
var doc = goog.dom.getOwnerDocument(elem);
var container = getContainer(elem);
var offset;
while (container &&
container != doc.documentElement &&
container != doc.body) {
offset = webdriver.chrome.computeOffsetInContainer_(
/** @type {!Element} */ (container), elem);
var containerSize = new goog.math.Size(container.clientWidth,
container.clientHeight);
scrollHelper(container, containerSize, offset, region, center);
container = getContainer(container);
}
offset = goog.style.getClientPosition(elem);
var windowSize = goog.dom.getDomHelper(elem).getViewportSize();
// Chrome uses either doc.documentElement or doc.body, depending on
// compatibility settings. For reliability, call scrollHelper on both.
// Calling scrollHelper on the wrong object is harmless.
scrollHelper(doc.documentElement, windowSize, offset, region, center);
if (doc.body)
scrollHelper(doc.body, windowSize, offset, region, center);
};
/**
* Scrolls a region of the given element into the client's view and returns
* its position relative to the client viewport. If the element or region is too
* large to fit in the view, it will be centered or aligned to the top-left,
* depending on the value of "center".
*
* scrollIntoView is not used because it does not work correctly in Chrome:
* http://crbug.com/73953.
*
* The element should be attached to the current document.
*
* @param {!Element} elem The element to use.
* @param {boolean} center If true, center the region when it is too big
* to fit in the view.
* @param {!goog.math.Rect} opt_region The region relative to the element's
* border box to be scrolled into view. If null, the border box will be
* used.
* @return {!goog.math.Coordinate} The top-left coordinate of the element's
* region in client space.
*/
webdriver.chrome.getLocationInView = function(elem, center, opt_region) {
var region = opt_region;
if (!region)
region = new goog.math.Rect(0, 0, elem.offsetWidth, elem.offsetHeight);
if (elem != elem.ownerDocument.documentElement)
webdriver.chrome.scrollIntoView_(elem, region, center);
var elemClientPos = goog.style.getClientPosition(elem);
return new goog.math.Coordinate(
elemClientPos.x + region.left, elemClientPos.y + region.top);
};
/**
* Returns the first client rect of the given element, relative to the
* element's border box. If the element does not have any client rects,
* throws an error.
*
* @param {!Element} elem The element to use.
* @return {!goog.math.Rect} The first client rect of the given element,
* relative to the element's border box.
*/
webdriver.chrome.getFirstClientRect = function(elem) {
var clientRects = elem.getClientRects();
if (clientRects.length == 0)
throw new Error('Element does not have any client rects');
var clientRect = clientRects[0];
var clientPos = goog.style.getClientPosition(elem);
return new goog.math.Rect(
clientRect.left - clientPos.x, clientRect.top - clientPos.y,
clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
};
/**
* Returns whether the element or any of its descendants would receive a click
* at the given location. Useful for debugging test clicking issues.
*
* @param {!Element} elem The element to use.
* @param {!goog.math.Coordinate} coord The coordinate to use.
* @return {{clickable:boolean, message: (string|undefined)}} Object containing
* a boolean "clickable" property, as to whether it can be clicked, and an
* optional "message" string property, which contains any warning/error
* message.
*/
webdriver.chrome.isElementClickable = function(elem, coord) {
/**
* @param {boolean} clickable .
* @param {string=} opt_msg .
* @return {{clickable: boolean, message: (string|undefined)}} .
*/
function makeResult(clickable, opt_msg) {
var dict = {'clickable': clickable};
if (opt_msg)
dict['message'] = opt_msg;
return dict;
}
// get the outermost ancestor of the element. This will be either the document
// or a shadow root.
var owner = elem;
while (owner.parentNode) {
owner = owner.parentNode;
}
var elemAtPoint = owner.elementFromPoint(coord.x, coord.y);
if (elemAtPoint == elem)
return makeResult(true);
var coordStr = '(' + coord.x + ', ' + coord.y + ')';
if (elemAtPoint == null) {
return makeResult(
false, 'Element is not clickable at point ' + coordStr);
}
var elemAtPointHTML = elemAtPoint.outerHTML.replace(elemAtPoint.innerHTML,
elemAtPoint.hasChildNodes()
? '...' : '');
var parentElemIter = elemAtPoint.parentNode;
while (parentElemIter) {
if (parentElemIter == elem) {
return makeResult(
true,
'Element\'s descendant would receive the click. Consider ' +
'clicking the descendant instead. Descendant: ' +
elemAtPointHTML);
}
parentElemIter = parentElemIter.parentNode;
}
var elemHTML = elem.outerHTML.replace(elem.innerHTML,
elem.hasChildNodes() ? '...' : '');
return makeResult(
false,
'Element ' + elemHTML + ' is not clickable at point '
+ coordStr + '. Other element ' +
'would receive the click: ' + elemAtPointHTML);
};
/**
* Returns the current page zoom ratio for the page with the given element.
*
* @param {!Element} elem The element to use.
* @return {number} Page zoom ratio.
*/
webdriver.chrome.getPageZoom = function(elem) {
// From http://stackoverflow.com/questions/1713771/
// how-to-detect-page-zoom-level-in-all-modern-browsers
var doc = goog.dom.getOwnerDocument(elem);
var docElem = doc.documentElement;
var width = Math.max(
docElem.clientWidth, docElem.offsetWidth, docElem.scrollWidth);
return doc.width / width;
};
/**
* Determines whether an element is what a user would call "shown". Mainly based
* on bot.dom.isShown, but with extra intelligence regarding shadow DOM.
*
* @param {!Element} elem The element to consider.
* @param {boolean=} opt_inComposedDom Whether to check if the element is shown
* within the composed DOM; defaults to false.
* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity
* when determining whether it is shown; defaults to false.
* @return {boolean} Whether or not the element is visible.
*/
webdriver.chrome.isElementDisplayed = function(elem,
opt_inComposedDom,
opt_ignoreOpacity) {
if (!bot.dom.isShown(elem, opt_ignoreOpacity)) {
return false;
}
// if it's not invisible then check if the element is within the shadow DOM
// of an invisible element, using recursive calls to this function
if (SHADOW_DOM_ENABLED) {
var topLevelNode = elem;
while (topLevelNode.parentNode) {
topLevelNode = topLevelNode.parentNode;
}
if (topLevelNode instanceof ShadowRoot) {
return webdriver.chrome.isElementDisplayed(topLevelNode.host,
opt_inComposedDom);
}
}
// if it's not invisible, or in a shadow DOM, then it's definitely visible
return true;
};
/**
* Same as bot.locators.findElement (description copied below), but
* with workarounds for shadow DOM.
*
* Find the first element in the DOM matching the target. The target
* object should have a single key, the name of which determines the
* locator strategy and the value of which gives the value to be
* searched for. For example {id: 'foo'} indicates that the first
* element on the DOM with the ID 'foo' should be returned.
*
* @param {!Object} target The selector to search for.
* @param {(Document|Element)=} opt_root The node from which to start the
* search. If not specified, will use {@code document} as the root.
* @return {Element} The first matching element found in the DOM, or null if no
* such element could be found.
*/
webdriver.chrome.findElement = function(target, opt_root) {
// This works fine if opt_root is outside of a shadow DOM, but for various
// (presumably performance-based) reasons, it works by getting opt_root's
// owning document, searching that, and then checking if the result is owned
// by opt_root. Searching the owning document for a child of a shadow root
// obviously doesn't work. However we try the performance-optimised version
// first...
var elem = bot.locators.findElement(target, opt_root);
if (elem) {
return elem;
}
// If we didn't find anything using that method, check to see if opt_root
// is within a shadow DOM...
if (SHADOW_DOM_ENABLED && opt_root) {
var topLevelNode = opt_root;
while (topLevelNode.parentNode) {
topLevelNode = topLevelNode.parentNode;
}
if (topLevelNode instanceof ShadowRoot) {
// findElement_s_ works fine if passed an root that's in a shadow root.
elem = bot.locators.findElements(target, opt_root)[0];
if (elem) {
return elem;
}
}
}
return null;
};