Bug 1586798 - Use WalkerFront from the currently selected element in onTagEdit()...
[gecko.git] / testing / marionette / element.js
blob7b4d522ffba69f5f42edbb22bf57ad7cbe9f1098
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/. */
5 "use strict";
6 /* global XPCNativeWrapper */
8 const { assert } = ChromeUtils.import("chrome://marionette/content/assert.js");
9 const { atom } = ChromeUtils.import("chrome://marionette/content/atom.js");
10 const {
11   InvalidArgumentError,
12   InvalidSelectorError,
13   NoSuchElementError,
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 = [
22   "ChromeWebElement",
23   "ContentWebElement",
24   "ContentWebFrame",
25   "ContentWebWindow",
26   "element",
27   "WebElement",
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([
44   "menu",
45   "menuitem",
46   "menuseparator",
47   "radio",
48   "richlistitem",
49   "tab",
50 ]);
52 const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
53   Ci.nsIUUIDGenerator
56 /**
57  * This module provides shared functionality for dealing with DOM-
58  * and web elements in Marionette.
59  *
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.
62  *
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
66  * is the same.
67  *
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.
71  *
72  * @namespace
73  */
74 this.element = {};
76 element.Strategy = {
77   ClassName: "class name",
78   Selector: "css selector",
79   ID: "id",
80   Name: "name",
81   LinkText: "link text",
82   PartialLinkText: "partial link text",
83   TagName: "tag name",
84   XPath: "xpath",
85   Anon: "anon",
86   AnonAttribute: "anon attribute",
89 /**
90  * Stores known/seen elements and their associated web element
91  * references.
92  *
93  * Elements are added by calling {@link #add()} or {@link addAll()},
94  * and may be queried by their web element reference using {@link get()}.
95  *
96  * @class
97  * @memberof element
98  */
99 element.Store = class {
100   constructor() {
101     this.els = {};
102   }
104   clear() {
105     this.els = {};
106   }
108   /**
109    * Make a collection of elements seen.
110    *
111    * The oder of the returned web element references is guaranteed to
112    * match that of the collection passed in.
113    *
114    * @param {NodeList} els
115    *     Sequence of elements to add to set of seen elements.
116    *
117    * @return {Array.<WebElement>}
118    *     List of the web element references associated with each element
119    *     from <var>els</var>.
120    */
121   addAll(els) {
122     let add = this.add.bind(this);
123     return [...els].map(add);
124   }
126   /**
127    * Make an element seen.
128    *
129    * @param {(Element|WindowProxy|XULElement)} el
130    *    Element to add to set of seen elements.
131    *
132    * @return {WebElement}
133    *     Web element reference associated with element.
134    *
135    * @throws {TypeError}
136    *     If <var>el</var> is not an {@link Element} or a {@link XULElement}.
137    */
138   add(el) {
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)) {
145       throw new TypeError(
146         "Expected an element or WindowProxy, " + pprint`got: ${el}`
147       );
148     }
150     for (let i in this.els) {
151       let foundEl;
152       try {
153         foundEl = this.els[i].get();
154       } catch (e) {}
156       if (foundEl) {
157         if (new XPCNativeWrapper(foundEl) == new XPCNativeWrapper(el)) {
158           return WebElement.fromUUID(i, context);
159         }
161         // cleanup reference to gc'd element
162       } else {
163         delete this.els[i];
164       }
165     }
167     let webEl = WebElement.from(el);
168     this.els[webEl.uuid] = Cu.getWeakReference(el);
169     return webEl;
170   }
172   /**
173    * Determine if the provided web element reference has been seen
174    * before/is in the element store.
175    *
176    * Unlike when getting the element, a staleness check is not
177    * performed.
178    *
179    * @param {WebElement} webEl
180    *     Element's associated web element reference.
181    *
182    * @return {boolean}
183    *     True if element is in the store, false otherwise.
184    *
185    * @throws {TypeError}
186    *     If <var>webEl</var> is not a {@link WebElement}.
187    */
188   has(webEl) {
189     if (!(webEl instanceof WebElement)) {
190       throw new TypeError(pprint`Expected web element, got: ${webEl}`);
191     }
192     return Object.keys(this.els).includes(webEl.uuid);
193   }
195   /**
196    * Retrieve a DOM {@link Element} or a {@link XULElement} by its
197    * unique {@link WebElement} reference.
198    *
199    * @param {WebElement} webEl
200    *     Web element reference to find the associated {@link Element}
201    *     of.
202    * @param {WindowProxy} win
203    *     Current browsing context, which may differ from the associate
204    *     browsing context of <var>el</var>.
205    *
206    * @returns {(Element|XULElement)}
207    *     Element associated with reference.
208    *
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
213    *     seen before.
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
217    *     active document.
218    */
219   get(webEl, win) {
220     if (!(webEl instanceof WebElement)) {
221       throw new TypeError(pprint`Expected web element, got: ${webEl}`);
222     }
223     if (!this.has(webEl)) {
224       throw new NoSuchElementError(
225         "Web element reference not seen before: " + webEl.uuid
226       );
227     }
229     let el;
230     let ref = this.els[webEl.uuid];
231     try {
232       el = ref.get();
233     } catch (e) {
234       delete this.els[webEl.uuid];
235     }
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"
243       );
244     }
246     return el;
247   }
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>:
263  * <dl>
264  *   <dt><code>all</code>
265  *   <dd>
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>
270  *   <dd>
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
287  *     Options.
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.
299  */
300 element.find = function(container, strategy, selector, opts = {}) {
301   let all = !!opts.all;
302   let timeout = opts.timeout || 0;
303   let startNode = opts.startNode;
305   let searchFn;
306   if (opts.all) {
307     searchFn = findElements.bind(this);
308   } else {
309     searchFn = findElement.bind(this);
310   }
312   return new Promise((resolve, reject) => {
313     let findElements = new PollPromise(
314       (resolve, reject) => {
315         let res = find_(container, strategy, selector, searchFn, {
316           all,
317           startNode,
318         });
319         if (res.length > 0) {
320           resolve(Array.from(res));
321         } else {
322           reject([]);
323         }
324       },
325       { timeout }
326     );
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)) {
332         let msg;
333         switch (strategy) {
334           case element.Strategy.AnonAttribute:
335             msg =
336               "Unable to locate anonymous element: " + JSON.stringify(selector);
337             break;
339           default:
340             msg = "Unable to locate element: " + selector;
341         }
343         reject(new NoSuchElementError(msg));
344       }
346       if (opts.all) {
347         resolve(foundEls);
348       }
349       resolve(foundEls[0]);
350     }, reject);
351   });
354 function find_(
355   container,
356   strategy,
357   selector,
358   searchFn,
359   { startNode = null, all = false } = {}
360 ) {
361   let rootNode = container.shadowRoot || container.frame.document;
363   if (!startNode) {
364     switch (strategy) {
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;
371         }
372         break;
374       default:
375         startNode = rootNode;
376     }
377   }
379   let res;
380   try {
381     res = searchFn(strategy, selector, rootNode, startNode);
382   } catch (e) {
383     throw new InvalidSelectorError(
384       `Given ${strategy} expression "${selector}" is invalid: ${e}`
385     );
386   }
388   if (res) {
389     if (all) {
390       return res;
391     }
392     return [res];
393   }
394   return [];
398  * Find a single element by XPath expression.
400  * @param {HTMLDocument} document
401  *     Document root.
402  * @param {Element} startNode
403  *     Where in the DOM hiearchy to begin searching.
404  * @param {string} expression
405  *     XPath search expression.
407  * @return {Node}
408  *     First element matching <var>expression</var>.
409  */
410 element.findByXPath = function(document, startNode, expression) {
411   let iter = document.evaluate(
412     expression,
413     startNode,
414     null,
415     FIRST_ORDERED_NODE_TYPE,
416     null
417   );
418   return iter.singleNodeValue;
422  * Find elements by XPath expression.
424  * @param {HTMLDocument} document
425  *     Document root.
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>.
433  */
434 element.findByXPathAll = function*(document, startNode, expression) {
435   let iter = document.evaluate(
436     expression,
437     startNode,
438     null,
439     ORDERED_NODE_ITERATOR_TYPE,
440     null
441   );
442   let el = iter.iterateNext();
443   while (el) {
444     yield el;
445     el = iter.iterateNext();
446   }
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>.
460  */
461 element.findByLinkText = function(startNode, linkText) {
462   return filterLinks(
463     startNode,
464     link => atom.getElementText(link).trim() === linkText
465   );
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>.
480  */
481 element.findByPartialLinkText = function(startNode, linkText) {
482   return filterLinks(startNode, link =>
483     atom.getElementText(link).includes(linkText)
484   );
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.
497  */
498 element.findAnonymousNodes = function*(document, node) {
499   let anons = document.getAnonymousNodes(node) || [];
500   for (let node of anons) {
501     yield node;
502   }
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>.
517  */
518 function* filterLinks(startNode, predicate) {
519   for (let link of startNode.getElementsByTagName("a")) {
520     if (predicate(link)) {
521       yield link;
522     }
523   }
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
534  *     Document root.
535  * @param {Element=} startNode
536  *     Optional node from which to start searching.
538  * @return {Element}
539  *     Found elements.
541  * @throws {InvalidSelectorError}
542  *     If strategy <var>using</var> is not recognised.
543  * @throws {Error}
544  *     If selector expression <var>selector</var> is malformed.
545  */
546 function findElement(strategy, selector, document, startNode = undefined) {
547   switch (strategy) {
548     case element.Strategy.ID: {
549       if (startNode.getElementById) {
550         return startNode.getElementById(selector);
551       }
552       let expr = `.//*[@id="${selector}"]`;
553       return element.findByXPath(document, startNode, expr);
554     }
556     case element.Strategy.Name: {
557       if (startNode.getElementsByName) {
558         return startNode.getElementsByName(selector)[0];
559       }
560       let expr = `.//*[@name="${selector}"]`;
561       return element.findByXPath(document, startNode, expr);
562     }
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) {
576           return link;
577         }
578       }
579       return undefined;
581     case element.Strategy.PartialLinkText:
582       for (let link of startNode.getElementsByTagName("a")) {
583         if (atom.getElementText(link).includes(selector)) {
584           return link;
585         }
586       }
587       return undefined;
589     case element.Strategy.Selector:
590       try {
591         return startNode.querySelector(selector);
592       } catch (e) {
593         throw new InvalidSelectorError(`${e.message}: "${selector}"`);
594       }
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(
602         startNode,
603         attr,
604         selector[attr]
605       );
606   }
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
619  *     Document root.
620  * @param {Element=} startNode
621  *     Optional node from which to start searching.
623  * @return {Array.<Element>}
624  *     Found elements.
626  * @throws {InvalidSelectorError}
627  *     If strategy <var>strategy</var> is not recognised.
628  * @throws {Error}
629  *     If selector expression <var>selector</var> is malformed.
630  */
631 function findElements(strategy, selector, document, startNode = undefined) {
632   switch (strategy) {
633     case element.Strategy.ID:
634       selector = `.//*[@id="${selector}"]`;
636     // fall through
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);
643       }
644       return [
645         ...element.findByXPathAll(
646           document,
647           startNode,
648           `.//*[@name="${selector}"]`
649         ),
650       ];
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(
673         startNode,
674         attr,
675         selector[attr]
676       );
677       if (el) {
678         return [el];
679       }
680       return [];
682     default:
683       throw new InvalidSelectorError(`No such strategy: ${strategy}`);
684   }
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.
697  * @return {Node=}
698  *     First match to <var>selector</var>, or null if no match was found.
699  */
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)) {
705       return node;
706     }
707   }
708   return null;
712  * Determines if <var>obj<var> is an HTML or JS collection.
714  * @param {*} seq
715  *     Type to determine.
717  * @return {boolean}
718  *     True if <var>seq</va> is collection.
719  */
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]":
730       return true;
732     default:
733       return false;
734   }
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
742  * context.
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>&lt;iframe&gt;</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.
760  * @return {boolean}
761  *     True if <var>el</var> is stale, false otherwise.
762  */
763 element.isStale = function(el, win = undefined) {
764   if (typeof win == "undefined") {
765     win = el.ownerGlobal;
766   }
768   if (el === null || !el.ownerGlobal || el.ownerDocument !== win.document) {
769     return true;
770   }
772   return !el.isConnected;
776  * Determine if <var>el</var> is selected or not.
778  * This operation only makes sense on
779  * <tt>&lt;input type=checkbox&gt;</tt>,
780  * <tt>&lt;input type=radio&gt;</tt>,
781  * and <tt>&gt;option&gt;</tt> elements.
783  * @param {(DOMElement|XULElement)} el
784  *     Element to test if selected.
786  * @return {boolean}
787  *     True if element is selected, false otherwise.
788  */
789 element.isSelected = function(el) {
790   if (!el) {
791     return false;
792   }
794   if (element.isXULElement(el)) {
795     if (XUL_CHECKED_ELS.has(el.tagName)) {
796       return el.checked;
797     } else if (XUL_SELECTED_ELS.has(el.tagName)) {
798       return el.selected;
799     }
800   } else if (element.isDOMElement(el)) {
801     if (el.localName == "input" && ["checkbox", "radio"].includes(el.type)) {
802       return el.checked;
803     } else if (el.localName == "option") {
804       return el.selected;
805     }
806   }
808   return false;
812  * An element is considered read only if it is an
813  * <code>&lt;input&gt;</code> or <code>&lt;textarea&gt;</code>
814  * element whose <code>readOnly</code> content IDL attribute is set.
816  * @param {Element} el
817  *     Element to test is read only.
819  * @return {boolean}
820  *     True if element is read only.
821  */
822 element.isReadOnly = function(el) {
823   return (
824     element.isDOMElement(el) &&
825     ["input", "textarea"].includes(el.localName) &&
826     el.readOnly
827   );
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.
838  * @return {boolean}
839  *     True if element, or its container group, is disabled.
840  */
841 element.isDisabled = function(el) {
842   if (!element.isDOMElement(el)) {
843     return false;
844   }
846   switch (el.localName) {
847     case "option":
848     case "optgroup":
849       if (el.disabled) {
850         return true;
851       }
852       let parent = element.findClosest(el, "optgroup,select");
853       return element.isDisabled(parent);
855     case "button":
856     case "input":
857     case "select":
858     case "textarea":
859       return el.disabled;
861     default:
862       return false;
863   }
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>&lt;input&gt;</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>&lt;textarea&gt;</code> elements.
875  * @param {Element} el
876  *     Element to test.
878  * @return {boolean}
879  *     True if editable, false otherwise.
880  */
881 element.isMutableFormControl = function(el) {
882   if (!element.isDOMElement(el)) {
883     return false;
884   }
885   if (element.isReadOnly(el) || element.isDisabled(el)) {
886     return false;
887   }
889   if (el.localName == "textarea") {
890     return true;
891   }
893   if (el.localName != "input") {
894     return false;
895   }
897   switch (el.type) {
898     case "color":
899     case "date":
900     case "datetime-local":
901     case "email":
902     case "file":
903     case "month":
904     case "number":
905     case "password":
906     case "range":
907     case "search":
908     case "tel":
909     case "text":
910     case "time":
911     case "url":
912     case "week":
913       return true;
915     default:
916       return false;
917   }
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.
928  * @return {boolean}
929  *     True if editing host, false otherwise.
930  */
931 element.isEditingHost = function(el) {
932   return (
933     element.isDOMElement(el) &&
934     (el.isContentEditable || el.ownerDocument.designMode == "on")
935   );
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:
944  * <ul>
945  * <li>It is a <code>&lt;textarea&gt;</code> element.
947  * <li>It is an <code>&lt;input&gt;</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.
954  * </ul>
956  * @param {Element}
957  *     Element to test if editable.
959  * @return {boolean}
960  *     True if editable, false otherwise.
961  */
962 element.isEditable = function(el) {
963   if (!element.isDOMElement(el)) {
964     return false;
965   }
967   if (element.isReadOnly(el) || element.isDisabled(el)) {
968     return false;
969   }
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
977  * top-left corner.
979  * @param {Node} node
980  *     Target node.
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.
991  * @throws TypeError
992  *     If <var>xOffset</var> or <var>yOffset</var> are not numbers.
993  */
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;
999   }
1000   if (typeof yOffset == "undefined" || yOffset === null) {
1001     yOffset = box.height / 2.0;
1002   }
1004   if (typeof yOffset != "number" || typeof xOffset != "number") {
1005     throw new TypeError("Offset must be a number");
1006   }
1008   return {
1009     x: box.left + xOffset,
1010     y: box.top + yOffset,
1011   };
1015  * This function returns true if the node is in the viewport.
1017  * @param {Element} el
1018  *     Target element.
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.
1026  * @return {boolean}
1027  *     True if if <var>el</var> is in viewport, false otherwise.
1028  */
1029 element.inViewport = function(el, x = undefined, y = undefined) {
1030   let win = el.ownerGlobal;
1031   let c = element.coordinates(el, x, y);
1032   let vp = {
1033     top: win.pageYOffset,
1034     left: win.pageXOffset,
1035     bottom: win.pageYOffset + win.innerHeight,
1036     right: win.pageXOffset + win.innerWidth,
1037   };
1039   return (
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
1044   );
1048  * Gets the element's container element.
1050  * An element container is defined by the WebDriver
1051  * specification to be an <tt>&lt;option&gt;</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>&lt;datalist&gt;</tt> or <tt>&lt;select&gt;</tt>.
1056  * If the element does not have a valid context, its container element
1057  * is itself.
1059  * @param {Element} el
1060  *     Element to get the container of.
1062  * @return {Element}
1063  *     Container element of <var>el</var>.
1064  */
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;
1070   }
1072   return el;
1076  * An element is in view if it is a member of its own pointer-interactable
1077  * paint tree.
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.
1093  * @return {boolean}
1094  *     True if <var>el</var> is inside the viewport, or false otherwise.
1095  */
1096 element.isInView = function(el) {
1097   let originalPointerEvents = el.style.pointerEvents;
1099   try {
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]);
1107     }
1109     return tree.includes(el);
1110   } finally {
1111     el.style.pointerEvents = originalPointerEvents;
1112   }
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.
1128  * @return {boolean}
1129  *     True if visible, false otherwise.
1130  */
1131 element.isVisible = function(el, x = undefined, y = undefined) {
1132   let win = el.ownerGlobal;
1134   if (!atom.isElementDisplayed(el, win)) {
1135     return false;
1136   }
1138   if (el.tagName.toLowerCase() == "body") {
1139     return true;
1140   }
1142   if (!element.inViewport(el, x, y)) {
1143     element.scrollIntoView(el);
1144     if (!element.inViewport(el)) {
1145       return false;
1146     }
1147   }
1148   return true;
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.
1164  * @return {boolean}
1165  *     True if element is obscured, false otherwise.
1166  */
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
1199  *     `rect`.
1200  */
1201 element.getInViewCentrePoint = function(rect, win) {
1202   const { floor, max, min } = Math;
1204   // calculate the intersection of the rect that is inside the viewport
1205   let visible = {
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)),
1210   };
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
1217   x = floor(x);
1218   y = floor(y);
1220   return { x, y };
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.
1235  */
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) {
1243     return [];
1244   }
1246   // steps 2-3
1247   let rects = el.getClientRects();
1248   if (rects.length == 0) {
1249     return [];
1250   }
1252   // step 4
1253   let centre = element.getInViewCentrePoint(rects[0], win);
1255   // step 5
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.
1268  */
1269 element.scrollIntoView = function(el) {
1270   if (el.scrollIntoView) {
1271     el.scrollIntoView({ block: "end", inline: "nearest", behavior: "instant" });
1272   }
1276  * Ascertains whether <var>node</var> is a DOM-, SVG-, or XUL element.
1278  * @param {*} node
1279  *     Element thought to be an <code>Element</code> or
1280  *     <code>XULElement</code>.
1282  * @return {boolean}
1283  *     True if <var>node</var> is an element, false otherwise.
1284  */
1285 element.isElement = function(node) {
1286   return element.isDOMElement(node) || element.isXULElement(node);
1290  * Ascertains whether <var>node</var> is a DOM element.
1292  * @param {*} node
1293  *     Element thought to be an <code>Element</code>.
1295  * @return {boolean}
1296  *     True if <var>node</var> is a DOM element, false otherwise.
1297  */
1298 element.isDOMElement = function(node) {
1299   return (
1300     typeof node == "object" &&
1301     node !== null &&
1302     "nodeType" in node &&
1303     [ELEMENT_NODE, DOCUMENT_NODE].includes(node.nodeType) &&
1304     !element.isXULElement(node)
1305   );
1309  * Ascertains whether <var>el</var> is a XUL- or XBL element.
1311  * @param {*} node
1312  *     Element thought to be a XUL- or XBL element.
1314  * @return {boolean}
1315  *     True if <var>node</var> is a XULElement or XBLElement,
1316  *     false otherwise.
1317  */
1318 element.isXULElement = function(node) {
1319   return (
1320     typeof node == "object" &&
1321     node !== null &&
1322     "nodeType" in node &&
1323     node.nodeType === node.ELEMENT_NODE &&
1324     [XBLNS, XULNS].includes(node.namespaceURI)
1325   );
1329  * Ascertains whether <var>node</var> is a <code>WindowProxy</code>.
1331  * @param {*} node
1332  *     Node thought to be a <code>WindowProxy</code>.
1334  * @return {boolean}
1335  *     True if <var>node</var> is a DOM window.
1336  */
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.
1341   return (
1342     typeof node == "object" &&
1343     node !== null &&
1344     typeof node.toString == "function" &&
1345     node.toString() == "[object Window]" &&
1346     node.self === node
1347   );
1350 const boolEls = {
1351   audio: ["autoplay", "controls", "loop", "muted"],
1352   button: ["autofocus", "disabled", "formnovalidate"],
1353   details: ["open"],
1354   dialog: ["open"],
1355   fieldset: ["disabled"],
1356   form: ["novalidate"],
1357   iframe: ["allowfullscreen"],
1358   img: ["ismap"],
1359   input: [
1360     "autofocus",
1361     "checked",
1362     "disabled",
1363     "formnovalidate",
1364     "multiple",
1365     "readonly",
1366     "required",
1367   ],
1368   keygen: ["autofocus", "disabled"],
1369   menuitem: ["checked", "default", "disabled"],
1370   ol: ["reversed"],
1371   optgroup: ["disabled"],
1372   option: ["disabled", "selected"],
1373   script: ["async", "defer"],
1374   select: ["autofocus", "disabled", "multiple", "required"],
1375   textarea: ["autofocus", "disabled", "readonly", "required"],
1376   track: ["default"],
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.
1388  * @return {boolean}
1389  *     True if the attribute is boolean, false otherwise.
1390  */
1391 element.isBooleanAttribute = function(el, attr) {
1392   if (!element.isDOMElement(el)) {
1393     return false;
1394   }
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) {
1400     return true;
1401   }
1403   if (!boolEls.hasOwnProperty(el.localName)) {
1404     return false;
1405   }
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.
1415  */
1416 class WebElement {
1417   /**
1418    * @param {string} uuid
1419    *     Identifier that must be unique across all browsing contexts
1420    *     for the contract to be upheld.
1421    */
1422   constructor(uuid) {
1423     this.uuid = assert.string(uuid);
1424   }
1426   /**
1427    * Performs an equality check between this web element and
1428    * <var>other</var>.
1429    *
1430    * @param {WebElement} other
1431    *     Web element to compare with this.
1432    *
1433    * @return {boolean}
1434    *     True if this and <var>other</var> are the same.  False
1435    *     otherwise.
1436    */
1437   is(other) {
1438     return other instanceof WebElement && this.uuid === other.uuid;
1439   }
1441   toString() {
1442     return `[object ${this.constructor.name} uuid=${this.uuid}]`;
1443   }
1445   /**
1446    * Returns a new {@link WebElement} reference for a DOM element,
1447    * <code>WindowProxy</code>, or XUL element.
1448    *
1449    * @param {(Element|WindowProxy|XULElement)} node
1450    *     Node to construct a web element reference for.
1451    *
1452    * @return {(ContentWebElement|ChromeWebElement)}
1453    *     Web element reference for <var>el</var>.
1454    *
1455    * @throws {InvalidArgumentError}
1456    *     If <var>node</var> is neither a <code>WindowProxy</code>,
1457    *     DOM element, or a XUL element.
1458    */
1459   static from(node) {
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);
1467       }
1468       return new ContentWebFrame(uuid);
1469     } else if (element.isXULElement(node)) {
1470       return new ChromeWebElement(uuid);
1471     }
1473     throw new InvalidArgumentError(
1474       "Expected DOM window/element " + pprint`or XUL element, got: ${node}`
1475     );
1476   }
1478   /**
1479    * Unmarshals a JSON Object to one of {@link ContentWebElement},
1480    * {@link ContentWebWindow}, {@link ContentWebFrame}, or
1481    * {@link ChromeWebElement}.
1482    *
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.
1487    *
1488    * @return {WebElement}
1489    *     Representation of the web element.
1490    *
1491    * @throws {InvalidArgumentError}
1492    *     If <var>json</var> is not a web element reference.
1493    */
1494   static fromJSON(json) {
1495     assert.object(json);
1496     let keys = Object.keys(json);
1498     for (let key of keys) {
1499       switch (key) {
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);
1511       }
1512     }
1514     throw new InvalidArgumentError(
1515       pprint`Expected web element reference, got: ${json}`
1516     );
1517   }
1519   /**
1520    * Constructs a {@link ContentWebElement} or {@link ChromeWebElement}
1521    * from a a string <var>uuid</var>.
1522    *
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.
1528    *
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.
1534    *
1535    * @return {WebElement}
1536    *     One of {@link ContentWebElement} or {@link ChromeWebElement},
1537    *     based on <var>context</var>.
1538    *
1539    * @throws {InvalidArgumentError}
1540    *     If <var>uuid</var> is not a string or <var>context</var>
1541    *     is an invalid context.
1542    */
1543   static fromUUID(uuid, context) {
1544     assert.string(uuid);
1546     switch (context) {
1547       case "chrome":
1548         return new ChromeWebElement(uuid);
1550       case "content":
1551         return new ContentWebElement(uuid);
1553       default:
1554         throw new InvalidArgumentError("Unknown context: " + context);
1555     }
1556   }
1558   /**
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.
1562    *
1563    * @param {Object.<string, string>} obj
1564    *     Object that represents a reference to a {@link WebElement}.
1565    * @return {boolean}
1566    *     True if <var>obj</var> is a {@link WebElement}, false otherwise.
1567    */
1568   static isReference(obj) {
1569     if (Object.prototype.toString.call(obj) != "[object Object]") {
1570       return false;
1571     }
1573     if (
1574       ContentWebElement.Identifier in obj ||
1575       ContentWebWindow.Identifier in obj ||
1576       ContentWebFrame.Identifier in obj ||
1577       ChromeWebElement.Identifier in obj
1578     ) {
1579       return true;
1580     }
1581     return false;
1582   }
1584   /**
1585    * Generates a unique identifier.
1586    *
1587    * @return {string}
1588    *     UUID.
1589    */
1590   static generateUUID() {
1591     let uuid = uuidGen.generateUUID().toString();
1592     return uuid.substring(1, uuid.length - 1);
1593   }
1595 this.WebElement = WebElement;
1598  * DOM elements are represented as web elements when they are
1599  * transported over the wire protocol.
1600  */
1601 class ContentWebElement extends WebElement {
1602   toJSON() {
1603     return { [ContentWebElement.Identifier]: this.uuid };
1604   }
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}`
1612       );
1613     }
1615     let uuid = json[Identifier];
1616     return new ContentWebElement(uuid);
1617   }
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.
1626  */
1627 class ContentWebWindow extends WebElement {
1628   toJSON() {
1629     return { [ContentWebWindow.Identifier]: this.uuid };
1630   }
1632   static fromJSON(json) {
1633     if (!(ContentWebWindow.Identifier in json)) {
1634       throw new InvalidArgumentError(
1635         pprint`Expected web window reference, got: ${json}`
1636       );
1637     }
1638     let uuid = json[ContentWebWindow.Identifier];
1639     return new ContentWebWindow(uuid);
1640   }
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>&lt;frame&gt;</tt> and <tt>&lt;iframe&gt;</tt>,
1648  * are represented as web frames over the wire protocol.
1649  */
1650 class ContentWebFrame extends WebElement {
1651   toJSON() {
1652     return { [ContentWebFrame.Identifier]: this.uuid };
1653   }
1655   static fromJSON(json) {
1656     if (!(ContentWebFrame.Identifier in json)) {
1657       throw new InvalidArgumentError(
1658         pprint`Expected web frame reference, got: ${json}`
1659       );
1660     }
1661     let uuid = json[ContentWebFrame.Identifier];
1662     return new ContentWebFrame(uuid);
1663   }
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.
1671  */
1672 class ChromeWebElement extends WebElement {
1673   toJSON() {
1674     return { [ChromeWebElement.Identifier]: this.uuid };
1675   }
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}`
1682       );
1683     }
1684     let uuid = json[ChromeWebElement.Identifier];
1685     return new ChromeWebElement(uuid);
1686   }
1688 ChromeWebElement.Identifier = "chromeelement-9fc5-4b51-a3c8-01716eedeb04";
1689 this.ChromeWebElement = ChromeWebElement;