1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /* global XPCNativeWrapper */
8 const { assert } = ChromeUtils.import("chrome://marionette/content/assert.js");
9 const { atom } = ChromeUtils.import("chrome://marionette/content/atom.js");
14 StaleElementReferenceError,
15 } = ChromeUtils.import("chrome://marionette/content/error.js");
16 const { pprint } = ChromeUtils.import("chrome://marionette/content/format.js");
17 const { PollPromise } = ChromeUtils.import(
18 "chrome://marionette/content/sync.js"
21 this.EXPORTED_SYMBOLS = [
30 const ORDERED_NODE_ITERATOR_TYPE = 5;
31 const FIRST_ORDERED_NODE_TYPE = 9;
33 const ELEMENT_NODE = 1;
34 const DOCUMENT_NODE = 9;
36 const XBLNS = "http://www.mozilla.org/xbl";
37 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
39 /** XUL elements that support checked property. */
40 const XUL_CHECKED_ELS = new Set(["button", "checkbox", "toolbarbutton"]);
42 /** XUL elements that support selected property. */
43 const XUL_SELECTED_ELS = new Set([
52 const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
57 * This module provides shared functionality for dealing with DOM-
58 * and web elements in Marionette.
60 * A web element is an abstraction used to identify an element when it
61 * is transported across the protocol, between remote- and local ends.
63 * Each element has an associated web element reference (a UUID) that
64 * uniquely identifies the the element across all browsing contexts. The
65 * web element reference for every element representing the same element
68 * The {@link element.Store} provides a mapping between web element
69 * references and DOM elements for each browsing context. It also provides
70 * functionality for looking up and retrieving elements.
77 ClassName: "class name",
78 Selector: "css selector",
81 LinkText: "link text",
82 PartialLinkText: "partial link text",
86 AnonAttribute: "anon attribute",
90 * Stores known/seen elements and their associated web element
93 * Elements are added by calling {@link #add()} or {@link addAll()},
94 * and may be queried by their web element reference using {@link get()}.
99 element.Store = class {
109 * Make a collection of elements seen.
111 * The oder of the returned web element references is guaranteed to
112 * match that of the collection passed in.
114 * @param {NodeList} els
115 * Sequence of elements to add to set of seen elements.
117 * @return {Array.<WebElement>}
118 * List of the web element references associated with each element
119 * from <var>els</var>.
122 let add = this.add.bind(this);
123 return [...els].map(add);
127 * Make an element seen.
129 * @param {(Element|WindowProxy|XULElement)} el
130 * Element to add to set of seen elements.
132 * @return {WebElement}
133 * Web element reference associated with element.
135 * @throws {TypeError}
136 * If <var>el</var> is not an {@link Element} or a {@link XULElement}.
139 const isDOMElement = element.isDOMElement(el);
140 const isDOMWindow = element.isDOMWindow(el);
141 const isXULElement = element.isXULElement(el);
142 const context = isXULElement ? "chrome" : "content";
144 if (!(isDOMElement || isDOMWindow || isXULElement)) {
146 "Expected an element or WindowProxy, " + pprint`got: ${el}`
150 for (let i in this.els) {
153 foundEl = this.els[i].get();
157 if (new XPCNativeWrapper(foundEl) == new XPCNativeWrapper(el)) {
158 return WebElement.fromUUID(i, context);
161 // cleanup reference to gc'd element
167 let webEl = WebElement.from(el);
168 this.els[webEl.uuid] = Cu.getWeakReference(el);
173 * Determine if the provided web element reference has been seen
174 * before/is in the element store.
176 * Unlike when getting the element, a staleness check is not
179 * @param {WebElement} webEl
180 * Element's associated web element reference.
183 * True if element is in the store, false otherwise.
185 * @throws {TypeError}
186 * If <var>webEl</var> is not a {@link WebElement}.
189 if (!(webEl instanceof WebElement)) {
190 throw new TypeError(pprint`Expected web element, got: ${webEl}`);
192 return Object.keys(this.els).includes(webEl.uuid);
196 * Retrieve a DOM {@link Element} or a {@link XULElement} by its
197 * unique {@link WebElement} reference.
199 * @param {WebElement} webEl
200 * Web element reference to find the associated {@link Element}
202 * @param {WindowProxy} win
203 * Current browsing context, which may differ from the associate
204 * browsing context of <var>el</var>.
206 * @returns {(Element|XULElement)}
207 * Element associated with reference.
209 * @throws {TypeError}
210 * If <var>webEl</var> is not a {@link WebElement}.
211 * @throws {NoSuchElementError}
212 * If the web element reference <var>uuid</var> has not been
214 * @throws {StaleElementReferenceError}
215 * If the element has gone stale, indicating it is no longer
216 * attached to the DOM, or its node document is no longer the
220 if (!(webEl instanceof WebElement)) {
221 throw new TypeError(pprint`Expected web element, got: ${webEl}`);
223 if (!this.has(webEl)) {
224 throw new NoSuchElementError(
225 "Web element reference not seen before: " + webEl.uuid
230 let ref = this.els[webEl.uuid];
234 delete this.els[webEl.uuid];
237 if (element.isStale(el, win)) {
238 throw new StaleElementReferenceError(
239 pprint`The element reference of ${el || webEl.uuid} is stale; ` +
240 "either the element is no longer attached to the DOM, " +
241 "it is not in the current frame context, " +
242 "or the document has been refreshed"
251 * Find a single element or a collection of elements starting at the
252 * document root or a given node.
254 * If |timeout| is above 0, an implicit search technique is used.
255 * This will wait for the duration of <var>timeout</var> for the
256 * element to appear in the DOM.
258 * See the {@link element.Strategy} enum for a full list of supported
259 * search strategies that can be passed to <var>strategy</var>.
261 * Available flags for <var>opts</var>:
264 * <dt><code>all</code>
266 * If true, a multi-element search selector is used and a sequence
267 * of elements will be returned. Otherwise a single element.
269 * <dt><code>timeout</code>
271 * Duration to wait before timing out the search. If <code>all</code>
272 * is false, a {@link NoSuchElementError} is thrown if unable to
273 * find the element within the timeout duration.
275 * <dt><code>startNode</code>
276 * <dd>Element to use as the root of the search.
278 * @param {Object.<string, WindowProxy>} container
279 * Window object and an optional shadow root that contains the
280 * root shadow DOM element.
281 * @param {string} strategy
282 * Search strategy whereby to locate the element(s).
283 * @param {string} selector
284 * Selector search pattern. The selector must be compatible with
285 * the chosen search <var>strategy</var>.
286 * @param {Object.<string, ?>} opts
289 * @return {Promise.<(Element|Array.<Element>)>}
290 * Single element or a sequence of elements.
292 * @throws InvalidSelectorError
293 * If <var>strategy</var> is unknown.
294 * @throws InvalidSelectorError
295 * If <var>selector</var> is malformed.
296 * @throws NoSuchElementError
297 * If a single element is requested, this error will throw if the
298 * element is not found.
300 element.find = function(container, strategy, selector, opts = {}) {
301 let all = !!opts.all;
302 let timeout = opts.timeout || 0;
303 let startNode = opts.startNode;
307 searchFn = findElements.bind(this);
309 searchFn = findElement.bind(this);
312 return new Promise((resolve, reject) => {
313 let findElements = new PollPromise(
314 (resolve, reject) => {
315 let res = find_(container, strategy, selector, searchFn, {
319 if (res.length > 0) {
320 resolve(Array.from(res));
328 findElements.then(foundEls => {
329 // the following code ought to be moved into findElement
330 // and findElements when bug 1254486 is addressed
331 if (!opts.all && (!foundEls || foundEls.length == 0)) {
334 case element.Strategy.AnonAttribute:
336 "Unable to locate anonymous element: " + JSON.stringify(selector);
340 msg = "Unable to locate element: " + selector;
343 reject(new NoSuchElementError(msg));
349 resolve(foundEls[0]);
359 { startNode = null, all = false } = {}
361 let rootNode = container.shadowRoot || container.frame.document;
365 // For anonymous nodes the start node needs to be of type
366 // DOMElement, which will refer to :root in case of a DOMDocument.
367 case element.Strategy.Anon:
368 case element.Strategy.AnonAttribute:
369 if (rootNode.nodeType == rootNode.DOCUMENT_NODE) {
370 startNode = rootNode.documentElement;
375 startNode = rootNode;
381 res = searchFn(strategy, selector, rootNode, startNode);
383 throw new InvalidSelectorError(
384 `Given ${strategy} expression "${selector}" is invalid: ${e}`
398 * Find a single element by XPath expression.
400 * @param {HTMLDocument} document
402 * @param {Element} startNode
403 * Where in the DOM hiearchy to begin searching.
404 * @param {string} expression
405 * XPath search expression.
408 * First element matching <var>expression</var>.
410 element.findByXPath = function(document, startNode, expression) {
411 let iter = document.evaluate(
415 FIRST_ORDERED_NODE_TYPE,
418 return iter.singleNodeValue;
422 * Find elements by XPath expression.
424 * @param {HTMLDocument} document
426 * @param {Element} startNode
427 * Where in the DOM hierarchy to begin searching.
428 * @param {string} expression
429 * XPath search expression.
431 * @return {Iterable.<Node>}
432 * Iterator over elements matching <var>expression</var>.
434 element.findByXPathAll = function*(document, startNode, expression) {
435 let iter = document.evaluate(
439 ORDERED_NODE_ITERATOR_TYPE,
442 let el = iter.iterateNext();
445 el = iter.iterateNext();
450 * Find all hyperlinks descendant of <var>startNode</var> which
451 * link text is <var>linkText</var>.
453 * @param {Element} startNode
454 * Where in the DOM hierarchy to begin searching.
455 * @param {string} linkText
456 * Link text to search for.
458 * @return {Iterable.<HTMLAnchorElement>}
459 * Sequence of link elements which text is <var>s</var>.
461 element.findByLinkText = function(startNode, linkText) {
464 link => atom.getElementText(link).trim() === linkText
469 * Find all hyperlinks descendant of <var>startNode</var> which
470 * link text contains <var>linkText</var>.
472 * @param {Element} startNode
473 * Where in the DOM hierachy to begin searching.
474 * @param {string} linkText
475 * Link text to search for.
477 * @return {Iterable.<HTMLAnchorElement>}
478 * Iterator of link elements which text containins
479 * <var>linkText</var>.
481 element.findByPartialLinkText = function(startNode, linkText) {
482 return filterLinks(startNode, link =>
483 atom.getElementText(link).includes(linkText)
488 * Find anonymous nodes of <var>node</var>.
490 * @param {HTMLDocument} document
491 * Root node of the document.
492 * @param {XULElement} node
493 * Where in the DOM hierarchy to begin searching.
495 * @return {Iterable.<XULElement>}
496 * Iterator over anonymous elements.
498 element.findAnonymousNodes = function*(document, node) {
499 let anons = document.getAnonymousNodes(node) || [];
500 for (let node of anons) {
506 * Filters all hyperlinks that are descendant of <var>startNode</var>
507 * by <var>predicate</var>.
509 * @param {Element} startNode
510 * Where in the DOM hierarchy to begin searching.
511 * @param {function(HTMLAnchorElement): boolean} predicate
512 * Function that determines if given link should be included in
513 * return value or filtered away.
515 * @return {Iterable.<HTMLAnchorElement>}
516 * Iterator of link elements matching <var>predicate</var>.
518 function* filterLinks(startNode, predicate) {
519 for (let link of startNode.getElementsByTagName("a")) {
520 if (predicate(link)) {
527 * Finds a single element.
529 * @param {element.Strategy} strategy
530 * Selector strategy to use.
531 * @param {string} selector
532 * Selector expression.
533 * @param {HTMLDocument} document
535 * @param {Element=} startNode
536 * Optional node from which to start searching.
541 * @throws {InvalidSelectorError}
542 * If strategy <var>using</var> is not recognised.
544 * If selector expression <var>selector</var> is malformed.
546 function findElement(strategy, selector, document, startNode = undefined) {
548 case element.Strategy.ID: {
549 if (startNode.getElementById) {
550 return startNode.getElementById(selector);
552 let expr = `.//*[@id="${selector}"]`;
553 return element.findByXPath(document, startNode, expr);
556 case element.Strategy.Name: {
557 if (startNode.getElementsByName) {
558 return startNode.getElementsByName(selector)[0];
560 let expr = `.//*[@name="${selector}"]`;
561 return element.findByXPath(document, startNode, expr);
564 case element.Strategy.ClassName:
565 return startNode.getElementsByClassName(selector)[0];
567 case element.Strategy.TagName:
568 return startNode.getElementsByTagName(selector)[0];
570 case element.Strategy.XPath:
571 return element.findByXPath(document, startNode, selector);
573 case element.Strategy.LinkText:
574 for (let link of startNode.getElementsByTagName("a")) {
575 if (atom.getElementText(link).trim() === selector) {
581 case element.Strategy.PartialLinkText:
582 for (let link of startNode.getElementsByTagName("a")) {
583 if (atom.getElementText(link).includes(selector)) {
589 case element.Strategy.Selector:
591 return startNode.querySelector(selector);
593 throw new InvalidSelectorError(`${e.message}: "${selector}"`);
596 case element.Strategy.Anon:
597 return element.findAnonymousNodes(document, startNode).next().value;
599 case element.Strategy.AnonAttribute:
600 let attr = Object.keys(selector)[0];
601 return document.getAnonymousElementByAttribute(
608 throw new InvalidSelectorError(`No such strategy: ${strategy}`);
612 * Find multiple elements.
614 * @param {element.Strategy} strategy
615 * Selector strategy to use.
616 * @param {string} selector
617 * Selector expression.
618 * @param {HTMLDocument} document
620 * @param {Element=} startNode
621 * Optional node from which to start searching.
623 * @return {Array.<Element>}
626 * @throws {InvalidSelectorError}
627 * If strategy <var>strategy</var> is not recognised.
629 * If selector expression <var>selector</var> is malformed.
631 function findElements(strategy, selector, document, startNode = undefined) {
633 case element.Strategy.ID:
634 selector = `.//*[@id="${selector}"]`;
637 case element.Strategy.XPath:
638 return [...element.findByXPathAll(document, startNode, selector)];
640 case element.Strategy.Name:
641 if (startNode.getElementsByName) {
642 return startNode.getElementsByName(selector);
645 ...element.findByXPathAll(
648 `.//*[@name="${selector}"]`
652 case element.Strategy.ClassName:
653 return startNode.getElementsByClassName(selector);
655 case element.Strategy.TagName:
656 return startNode.getElementsByTagName(selector);
658 case element.Strategy.LinkText:
659 return [...element.findByLinkText(startNode, selector)];
661 case element.Strategy.PartialLinkText:
662 return [...element.findByPartialLinkText(startNode, selector)];
664 case element.Strategy.Selector:
665 return startNode.querySelectorAll(selector);
667 case element.Strategy.Anon:
668 return [...element.findAnonymousNodes(document, startNode)];
670 case element.Strategy.AnonAttribute:
671 let attr = Object.keys(selector)[0];
672 let el = document.getAnonymousElementByAttribute(
683 throw new InvalidSelectorError(`No such strategy: ${strategy}`);
688 * Finds the closest parent node of <var>startNode</var> by CSS a
689 * <var>selector</var> expression.
691 * @param {Node} startNode
692 * Cyce through <var>startNode</var>'s parent nodes in tree-order
693 * and return the first match to <var>selector</var>.
694 * @param {string} selector
695 * CSS selector expression.
698 * First match to <var>selector</var>, or null if no match was found.
700 element.findClosest = function(startNode, selector) {
701 let node = startNode;
702 while (node.parentNode && node.parentNode.nodeType == ELEMENT_NODE) {
703 node = node.parentNode;
704 if (node.matches(selector)) {
712 * Determines if <var>obj<var> is an HTML or JS collection.
718 * True if <var>seq</va> is collection.
720 element.isCollection = function(seq) {
721 switch (Object.prototype.toString.call(seq)) {
722 case "[object Arguments]":
723 case "[object Array]":
724 case "[object FileList]":
725 case "[object HTMLAllCollection]":
726 case "[object HTMLCollection]":
727 case "[object HTMLFormControlsCollection]":
728 case "[object HTMLOptionsCollection]":
729 case "[object NodeList]":
738 * Determines if <var>el</var> is stale.
740 * A stale element is an element no longer attached to the DOM or which
741 * node document is not the active document of the current browsing
744 * The currently selected browsing context, specified through
745 * <var>window<var>, is a WebDriver concept defining the target
746 * against which commands will run. As the current browsing context
747 * may differ from <var>el</var>'s associated context, an element is
748 * considered stale even if it is connected to a living (not discarded)
749 * browsing context such as an <tt><iframe></tt>.
751 * @param {Element=} el
752 * DOM element to check for staleness. If null, which may be
753 * the case if the element has been unwrapped from a weak
754 * reference, it is always considered stale.
755 * @param {WindowProxy=} win
756 * Current browsing context, which may differ from the associate
757 * browsing context of <var>el</var>. When retrieving XUL
758 * elements, this is optional.
761 * True if <var>el</var> is stale, false otherwise.
763 element.isStale = function(el, win = undefined) {
764 if (typeof win == "undefined") {
765 win = el.ownerGlobal;
768 if (el === null || !el.ownerGlobal || el.ownerDocument !== win.document) {
772 return !el.isConnected;
776 * Determine if <var>el</var> is selected or not.
778 * This operation only makes sense on
779 * <tt><input type=checkbox></tt>,
780 * <tt><input type=radio></tt>,
781 * and <tt>>option></tt> elements.
783 * @param {(DOMElement|XULElement)} el
784 * Element to test if selected.
787 * True if element is selected, false otherwise.
789 element.isSelected = function(el) {
794 if (element.isXULElement(el)) {
795 if (XUL_CHECKED_ELS.has(el.tagName)) {
797 } else if (XUL_SELECTED_ELS.has(el.tagName)) {
800 } else if (element.isDOMElement(el)) {
801 if (el.localName == "input" && ["checkbox", "radio"].includes(el.type)) {
803 } else if (el.localName == "option") {
812 * An element is considered read only if it is an
813 * <code><input></code> or <code><textarea></code>
814 * element whose <code>readOnly</code> content IDL attribute is set.
816 * @param {Element} el
817 * Element to test is read only.
820 * True if element is read only.
822 element.isReadOnly = function(el) {
824 element.isDOMElement(el) &&
825 ["input", "textarea"].includes(el.localName) &&
831 * An element is considered disabled if it is a an element
832 * that can be disabled, or it belongs to a container group which
833 * <code>disabled</code> content IDL attribute affects it.
835 * @param {Element} el
836 * Element to test for disabledness.
839 * True if element, or its container group, is disabled.
841 element.isDisabled = function(el) {
842 if (!element.isDOMElement(el)) {
846 switch (el.localName) {
852 let parent = element.findClosest(el, "optgroup,select");
853 return element.isDisabled(parent);
867 * Denotes elements that can be used for typing and clearing.
869 * Elements that are considered WebDriver-editable are non-readonly
870 * and non-disabled <code><input></code> elements in the Text,
871 * Search, URL, Telephone, Email, Password, Date, Month, Date and
872 * Time Local, Number, Range, Color, and File Upload states, and
873 * <code><textarea></code> elements.
875 * @param {Element} el
879 * True if editable, false otherwise.
881 element.isMutableFormControl = function(el) {
882 if (!element.isDOMElement(el)) {
885 if (element.isReadOnly(el) || element.isDisabled(el)) {
889 if (el.localName == "textarea") {
893 if (el.localName != "input") {
900 case "datetime-local":
921 * An editing host is a node that is either an HTML element with a
922 * <code>contenteditable</code> attribute, or the HTML element child
923 * of a document whose <code>designMode</code> is enabled.
925 * @param {Element} el
926 * Element to determine if is an editing host.
929 * True if editing host, false otherwise.
931 element.isEditingHost = function(el) {
933 element.isDOMElement(el) &&
934 (el.isContentEditable || el.ownerDocument.designMode == "on")
939 * Determines if an element is editable according to WebDriver.
941 * An element is considered editable if it is not read-only or
942 * disabled, and one of the following conditions are met:
945 * <li>It is a <code><textarea></code> element.
947 * <li>It is an <code><input></code> element that is not of
948 * the <code>checkbox</code>, <code>radio</code>, <code>hidden</code>,
949 * <code>submit</code>, <code>button</code>, or <code>image</code> types.
951 * <li>It is content-editable.
953 * <li>It belongs to a document in design mode.
957 * Element to test if editable.
960 * True if editable, false otherwise.
962 element.isEditable = function(el) {
963 if (!element.isDOMElement(el)) {
967 if (element.isReadOnly(el) || element.isDisabled(el)) {
971 return element.isMutableFormControl(el) || element.isEditingHost(el);
975 * This function generates a pair of coordinates relative to the viewport
976 * given a target element and coordinates relative to that element's
981 * @param {number=} xOffset
982 * Horizontal offset relative to target's top-left corner.
983 * Defaults to the centre of the target's bounding box.
984 * @param {number=} yOffset
985 * Vertical offset relative to target's top-left corner. Defaults to
986 * the centre of the target's bounding box.
988 * @return {Object.<string, number>}
989 * X- and Y coordinates.
992 * If <var>xOffset</var> or <var>yOffset</var> are not numbers.
994 element.coordinates = function(node, xOffset = undefined, yOffset = undefined) {
995 let box = node.getBoundingClientRect();
997 if (typeof xOffset == "undefined" || xOffset === null) {
998 xOffset = box.width / 2.0;
1000 if (typeof yOffset == "undefined" || yOffset === null) {
1001 yOffset = box.height / 2.0;
1004 if (typeof yOffset != "number" || typeof xOffset != "number") {
1005 throw new TypeError("Offset must be a number");
1009 x: box.left + xOffset,
1010 y: box.top + yOffset,
1015 * This function returns true if the node is in the viewport.
1017 * @param {Element} el
1019 * @param {number=} x
1020 * Horizontal offset relative to target. Defaults to the centre of
1021 * the target's bounding box.
1022 * @param {number=} y
1023 * Vertical offset relative to target. Defaults to the centre of
1024 * the target's bounding box.
1027 * True if if <var>el</var> is in viewport, false otherwise.
1029 element.inViewport = function(el, x = undefined, y = undefined) {
1030 let win = el.ownerGlobal;
1031 let c = element.coordinates(el, x, y);
1033 top: win.pageYOffset,
1034 left: win.pageXOffset,
1035 bottom: win.pageYOffset + win.innerHeight,
1036 right: win.pageXOffset + win.innerWidth,
1040 vp.left <= c.x + win.pageXOffset &&
1041 c.x + win.pageXOffset <= vp.right &&
1042 vp.top <= c.y + win.pageYOffset &&
1043 c.y + win.pageYOffset <= vp.bottom
1048 * Gets the element's container element.
1050 * An element container is defined by the WebDriver
1051 * specification to be an <tt><option></tt> element in a
1052 * <a href="https://html.spec.whatwg.org/#concept-element-contexts">valid
1053 * element context</a>, meaning that it has an ancestral element
1054 * that is either <tt><datalist></tt> or <tt><select></tt>.
1056 * If the element does not have a valid context, its container element
1059 * @param {Element} el
1060 * Element to get the container of.
1063 * Container element of <var>el</var>.
1065 element.getContainer = function(el) {
1066 // Does <option> or <optgroup> have a valid context,
1067 // meaning is it a child of <datalist> or <select>?
1068 if (["option", "optgroup"].includes(el.localName)) {
1069 return element.findClosest(el, "datalist,select") || el;
1076 * An element is in view if it is a member of its own pointer-interactable
1079 * This means an element is considered to be in view, but not necessarily
1080 * pointer-interactable, if it is found somewhere in the
1081 * <code>elementsFromPoint</code> list at <var>el</var>'s in-view
1082 * centre coordinates.
1084 * Before running the check, we change <var>el</var>'s pointerEvents
1085 * style property to "auto", since elements without pointer events
1086 * enabled do not turn up in the paint tree we get from
1087 * document.elementsFromPoint. This is a specialisation that is only
1088 * relevant when checking if the element is in view.
1090 * @param {Element} el
1091 * Element to check if is in view.
1094 * True if <var>el</var> is inside the viewport, or false otherwise.
1096 element.isInView = function(el) {
1097 let originalPointerEvents = el.style.pointerEvents;
1100 el.style.pointerEvents = "auto";
1101 const tree = element.getPointerInteractablePaintTree(el);
1103 // Bug 1413493 - <tr> is not part of the returned paint tree yet. As
1104 // workaround check the visibility based on the first contained cell.
1105 if (el.localName === "tr" && el.cells && el.cells.length > 0) {
1106 return tree.includes(el.cells[0]);
1109 return tree.includes(el);
1111 el.style.pointerEvents = originalPointerEvents;
1116 * This function throws the visibility of the element error if the element is
1117 * not displayed or the given coordinates are not within the viewport.
1119 * @param {Element} el
1120 * Element to check if visible.
1121 * @param {number=} x
1122 * Horizontal offset relative to target. Defaults to the centre of
1123 * the target's bounding box.
1124 * @param {number=} y
1125 * Vertical offset relative to target. Defaults to the centre of
1126 * the target's bounding box.
1129 * True if visible, false otherwise.
1131 element.isVisible = function(el, x = undefined, y = undefined) {
1132 let win = el.ownerGlobal;
1134 if (!atom.isElementDisplayed(el, win)) {
1138 if (el.tagName.toLowerCase() == "body") {
1142 if (!element.inViewport(el, x, y)) {
1143 element.scrollIntoView(el);
1144 if (!element.inViewport(el)) {
1152 * A pointer-interactable element is defined to be the first
1153 * non-transparent element, defined by the paint order found at the centre
1154 * point of its rectangle that is inside the viewport, excluding the size
1155 * of any rendered scrollbars.
1157 * An element is obscured if the pointer-interactable paint tree at its
1158 * centre point is empty, or the first element in this tree is not an
1159 * inclusive descendant of itself.
1161 * @param {DOMElement} el
1162 * Element determine if is pointer-interactable.
1165 * True if element is obscured, false otherwise.
1167 element.isObscured = function(el) {
1168 let tree = element.getPointerInteractablePaintTree(el);
1169 return !el.contains(tree[0]);
1172 // TODO(ato): Only used by deprecated action API
1173 // https://bugzil.la/1354578
1175 * Calculates the in-view centre point of an element's client rect.
1177 * The portion of an element that is said to be _in view_, is the
1178 * intersection of two squares: the first square being the initial
1179 * viewport, and the second a DOM element. From this square we
1180 * calculate the in-view _centre point_ and convert it into CSS pixels.
1182 * Although Gecko's system internals allow click points to be
1183 * given in floating point precision, the DOM operates in CSS pixels.
1184 * When the in-view centre point is later used to retrieve a coordinate's
1185 * paint tree, we need to ensure to operate in the same language.
1187 * As a word of warning, there appears to be inconsistencies between
1188 * how `DOMElement.elementsFromPoint` and `DOMWindowUtils.sendMouseEvent`
1189 * internally rounds (ceils/floors) coordinates.
1191 * @param {DOMRect} rect
1192 * Element off a DOMRect sequence produced by calling
1193 * `getClientRects` on an {@link Element}.
1194 * @param {WindowProxy} win
1195 * Current window global.
1197 * @return {Map.<string, number>}
1198 * X and Y coordinates that denotes the in-view centre point of
1201 element.getInViewCentrePoint = function(rect, win) {
1202 const { floor, max, min } = Math;
1204 // calculate the intersection of the rect that is inside the viewport
1206 left: max(0, min(rect.x, rect.x + rect.width)),
1207 right: min(win.innerWidth, max(rect.x, rect.x + rect.width)),
1208 top: max(0, min(rect.y, rect.y + rect.height)),
1209 bottom: min(win.innerHeight, max(rect.y, rect.y + rect.height)),
1212 // arrive at the centre point of the visible rectangle
1213 let x = (visible.left + visible.right) / 2.0;
1214 let y = (visible.top + visible.bottom) / 2.0;
1216 // convert to CSS pixels, as centre point can be float
1224 * Produces a pointer-interactable elements tree from a given element.
1226 * The tree is defined by the paint order found at the centre point of
1227 * the element's rectangle that is inside the viewport, excluding the size
1228 * of any rendered scrollbars.
1230 * @param {DOMElement} el
1231 * Element to determine if is pointer-interactable.
1233 * @return {Array.<DOMElement>}
1234 * Sequence of elements in paint order.
1236 element.getPointerInteractablePaintTree = function(el) {
1237 const doc = el.ownerDocument;
1238 const win = doc.defaultView;
1239 const rootNode = el.getRootNode();
1241 // pointer-interactable elements tree, step 1
1242 if (!el.isConnected) {
1247 let rects = el.getClientRects();
1248 if (rects.length == 0) {
1253 let centre = element.getInViewCentrePoint(rects[0], win);
1256 return rootNode.elementsFromPoint(centre.x, centre.y);
1259 // TODO(ato): Not implemented.
1260 // In fact, it's not defined in the spec.
1261 element.isKeyboardInteractable = () => true;
1264 * Attempts to scroll into view |el|.
1266 * @param {DOMElement} el
1267 * Element to scroll into view.
1269 element.scrollIntoView = function(el) {
1270 if (el.scrollIntoView) {
1271 el.scrollIntoView({ block: "end", inline: "nearest", behavior: "instant" });
1276 * Ascertains whether <var>node</var> is a DOM-, SVG-, or XUL element.
1279 * Element thought to be an <code>Element</code> or
1280 * <code>XULElement</code>.
1283 * True if <var>node</var> is an element, false otherwise.
1285 element.isElement = function(node) {
1286 return element.isDOMElement(node) || element.isXULElement(node);
1290 * Ascertains whether <var>node</var> is a DOM element.
1293 * Element thought to be an <code>Element</code>.
1296 * True if <var>node</var> is a DOM element, false otherwise.
1298 element.isDOMElement = function(node) {
1300 typeof node == "object" &&
1302 "nodeType" in node &&
1303 [ELEMENT_NODE, DOCUMENT_NODE].includes(node.nodeType) &&
1304 !element.isXULElement(node)
1309 * Ascertains whether <var>el</var> is a XUL- or XBL element.
1312 * Element thought to be a XUL- or XBL element.
1315 * True if <var>node</var> is a XULElement or XBLElement,
1318 element.isXULElement = function(node) {
1320 typeof node == "object" &&
1322 "nodeType" in node &&
1323 node.nodeType === node.ELEMENT_NODE &&
1324 [XBLNS, XULNS].includes(node.namespaceURI)
1329 * Ascertains whether <var>node</var> is a <code>WindowProxy</code>.
1332 * Node thought to be a <code>WindowProxy</code>.
1335 * True if <var>node</var> is a DOM window.
1337 element.isDOMWindow = function(node) {
1338 // TODO(ato): This should use Object.prototype.toString.call(node)
1339 // but it's not clear how to write a good xpcshell test for that,
1340 // seeing as we stub out a WindowProxy.
1342 typeof node == "object" &&
1344 typeof node.toString == "function" &&
1345 node.toString() == "[object Window]" &&
1351 audio: ["autoplay", "controls", "loop", "muted"],
1352 button: ["autofocus", "disabled", "formnovalidate"],
1355 fieldset: ["disabled"],
1356 form: ["novalidate"],
1357 iframe: ["allowfullscreen"],
1368 keygen: ["autofocus", "disabled"],
1369 menuitem: ["checked", "default", "disabled"],
1371 optgroup: ["disabled"],
1372 option: ["disabled", "selected"],
1373 script: ["async", "defer"],
1374 select: ["autofocus", "disabled", "multiple", "required"],
1375 textarea: ["autofocus", "disabled", "readonly", "required"],
1377 video: ["autoplay", "controls", "loop", "muted"],
1381 * Tests if the attribute is a boolean attribute on element.
1383 * @param {DOMElement} el
1384 * Element to test if <var>attr</var> is a boolean attribute on.
1385 * @param {string} attr
1386 * Attribute to test is a boolean attribute.
1389 * True if the attribute is boolean, false otherwise.
1391 element.isBooleanAttribute = function(el, attr) {
1392 if (!element.isDOMElement(el)) {
1396 // global boolean attributes that apply to all HTML elements,
1397 // except for custom elements
1398 const customElement = !el.localName.includes("-");
1399 if ((attr == "hidden" || attr == "itemscope") && customElement) {
1403 if (!boolEls.hasOwnProperty(el.localName)) {
1406 return boolEls[el.localName].includes(attr);
1410 * A web element is an abstraction used to identify an element when
1411 * it is transported via the protocol, between remote- and local ends.
1413 * In Marionette this abstraction can represent DOM elements,
1414 * WindowProxies, and XUL elements.
1418 * @param {string} uuid
1419 * Identifier that must be unique across all browsing contexts
1420 * for the contract to be upheld.
1423 this.uuid = assert.string(uuid);
1427 * Performs an equality check between this web element and
1430 * @param {WebElement} other
1431 * Web element to compare with this.
1434 * True if this and <var>other</var> are the same. False
1438 return other instanceof WebElement && this.uuid === other.uuid;
1442 return `[object ${this.constructor.name} uuid=${this.uuid}]`;
1446 * Returns a new {@link WebElement} reference for a DOM element,
1447 * <code>WindowProxy</code>, or XUL element.
1449 * @param {(Element|WindowProxy|XULElement)} node
1450 * Node to construct a web element reference for.
1452 * @return {(ContentWebElement|ChromeWebElement)}
1453 * Web element reference for <var>el</var>.
1455 * @throws {InvalidArgumentError}
1456 * If <var>node</var> is neither a <code>WindowProxy</code>,
1457 * DOM element, or a XUL element.
1460 const uuid = WebElement.generateUUID();
1462 if (element.isDOMElement(node)) {
1463 return new ContentWebElement(uuid);
1464 } else if (element.isDOMWindow(node)) {
1465 if (node.parent === node) {
1466 return new ContentWebWindow(uuid);
1468 return new ContentWebFrame(uuid);
1469 } else if (element.isXULElement(node)) {
1470 return new ChromeWebElement(uuid);
1473 throw new InvalidArgumentError(
1474 "Expected DOM window/element " + pprint`or XUL element, got: ${node}`
1479 * Unmarshals a JSON Object to one of {@link ContentWebElement},
1480 * {@link ContentWebWindow}, {@link ContentWebFrame}, or
1481 * {@link ChromeWebElement}.
1483 * @param {Object.<string, string>} json
1484 * Web element reference, which is supposed to be a JSON Object
1485 * where the key is one of the {@link WebElement} concrete
1486 * classes' UUID identifiers.
1488 * @return {WebElement}
1489 * Representation of the web element.
1491 * @throws {InvalidArgumentError}
1492 * If <var>json</var> is not a web element reference.
1494 static fromJSON(json) {
1495 assert.object(json);
1496 let keys = Object.keys(json);
1498 for (let key of keys) {
1500 case ContentWebElement.Identifier:
1501 return ContentWebElement.fromJSON(json);
1503 case ContentWebWindow.Identifier:
1504 return ContentWebWindow.fromJSON(json);
1506 case ContentWebFrame.Identifier:
1507 return ContentWebFrame.fromJSON(json);
1509 case ChromeWebElement.Identifier:
1510 return ChromeWebElement.fromJSON(json);
1514 throw new InvalidArgumentError(
1515 pprint`Expected web element reference, got: ${json}`
1520 * Constructs a {@link ContentWebElement} or {@link ChromeWebElement}
1521 * from a a string <var>uuid</var>.
1523 * This whole function is a workaround for the fact that clients
1524 * to Marionette occasionally pass <code>{id: <uuid>}</code> JSON
1525 * Objects instead of web element representations. For that reason
1526 * we need the <var>context</var> argument to determine what kind of
1527 * {@link WebElement} to return.
1529 * @param {string} uuid
1530 * UUID to be associated with the web element.
1531 * @param {Context} context
1532 * Context, which is used to determine if the returned type
1533 * should be a content web element or a chrome web element.
1535 * @return {WebElement}
1536 * One of {@link ContentWebElement} or {@link ChromeWebElement},
1537 * based on <var>context</var>.
1539 * @throws {InvalidArgumentError}
1540 * If <var>uuid</var> is not a string or <var>context</var>
1541 * is an invalid context.
1543 static fromUUID(uuid, context) {
1544 assert.string(uuid);
1548 return new ChromeWebElement(uuid);
1551 return new ContentWebElement(uuid);
1554 throw new InvalidArgumentError("Unknown context: " + context);
1559 * Checks if <var>ref<var> is a {@link WebElement} reference,
1560 * i.e. if it has {@link ContentWebElement.Identifier}, or
1561 * {@link ChromeWebElement.Identifier} as properties.
1563 * @param {Object.<string, string>} obj
1564 * Object that represents a reference to a {@link WebElement}.
1566 * True if <var>obj</var> is a {@link WebElement}, false otherwise.
1568 static isReference(obj) {
1569 if (Object.prototype.toString.call(obj) != "[object Object]") {
1574 ContentWebElement.Identifier in obj ||
1575 ContentWebWindow.Identifier in obj ||
1576 ContentWebFrame.Identifier in obj ||
1577 ChromeWebElement.Identifier in obj
1585 * Generates a unique identifier.
1590 static generateUUID() {
1591 let uuid = uuidGen.generateUUID().toString();
1592 return uuid.substring(1, uuid.length - 1);
1595 this.WebElement = WebElement;
1598 * DOM elements are represented as web elements when they are
1599 * transported over the wire protocol.
1601 class ContentWebElement extends WebElement {
1603 return { [ContentWebElement.Identifier]: this.uuid };
1606 static fromJSON(json) {
1607 const { Identifier } = ContentWebElement;
1609 if (!(Identifier in json)) {
1610 throw new InvalidArgumentError(
1611 pprint`Expected web element reference, got: ${json}`
1615 let uuid = json[Identifier];
1616 return new ContentWebElement(uuid);
1619 ContentWebElement.Identifier = "element-6066-11e4-a52e-4f735466cecf";
1620 this.ContentWebElement = ContentWebElement;
1623 * Top-level browsing contexts, such as <code>WindowProxy</code>
1624 * whose <code>opener</code> is null, are represented as web windows
1625 * over the wire protocol.
1627 class ContentWebWindow extends WebElement {
1629 return { [ContentWebWindow.Identifier]: this.uuid };
1632 static fromJSON(json) {
1633 if (!(ContentWebWindow.Identifier in json)) {
1634 throw new InvalidArgumentError(
1635 pprint`Expected web window reference, got: ${json}`
1638 let uuid = json[ContentWebWindow.Identifier];
1639 return new ContentWebWindow(uuid);
1642 ContentWebWindow.Identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f";
1643 this.ContentWebWindow = ContentWebWindow;
1646 * Nested browsing contexts, such as the <code>WindowProxy</code>
1647 * associated with <tt><frame></tt> and <tt><iframe></tt>,
1648 * are represented as web frames over the wire protocol.
1650 class ContentWebFrame extends WebElement {
1652 return { [ContentWebFrame.Identifier]: this.uuid };
1655 static fromJSON(json) {
1656 if (!(ContentWebFrame.Identifier in json)) {
1657 throw new InvalidArgumentError(
1658 pprint`Expected web frame reference, got: ${json}`
1661 let uuid = json[ContentWebFrame.Identifier];
1662 return new ContentWebFrame(uuid);
1665 ContentWebFrame.Identifier = "frame-075b-4da1-b6ba-e579c2d3230a";
1666 this.ContentWebFrame = ContentWebFrame;
1669 * XUL elements in chrome space are represented as chrome web elements
1670 * over the wire protocol.
1672 class ChromeWebElement extends WebElement {
1674 return { [ChromeWebElement.Identifier]: this.uuid };
1677 static fromJSON(json) {
1678 if (!(ChromeWebElement.Identifier in json)) {
1679 throw new InvalidArgumentError(
1680 "Expected chrome element reference " +
1681 pprint`for XUL/XBL element, got: ${json}`
1684 let uuid = json[ChromeWebElement.Identifier];
1685 return new ChromeWebElement(uuid);
1688 ChromeWebElement.Identifier = "chromeelement-9fc5-4b51-a3c8-01716eedeb04";
1689 this.ChromeWebElement = ChromeWebElement;