// 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 DOM manipulation and querying routines.19*/2021goog.provide('bot.dom');2223goog.require('bot');24goog.require('bot.color');25goog.require('bot.dom.core');26goog.require('bot.locators.css');27goog.require('bot.userAgent');28goog.require('goog.array');29goog.require('goog.dom');30goog.require('goog.dom.DomHelper');31goog.require('goog.dom.NodeType');32goog.require('goog.dom.TagName');33goog.require('goog.math');34goog.require('goog.math.Coordinate');35goog.require('goog.math.Rect');36goog.require('goog.string');37goog.require('goog.style');38goog.require('goog.userAgent');394041/**42* Whether Shadow DOM operations are supported by the browser.43* @const {boolean}44*/45bot.dom.IS_SHADOW_DOM_ENABLED = (typeof ShadowRoot === 'function');464748/**49* Retrieves the active element for a node's owner document.50* @param {(!Node|!Window)} nodeOrWindow The node whose owner document to get51* the active element for.52* @return {?Element} The active element, if any.53*/54bot.dom.getActiveElement = function (nodeOrWindow) {55var active = goog.dom.getActiveElement(56goog.dom.getOwnerDocument(nodeOrWindow));57// IE has the habit of returning an empty object from58// goog.dom.getActiveElement instead of null.59if (goog.userAgent.IE &&60active &&61typeof active.nodeType === 'undefined') {62return null;63}64return active;65};666768/**69* @const70*/71bot.dom.isElement = bot.dom.core.isElement;727374/**75* Returns whether an element is in an interactable state: whether it is shown76* to the user, ignoring its opacity, and whether it is enabled.77*78* @param {!Element} element The element to check.79* @return {boolean} Whether the element is interactable.80* @see bot.dom.isShown.81* @see bot.dom.isEnabled82*/83bot.dom.isInteractable = function (element) {84return bot.dom.isShown(element, /*ignoreOpacity=*/true) &&85bot.dom.isEnabled(element) &&86!bot.dom.hasPointerEventsDisabled_(element);87};888990/**91* @param {!Element} element Element.92* @return {boolean} Whether element is set by the CSS pointer-events property93* not to be interactable.94* @private95*/96bot.dom.hasPointerEventsDisabled_ = function (element) {97if (goog.userAgent.IE ||98(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {99// Don't support pointer events100return false;101}102return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';103};104105106/**107* @const108*/109bot.dom.isSelectable = bot.dom.core.isSelectable;110111112/**113* @const114*/115bot.dom.isSelected = bot.dom.core.isSelected;116117118/**119* List of the focusable fields, according to120* http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus121* @private {!Array.<!goog.dom.TagName>}122* @const123*/124bot.dom.FOCUSABLE_FORM_FIELDS_ = [125goog.dom.TagName.A,126goog.dom.TagName.AREA,127goog.dom.TagName.BUTTON,128goog.dom.TagName.INPUT,129goog.dom.TagName.LABEL,130goog.dom.TagName.SELECT,131goog.dom.TagName.TEXTAREA132];133134135/**136* Returns whether a node is a focusable element. An element may receive focus137* if it is a form field, has a non-negative tabindex, or is editable.138* @param {!Element} element The node to test.139* @return {boolean} Whether the node is focusable.140*/141bot.dom.isFocusable = function (element) {142return goog.array.some(bot.dom.FOCUSABLE_FORM_FIELDS_, tagNameMatches) ||143(bot.dom.getAttribute(element, 'tabindex') != null &&144Number(bot.dom.getProperty(element, 'tabIndex')) >= 0) ||145bot.dom.isEditable(element);146147function tagNameMatches(tagName) {148return bot.dom.isElement(element, tagName);149}150};151152153/**154* @const155*/156bot.dom.getProperty = bot.dom.core.getProperty;157158159/**160* @const161*/162bot.dom.getAttribute = bot.dom.core.getAttribute;163164165/**166* List of elements that support the "disabled" attribute, as defined by the167* HTML 4.01 specification.168* @private {!Array.<!goog.dom.TagName>}169* @const170* @see http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1171*/172bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_ = [173goog.dom.TagName.BUTTON,174goog.dom.TagName.INPUT,175goog.dom.TagName.OPTGROUP,176goog.dom.TagName.OPTION,177goog.dom.TagName.SELECT,178goog.dom.TagName.TEXTAREA179];180181182/**183* Determines if an element is enabled. An element is considered enabled if it184* does not support the "disabled" attribute, or if it is not disabled.185* @param {!Element} el The element to test.186* @return {boolean} Whether the element is enabled.187*/188bot.dom.isEnabled = function (el) {189var isSupported = goog.array.some(190bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_,191function (tagName) { return bot.dom.isElement(el, tagName); });192if (!isSupported) {193return true;194}195196if (bot.dom.getProperty(el, 'disabled')) {197return false;198}199200// The element is not explicitly disabled, but if it is an OPTION or OPTGROUP,201// we must test if it inherits its state from a parent.202if (el.parentNode &&203el.parentNode.nodeType == goog.dom.NodeType.ELEMENT &&204bot.dom.isElement(el, goog.dom.TagName.OPTGROUP) ||205bot.dom.isElement(el, goog.dom.TagName.OPTION)) {206return bot.dom.isEnabled(/**@type{!Element}*/(el.parentNode));207}208209// Is there an ancestor of the current element that is a disabled fieldset210// and whose child is also an ancestor-or-self of the current element but is211// not the first legend child of the fieldset. If so then the element is212// disabled.213return !goog.dom.getAncestor(el, function (e) {214var parent = e.parentNode;215216if (parent &&217bot.dom.isElement(parent, goog.dom.TagName.FIELDSET) &&218bot.dom.getProperty(/** @type {!Element} */(parent), 'disabled')) {219if (!bot.dom.isElement(e, goog.dom.TagName.LEGEND)) {220return true;221}222223var sibling = e;224// Are there any previous legend siblings? If so then we are not the225// first and the element is disabled226while (sibling = goog.dom.getPreviousElementSibling(sibling)) {227if (bot.dom.isElement(sibling, goog.dom.TagName.LEGEND)) {228return true;229}230}231}232return false;233}, true);234};235236237/**238* List of input types that create text fields.239* @private {!Array.<string>}240* @const241* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#attr-input-type242*/243bot.dom.TEXTUAL_INPUT_TYPES_ = [244'text',245'search',246'tel',247'url',248'email',249'password',250'number'251];252253254/**255* TODO: Add support for designMode elements.256*257* @param {!Element} element The element to check.258* @return {boolean} Whether the element accepts user-typed text.259*/260bot.dom.isTextual = function (element) {261if (bot.dom.isElement(element, goog.dom.TagName.TEXTAREA)) {262return true;263}264265if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {266var type = element.type.toLowerCase();267return goog.array.contains(bot.dom.TEXTUAL_INPUT_TYPES_, type);268}269270if (bot.dom.isContentEditable(element)) {271return true;272}273274return false;275};276277278/**279* @param {!Element} element The element to check.280* @return {boolean} Whether the element is a file input.281*/282bot.dom.isFileInput = function (element) {283if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {284var type = element.type.toLowerCase();285return type == 'file';286}287288return false;289};290291292/**293* @param {!Element} element The element to check.294* @param {string} inputType The type of input to check.295* @return {boolean} Whether the element is an input with specified type.296*/297bot.dom.isInputType = function (element, inputType) {298if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {299var type = element.type.toLowerCase();300return type == inputType;301}302303return false;304};305306307/**308* @param {!Element} element The element to check.309* @return {boolean} Whether the element is contentEditable.310*/311bot.dom.isContentEditable = function (element) {312// Check if browser supports contentEditable.313if (element['contentEditable'] === undefined) {314return false;315}316317// Checking the element's isContentEditable property is preferred except for318// IE where that property is not reliable on IE versions 7, 8, and 9.319if (!goog.userAgent.IE && element['isContentEditable'] !== undefined) {320return element.isContentEditable;321}322323// For IE and for browsers where contentEditable is supported but324// isContentEditable is not, traverse up the ancestors:325function legacyIsContentEditable(e) {326if (e.contentEditable == 'inherit') {327var parent = bot.dom.getParentElement(e);328return parent ? legacyIsContentEditable(parent) : false;329} else {330return e.contentEditable == 'true';331}332}333return legacyIsContentEditable(element);334};335336337/**338* TODO: Merge isTextual into this function and move to bot.dom.339* For Puppet, requires adding support to getVisibleText for grabbing340* text from all textual elements.341*342* Whether the element may contain text the user can edit.343*344* @param {!Element} element The element to check.345* @return {boolean} Whether the element accepts user-typed text.346*/347bot.dom.isEditable = function (element) {348return (bot.dom.isTextual(element) ||349bot.dom.isFileInput(element) ||350bot.dom.isInputType(element, 'range') ||351bot.dom.isInputType(element, 'date') ||352bot.dom.isInputType(element, 'month') ||353bot.dom.isInputType(element, 'week') ||354bot.dom.isInputType(element, 'time') ||355bot.dom.isInputType(element, 'datetime-local') ||356bot.dom.isInputType(element, 'color')) &&357!bot.dom.getProperty(element, 'readOnly');358};359360361/**362* Returns the parent element of the given node, or null. This is required363* because the parent node may not be another element.364*365* @param {!Node} node The node who's parent is desired.366* @return {Element} The parent element, if available, null otherwise.367*/368bot.dom.getParentElement = function (node) {369var elem = node.parentNode;370371while (elem &&372elem.nodeType != goog.dom.NodeType.ELEMENT &&373elem.nodeType != goog.dom.NodeType.DOCUMENT &&374elem.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {375elem = elem.parentNode;376}377return /** @type {Element} */ (bot.dom.isElement(elem) ? elem : null);378};379380381/**382* Retrieves an explicitly-set, inline style value of an element. This returns383* '' if there isn't a style attribute on the element or if this style property384* has not been explicitly set in script.385*386* @param {!Element} elem Element to get the style value from.387* @param {string} styleName Name of the style property in selector-case.388* @return {string} The value of the style property.389*/390bot.dom.getInlineStyle = function (elem, styleName) {391return goog.style.getStyle(elem, styleName);392};393394395/**396* Retrieves the implicitly-set, effective style of an element, or null if it is397* unknown. It returns the computed style where available; otherwise it looks398* up the DOM tree for the first style value not equal to 'inherit,' using the399* IE currentStyle of each node if available, and otherwise the inline style.400* Since the computed, current, and inline styles can be different, the return401* value of this function is not always consistent across browsers. See:402* http://code.google.com/p/doctype/wiki/ArticleComputedStyleVsCascadedStyle403*404* @param {!Element} elem Element to get the style value from.405* @param {string} propertyName Name of the CSS property.406* @return {?string} The value of the style property, or null.407*/408bot.dom.getEffectiveStyle = function (elem, propertyName) {409var styleName = goog.string.toCamelCase(propertyName);410if (styleName == 'float' ||411styleName == 'cssFloat' ||412styleName == 'styleFloat') {413styleName = bot.userAgent.IE_DOC_PRE9 ? 'styleFloat' : 'cssFloat';414}415var style = goog.style.getComputedStyle(elem, styleName) ||416bot.dom.getCascadedStyle_(elem, styleName);417if (style === null) {418return null;419}420return bot.color.standardizeColor(styleName, style);421};422423424/**425* Looks up the DOM tree for the first style value not equal to 'inherit,' using426* the currentStyle of each node if available, and otherwise the inline style.427*428* @param {!Element} elem Element to get the style value from.429* @param {string} styleName CSS style property in camelCase.430* @return {?string} The value of the style property, or null.431* @private432*/433bot.dom.getCascadedStyle_ = function (elem, styleName) {434var style = elem.currentStyle || elem.style;435var value = style[styleName];436if (value === undefined && typeof style.getPropertyValue === 'function') {437value = style.getPropertyValue(styleName);438}439440if (value != 'inherit') {441return value !== undefined ? value : null;442}443var parent = bot.dom.getParentElement(elem);444return parent ? bot.dom.getCascadedStyle_(parent, styleName) : null;445};446447448/**449* Extracted code from bot.dom.isShown.450*451* @param {!Element} elem The element to consider.452* @param {boolean} ignoreOpacity Whether to ignore the element's opacity453* when determining whether it is shown.454* @param {function(!Element):boolean} displayedFn a function that's used455* to tell if the chain of ancestors or descendants are all shown.456* @return {boolean} Whether or not the element is visible.457* @private458*/459bot.dom.isShown_ = function (elem, ignoreOpacity, displayedFn) {460if (!bot.dom.isElement(elem)) {461throw new Error('Argument to isShown must be of type Element');462}463464// By convention, BODY element is always shown: BODY represents the document465// and even if there's nothing rendered in there, user can always see there's466// the document.467if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) {468return true;469}470471// Option or optgroup is shown iff enclosing select is shown (ignoring the472// select's opacity).473if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) ||474bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) {475var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function (e) {476return bot.dom.isElement(e, goog.dom.TagName.SELECT);477}));478return !!select && bot.dom.isShown_(select, true, displayedFn);479}480481// Image map elements are shown if image that uses it is shown, and482// the area of the element is positive.483var imageMap = bot.dom.maybeFindImageMap_(elem);484if (imageMap) {485return !!imageMap.image &&486imageMap.rect.width > 0 && imageMap.rect.height > 0 &&487bot.dom.isShown_(488imageMap.image, ignoreOpacity, displayedFn);489}490491// Any hidden input is not shown.492if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) &&493elem.type.toLowerCase() == 'hidden') {494return false;495}496497// Any NOSCRIPT element is not shown.498if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) {499return false;500}501502// Any element with hidden/collapsed visibility is not shown.503var visibility = bot.dom.getEffectiveStyle(elem, 'visibility');504if (visibility == 'collapse' || visibility == 'hidden') {505return false;506}507508if (!displayedFn(elem)) {509return false;510}511512// Any transparent element is not shown.513if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) {514return false;515}516517// Any element without positive size dimensions is not shown.518function positiveSize(e) {519var rect = bot.dom.getClientRect(e);520if (rect.height > 0 && rect.width > 0) {521return true;522}523// A vertical or horizontal SVG Path element will report zero width or524// height but is "shown" if it has a positive stroke-width.525if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) {526var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');527return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);528}529530// Any element with hidden/collapsed visibility is not shown.531var visibility = bot.dom.getEffectiveStyle(e, 'visibility');532if (visibility == 'collapse' || visibility == 'hidden') {533return false;534}535536if (!displayedFn(e)) {537return false;538}539// Zero-sized elements should still be considered to have positive size540// if they have a child element or text node with positive size, unless541// the element has an 'overflow' style of 'hidden'.542// Note: Text nodes containing only structural whitespace (with newlines543// or tabs) are ignored as they are likely just HTML formatting, not544// visible content.545return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' &&546goog.array.some(e.childNodes, function (n) {547if (n.nodeType == goog.dom.NodeType.TEXT) {548var text = n.nodeValue;549// Ignore text nodes that are purely structural whitespace550// (contain newlines or tabs and nothing else besides spaces)551if (/^[\s]*$/.test(text) && /[\n\r\t]/.test(text)) {552return false;553}554return true;555}556return bot.dom.isElement(n) && positiveSize(n);557});558}559if (!positiveSize(elem)) {560return false;561}562563// Elements that are hidden by overflow are not shown.564function hiddenByOverflow(e) {565return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&566goog.array.every(e.childNodes, function (n) {567return !bot.dom.isElement(n) || hiddenByOverflow(n) ||568!positiveSize(n);569});570}571return !hiddenByOverflow(elem);572};573574575/**576* Determines whether an element is what a user would call "shown". This means577* that the element is shown in the viewport of the browser, and only has578* height and width greater than 0px, and that its visibility is not "hidden"579* and its display property is not "none".580* Options and Optgroup elements are treated as special cases: they are581* considered shown iff they have a enclosing select element that is shown.582*583* Elements in Shadow DOMs with younger shadow roots are not visible, and584* elements distributed into shadow DOMs check the visibility of the585* ancestors in the Composed DOM, rather than their ancestors in the logical586* DOM.587*588* @param {!Element} elem The element to consider.589* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity590* when determining whether it is shown; defaults to false.591* @return {boolean} Whether or not the element is visible.592*/593bot.dom.isShown = function (elem, opt_ignoreOpacity) {594/**595* Determines whether an element or its parents have `display: none` or similar CSS properties set596* @param {!Node} e the element597* @return {!boolean}598*/599function displayed(e) {600if (bot.dom.isElement(e)) {601var elem = /** @type {!Element} */ (e);602if ((bot.dom.getEffectiveStyle(elem, 'display') == 'none')603|| (bot.dom.getEffectiveStyle(elem, 'content-visibility') == 'hidden')) {604return false;605}606}607608var parent = bot.dom.getParentNodeInComposedDom(e);609610if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {611if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {612// There is a younger shadow root, which will take precedence over613// the shadow this element is in, thus this element won't be614// displayed.615return false;616} else {617parent = parent.host;618}619}620621if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||622parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {623return true;624}625626// Child of DETAILS element is not shown unless the DETAILS element is open627// or the child is a SUMMARY element.628if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) &&629!parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) {630return false;631}632633return !!parent && displayed(parent);634}635636return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);637};638639640/**641* The kind of overflow area in which an element may be located. NONE if it does642* not overflow any ancestor element; HIDDEN if it overflows and cannot be643* scrolled into view; SCROLL if it overflows but can be scrolled into view.644*645* @enum {string}646*/647bot.dom.OverflowState = {648NONE: 'none',649HIDDEN: 'hidden',650SCROLL: 'scroll'651};652653654/**655* Returns the overflow state of the given element.656*657* If an optional coordinate or rectangle region is provided, returns the658* overflow state of that region relative to the element. A coordinate is659* treated as a 1x1 rectangle whose top-left corner is the coordinate.660*661* @param {!Element} elem Element.662* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region663* Coordinate or rectangle relative to the top-left corner of the element.664* @return {bot.dom.OverflowState} Overflow state of the element.665*/666bot.dom.getOverflowState = function (elem, opt_region) {667var region = bot.dom.getClientRegion(elem, opt_region);668var ownerDoc = goog.dom.getOwnerDocument(elem);669var htmlElem = ownerDoc.documentElement;670var bodyElem = ownerDoc.body;671var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow');672var treatAsFixedPosition;673674// Return the closest ancestor that the given element may overflow.675function getOverflowParent(e) {676var position = bot.dom.getEffectiveStyle(e, 'position');677if (position == 'fixed') {678treatAsFixedPosition = true;679// Fixed-position element may only overflow the viewport.680return e == htmlElem ? null : htmlElem;681} else {682var parent = bot.dom.getParentElement(e);683while (parent && !canBeOverflowed(parent)) {684parent = bot.dom.getParentElement(parent);685}686return parent;687}688689function canBeOverflowed(container) {690// The HTML element can always be overflowed.691if (container == htmlElem) {692return true;693}694// An element cannot overflow an element with an inline or contents display style.695var containerDisplay = /** @type {string} */ (696bot.dom.getEffectiveStyle(container, 'display'));697if (goog.string.startsWith(containerDisplay, 'inline') ||698(containerDisplay == 'contents')) {699return false;700}701// An absolute-positioned element cannot overflow a static-positioned one.702if (position == 'absolute' &&703bot.dom.getEffectiveStyle(container, 'position') == 'static') {704return false;705}706return true;707}708}709710// Return the x and y overflow styles for the given element.711function getOverflowStyles(e) {712// When the <html> element has an overflow style of 'visible', it assumes713// the overflow style of the body, and the body is really overflow:visible.714var overflowElem = e;715if (htmlOverflowStyle == 'visible') {716// Note: bodyElem will be null/undefined in SVG documents.717if (e == htmlElem && bodyElem) {718overflowElem = bodyElem;719} else if (e == bodyElem) {720return { x: 'visible', y: 'visible' };721}722}723var overflow = {724x: bot.dom.getEffectiveStyle(overflowElem, 'overflow-x'),725y: bot.dom.getEffectiveStyle(overflowElem, 'overflow-y')726};727// The <html> element cannot have a genuine 'visible' overflow style,728// because the viewport can't expand; 'visible' is really 'auto'.729if (e == htmlElem) {730overflow.x = overflow.x == 'visible' ? 'auto' : overflow.x;731overflow.y = overflow.y == 'visible' ? 'auto' : overflow.y;732}733return overflow;734}735736// Returns the scroll offset of the given element.737function getScroll(e) {738if (e == htmlElem) {739return new goog.dom.DomHelper(ownerDoc).getDocumentScroll();740} else {741return new goog.math.Coordinate(e.scrollLeft, e.scrollTop);742}743}744745// Check if the element overflows any ancestor element.746for (var container = getOverflowParent(elem);747!!container;748container = getOverflowParent(container)) {749var containerOverflow = getOverflowStyles(container);750751// If the container has overflow:visible, the element cannot overflow it.752if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') {753continue;754}755756var containerRect = bot.dom.getClientRect(container);757758// Zero-sized containers without overflow:visible hide all descendants.759if (containerRect.width == 0 || containerRect.height == 0) {760return bot.dom.OverflowState.HIDDEN;761}762763// Check "underflow": if an element is to the left or above the container764var underflowsX = region.right < containerRect.left;765var underflowsY = region.bottom < containerRect.top;766if ((underflowsX && containerOverflow.x == 'hidden') ||767(underflowsY && containerOverflow.y == 'hidden')) {768return bot.dom.OverflowState.HIDDEN;769} else if ((underflowsX && containerOverflow.x != 'visible') ||770(underflowsY && containerOverflow.y != 'visible')) {771// When the element is positioned to the left or above a container, we772// have to distinguish between the element being completely outside the773// container and merely scrolled out of view within the container.774var containerScroll = getScroll(container);775var unscrollableX = region.right < containerRect.left - containerScroll.x;776var unscrollableY = region.bottom < containerRect.top - containerScroll.y;777if ((unscrollableX && containerOverflow.x != 'visible') ||778(unscrollableY && containerOverflow.x != 'visible')) {779return bot.dom.OverflowState.HIDDEN;780}781var containerState = bot.dom.getOverflowState(container);782return containerState == bot.dom.OverflowState.HIDDEN ?783bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;784}785786// Check "overflow": if an element is to the right or below a container787var overflowsX = region.left >= containerRect.left + containerRect.width;788var overflowsY = region.top >= containerRect.top + containerRect.height;789if ((overflowsX && containerOverflow.x == 'hidden') ||790(overflowsY && containerOverflow.y == 'hidden')) {791return bot.dom.OverflowState.HIDDEN;792} else if ((overflowsX && containerOverflow.x != 'visible') ||793(overflowsY && containerOverflow.y != 'visible')) {794// If the element has fixed position and falls outside the scrollable area795// of the document, then it is hidden.796if (treatAsFixedPosition) {797var docScroll = getScroll(container);798if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||799(region.right >= htmlElem.scrollHeight - docScroll.y)) {800return bot.dom.OverflowState.HIDDEN;801}802}803// If the element can be scrolled into view of the parent, it has a scroll804// state; unless the parent itself is entirely hidden by overflow, in805// which it is also hidden by overflow.806var containerState = bot.dom.getOverflowState(container);807return containerState == bot.dom.OverflowState.HIDDEN ?808bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;809}810}811812// Does not overflow any ancestor.813return bot.dom.OverflowState.NONE;814};815816817/**818* A regular expression to match the CSS transform matrix syntax.819* @private {!RegExp}820* @const821*/822bot.dom.CSS_TRANSFORM_MATRIX_REGEX_ =823new RegExp('matrix\\(([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +824'([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +825'([\\d\\.\\-]+)(?:px)?, ([\\d\\.\\-]+)(?:px)?\\)');826827828/**829* Gets the client rectangle of the DOM element. It often returns the same value830* as Element.getBoundingClientRect, but is "fixed" for various scenarios:831* 1. Like goog.style.getClientPosition, it adjusts for the inset border in IE.832* 2. Gets a rect for <map>'s and <area>'s relative to the image using them.833* 3. Gets a rect for SVG elements representing their true bounding box.834* 4. Defines the client rect of the <html> element to be the window viewport.835*836* @param {!Element} elem The element to use.837* @return {!goog.math.Rect} The interaction box of the element.838*/839bot.dom.getClientRect = function (elem) {840var imageMap = bot.dom.maybeFindImageMap_(elem);841if (imageMap) {842return imageMap.rect;843} else if (bot.dom.isElement(elem, goog.dom.TagName.HTML)) {844// Define the client rect of the <html> element to be the viewport.845var doc = goog.dom.getOwnerDocument(elem);846var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(doc));847return new goog.math.Rect(0, 0, viewportSize.width, viewportSize.height);848} else {849var nativeRect;850try {851// TODO: in IE and Firefox, getBoundingClientRect includes stroke width,852// but getBBox does not.853nativeRect = elem.getBoundingClientRect();854} catch (e) {855// On IE < 9, calling getBoundingClientRect on an orphan element raises856// an "Unspecified Error". All other browsers return zeros.857return new goog.math.Rect(0, 0, 0, 0);858}859860var rect = new goog.math.Rect(nativeRect.left, nativeRect.top,861nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);862863// In IE, the element can additionally be offset by a border around the864// documentElement or body element that we have to subtract.865if (goog.userAgent.IE && elem.ownerDocument.body) {866var doc = goog.dom.getOwnerDocument(elem);867rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;868rect.top -= doc.documentElement.clientTop + doc.body.clientTop;869}870871return rect;872}873};874875876/**877* If given a <map> or <area> element, finds the corresponding image and client878* rectangle of the element; otherwise returns null. The return value is an879* object with 'image' and 'rect' properties. When no image uses the given880* element, the returned rectangle is present but has zero size.881*882* @param {!Element} elem Element to test.883* @return {?{image: Element, rect: !goog.math.Rect}} Image and rectangle.884* @private885*/886bot.dom.maybeFindImageMap_ = function (elem) {887// If not a <map> or <area>, return null indicating so.888var isMap = bot.dom.isElement(elem, goog.dom.TagName.MAP);889if (!isMap && !bot.dom.isElement(elem, goog.dom.TagName.AREA)) {890return null;891}892893// Get the <map> associated with this element, or null if none.894var map = isMap ? elem :895(bot.dom.isElement(elem.parentNode, goog.dom.TagName.MAP) ?896elem.parentNode : null);897898var image = null, rect = null;899if (map && map.name) {900var mapDoc = goog.dom.getOwnerDocument(map);901902// TODO: Restrict to applet, img, input:image, and object nodes.903var locator = '*[usemap="#' + map.name + '"]';904905// TODO: Break dependency of bot.locators on bot.dom,906// so bot.locators.findElement can be called here instead.907image = bot.locators.css.single(locator, mapDoc);908909if (image) {910rect = bot.dom.getClientRect(image);911if (!isMap && elem.shape.toLowerCase() != 'default') {912// Shift and crop the relative area rectangle to the map.913var relRect = bot.dom.getAreaRelativeRect_(elem);914var relX = Math.min(Math.max(relRect.left, 0), rect.width);915var relY = Math.min(Math.max(relRect.top, 0), rect.height);916var w = Math.min(relRect.width, rect.width - relX);917var h = Math.min(relRect.height, rect.height - relY);918rect = new goog.math.Rect(relX + rect.left, relY + rect.top, w, h);919}920}921}922923return { image: image, rect: rect || new goog.math.Rect(0, 0, 0, 0) };924};925926927/**928* Returns the bounding box around an <area> element relative to its enclosing929* <map>. Does not apply to <area> elements with shape=='default'.930*931* @param {!Element} area Area element.932* @return {!goog.math.Rect} Bounding box of the area element.933* @private934*/935bot.dom.getAreaRelativeRect_ = function (area) {936var shape = area.shape.toLowerCase();937var coords = area.coords.split(',');938if (shape == 'rect' && coords.length == 4) {939var x = coords[0], y = coords[1];940return new goog.math.Rect(x, y, coords[2] - x, coords[3] - y);941} else if (shape == 'circle' && coords.length == 3) {942var centerX = coords[0], centerY = coords[1], radius = coords[2];943return new goog.math.Rect(centerX - radius, centerY - radius,9442 * radius, 2 * radius);945} else if (shape == 'poly' && coords.length > 2) {946var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;947for (var i = 2; i + 1 < coords.length; i += 2) {948minX = Math.min(minX, coords[i]);949maxX = Math.max(maxX, coords[i]);950minY = Math.min(minY, coords[i + 1]);951maxY = Math.max(maxY, coords[i + 1]);952}953return new goog.math.Rect(minX, minY, maxX - minX, maxY - minY);954}955return new goog.math.Rect(0, 0, 0, 0);956};957958959/**960* Gets the element's client rectangle as a box, optionally clipped to the961* given coordinate or rectangle relative to the client's position. A coordinate962* is treated as a 1x1 rectangle whose top-left corner is the coordinate.963*964* @param {!Element} elem The element.965* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region966* Coordinate or rectangle relative to the top-left corner of the element.967* @return {!goog.math.Box} The client region box.968*/969bot.dom.getClientRegion = function (elem, opt_region) {970var region = bot.dom.getClientRect(elem).toBox();971972if (opt_region) {973var rect = opt_region instanceof goog.math.Rect ? opt_region :974new goog.math.Rect(opt_region.x, opt_region.y, 1, 1);975region.left = goog.math.clamp(976region.left + rect.left, region.left, region.right);977region.top = goog.math.clamp(978region.top + rect.top, region.top, region.bottom);979region.right = goog.math.clamp(980region.left + rect.width, region.left, region.right);981region.bottom = goog.math.clamp(982region.top + rect.height, region.top, region.bottom);983}984985return region;986};987988989/**990* Trims leading and trailing whitespace from strings, leaving non-breaking991* space characters in place.992*993* @param {string} str The string to trim.994* @return {string} str without any leading or trailing whitespace characters995* except non-breaking spaces.996* @private997*/998bot.dom.trimExcludingNonBreakingSpaceCharacters_ = function (str) {999return str.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, '');1000};100110021003/**1004* Helper function for getVisibleText[InDisplayedDom].1005* @param {!Array.<string>} lines Accumulated visible lines of text.1006* @return {string} cleaned up concatenated lines1007* @private1008*/1009bot.dom.concatenateCleanedLines_ = function (lines) {1010lines = goog.array.map(1011lines,1012bot.dom.trimExcludingNonBreakingSpaceCharacters_);1013var joined = lines.join('\n');1014var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined);10151016// Replace non-breakable spaces with regular ones.1017return trimmed.replace(/\xa0/g, ' ');1018};101910201021/**1022* @param {!Element} elem The element to consider.1023* @return {string} visible text.1024*/1025bot.dom.getVisibleText = function (elem) {1026var lines = [];10271028if (bot.dom.IS_SHADOW_DOM_ENABLED) {1029bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines);1030} else {1031bot.dom.appendVisibleTextLinesFromElement_(elem, lines);1032}1033return bot.dom.concatenateCleanedLines_(lines);1034};103510361037/**1038* Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and1039* bot.dom.appendVisibleTextLinesFromElementInComposedDom_1040* @param {!Element} elem Element.1041* @param {!Array.<string>} lines Accumulated visible lines of text.1042* @param {function(!Element):boolean} isShownFn function to call to1043* tell if an element is shown1044* @param {function(!Node, !Array.<string>, boolean, ?string, ?string):void}1045* childNodeFn function to call to append lines from any child nodes1046* @private1047*/1048bot.dom.appendVisibleTextLinesFromElementCommon_ = function (1049elem, lines, isShownFn, childNodeFn) {1050function currLine() {1051return /** @type {string|undefined} */ (goog.array.peek(lines)) || '';1052}10531054// TODO: Add case here for textual form elements.1055if (bot.dom.isElement(elem, goog.dom.TagName.BR)) {1056lines.push('');1057} else {1058// TODO: properly handle display:run-in1059var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD);1060var display = bot.dom.getEffectiveStyle(elem, 'display');1061// On some browsers, table cells incorrectly show up with block styles.1062var isBlock = !isTD &&1063!goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display);10641065// Add a newline before block elems when there is text on the current line,1066// except when the previous sibling has a display: run-in.1067// Also, do not run-in the previous sibling if this element is floated.10681069var previousElementSibling = goog.dom.getPreviousElementSibling(elem);1070var prevDisplay = (previousElementSibling) ?1071bot.dom.getEffectiveStyle(previousElementSibling, 'display') : '';1072// TODO: getEffectiveStyle should mask this for us1073var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') ||1074bot.dom.getEffectiveStyle(elem, 'cssFloat') ||1075bot.dom.getEffectiveStyle(elem, 'styleFloat');1076var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none';1077if (isBlock && !runIntoThis &&1078!goog.string.isEmptyOrWhitespace(currLine())) {1079lines.push('');1080}10811082// This element may be considered unshown, but have a child that is1083// explicitly shown (e.g. this element has "visibility:hidden").1084// Nevertheless, any text nodes that are direct descendants of this1085// element will not contribute to the visible text.1086var shown = isShownFn(elem);10871088// All text nodes that are children of this element need to know the1089// effective "white-space" and "text-transform" styles to properly1090// compute their contribution to visible text. Compute these values once.1091var whitespace = null, textTransform = null;1092if (shown) {1093whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');1094textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');1095}10961097goog.array.forEach(elem.childNodes, function (node) {1098childNodeFn(node, lines, shown, whitespace, textTransform);1099});11001101var line = currLine();11021103// Here we differ from standard innerText implementations (if there were1104// such a thing). Usually, table cells are separated by a tab, but we1105// normalize tabs into single spaces.1106if ((isTD || display == 'table-cell') && line &&1107!goog.string.endsWith(line, ' ')) {1108lines[lines.length - 1] += ' ';1109}11101111// Add a newline after block elems when there is text on the current line,1112// and the current element isn't marked as run-in.1113if (isBlock && display != 'run-in' &&1114!goog.string.isEmptyOrWhitespace(line)) {1115lines.push('');1116}1117}1118};111911201121/**1122* @param {!Element} elem Element.1123* @param {!Array.<string>} lines Accumulated visible lines of text.1124* @private1125*/1126bot.dom.appendVisibleTextLinesFromElement_ = function (elem, lines) {1127bot.dom.appendVisibleTextLinesFromElementCommon_(1128elem, lines, bot.dom.isShown,1129function (node, lines, shown, whitespace, textTransform) {1130if (node.nodeType == goog.dom.NodeType.TEXT && shown) {1131var textNode = /** @type {!Text} */ (node);1132bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,1133whitespace, textTransform);1134} else if (bot.dom.isElement(node)) {1135var castElem = /** @type {!Element} */ (node);1136bot.dom.appendVisibleTextLinesFromElement_(castElem, lines);1137}1138});1139};114011411142/**1143* Elements with one of these effective "display" styles are treated as inline1144* display boxes and have their visible text appended to the current line.1145* @private {!Array.<string>}1146* @const1147*/1148bot.dom.INLINE_DISPLAY_BOXES_ = [1149'inline',1150'inline-block',1151'inline-table',1152'none',1153'table-cell',1154'table-column',1155'table-column-group'1156];115711581159/**1160* @param {!Text} textNode Text node.1161* @param {!Array.<string>} lines Accumulated visible lines of text.1162* @param {?string} whitespace Parent element's "white-space" style.1163* @param {?string} textTransform Parent element's "text-transform" style.1164* @private1165*/1166bot.dom.appendVisibleTextLinesFromTextNode_ = function (textNode, lines,1167whitespace, textTransform) {11681169// First, remove zero-width characters. Do this before regularizing spaces as1170// the zero-width space is both zero-width and a space, but we do not want to1171// make it visible by converting it to a regular space.1172// The replaced characters are:1173// U+200B: Zero-width space1174// U+200E: Left-to-right mark1175// U+200F: Right-to-left mark1176var text = textNode.nodeValue.replace(/[\u200b\u200e\u200f]/g, '');11771178// Canonicalize the new lines, and then collapse new lines1179// for the whitespace styles that collapse. See:1180// https://developer.mozilla.org/en/CSS/white-space1181text = goog.string.canonicalizeNewlines(text);1182if (whitespace == 'normal' || whitespace == 'nowrap') {1183text = text.replace(/\n/g, ' ');1184}11851186// For pre and pre-wrap whitespace styles, convert all breaking spaces to be1187// non-breaking, otherwise, collapse all breaking spaces. Breaking spaces are1188// converted to regular spaces by getVisibleText().1189if (whitespace == 'pre' || whitespace == 'pre-wrap') {1190text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0');1191} else {1192text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' ');1193}11941195if (textTransform == 'capitalize') {1196// 1) don't treat '_' as a separator (protects snake_case)1197var re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])/g;1198text = text.replace(re, function () {1199return arguments[1] + arguments[2].toUpperCase();1200});12011202// 2) capitalize after opening "_" or "*"1203// Preceded by start or a non-word (so it won't fire for snake_case)1204re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])([_*])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24D0-\u24E9])/g;1205text = text.replace(re, function () {1206return arguments[1] + arguments[2] + arguments[3].toUpperCase();1207});1208} else if (textTransform == 'uppercase') {1209text = text.toUpperCase();1210} else if (textTransform == 'lowercase') {1211text = text.toLowerCase();1212}12131214var currLine = lines.pop() || '';1215if (goog.string.endsWith(currLine, ' ') &&1216goog.string.startsWith(text, ' ')) {1217text = text.substr(1);1218}1219lines.push(currLine + text);1220};122112221223/**1224* Gets the opacity of a node (x-browser).1225* This gets the inline style opacity of the node and takes into account the1226* cascaded or the computed style for this node.1227*1228* @param {!Element} elem Element whose opacity has to be found.1229* @return {number} Opacity between 0 and 1.1230*/1231bot.dom.getOpacity = function (elem) {1232// TODO: Does this need to deal with rgba colors?1233if (!bot.userAgent.IE_DOC_PRE9) {1234return bot.dom.getOpacityNonIE_(elem);1235} else {1236if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') {1237// Filter does not apply to non positioned elements.1238return 1;1239}12401241var opacityStyle = bot.dom.getEffectiveStyle(elem, 'filter');1242var groups = opacityStyle.match(/^alpha\(opacity=(\d*)\)/) ||1243opacityStyle.match(1244/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/);12451246if (groups) {1247return Number(groups[1]) / 100;1248} else {1249return 1; // Opaque.1250}1251}1252};125312541255/**1256* Implementation of getOpacity for browsers that do support1257* the "opacity" style.1258*1259* @param {!Element} elem Element whose opacity has to be found.1260* @return {number} Opacity between 0 and 1.1261* @private1262*/1263bot.dom.getOpacityNonIE_ = function (elem) {1264// By default the element is opaque.1265var elemOpacity = 1;12661267var opacityStyle = bot.dom.getEffectiveStyle(elem, 'opacity');1268if (opacityStyle) {1269elemOpacity = Number(opacityStyle);1270}12711272// Let's apply the parent opacity to the element.1273var parentElement = bot.dom.getParentElement(elem);1274if (parentElement) {1275elemOpacity = elemOpacity * bot.dom.getOpacityNonIE_(parentElement);1276}1277return elemOpacity;1278};127912801281/**1282* Returns the display parent element of the given node, or null. This method1283* differs from bot.dom.getParentElement in the presence of ShadowDOM and1284* <shadow> or <content> tags. For example if1285* <ul>1286* <li>div A contains div B1287* <li>div B has a css class .C1288* <li>div A contains a Shadow DOM with a div D1289* <li>div D contains a contents tag selecting all items of class .C1290* </ul>1291* then calling bot.dom.getParentElement on B will return A, but calling1292* getDisplayParentElement on B will return D.1293*1294* @param {!Node} node The node whose parent is desired.1295* @return {Node} The parent node, if available, null otherwise.1296*/1297bot.dom.getParentNodeInComposedDom = function (node) {1298var /**@type {Node}*/ parent = node.parentNode;12991300// Shadow DOM v11301if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {1302// Can be null on purpose, meaning it has no parent as1303// it hasn't yet been slotted1304return node.assignedSlot ? node.assignedSlot.parentNode : null;1305}13061307// Shadow DOM V0 (deprecated)1308if (node.getDestinationInsertionPoints) {1309var destinations = node.getDestinationInsertionPoints();1310if (destinations.length > 0) {1311return destinations[destinations.length - 1];1312}1313}13141315return parent;1316};131713181319/**1320* @param {!Node} node Node.1321* @param {!Array.<string>} lines Accumulated visible lines of text.1322* @param {boolean} shown whether the node is visible1323* @param {?string} whitespace the node's 'white-space' effectiveStyle1324* @param {?string} textTransform the node's 'text-transform' effectiveStyle1325* @private1326* @suppress {missingProperties}1327*/1328bot.dom.appendVisibleTextLinesFromNodeInComposedDom_ = function (1329node, lines, shown, whitespace, textTransform) {13301331if (node.nodeType == goog.dom.NodeType.TEXT && shown) {1332var textNode = /** @type {!Text} */ (node);1333bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,1334whitespace, textTransform);1335} else if (bot.dom.isElement(node)) {1336var castElem = /** @type {!Element} */ (node);13371338if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) {1339var parentNode = node;1340while (parentNode.parentNode) {1341parentNode = parentNode.parentNode;1342}1343if (parentNode instanceof ShadowRoot) {1344// If the element is <content> and we're inside a shadow DOM then just1345// append the contents of the nodes that have been distributed into it.1346var contentElem = /** @type {!Object} */ (node);1347var shadowChildren;1348if (bot.dom.isElement(node, 'CONTENT')) {1349shadowChildren = contentElem.getDistributedNodes();1350} else {1351shadowChildren = contentElem.assignedNodes();1352}1353const childrenToTraverse =1354shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes;1355goog.array.forEach(childrenToTraverse, function (node) {1356bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1357node, lines, shown, whitespace, textTransform);1358});1359} else {1360// if we're not inside a shadow DOM, then we just treat <content>1361// as an unknown element and use anything inside the tag1362bot.dom.appendVisibleTextLinesFromElementInComposedDom_(1363castElem, lines);1364}1365} else if (bot.dom.isElement(node, 'SHADOW')) {1366// if the element is <shadow> then find the owning shadowRoot1367var parentNode = node;1368while (parentNode.parentNode) {1369parentNode = parentNode.parentNode;1370}1371if (parentNode instanceof ShadowRoot) {1372var thisShadowRoot = /** @type {!ShadowRoot} */ (parentNode);1373if (thisShadowRoot) {1374// then go through the owning shadowRoots older siblings and append1375// their contents1376var olderShadowRoot = thisShadowRoot.olderShadowRoot;1377while (olderShadowRoot) {1378goog.array.forEach(1379olderShadowRoot.childNodes, function (childNode) {1380bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1381childNode, lines, shown, whitespace, textTransform);1382});1383olderShadowRoot = olderShadowRoot.olderShadowRoot;1384}1385}1386}1387} else {1388// otherwise append the contents of an element as per normal.1389bot.dom.appendVisibleTextLinesFromElementInComposedDom_(1390castElem, lines);1391}1392}1393};139413951396/**1397* Determines whether a given node has been distributed into a ShadowDOM1398* element somewhere.1399* @param {!Node} node The node to check1400* @return {boolean} True if the node has been distributed.1401*/1402bot.dom.isNodeDistributedIntoShadowDom = function (node) {1403var elemOrText = null;1404if (node.nodeType == goog.dom.NodeType.ELEMENT) {1405elemOrText = /** @type {!Element} */ (node);1406} else if (node.nodeType == goog.dom.NodeType.TEXT) {1407elemOrText = /** @type {!Text} */ (node);1408}1409return elemOrText != null &&1410(elemOrText.assignedSlot != null ||1411(elemOrText.getDestinationInsertionPoints &&1412elemOrText.getDestinationInsertionPoints().length > 0)1413);1414};141514161417/**1418* @param {!Element} elem Element.1419* @param {!Array.<string>} lines Accumulated visible lines of text.1420* @private1421*/1422bot.dom.appendVisibleTextLinesFromElementInComposedDom_ = function (1423elem, lines) {1424if (elem.shadowRoot) {1425// Get the effective styles from the shadow host element for text nodes in shadow DOM1426var whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');1427var textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');14281429goog.array.forEach(elem.shadowRoot.childNodes, function (node) {1430bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1431node, lines, true, whitespace, textTransform);1432});1433}14341435bot.dom.appendVisibleTextLinesFromElementCommon_(1436elem, lines, bot.dom.isShown,1437function (node, lines, shown, whitespace, textTransform) {1438// If the node has been distributed into a shadowDom element1439// to be displayed elsewhere, then we shouldn't append1440// its contents here).1441if (!bot.dom.isNodeDistributedIntoShadowDom(node)) {1442bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1443node, lines, shown, whitespace, textTransform);1444}1445});1446};144714481449