12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
- Cu.import("resource://gre/modules/Log.jsm");
- Cu.import("chrome://marionette/content/assert.js");
- Cu.import("chrome://marionette/content/atom.js");
- Cu.import("chrome://marionette/content/error.js");
- const logger = Log.repository.getLogger("Marionette");
- /**
- * This module provides shared functionality for dealing with DOM-
- * and web elements in Marionette.
- *
- * A web element is an abstraction used to identify an element when it
- * is transported across the protocol, between remote- and local ends.
- *
- * Each element has an associated web element reference (a UUID) that
- * uniquely identifies the the element across all browsing contexts. The
- * web element reference for every element representing the same element
- * is the same.
- *
- * The @code{element.Store} provides a mapping between web element
- * references and DOM elements for each browsing context. It also provides
- * functionality for looking up and retrieving elements.
- */
- this.EXPORTED_SYMBOLS = ["element"];
- const DOCUMENT_POSITION_DISCONNECTED = 1;
- const XMLNS = "http://www.w3.org/1999/xhtml";
- const uuidGen = Cc["@mozilla.org/uuid-generator;1"]
- .getService(Ci.nsIUUIDGenerator);
- this.element = {};
- element.Key = "element-6066-11e4-a52e-4f735466cecf";
- element.LegacyKey = "ELEMENT";
- element.Strategy = {
- ClassName: "class name",
- Selector: "css selector",
- ID: "id",
- Name: "name",
- LinkText: "link text",
- PartialLinkText: "partial link text",
- TagName: "tag name",
- XPath: "xpath",
- Anon: "anon",
- AnonAttribute: "anon attribute",
- };
- /**
- * Stores known/seen elements and their associated web element
- * references.
- *
- * Elements are added by calling |add(el)| or |addAll(elements)|, and
- * may be queried by their web element reference using |get(element)|.
- */
- element.Store = class {
- constructor() {
- this.els = {};
- this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- }
- clear() {
- this.els = {};
- }
- /**
- * Make a collection of elements seen.
- *
- * The oder of the returned web element references is guaranteed to
- * match that of the collection passed in.
- *
- * @param {NodeList} els
- * Sequence of elements to add to set of seen elements.
- *
- * @return {Array.<WebElement>}
- * List of the web element references associated with each element
- * from |els|.
- */
- addAll(els) {
- let add = this.add.bind(this);
- return [...els].map(add);
- }
- /**
- * Make an element seen.
- *
- * @param {nsIDOMElement} el
- * Element to add to set of seen elements.
- *
- * @return {string}
- * Web element reference associated with element.
- */
- add(el) {
- for (let i in this.els) {
- let foundEl;
- try {
- foundEl = this.els[i].get();
- } catch (e) {}
- if (foundEl) {
- if (new XPCNativeWrapper(foundEl) == new XPCNativeWrapper(el)) {
- return i;
- }
- // cleanup reference to gc'd element
- } else {
- delete this.els[i];
- }
- }
- let id = element.generateUUID();
- this.els[id] = Cu.getWeakReference(el);
- return id;
- }
- /**
- * Determine if the provided web element reference has been seen
- * before/is in the element store.
- *
- * @param {string} uuid
- * Element's associated web element reference.
- *
- * @return {boolean}
- * True if element is in the store, false otherwise.
- */
- has(uuid) {
- return Object.keys(this.els).includes(uuid);
- }
- /**
- * Retrieve a DOM element by its unique web element reference/UUID.
- *
- * @param {string} uuid
- * Web element reference, or UUID.
- * @param {(nsIDOMWindow|ShadowRoot)} container
- * Window and an optional shadow root that contains the element.
- *
- * @returns {nsIDOMElement}
- * Element associated with reference.
- *
- * @throws {JavaScriptError}
- * If the provided reference is unknown.
- * @throws {StaleElementReferenceError}
- * If element has gone stale, indicating it is no longer attached to
- * the DOM provided in the container.
- */
- get(uuid, container) {
- let el = this.els[uuid];
- if (!el) {
- throw new JavaScriptError(`Element reference not seen before: ${uuid}`);
- }
- try {
- el = el.get();
- } catch (e) {
- el = null;
- delete this.els[id];
- }
- // use XPCNativeWrapper to compare elements (see bug 834266)
- let wrappedFrame = new XPCNativeWrapper(container.frame);
- let wrappedShadowRoot;
- if (container.shadowRoot) {
- wrappedShadowRoot = new XPCNativeWrapper(container.shadowRoot);
- }
- let wrappedEl = new XPCNativeWrapper(el);
- let wrappedContainer = {
- frame: wrappedFrame,
- shadowRoot: wrappedShadowRoot,
- };
- if (!el ||
- !(wrappedEl.ownerDocument == wrappedFrame.document) ||
- element.isDisconnected(wrappedEl, wrappedContainer)) {
- throw new StaleElementReferenceError(
- error.pprint`The element reference of ${el} stale: ` +
- "either the element is no longer attached to the DOM " +
- "or the page has been refreshed");
- }
- return el;
- }
- };
- /**
- * Find a single element or a collection of elements starting at the
- * document root or a given node.
- *
- * If |timeout| is above 0, an implicit search technique is used.
- * This will wait for the duration of |timeout| for the element
- * to appear in the DOM.
- *
- * See the |element.Strategy| enum for a full list of supported
- * search strategies that can be passed to |strategy|.
- *
- * Available flags for |opts|:
- *
- * |all|
- * If true, a multi-element search selector is used and a sequence
- * of elements will be returned. Otherwise a single element.
- *
- * |timeout|
- * Duration to wait before timing out the search. If |all| is
- * false, a NoSuchElementError is thrown if unable to find
- * the element within the timeout duration.
- *
- * |startNode|
- * Element to use as the root of the search.
- *
- * @param {Object.<string, Window>} container
- * Window object and an optional shadow root that contains the
- * root shadow DOM element.
- * @param {string} strategy
- * Search strategy whereby to locate the element(s).
- * @param {string} selector
- * Selector search pattern. The selector must be compatible with
- * the chosen search |strategy|.
- * @param {Object.<string, ?>} opts
- * Options.
- *
- * @return {Promise: (nsIDOMElement|Array<nsIDOMElement>)}
- * Single element or a sequence of elements.
- *
- * @throws InvalidSelectorError
- * If |strategy| is unknown.
- * @throws InvalidSelectorError
- * If |selector| is malformed.
- * @throws NoSuchElementError
- * If a single element is requested, this error will throw if the
- * element is not found.
- */
- element.find = function (container, strategy, selector, opts = {}) {
- opts.all = !!opts.all;
- opts.timeout = opts.timeout || 0;
- let searchFn;
- if (opts.all) {
- searchFn = findElements.bind(this);
- } else {
- searchFn = findElement.bind(this);
- }
- return new Promise((resolve, reject) => {
- let findElements = implicitlyWaitFor(
- () => find_(container, strategy, selector, searchFn, opts),
- opts.timeout);
- findElements.then(foundEls => {
- // the following code ought to be moved into findElement
- // and findElements when bug 1254486 is addressed
- if (!opts.all && (!foundEls || foundEls.length == 0)) {
- let msg;
- switch (strategy) {
- case element.Strategy.AnonAttribute:
- msg = "Unable to locate anonymous element: " + JSON.stringify(selector);
- break;
- default:
- msg = "Unable to locate element: " + selector;
- }
- reject(new NoSuchElementError(msg));
- }
- if (opts.all) {
- resolve(foundEls);
- }
- resolve(foundEls[0]);
- }, reject);
- });
- };
- function find_(container, strategy, selector, searchFn, opts) {
- let rootNode = container.shadowRoot || container.frame.document;
- let startNode;
- if (opts.startNode) {
- startNode = opts.startNode;
- } else {
- switch (strategy) {
- // For anonymous nodes the start node needs to be of type DOMElement, which
- // will refer to :root in case of a DOMDocument.
- case element.Strategy.Anon:
- case element.Strategy.AnonAttribute:
- if (rootNode instanceof Ci.nsIDOMDocument) {
- startNode = rootNode.documentElement;
- }
- break;
- default:
- startNode = rootNode;
- }
- }
- let res;
- try {
- res = searchFn(strategy, selector, rootNode, startNode);
- } catch (e) {
- throw new InvalidSelectorError(
- `Given ${strategy} expression "${selector}" is invalid: ${e}`);
- }
- if (res) {
- if (opts.all) {
- return res;
- }
- return [res];
- }
- return [];
- }
- /**
- * Find a single element by XPath expression.
- *
- * @param {DOMElement} root
- * Document root
- * @param {DOMElement} startNode
- * Where in the DOM hiearchy to begin searching.
- * @param {string} expr
- * XPath search expression.
- *
- * @return {DOMElement}
- * First element matching expression.
- */
- element.findByXPath = function (root, startNode, expr) {
- let iter = root.evaluate(expr, startNode, null,
- Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
- return iter.singleNodeValue;
- };
- /**
- * Find elements by XPath expression.
- *
- * @param {DOMElement} root
- * Document root.
- * @param {DOMElement} startNode
- * Where in the DOM hierarchy to begin searching.
- * @param {string} expr
- * XPath search expression.
- *
- * @return {Array.<DOMElement>}
- * Sequence of found elements matching expression.
- */
- element.findByXPathAll = function (root, startNode, expr) {
- let rv = [];
- let iter = root.evaluate(expr, startNode, null,
- Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
- let el = iter.iterateNext();
- while (el) {
- rv.push(el);
- el = iter.iterateNext();
- }
- return rv;
- };
- /**
- * Find all hyperlinks dscendant of |node| which link text is |s|.
- *
- * @param {DOMElement} node
- * Where in the DOM hierarchy to being searching.
- * @param {string} s
- * Link text to search for.
- *
- * @return {Array.<DOMAnchorElement>}
- * Sequence of link elements which text is |s|.
- */
- element.findByLinkText = function (node, s) {
- return filterLinks(node, link => link.text.trim() === s);
- };
- /**
- * Find all hyperlinks descendant of |node| which link text contains |s|.
- *
- * @param {DOMElement} node
- * Where in the DOM hierachy to begin searching.
- * @param {string} s
- * Link text to search for.
- *
- * @return {Array.<DOMAnchorElement>}
- * Sequence of link elements which text containins |s|.
- */
- element.findByPartialLinkText = function (node, s) {
- return filterLinks(node, link => link.text.indexOf(s) != -1);
- };
- /**
- * Filters all hyperlinks that are descendant of |node| by |predicate|.
- *
- * @param {DOMElement} node
- * Where in the DOM hierarchy to begin searching.
- * @param {function(DOMAnchorElement): boolean} predicate
- * Function that determines if given link should be included in
- * return value or filtered away.
- *
- * @return {Array.<DOMAnchorElement>}
- * Sequence of link elements matching |predicate|.
- */
- function filterLinks(node, predicate) {
- let rv = [];
- for (let link of node.getElementsByTagName("a")) {
- if (predicate(link)) {
- rv.push(link);
- }
- }
- return rv;
- }
- /**
- * Finds a single element.
- *
- * @param {element.Strategy} using
- * Selector strategy to use.
- * @param {string} value
- * Selector expression.
- * @param {DOMElement} rootNode
- * Document root.
- * @param {DOMElement=} startNode
- * Optional node from which to start searching.
- *
- * @return {DOMElement}
- * Found elements.
- *
- * @throws {InvalidSelectorError}
- * If strategy |using| is not recognised.
- * @throws {Error}
- * If selector expression |value| is malformed.
- */
- function findElement(using, value, rootNode, startNode) {
- switch (using) {
- case element.Strategy.ID:
- if (startNode.getElementById) {
- return startNode.getElementById(value);
- }
- return element.findByXPath(rootNode, startNode, `.//*[@id="${value}"]`);
- case element.Strategy.Name:
- if (startNode.getElementsByName) {
- return startNode.getElementsByName(value)[0];
- }
- return element.findByXPath(rootNode, startNode, `.//*[@name="${value}"]`);
- case element.Strategy.ClassName:
- // works for >= Firefox 3
- return startNode.getElementsByClassName(value)[0];
- case element.Strategy.TagName:
- // works for all elements
- return startNode.getElementsByTagName(value)[0];
- case element.Strategy.XPath:
- return element.findByXPath(rootNode, startNode, value);
- case element.Strategy.LinkText:
- for (let link of startNode.getElementsByTagName("a")) {
- if (link.text.trim() === value) {
- return link;
- }
- }
- break;
- case element.Strategy.PartialLinkText:
- for (let link of startNode.getElementsByTagName("a")) {
- if (link.text.indexOf(value) != -1) {
- return link;
- }
- }
- break;
- case element.Strategy.Selector:
- try {
- return startNode.querySelector(value);
- } catch (e) {
- throw new InvalidSelectorError(`${e.message}: "${value}"`);
- }
- break;
- case element.Strategy.Anon:
- return rootNode.getAnonymousNodes(startNode);
- case element.Strategy.AnonAttribute:
- let attr = Object.keys(value)[0];
- return rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
- default:
- throw new InvalidSelectorError(`No such strategy: ${using}`);
- }
- }
- /**
- * Find multiple elements.
- *
- * @param {element.Strategy} using
- * Selector strategy to use.
- * @param {string} value
- * Selector expression.
- * @param {DOMElement} rootNode
- * Document root.
- * @param {DOMElement=} startNode
- * Optional node from which to start searching.
- *
- * @return {DOMElement}
- * Found elements.
- *
- * @throws {InvalidSelectorError}
- * If strategy |using| is not recognised.
- * @throws {Error}
- * If selector expression |value| is malformed.
- */
- function findElements(using, value, rootNode, startNode) {
- switch (using) {
- case element.Strategy.ID:
- value = `.//*[@id="${value}"]`;
- // fall through
- case element.Strategy.XPath:
- return element.findByXPathAll(rootNode, startNode, value);
- case element.Strategy.Name:
- if (startNode.getElementsByName) {
- return startNode.getElementsByName(value);
- }
- return element.findByXPathAll(rootNode, startNode, `.//*[@name="${value}"]`);
- case element.Strategy.ClassName:
- return startNode.getElementsByClassName(value);
- case element.Strategy.TagName:
- return startNode.getElementsByTagName(value);
- case element.Strategy.LinkText:
- return element.findByLinkText(startNode, value);
- case element.Strategy.PartialLinkText:
- return element.findByPartialLinkText(startNode, value);
- case element.Strategy.Selector:
- return startNode.querySelectorAll(value);
- case element.Strategy.Anon:
- return rootNode.getAnonymousNodes(startNode);
- case element.Strategy.AnonAttribute:
- let attr = Object.keys(value)[0];
- let el = rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
- if (el) {
- return [el];
- }
- return [];
- default:
- throw new InvalidSelectorError(`No such strategy: ${using}`);
- }
- }
- /**
- * Runs function off the main thread until its return value is truthy
- * or the provided timeout is reached. The function is guaranteed to be
- * run at least once, irregardless of the timeout.
- *
- * A truthy return value constitutes a truthful boolean, positive number,
- * object, or non-empty array.
- *
- * The |func| is evaluated every |interval| for as long as its runtime
- * duration does not exceed |interval|. If the runtime evaluation duration
- * of |func| is greater than |interval|, evaluations of |func| are queued.
- *
- * @param {function(): ?} func
- * Function to run off the main thread.
- * @param {number} timeout
- * Desired timeout. If 0 or less than the runtime evaluation time
- * of |func|, |func| is guaranteed to run at least once.
- * @param {number=} interval
- * Duration between each poll of |func| in milliseconds. Defaults to
- * 100 milliseconds.
- *
- * @return {Promise}
- * Yields the return value from |func|. The promise is rejected if
- * |func| throws.
- */
- function implicitlyWaitFor(func, timeout, interval = 100) {
- let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- return new Promise((resolve, reject) => {
- let startTime = new Date().getTime();
- let endTime = startTime + timeout;
- let elementSearch = function() {
- let res;
- try {
- res = func();
- } catch (e) {
- reject(e);
- }
- if (
- // collections that might contain web elements
- // should be checked until they are not empty
- (element.isCollection(res) && res.length > 0)
- // !![] (ensuring boolean type on empty array) always returns true
- // and we can only use it on non-collections
- || (!element.isCollection(res) && !!res)
- // return immediately if timeout is 0,
- // allowing |func| to be evaluted at least once
- || startTime == endTime
- // return if timeout has elapsed
- || new Date().getTime() >= endTime
- ) {
- resolve(res);
- }
- };
- // the repeating slack timer waits |interval|
- // before invoking |elementSearch|
- elementSearch();
- timer.init(elementSearch, interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
- // cancel timer and propagate result
- }).then(res => {
- timer.cancel();
- return res;
- }, err => {
- timer.cancel();
- throw err;
- });
- }
- /** Determines if |obj| is an HTML or JS collection. */
- element.isCollection = function (seq) {
- switch (Object.prototype.toString.call(seq)) {
- case "[object Arguments]":
- case "[object Array]":
- case "[object FileList]":
- case "[object HTMLAllCollection]":
- case "[object HTMLCollection]":
- case "[object HTMLFormControlsCollection]":
- case "[object HTMLOptionsCollection]":
- case "[object NodeList]":
- return true;
- default:
- return false;
- }
- };
- element.makeWebElement = function (uuid) {
- return {
- [element.Key]: uuid,
- [element.LegacyKey]: uuid,
- };
- };
- /**
- * Checks if |ref| has either |element.Key| or |element.LegacyKey| as properties.
- *
- * @param {?} ref
- * Object that represents a web element reference.
- * @return {boolean}
- * True if |ref| has either expected property.
- */
- element.isWebElementReference = function (ref) {
- let properties = Object.getOwnPropertyNames(ref);
- return properties.includes(element.Key) || properties.includes(element.LegacyKey);
- };
- element.generateUUID = function() {
- let uuid = uuidGen.generateUUID().toString();
- return uuid.substring(1, uuid.length - 1);
- };
- /**
- * Convert any web elements in arbitrary objects to DOM elements by
- * looking them up in the seen element store.
- *
- * @param {?} obj
- * Arbitrary object containing web elements.
- * @param {element.Store} seenEls
- * Element store to use for lookup of web element references.
- * @param {Window} win
- * Window.
- * @param {ShadowRoot} shadowRoot
- * Shadow root.
- *
- * @return {?}
- * Same object as provided by |obj| with the web elements replaced
- * by DOM elements.
- */
- element.fromJson = function (
- obj, seenEls, win, shadowRoot = undefined) {
- switch (typeof obj) {
- case "boolean":
- case "number":
- case "string":
- return obj;
- case "object":
- if (obj === null) {
- return obj;
- }
- // arrays
- else if (Array.isArray(obj)) {
- return obj.map(e => element.fromJson(e, seenEls, win, shadowRoot));
- }
- // web elements
- else if (Object.keys(obj).includes(element.Key) ||
- Object.keys(obj).includes(element.LegacyKey)) {
- let uuid = obj[element.Key] || obj[element.LegacyKey];
- let el = seenEls.get(uuid, {frame: win, shadowRoot: shadowRoot});
- if (!el) {
- throw new WebDriverError(`Unknown element: ${uuid}`);
- }
- return el;
- }
- // arbitrary objects
- else {
- let rv = {};
- for (let prop in obj) {
- rv[prop] = element.fromJson(obj[prop], seenEls, win, shadowRoot);
- }
- return rv;
- }
- }
- };
- /**
- * Convert arbitrary objects to JSON-safe primitives that can be
- * transported over the Marionette protocol.
- *
- * Any DOM elements are converted to web elements by looking them up
- * and/or adding them to the element store provided.
- *
- * @param {?} obj
- * Object to be marshaled.
- * @param {element.Store} seenEls
- * Element store to use for lookup of web element references.
- *
- * @return {?}
- * Same object as provided by |obj| with the elements replaced by
- * web elements.
- */
- element.toJson = function (obj, seenEls) {
- let t = Object.prototype.toString.call(obj);
- // null
- if (t == "[object Undefined]" || t == "[object Null]") {
- return null;
- }
- // literals
- else if (t == "[object Boolean]" || t == "[object Number]" || t == "[object String]") {
- return obj;
- }
- // Array, NodeList, HTMLCollection, et al.
- else if (element.isCollection(obj)) {
- return [...obj].map(el => element.toJson(el, seenEls));
- }
- // HTMLElement
- else if ("nodeType" in obj && obj.nodeType == 1) {
- let uuid = seenEls.add(obj);
- return element.makeWebElement(uuid);
- }
- // arbitrary objects + files
- else {
- let rv = {};
- for (let prop in obj) {
- try {
- rv[prop] = element.toJson(obj[prop], seenEls);
- } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
- logger.debug(`Skipping ${prop}: ${e.message}`);
- }
- }
- return rv;
- }
- };
- /**
- * Check if the element is detached from the current frame as well as
- * the optional shadow root (when inside a Shadow DOM context).
- *
- * @param {nsIDOMElement} el
- * Element to be checked.
- * @param {Container} container
- * Container with |frame|, which is the window object that contains
- * the element, and an optional |shadowRoot|.
- *
- * @return {boolean}
- * Flag indicating that the element is disconnected.
- */
- element.isDisconnected = function (el, container = {}) {
- const {frame, shadowRoot} = container;
- assert.defined(frame);
- // shadow dom
- if (frame.ShadowRoot && shadowRoot) {
- if (el.compareDocumentPosition(shadowRoot) &
- DOCUMENT_POSITION_DISCONNECTED) {
- return true;
- }
- // looking for next possible ShadowRoot ancestor
- let parent = shadowRoot.host;
- while (parent && !(parent instanceof frame.ShadowRoot)) {
- parent = parent.parentNode;
- }
- return element.isDisconnected(
- shadowRoot.host,
- {frame: frame, shadowRoot: parent});
- // outside shadow dom
- } else {
- let docEl = frame.document.documentElement;
- return el.compareDocumentPosition(docEl) &
- DOCUMENT_POSITION_DISCONNECTED;
- }
- };
- /**
- * This function generates a pair of coordinates relative to the viewport
- * given a target element and coordinates relative to that element's
- * top-left corner.
- *
- * @param {Node} node
- * Target node.
- * @param {number=} xOffset
- * Horizontal offset relative to target's top-left corner.
- * Defaults to the centre of the target's bounding box.
- * @param {number=} yOffset
- * Vertical offset relative to target's top-left corner. Defaults to
- * the centre of the target's bounding box.
- *
- * @return {Object.<string, number>}
- * X- and Y coordinates.
- *
- * @throws TypeError
- * If |xOffset| or |yOffset| are not numbers.
- */
- element.coordinates = function (
- node, xOffset = undefined, yOffset = undefined) {
- let box = node.getBoundingClientRect();
- if (typeof xOffset == "undefined" || xOffset === null) {
- xOffset = box.width / 2.0;
- }
- if (typeof yOffset == "undefined" || yOffset === null) {
- yOffset = box.height / 2.0;
- }
- if (typeof yOffset != "number" || typeof xOffset != "number") {
- throw new TypeError("Offset must be a number");
- }
- return {
- x: box.left + xOffset,
- y: box.top + yOffset,
- };
- };
- /**
- * This function returns true if the node is in the viewport.
- *
- * @param {Element} el
- * Target element.
- * @param {number=} x
- * Horizontal offset relative to target. Defaults to the centre of
- * the target's bounding box.
- * @param {number=} y
- * Vertical offset relative to target. Defaults to the centre of
- * the target's bounding box.
- *
- * @return {boolean}
- * True if if |el| is in viewport, false otherwise.
- */
- element.inViewport = function (el, x = undefined, y = undefined) {
- let win = el.ownerDocument.defaultView;
- let c = element.coordinates(el, x, y);
- let vp = {
- top: win.pageYOffset,
- left: win.pageXOffset,
- bottom: (win.pageYOffset + win.innerHeight),
- right: (win.pageXOffset + win.innerWidth)
- };
- return (vp.left <= c.x + win.pageXOffset &&
- c.x + win.pageXOffset <= vp.right &&
- vp.top <= c.y + win.pageYOffset &&
- c.y + win.pageYOffset <= vp.bottom);
- };
- /**
- * Gets the element's container element.
- *
- * An element container is defined by the WebDriver
- * specification to be an <option> element in a valid element context
- * (https://html.spec.whatwg.org/#concept-element-contexts), meaning
- * that it has an ancestral element that is either <datalist> or <select>.
- *
- * If the element does not have a valid context, its container element
- * is itself.
- *
- * @param {Element} el
- * Element to get the container of.
- *
- * @return {Element}
- * Container element of |el|.
- */
- element.getContainer = function (el) {
- if (el.localName != "option") {
- return el;
- }
- function validContext(ctx) {
- return ctx.localName == "datalist" || ctx.localName == "select";
- }
- // does <option> have a valid context,
- // meaning is it a child of <datalist> or <select>?
- let parent = el;
- while (parent.parentNode && !validContext(parent)) {
- parent = parent.parentNode;
- }
- if (!validContext(parent)) {
- return el;
- }
- return parent;
- };
- /**
- * An element is in view if it is a member of its own pointer-interactable
- * paint tree.
- *
- * This means an element is considered to be in view, but not necessarily
- * pointer-interactable, if it is found somewhere in the
- * |elementsFromPoint| list at |el|'s in-view centre coordinates.
- *
- * Before running the check, we change |el|'s pointerEvents style property
- * to "auto", since elements without pointer events enabled do not turn
- * up in the paint tree we get from document.elementsFromPoint. This is
- * a specialisation that is only relevant when checking if the element is
- * in view.
- *
- * @param {Element} el
- * Element to check if is in view.
- *
- * @return {boolean}
- * True if |el| is inside the viewport, or false otherwise.
- */
- element.isInView = function (el) {
- let originalPointerEvents = el.style.pointerEvents;
- try {
- el.style.pointerEvents = "auto";
- const tree = element.getPointerInteractablePaintTree(el);
- return tree.includes(el);
- } finally {
- el.style.pointerEvents = originalPointerEvents;
- }
- };
- /**
- * This function throws the visibility of the element error if the element is
- * not displayed or the given coordinates are not within the viewport.
- *
- * @param {Element} el
- * Element to check if visible.
- * @param {number=} x
- * Horizontal offset relative to target. Defaults to the centre of
- * the target's bounding box.
- * @param {number=} y
- * Vertical offset relative to target. Defaults to the centre of
- * the target's bounding box.
- *
- * @return {boolean}
- * True if visible, false otherwise.
- */
- element.isVisible = function (el, x = undefined, y = undefined) {
- let win = el.ownerDocument.defaultView;
- // Bug 1094246: webdriver's isShown doesn't work with content xul
- if (!element.isXULElement(el) && !atom.isElementDisplayed(el, win)) {
- return false;
- }
- if (el.tagName.toLowerCase() == "body") {
- return true;
- }
- if (!element.inViewport(el, x, y)) {
- element.scrollIntoView(el);
- if (!element.inViewport(el)) {
- return false;
- }
- }
- return true;
- };
- /**
- * A pointer-interactable element is defined to be the first
- * non-transparent element, defined by the paint order found at the centre
- * point of its rectangle that is inside the viewport, excluding the size
- * of any rendered scrollbars.
- *
- * @param {DOMElement} el
- * Element determine if is pointer-interactable.
- *
- * @return {boolean}
- * True if interactable, false otherwise.
- */
- element.isPointerInteractable = function (el) {
- let tree = element.getPointerInteractablePaintTree(el);
- return tree[0] === el;
- };
- /**
- * Calculate the in-view centre point of the area of the given DOM client
- * rectangle that is inside the viewport.
- *
- * @param {DOMRect} rect
- * Element off a DOMRect sequence produced by calling |getClientRects|
- * on a |DOMElement|.
- * @param {nsIDOMWindow} win
- * Current browsing context.
- *
- * @return {Map.<string, number>}
- * X and Y coordinates that denotes the in-view centre point of |rect|.
- */
- element.getInViewCentrePoint = function (rect, win) {
- const {max, min} = Math;
- let x = {
- left: max(0, min(rect.x, rect.x + rect.width)),
- right: min(win.innerWidth, max(rect.x, rect.x + rect.width)),
- };
- let y = {
- top: max(0, min(rect.y, rect.y + rect.height)),
- bottom: min(win.innerHeight, max(rect.y, rect.y + rect.height)),
- };
- return {
- x: (x.left + x.right) / 2,
- y: (y.top + y.bottom) / 2,
- };
- };
- /**
- * Produces a pointer-interactable elements tree from a given element.
- *
- * The tree is defined by the paint order found at the centre point of
- * the element's rectangle that is inside the viewport, excluding the size
- * of any rendered scrollbars.
- *
- * @param {DOMElement} el
- * Element to determine if is pointer-interactable.
- *
- * @return {Array.<DOMElement>}
- * Sequence of elements in paint order.
- */
- element.getPointerInteractablePaintTree = function (el) {
- const doc = el.ownerDocument;
- const win = doc.defaultView;
- // pointer-interactable elements tree, step 1
- if (element.isDisconnected(el, {frame: win})) {
- return [];
- }
- // steps 2-3
- let rects = el.getClientRects();
- if (rects.length == 0) {
- return [];
- }
- // step 4
- let centre = element.getInViewCentrePoint(rects[0], win);
- // step 5
- return doc.elementsFromPoint(centre.x, centre.y);
- };
- // TODO(ato): Not implemented.
- // In fact, it's not defined in the spec.
- element.isKeyboardInteractable = function (el) {
- return true;
- };
- /**
- * Attempts to scroll into view |el|.
- *
- * @param {DOMElement} el
- * Element to scroll into view.
- */
- element.scrollIntoView = function (el) {
- if (el.scrollIntoView) {
- el.scrollIntoView({block: "end", inline: "nearest", behavior: "instant"});
- }
- };
- element.isXULElement = function (el) {
- let ns = atom.getElementAttribute(el, "namespaceURI");
- return ns.indexOf("there.is.only.xul") >= 0;
- };
- const boolEls = {
- audio: ["autoplay", "controls", "loop", "muted"],
- button: ["autofocus", "disabled", "formnovalidate"],
- details: ["open"],
- dialog: ["open"],
- fieldset: ["disabled"],
- form: ["novalidate"],
- iframe: ["allowfullscreen"],
- img: ["ismap"],
- input: ["autofocus", "checked", "disabled", "formnovalidate", "multiple", "readonly", "required"],
- keygen: ["autofocus", "disabled"],
- menuitem: ["checked", "default", "disabled"],
- object: ["typemustmatch"],
- ol: ["reversed"],
- optgroup: ["disabled"],
- option: ["disabled", "selected"],
- script: ["async", "defer"],
- select: ["autofocus", "disabled", "multiple", "required"],
- textarea: ["autofocus", "disabled", "readonly", "required"],
- track: ["default"],
- video: ["autoplay", "controls", "loop", "muted"],
- };
- /**
- * Tests if the attribute is a boolean attribute on element.
- *
- * @param {DOMElement} el
- * Element to test if |attr| is a boolean attribute on.
- * @param {string} attr
- * Attribute to test is a boolean attribute.
- *
- * @return {boolean}
- * True if the attribute is boolean, false otherwise.
- */
- element.isBooleanAttribute = function (el, attr) {
- if (el.namespaceURI !== XMLNS) {
- return false;
- }
- // global boolean attributes that apply to all HTML elements,
- // except for custom elements
- if ((attr == "hidden" || attr == "itemscope") && !el.localName.includes("-")) {
- return true;
- }
- if (!boolEls.hasOwnProperty(el.localName)) {
- return false;
- }
- return boolEls[el.localName].includes(attr)
- };
|