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/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 atom: "chrome://remote/content/marionette/atom.sys.mjs",
9 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
10 PollPromise: "chrome://remote/content/marionette/sync.sys.mjs",
13 const ORDERED_NODE_ITERATOR_TYPE = 5;
14 const FIRST_ORDERED_NODE_TYPE = 9;
16 const DOCUMENT_FRAGMENT_NODE = 11;
17 const ELEMENT_NODE = 1;
19 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
21 /** XUL elements that support checked property. */
22 const XUL_CHECKED_ELS = new Set(["button", "checkbox", "toolbarbutton"]);
24 /** XUL elements that support selected property. */
25 const XUL_SELECTED_ELS = new Set([
35 * This module provides shared functionality for dealing with DOM-
36 * and web elements in Marionette.
38 * A web element is an abstraction used to identify an element when it
39 * is transported across the protocol, between remote- and local ends.
41 * Each element has an associated web element reference (a UUID) that
42 * uniquely identifies the the element across all browsing contexts. The
43 * web element reference for every element representing the same element
48 export const dom = {};
51 ClassName: "class name",
52 Selector: "css selector",
55 LinkText: "link text",
56 PartialLinkText: "partial link text",
62 * Find a single element or a collection of elements starting at the
63 * document root or a given node.
65 * If |timeout| is above 0, an implicit search technique is used.
66 * This will wait for the duration of <var>timeout</var> for the
67 * element to appear in the DOM.
69 * See the {@link dom.Strategy} enum for a full list of supported
70 * search strategies that can be passed to <var>strategy</var>.
72 * @param {Object<string, WindowProxy>} container
74 * @param {string} strategy
75 * Search strategy whereby to locate the element(s).
76 * @param {string} selector
77 * Selector search pattern. The selector must be compatible with
78 * the chosen search <var>strategy</var>.
79 * @param {object=} options
80 * @param {boolean=} options.all
81 * If true, a multi-element search selector is used and a sequence of
82 * elements will be returned, otherwise a single element. Defaults to false.
83 * @param {Element=} options.startNode
84 * Element to use as the root of the search.
85 * @param {number=} options.timeout
86 * Duration to wait before timing out the search. If <code>all</code>
87 * is false, a {@link NoSuchElementError} is thrown if unable to
88 * find the element within the timeout duration.
90 * @returns {Promise.<(Element|Array.<Element>)>}
91 * Single element or a sequence of elements.
93 * @throws InvalidSelectorError
94 * If <var>strategy</var> is unknown.
95 * @throws InvalidSelectorError
96 * If <var>selector</var> is malformed.
97 * @throws NoSuchElementError
98 * If a single element is requested, this error will throw if the
99 * element is not found.
101 dom.find = function (container, strategy, selector, options = {}) {
102 const { all = false, startNode, timeout = 0 } = options;
106 searchFn = findElements.bind(this);
108 searchFn = findElement.bind(this);
111 return new Promise((resolve, reject) => {
112 let findElements = new lazy.PollPromise(
113 async (resolve, reject) => {
115 let res = await find_(container, strategy, selector, searchFn, {
120 resolve(Array.from(res));
131 findElements.then(foundEls => {
132 // the following code ought to be moved into findElement
133 // and findElements when bug 1254486 is addressed
134 if (!all && (!foundEls || !foundEls.length)) {
135 let msg = `Unable to locate element: ${selector}`;
136 reject(new lazy.error.NoSuchElementError(msg));
142 resolve(foundEls[0]);
147 async function find_(
152 { startNode = null, all = false } = {}
156 if (dom.isShadowRoot(startNode)) {
157 rootNode = startNode.ownerDocument;
159 rootNode = container.frame.document;
163 startNode = rootNode;
168 res = await searchFn(strategy, selector, rootNode, startNode);
170 throw new lazy.error.InvalidSelectorError(
171 `Given ${strategy} expression "${selector}" is invalid: ${e}`
185 * Find a single element by XPath expression.
187 * @param {Document} document
189 * @param {Element} startNode
190 * Where in the DOM hiearchy to begin searching.
191 * @param {string} expression
192 * XPath search expression.
195 * First element matching <var>expression</var>.
197 dom.findByXPath = function (document, startNode, expression) {
198 let iter = document.evaluate(
202 FIRST_ORDERED_NODE_TYPE,
205 return iter.singleNodeValue;
209 * Find elements by XPath expression.
211 * @param {Document} document
213 * @param {Element} startNode
214 * Where in the DOM hierarchy to begin searching.
215 * @param {string} expression
216 * XPath search expression.
218 * @returns {Iterable.<Node>}
219 * Iterator over nodes matching <var>expression</var>.
221 dom.findByXPathAll = function* (document, startNode, expression) {
222 let iter = document.evaluate(
226 ORDERED_NODE_ITERATOR_TYPE,
229 let el = iter.iterateNext();
232 el = iter.iterateNext();
237 * Find all hyperlinks descendant of <var>startNode</var> which
238 * link text is <var>linkText</var>.
240 * @param {Element} startNode
241 * Where in the DOM hierarchy to begin searching.
242 * @param {string} linkText
243 * Link text to search for.
245 * @returns {Iterable.<HTMLAnchorElement>}
246 * Sequence of link elements which text is <var>s</var>.
248 dom.findByLinkText = function (startNode, linkText) {
249 return filterLinks(startNode, async link => {
250 const visibleText = await lazy.atom.getVisibleText(link, link.ownerGlobal);
251 return visibleText.trim() === linkText;
256 * Find all hyperlinks descendant of <var>startNode</var> which
257 * link text contains <var>linkText</var>.
259 * @param {Element} startNode
260 * Where in the DOM hierachy to begin searching.
261 * @param {string} linkText
262 * Link text to search for.
264 * @returns {Iterable.<HTMLAnchorElement>}
265 * Iterator of link elements which text containins
266 * <var>linkText</var>.
268 dom.findByPartialLinkText = function (startNode, linkText) {
269 return filterLinks(startNode, async link => {
270 const visibleText = await lazy.atom.getVisibleText(link, link.ownerGlobal);
272 return visibleText.includes(linkText);
277 * Filters all hyperlinks that are descendant of <var>startNode</var>
278 * by <var>predicate</var>.
280 * @param {Element} startNode
281 * Where in the DOM hierarchy to begin searching.
282 * @param {function(HTMLAnchorElement): boolean} predicate
283 * Function that determines if given link should be included in
284 * return value or filtered away.
286 * @returns {Array.<HTMLAnchorElement>}
287 * Array of link elements matching <var>predicate</var>.
289 async function filterLinks(startNode, predicate) {
292 for (const link of getLinks(startNode)) {
293 if (await predicate(link)) {
302 * Finds a single element.
304 * @param {dom.Strategy} strategy
305 * Selector strategy to use.
306 * @param {string} selector
307 * Selector expression.
308 * @param {Document} document
310 * @param {Element=} startNode
311 * Optional Element from which to start searching.
316 * @throws {InvalidSelectorError}
317 * If strategy <var>using</var> is not recognised.
319 * If selector expression <var>selector</var> is malformed.
321 async function findElement(
325 startNode = undefined
328 case dom.Strategy.ID: {
329 if (startNode.getElementById) {
330 return startNode.getElementById(selector);
332 let expr = `.//*[@id="${selector}"]`;
333 return dom.findByXPath(document, startNode, expr);
336 case dom.Strategy.Name: {
337 if (startNode.getElementsByName) {
338 return startNode.getElementsByName(selector)[0];
340 let expr = `.//*[@name="${selector}"]`;
341 return dom.findByXPath(document, startNode, expr);
344 case dom.Strategy.ClassName:
345 return startNode.getElementsByClassName(selector)[0];
347 case dom.Strategy.TagName:
348 return startNode.getElementsByTagName(selector)[0];
350 case dom.Strategy.XPath:
351 return dom.findByXPath(document, startNode, selector);
353 case dom.Strategy.LinkText: {
354 const links = getLinks(startNode);
355 for (const link of links) {
356 const visibleText = await lazy.atom.getVisibleText(
360 if (visibleText.trim() === selector) {
367 case dom.Strategy.PartialLinkText: {
368 const links = getLinks(startNode);
369 for (const link of links) {
370 const visibleText = await lazy.atom.getVisibleText(
374 if (visibleText.includes(selector)) {
381 case dom.Strategy.Selector:
383 return startNode.querySelector(selector);
385 throw new lazy.error.InvalidSelectorError(
386 `${e.message}: "${selector}"`
391 throw new lazy.error.InvalidSelectorError(`No such strategy: ${strategy}`);
395 * Find multiple elements.
397 * @param {dom.Strategy} strategy
398 * Selector strategy to use.
399 * @param {string} selector
400 * Selector expression.
401 * @param {Document} document
403 * @param {Element=} startNode
404 * Optional Element from which to start searching.
406 * @returns {Array.<Element>}
409 * @throws {InvalidSelectorError}
410 * If strategy <var>strategy</var> is not recognised.
412 * If selector expression <var>selector</var> is malformed.
414 async function findElements(
418 startNode = undefined
421 case dom.Strategy.ID:
422 selector = `.//*[@id="${selector}"]`;
425 case dom.Strategy.XPath:
426 return [...dom.findByXPathAll(document, startNode, selector)];
428 case dom.Strategy.Name:
429 if (startNode.getElementsByName) {
430 return startNode.getElementsByName(selector);
433 ...dom.findByXPathAll(document, startNode, `.//*[@name="${selector}"]`),
436 case dom.Strategy.ClassName:
437 return startNode.getElementsByClassName(selector);
439 case dom.Strategy.TagName:
440 return startNode.getElementsByTagName(selector);
442 case dom.Strategy.LinkText:
443 return [...(await dom.findByLinkText(startNode, selector))];
445 case dom.Strategy.PartialLinkText:
446 return [...(await dom.findByPartialLinkText(startNode, selector))];
448 case dom.Strategy.Selector:
449 return startNode.querySelectorAll(selector);
452 throw new lazy.error.InvalidSelectorError(
453 `No such strategy: ${strategy}`
458 function getLinks(startNode) {
459 // DocumentFragment doesn't have `getElementsByTagName` so using `querySelectorAll`.
460 if (dom.isShadowRoot(startNode)) {
461 return startNode.querySelectorAll("a");
463 return startNode.getElementsByTagName("a");
467 * Finds the closest parent node of <var>startNode</var> matching a CSS
468 * <var>selector</var> expression.
470 * @param {Node} startNode
471 * Cycle through <var>startNode</var>'s parent nodes in tree-order
472 * and return the first match to <var>selector</var>.
473 * @param {string} selector
474 * CSS selector expression.
477 * First match to <var>selector</var>, or null if no match was found.
479 dom.findClosest = function (startNode, selector) {
480 let node = startNode;
481 while (node.parentNode && node.parentNode.nodeType == ELEMENT_NODE) {
482 node = node.parentNode;
483 if (node.matches(selector)) {
491 * Determines if <var>obj<var> is an HTML or JS collection.
493 * @param {object} seq
497 * True if <var>seq</va> is a collection.
499 dom.isCollection = function (seq) {
500 switch (Object.prototype.toString.call(seq)) {
501 case "[object Arguments]":
502 case "[object Array]":
503 case "[object DOMTokenList]":
504 case "[object FileList]":
505 case "[object HTMLAllCollection]":
506 case "[object HTMLCollection]":
507 case "[object HTMLFormControlsCollection]":
508 case "[object HTMLOptionsCollection]":
509 case "[object NodeList]":
518 * Determines if <var>shadowRoot</var> is detached.
520 * A ShadowRoot is detached if its node document is not the active document
521 * or if the element node referred to as its host is stale.
523 * @param {ShadowRoot} shadowRoot
524 * ShadowRoot to check for detached state.
527 * True if <var>shadowRoot</var> is detached, false otherwise.
529 dom.isDetached = function (shadowRoot) {
530 return !shadowRoot.ownerDocument.isActive() || dom.isStale(shadowRoot.host);
534 * Determines if <var>el</var> is stale.
536 * An element is stale if its node document is not the active document
537 * or if it is not connected.
539 * @param {Element} el
540 * Element to check for staleness.
543 * True if <var>el</var> is stale, false otherwise.
545 dom.isStale = function (el) {
546 if (!el.ownerGlobal) {
547 // Without a valid inner window the document is basically closed.
551 return !el.ownerDocument.isActive() || !el.isConnected;
555 * Determine if <var>el</var> is selected or not.
557 * This operation only makes sense on
558 * <tt><input type=checkbox></tt>,
559 * <tt><input type=radio></tt>,
560 * and <tt>>option></tt> elements.
562 * @param {Element} el
563 * Element to test if selected.
566 * True if element is selected, false otherwise.
568 dom.isSelected = function (el) {
573 if (dom.isXULElement(el)) {
574 if (XUL_CHECKED_ELS.has(el.tagName)) {
576 } else if (XUL_SELECTED_ELS.has(el.tagName)) {
579 } else if (dom.isDOMElement(el)) {
580 if (el.localName == "input" && ["checkbox", "radio"].includes(el.type)) {
582 } else if (el.localName == "option") {
591 * An element is considered read only if it is an
592 * <code><input></code> or <code><textarea></code>
593 * element whose <code>readOnly</code> content IDL attribute is set.
595 * @param {Element} el
596 * Element to test is read only.
599 * True if element is read only.
601 dom.isReadOnly = function (el) {
603 dom.isDOMElement(el) &&
604 ["input", "textarea"].includes(el.localName) &&
610 * An element is considered disabled if it is a an element
611 * that can be disabled, or it belongs to a container group which
612 * <code>disabled</code> content IDL attribute affects it.
614 * @param {Element} el
615 * Element to test for disabledness.
618 * True if element, or its container group, is disabled.
620 dom.isDisabled = function (el) {
621 if (!dom.isDOMElement(el)) {
625 switch (el.localName) {
631 let parent = dom.findClosest(el, "optgroup,select");
632 return dom.isDisabled(parent);
646 * Denotes elements that can be used for typing and clearing.
648 * Elements that are considered WebDriver-editable are non-readonly
649 * and non-disabled <code><input></code> elements in the Text,
650 * Search, URL, Telephone, Email, Password, Date, Month, Date and
651 * Time Local, Number, Range, Color, and File Upload states, and
652 * <code><textarea></code> elements.
654 * @param {Element} el
658 * True if editable, false otherwise.
660 dom.isMutableFormControl = function (el) {
661 if (!dom.isDOMElement(el)) {
664 if (dom.isReadOnly(el) || dom.isDisabled(el)) {
668 if (el.localName == "textarea") {
672 if (el.localName != "input") {
679 case "datetime-local":
700 * An editing host is a node that is either an HTML element with a
701 * <code>contenteditable</code> attribute, or the HTML element child
702 * of a document whose <code>designMode</code> is enabled.
704 * @param {Element} el
705 * Element to determine if is an editing host.
708 * True if editing host, false otherwise.
710 dom.isEditingHost = function (el) {
712 dom.isDOMElement(el) &&
713 (el.isContentEditable || el.ownerDocument.designMode == "on")
718 * Determines if an element is editable according to WebDriver.
720 * An element is considered editable if it is not read-only or
721 * disabled, and one of the following conditions are met:
724 * <li>It is a <code><textarea></code> element.
726 * <li>It is an <code><input></code> element that is not of
727 * the <code>checkbox</code>, <code>radio</code>, <code>hidden</code>,
728 * <code>submit</code>, <code>button</code>, or <code>image</code> types.
730 * <li>It is content-editable.
732 * <li>It belongs to a document in design mode.
735 * @param {Element} el
736 * Element to test if editable.
739 * True if editable, false otherwise.
741 dom.isEditable = function (el) {
742 if (!dom.isDOMElement(el)) {
746 if (dom.isReadOnly(el) || dom.isDisabled(el)) {
750 return dom.isMutableFormControl(el) || dom.isEditingHost(el);
754 * This function generates a pair of coordinates relative to the viewport
755 * given a target element and coordinates relative to that element's
760 * @param {number=} xOffset
761 * Horizontal offset relative to target's top-left corner.
762 * Defaults to the centre of the target's bounding box.
763 * @param {number=} yOffset
764 * Vertical offset relative to target's top-left corner. Defaults to
765 * the centre of the target's bounding box.
767 * @returns {Object<string, number>}
768 * X- and Y coordinates.
771 * If <var>xOffset</var> or <var>yOffset</var> are not numbers.
773 dom.coordinates = function (node, xOffset = undefined, yOffset = undefined) {
774 let box = node.getBoundingClientRect();
776 if (typeof xOffset == "undefined" || xOffset === null) {
777 xOffset = box.width / 2.0;
779 if (typeof yOffset == "undefined" || yOffset === null) {
780 yOffset = box.height / 2.0;
783 if (typeof yOffset != "number" || typeof xOffset != "number") {
784 throw new TypeError("Offset must be a number");
788 x: box.left + xOffset,
789 y: box.top + yOffset,
794 * This function returns true if the node is in the viewport.
796 * @param {Element} el
799 * Horizontal offset relative to target. Defaults to the centre of
800 * the target's bounding box.
802 * Vertical offset relative to target. Defaults to the centre of
803 * the target's bounding box.
806 * True if if <var>el</var> is in viewport, false otherwise.
808 dom.inViewport = function (el, x = undefined, y = undefined) {
809 let win = el.ownerGlobal;
810 let c = dom.coordinates(el, x, y);
812 top: win.pageYOffset,
813 left: win.pageXOffset,
814 bottom: win.pageYOffset + win.innerHeight,
815 right: win.pageXOffset + win.innerWidth,
819 vp.left <= c.x + win.pageXOffset &&
820 c.x + win.pageXOffset <= vp.right &&
821 vp.top <= c.y + win.pageYOffset &&
822 c.y + win.pageYOffset <= vp.bottom
827 * Gets the element's container element.
829 * An element container is defined by the WebDriver
830 * specification to be an <tt><option></tt> element in a
831 * <a href="https://html.spec.whatwg.org/#concept-element-contexts">valid
832 * element context</a>, meaning that it has an ancestral element
833 * that is either <tt><datalist></tt> or <tt><select></tt>.
835 * If the element does not have a valid context, its container element
838 * @param {Element} el
839 * Element to get the container of.
842 * Container element of <var>el</var>.
844 dom.getContainer = function (el) {
845 // Does <option> or <optgroup> have a valid context,
846 // meaning is it a child of <datalist> or <select>?
847 if (["option", "optgroup"].includes(el.localName)) {
848 return dom.findClosest(el, "datalist,select") || el;
855 * An element is in view if it is a member of its own pointer-interactable
858 * This means an element is considered to be in view, but not necessarily
859 * pointer-interactable, if it is found somewhere in the
860 * <code>elementsFromPoint</code> list at <var>el</var>'s in-view
861 * centre coordinates.
863 * Before running the check, we change <var>el</var>'s pointerEvents
864 * style property to "auto", since elements without pointer events
865 * enabled do not turn up in the paint tree we get from
866 * document.elementsFromPoint. This is a specialisation that is only
867 * relevant when checking if the element is in view.
869 * @param {Element} el
870 * Element to check if is in view.
873 * True if <var>el</var> is inside the viewport, or false otherwise.
875 dom.isInView = function (el) {
876 let originalPointerEvents = el.style.pointerEvents;
879 el.style.pointerEvents = "auto";
880 const tree = dom.getPointerInteractablePaintTree(el);
882 // Bug 1413493 - <tr> is not part of the returned paint tree yet. As
883 // workaround check the visibility based on the first contained cell.
884 if (el.localName === "tr" && el.cells && el.cells.length) {
885 return tree.includes(el.cells[0]);
888 return tree.includes(el);
890 el.style.pointerEvents = originalPointerEvents;
895 * This function throws the visibility of the element error if the element is
896 * not displayed or the given coordinates are not within the viewport.
898 * @param {Element} el
899 * Element to check if visible.
901 * Horizontal offset relative to target. Defaults to the centre of
902 * the target's bounding box.
904 * Vertical offset relative to target. Defaults to the centre of
905 * the target's bounding box.
908 * True if visible, false otherwise.
910 dom.isVisible = async function (el, x = undefined, y = undefined) {
911 let win = el.ownerGlobal;
913 if (!(await lazy.atom.isElementDisplayed(el, win))) {
917 if (el.tagName.toLowerCase() == "body") {
921 if (!dom.inViewport(el, x, y)) {
922 dom.scrollIntoView(el);
923 if (!dom.inViewport(el)) {
931 * A pointer-interactable element is defined to be the first
932 * non-transparent element, defined by the paint order found at the centre
933 * point of its rectangle that is inside the viewport, excluding the size
934 * of any rendered scrollbars.
936 * An element is obscured if the pointer-interactable paint tree at its
937 * centre point is empty, or the first element in this tree is not an
938 * inclusive descendant of itself.
940 * @param {DOMElement} el
941 * Element determine if is pointer-interactable.
944 * True if element is obscured, false otherwise.
946 dom.isObscured = function (el) {
947 let tree = dom.getPointerInteractablePaintTree(el);
948 return !el.contains(tree[0]);
951 // TODO(ato): Only used by deprecated action API
952 // https://bugzil.la/1354578
954 * Calculates the in-view centre point of an element's client rect.
956 * The portion of an element that is said to be _in view_, is the
957 * intersection of two squares: the first square being the initial
958 * viewport, and the second a DOM element. From this square we
959 * calculate the in-view _centre point_ and convert it into CSS pixels.
961 * Although Gecko's system internals allow click points to be
962 * given in floating point precision, the DOM operates in CSS pixels.
963 * When the in-view centre point is later used to retrieve a coordinate's
964 * paint tree, we need to ensure to operate in the same language.
966 * As a word of warning, there appears to be inconsistencies between
967 * how `DOMElement.elementsFromPoint` and `DOMWindowUtils.sendMouseEvent`
968 * internally rounds (ceils/floors) coordinates.
970 * @param {DOMRect} rect
971 * Element off a DOMRect sequence produced by calling
972 * `getClientRects` on an {@link Element}.
973 * @param {WindowProxy} win
974 * Current window global.
976 * @returns {Map.<string, number>}
977 * X and Y coordinates that denotes the in-view centre point of
980 dom.getInViewCentrePoint = function (rect, win) {
981 const { floor, max, min } = Math;
983 // calculate the intersection of the rect that is inside the viewport
985 left: max(0, min(rect.x, rect.x + rect.width)),
986 right: min(win.innerWidth, max(rect.x, rect.x + rect.width)),
987 top: max(0, min(rect.y, rect.y + rect.height)),
988 bottom: min(win.innerHeight, max(rect.y, rect.y + rect.height)),
991 // arrive at the centre point of the visible rectangle
992 let x = (visible.left + visible.right) / 2.0;
993 let y = (visible.top + visible.bottom) / 2.0;
995 // convert to CSS pixels, as centre point can be float
1003 * Produces a pointer-interactable elements tree from a given element.
1005 * The tree is defined by the paint order found at the centre point of
1006 * the element's rectangle that is inside the viewport, excluding the size
1007 * of any rendered scrollbars.
1009 * @param {DOMElement} el
1010 * Element to determine if is pointer-interactable.
1012 * @returns {Array.<DOMElement>}
1013 * Sequence of elements in paint order.
1015 dom.getPointerInteractablePaintTree = function (el) {
1016 const win = el.ownerGlobal;
1017 const rootNode = el.getRootNode();
1019 // pointer-interactable elements tree, step 1
1020 if (!el.isConnected) {
1025 let rects = el.getClientRects();
1026 if (!rects.length) {
1031 let centre = dom.getInViewCentrePoint(rects[0], win);
1034 return rootNode.elementsFromPoint(centre.x, centre.y);
1037 // TODO(ato): Not implemented.
1038 // In fact, it's not defined in the spec.
1039 dom.isKeyboardInteractable = () => true;
1042 * Attempts to scroll into view |el|.
1044 * @param {DOMElement} el
1045 * Element to scroll into view.
1047 dom.scrollIntoView = function (el) {
1048 if (el.scrollIntoView) {
1049 el.scrollIntoView({ block: "end", inline: "nearest" });
1054 * Ascertains whether <var>obj</var> is a DOM-, SVG-, or XUL element.
1056 * @param {object} obj
1057 * Object thought to be an <code>Element</code> or
1058 * <code>XULElement</code>.
1060 * @returns {boolean}
1061 * True if <var>obj</var> is an element, false otherwise.
1063 dom.isElement = function (obj) {
1064 return dom.isDOMElement(obj) || dom.isXULElement(obj);
1068 * Returns the shadow root of an element.
1070 * @param {Element} el
1071 * Element thought to have a <code>shadowRoot</code>
1072 * @returns {ShadowRoot}
1073 * Shadow root of the element.
1075 dom.getShadowRoot = function (el) {
1076 const shadowRoot = el.openOrClosedShadowRoot;
1078 throw new lazy.error.NoSuchShadowRootError();
1084 * Ascertains whether <var>node</var> is a shadow root.
1086 * @param {ShadowRoot} node
1087 * The node that will be checked to see if it has a shadow root
1089 * @returns {boolean}
1090 * True if <var>node</var> is a shadow root, false otherwise.
1092 dom.isShadowRoot = function (node) {
1095 node.nodeType === DOCUMENT_FRAGMENT_NODE &&
1096 node.containingShadowRoot == node
1101 * Ascertains whether <var>obj</var> is a DOM element.
1103 * @param {object} obj
1106 * @returns {boolean}
1107 * True if <var>obj</var> is a DOM element, false otherwise.
1109 dom.isDOMElement = function (obj) {
1110 return obj && obj.nodeType == ELEMENT_NODE && !dom.isXULElement(obj);
1114 * Ascertains whether <var>obj</var> is a XUL element.
1116 * @param {object} obj
1119 * @returns {boolean}
1120 * True if <var>obj</var> is a XULElement, false otherwise.
1122 dom.isXULElement = function (obj) {
1123 return obj && obj.nodeType === ELEMENT_NODE && obj.namespaceURI === XUL_NS;
1127 * Ascertains whether <var>node</var> is in a privileged document.
1129 * @param {Node} node
1132 * @returns {boolean}
1133 * True if <var>node</var> is in a privileged document,
1136 dom.isInPrivilegedDocument = function (node) {
1137 return !!node?.nodePrincipal?.isSystemPrincipal;
1141 * Ascertains whether <var>obj</var> is a <code>WindowProxy</code>.
1143 * @param {object} obj
1146 * @returns {boolean}
1147 * True if <var>obj</var> is a DOM window.
1149 dom.isDOMWindow = function (obj) {
1150 // TODO(ato): This should use Object.prototype.toString.call(node)
1151 // but it's not clear how to write a good xpcshell test for that,
1152 // seeing as we stub out a WindowProxy.
1154 typeof obj == "object" &&
1156 typeof obj.toString == "function" &&
1157 obj.toString() == "[object Window]" &&
1163 audio: ["autoplay", "controls", "loop", "muted"],
1164 button: ["autofocus", "disabled", "formnovalidate"],
1167 fieldset: ["disabled"],
1168 form: ["novalidate"],
1169 iframe: ["allowfullscreen"],
1180 keygen: ["autofocus", "disabled"],
1181 menuitem: ["checked", "default", "disabled"],
1183 optgroup: ["disabled"],
1184 option: ["disabled", "selected"],
1185 script: ["async", "defer"],
1186 select: ["autofocus", "disabled", "multiple", "required"],
1187 textarea: ["autofocus", "disabled", "readonly", "required"],
1189 video: ["autoplay", "controls", "loop", "muted"],
1193 * Tests if the attribute is a boolean attribute on element.
1195 * @param {Element} el
1196 * Element to test if <var>attr</var> is a boolean attribute on.
1197 * @param {string} attr
1198 * Attribute to test is a boolean attribute.
1200 * @returns {boolean}
1201 * True if the attribute is boolean, false otherwise.
1203 dom.isBooleanAttribute = function (el, attr) {
1204 if (!dom.isDOMElement(el)) {
1208 // global boolean attributes that apply to all HTML elements,
1209 // except for custom elements
1210 const customElement = !el.localName.includes("-");
1211 if ((attr == "hidden" || attr == "itemscope") && customElement) {
1215 if (!boolEls.hasOwnProperty(el.localName)) {
1218 return boolEls[el.localName].includes(attr);