// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Chrome specific atoms.19*20*/2122goog.provide('webdriver.chrome');2324goog.require('bot.dom');25goog.require('bot.locators');26goog.require('goog.dom');27goog.require('goog.math.Coordinate');28goog.require('goog.math.Rect');29goog.require('goog.math.Size');30goog.require('goog.style');3132/**33* True if shadow dom is enabled.34* @const35* @type {boolean}36*/37var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';3839/**40* Returns the minimum required offsets to scroll a given region into view.41* If the region is larger than the scrollable view, the region will be42* centered or aligned with the top-left of the scrollable view, depending43* on the value of "center".44*45* @param {!goog.math.Size} size The size of the scrollable view.46* @param {!goog.math.Rect} region The region of the scrollable to bring into47* view.48* @param {boolean} center If true, when the region is too big to view,49* center it instead of aligning with the top-left.50* @return {!goog.math.Coordinate} Offset by which to scroll.51* @private52*/53webdriver.chrome.computeScrollOffsets_ = function(size, region,54center) {55var scroll = [0, 0];56var scrollableSize = [size.width, size.height];57var regionLoc = [region.left, region.top];58var regionSize = [region.width, region.height];5960for (var i = 0; i < 2; i++) {61if (regionSize[i] > scrollableSize[i]) {62if (center)63scroll[i] = regionLoc[i] + regionSize[i] / 2 - scrollableSize[i] / 2;64else65scroll[i] = regionLoc[i];66} else {67var alignRight = regionLoc[i] - scrollableSize[i] + regionSize[i];68if (alignRight > 0)69scroll[i] = alignRight;70else if (regionLoc[i] < 0)71scroll[i] = regionLoc[i];72}73}7475return new goog.math.Coordinate(scroll[0], scroll[1]);76};777879/**80* Return the offset of the given element from its container.81*82* @param {!Element} container The container.83* @param {!Element} elem The element.84* @return {!goog.math.Coordinate} The offset.85* @private86*/87webdriver.chrome.computeOffsetInContainer_ = function(container, elem) {88var offset = goog.math.Coordinate.difference(89goog.style.getPageOffset(elem), goog.style.getPageOffset(container));90var containerBorder = goog.style.getBorderBox(container);91offset.x -= containerBorder.left;92offset.y -= containerBorder.top;93return offset;94};959697/**98* Scrolls the region of an element into view. If the region will not fit,99* it will be aligned at the top-left or centered, depending on100* "center".101*102* @param {!Element} elem The element with the region to scroll into view.103* @param {!goog.math.Rect} region The region, relative to the element's104* border box, to scroll into view.105* @param {boolean} center If true, when the region is too big to view,106* center it instead of aligning with the top-left.107* @private108*/109webdriver.chrome.scrollIntoView_ = function(elem, region, center) {110function scrollHelper(scrollable, size, offset, region, center) {111region = new goog.math.Rect(112offset.x + region.left, offset.y + region.top,113region.width, region.height);114115var scroll = webdriver.chrome.computeScrollOffsets_(size, region, center);116scrollable.scrollLeft += scroll.x;117scrollable.scrollTop += scroll.y;118}119120function getContainer(elem) {121var container = elem.parentNode;122if (SHADOW_DOM_ENABLED && (container instanceof ShadowRoot)) {123container = elem.host;124}125return container;126}127128var doc = goog.dom.getOwnerDocument(elem);129var container = getContainer(elem);130var offset;131while (container &&132container != doc.documentElement &&133container != doc.body) {134offset = webdriver.chrome.computeOffsetInContainer_(135/** @type {!Element} */ (container), elem);136var containerSize = new goog.math.Size(container.clientWidth,137container.clientHeight);138scrollHelper(container, containerSize, offset, region, center);139container = getContainer(container);140}141142offset = goog.style.getClientPosition(elem);143var windowSize = goog.dom.getDomHelper(elem).getViewportSize();144// Chrome uses either doc.documentElement or doc.body, depending on145// compatibility settings. For reliability, call scrollHelper on both.146// Calling scrollHelper on the wrong object is harmless.147scrollHelper(doc.documentElement, windowSize, offset, region, center);148if (doc.body)149scrollHelper(doc.body, windowSize, offset, region, center);150};151152153/**154* Scrolls a region of the given element into the client's view and returns155* its position relative to the client viewport. If the element or region is too156* large to fit in the view, it will be centered or aligned to the top-left,157* depending on the value of "center".158*159* scrollIntoView is not used because it does not work correctly in Chrome:160* http://crbug.com/73953.161*162* The element should be attached to the current document.163*164* @param {!Element} elem The element to use.165* @param {boolean} center If true, center the region when it is too big166* to fit in the view.167* @param {!goog.math.Rect} opt_region The region relative to the element's168* border box to be scrolled into view. If null, the border box will be169* used.170* @return {!goog.math.Coordinate} The top-left coordinate of the element's171* region in client space.172*/173webdriver.chrome.getLocationInView = function(elem, center, opt_region) {174var region = opt_region;175if (!region)176region = new goog.math.Rect(0, 0, elem.offsetWidth, elem.offsetHeight);177178if (elem != elem.ownerDocument.documentElement)179webdriver.chrome.scrollIntoView_(elem, region, center);180181var elemClientPos = goog.style.getClientPosition(elem);182return new goog.math.Coordinate(183elemClientPos.x + region.left, elemClientPos.y + region.top);184};185186187/**188* Returns the first client rect of the given element, relative to the189* element's border box. If the element does not have any client rects,190* throws an error.191*192* @param {!Element} elem The element to use.193* @return {!goog.math.Rect} The first client rect of the given element,194* relative to the element's border box.195*/196webdriver.chrome.getFirstClientRect = function(elem) {197var clientRects = elem.getClientRects();198if (clientRects.length == 0)199throw new Error('Element does not have any client rects');200var clientRect = clientRects[0];201var clientPos = goog.style.getClientPosition(elem);202return new goog.math.Rect(203clientRect.left - clientPos.x, clientRect.top - clientPos.y,204clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);205};206207208/**209* Returns whether the element or any of its descendants would receive a click210* at the given location. Useful for debugging test clicking issues.211*212* @param {!Element} elem The element to use.213* @param {!goog.math.Coordinate} coord The coordinate to use.214* @return {{clickable:boolean, message: (string|undefined)}} Object containing215* a boolean "clickable" property, as to whether it can be clicked, and an216* optional "message" string property, which contains any warning/error217* message.218*/219webdriver.chrome.isElementClickable = function(elem, coord) {220/**221* @param {boolean} clickable .222* @param {string=} opt_msg .223* @return {{clickable: boolean, message: (string|undefined)}} .224*/225function makeResult(clickable, opt_msg) {226var dict = {'clickable': clickable};227if (opt_msg)228dict['message'] = opt_msg;229return dict;230}231232// get the outermost ancestor of the element. This will be either the document233// or a shadow root.234var owner = elem;235while (owner.parentNode) {236owner = owner.parentNode;237}238239var elemAtPoint = owner.elementFromPoint(coord.x, coord.y);240if (elemAtPoint == elem)241return makeResult(true);242243var coordStr = '(' + coord.x + ', ' + coord.y + ')';244if (elemAtPoint == null) {245return makeResult(246false, 'Element is not clickable at point ' + coordStr);247}248var elemAtPointHTML = elemAtPoint.outerHTML.replace(elemAtPoint.innerHTML,249elemAtPoint.hasChildNodes()250? '...' : '');251var parentElemIter = elemAtPoint.parentNode;252while (parentElemIter) {253if (parentElemIter == elem) {254return makeResult(255true,256'Element\'s descendant would receive the click. Consider ' +257'clicking the descendant instead. Descendant: ' +258elemAtPointHTML);259}260parentElemIter = parentElemIter.parentNode;261}262var elemHTML = elem.outerHTML.replace(elem.innerHTML,263elem.hasChildNodes() ? '...' : '');264return makeResult(265false,266'Element ' + elemHTML + ' is not clickable at point '267+ coordStr + '. Other element ' +268'would receive the click: ' + elemAtPointHTML);269};270271272/**273* Returns the current page zoom ratio for the page with the given element.274*275* @param {!Element} elem The element to use.276* @return {number} Page zoom ratio.277*/278webdriver.chrome.getPageZoom = function(elem) {279// From http://stackoverflow.com/questions/1713771/280// how-to-detect-page-zoom-level-in-all-modern-browsers281var doc = goog.dom.getOwnerDocument(elem);282var docElem = doc.documentElement;283var width = Math.max(284docElem.clientWidth, docElem.offsetWidth, docElem.scrollWidth);285return doc.width / width;286};287288/**289* Determines whether an element is what a user would call "shown". Mainly based290* on bot.dom.isShown, but with extra intelligence regarding shadow DOM.291*292* @param {!Element} elem The element to consider.293* @param {boolean=} opt_inComposedDom Whether to check if the element is shown294* within the composed DOM; defaults to false.295* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity296* when determining whether it is shown; defaults to false.297* @return {boolean} Whether or not the element is visible.298*/299webdriver.chrome.isElementDisplayed = function(elem,300opt_inComposedDom,301opt_ignoreOpacity) {302if (!bot.dom.isShown(elem, opt_ignoreOpacity)) {303return false;304}305// if it's not invisible then check if the element is within the shadow DOM306// of an invisible element, using recursive calls to this function307if (SHADOW_DOM_ENABLED) {308var topLevelNode = elem;309while (topLevelNode.parentNode) {310topLevelNode = topLevelNode.parentNode;311}312if (topLevelNode instanceof ShadowRoot) {313return webdriver.chrome.isElementDisplayed(topLevelNode.host,314opt_inComposedDom);315}316}317// if it's not invisible, or in a shadow DOM, then it's definitely visible318return true;319};320321/**322* Same as bot.locators.findElement (description copied below), but323* with workarounds for shadow DOM.324*325* Find the first element in the DOM matching the target. The target326* object should have a single key, the name of which determines the327* locator strategy and the value of which gives the value to be328* searched for. For example {id: 'foo'} indicates that the first329* element on the DOM with the ID 'foo' should be returned.330*331* @param {!Object} target The selector to search for.332* @param {(Document|Element)=} opt_root The node from which to start the333* search. If not specified, will use {@code document} as the root.334* @return {Element} The first matching element found in the DOM, or null if no335* such element could be found.336*/337webdriver.chrome.findElement = function(target, opt_root) {338// This works fine if opt_root is outside of a shadow DOM, but for various339// (presumably performance-based) reasons, it works by getting opt_root's340// owning document, searching that, and then checking if the result is owned341// by opt_root. Searching the owning document for a child of a shadow root342// obviously doesn't work. However we try the performance-optimised version343// first...344var elem = bot.locators.findElement(target, opt_root);345if (elem) {346return elem;347}348// If we didn't find anything using that method, check to see if opt_root349// is within a shadow DOM...350if (SHADOW_DOM_ENABLED && opt_root) {351var topLevelNode = opt_root;352while (topLevelNode.parentNode) {353topLevelNode = topLevelNode.parentNode;354}355if (topLevelNode instanceof ShadowRoot) {356// findElement_s_ works fine if passed an root that's in a shadow root.357elem = bot.locators.findElements(target, opt_root)[0];358if (elem) {359return elem;360}361}362}363return null;364};365366367