element.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  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/. */
  4. "use strict";
  5. const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
  6. Cu.import("resource://gre/modules/Log.jsm");
  7. Cu.import("chrome://marionette/content/assert.js");
  8. Cu.import("chrome://marionette/content/atom.js");
  9. Cu.import("chrome://marionette/content/error.js");
  10. const logger = Log.repository.getLogger("Marionette");
  11. /**
  12. * This module provides shared functionality for dealing with DOM-
  13. * and web elements in Marionette.
  14. *
  15. * A web element is an abstraction used to identify an element when it
  16. * is transported across the protocol, between remote- and local ends.
  17. *
  18. * Each element has an associated web element reference (a UUID) that
  19. * uniquely identifies the the element across all browsing contexts. The
  20. * web element reference for every element representing the same element
  21. * is the same.
  22. *
  23. * The @code{element.Store} provides a mapping between web element
  24. * references and DOM elements for each browsing context. It also provides
  25. * functionality for looking up and retrieving elements.
  26. */
  27. this.EXPORTED_SYMBOLS = ["element"];
  28. const DOCUMENT_POSITION_DISCONNECTED = 1;
  29. const XMLNS = "http://www.w3.org/1999/xhtml";
  30. const uuidGen = Cc["@mozilla.org/uuid-generator;1"]
  31. .getService(Ci.nsIUUIDGenerator);
  32. this.element = {};
  33. element.Key = "element-6066-11e4-a52e-4f735466cecf";
  34. element.LegacyKey = "ELEMENT";
  35. element.Strategy = {
  36. ClassName: "class name",
  37. Selector: "css selector",
  38. ID: "id",
  39. Name: "name",
  40. LinkText: "link text",
  41. PartialLinkText: "partial link text",
  42. TagName: "tag name",
  43. XPath: "xpath",
  44. Anon: "anon",
  45. AnonAttribute: "anon attribute",
  46. };
  47. /**
  48. * Stores known/seen elements and their associated web element
  49. * references.
  50. *
  51. * Elements are added by calling |add(el)| or |addAll(elements)|, and
  52. * may be queried by their web element reference using |get(element)|.
  53. */
  54. element.Store = class {
  55. constructor() {
  56. this.els = {};
  57. this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  58. }
  59. clear() {
  60. this.els = {};
  61. }
  62. /**
  63. * Make a collection of elements seen.
  64. *
  65. * The oder of the returned web element references is guaranteed to
  66. * match that of the collection passed in.
  67. *
  68. * @param {NodeList} els
  69. * Sequence of elements to add to set of seen elements.
  70. *
  71. * @return {Array.<WebElement>}
  72. * List of the web element references associated with each element
  73. * from |els|.
  74. */
  75. addAll(els) {
  76. let add = this.add.bind(this);
  77. return [...els].map(add);
  78. }
  79. /**
  80. * Make an element seen.
  81. *
  82. * @param {nsIDOMElement} el
  83. * Element to add to set of seen elements.
  84. *
  85. * @return {string}
  86. * Web element reference associated with element.
  87. */
  88. add(el) {
  89. for (let i in this.els) {
  90. let foundEl;
  91. try {
  92. foundEl = this.els[i].get();
  93. } catch (e) {}
  94. if (foundEl) {
  95. if (new XPCNativeWrapper(foundEl) == new XPCNativeWrapper(el)) {
  96. return i;
  97. }
  98. // cleanup reference to gc'd element
  99. } else {
  100. delete this.els[i];
  101. }
  102. }
  103. let id = element.generateUUID();
  104. this.els[id] = Cu.getWeakReference(el);
  105. return id;
  106. }
  107. /**
  108. * Determine if the provided web element reference has been seen
  109. * before/is in the element store.
  110. *
  111. * @param {string} uuid
  112. * Element's associated web element reference.
  113. *
  114. * @return {boolean}
  115. * True if element is in the store, false otherwise.
  116. */
  117. has(uuid) {
  118. return Object.keys(this.els).includes(uuid);
  119. }
  120. /**
  121. * Retrieve a DOM element by its unique web element reference/UUID.
  122. *
  123. * @param {string} uuid
  124. * Web element reference, or UUID.
  125. * @param {(nsIDOMWindow|ShadowRoot)} container
  126. * Window and an optional shadow root that contains the element.
  127. *
  128. * @returns {nsIDOMElement}
  129. * Element associated with reference.
  130. *
  131. * @throws {JavaScriptError}
  132. * If the provided reference is unknown.
  133. * @throws {StaleElementReferenceError}
  134. * If element has gone stale, indicating it is no longer attached to
  135. * the DOM provided in the container.
  136. */
  137. get(uuid, container) {
  138. let el = this.els[uuid];
  139. if (!el) {
  140. throw new JavaScriptError(`Element reference not seen before: ${uuid}`);
  141. }
  142. try {
  143. el = el.get();
  144. } catch (e) {
  145. el = null;
  146. delete this.els[id];
  147. }
  148. // use XPCNativeWrapper to compare elements (see bug 834266)
  149. let wrappedFrame = new XPCNativeWrapper(container.frame);
  150. let wrappedShadowRoot;
  151. if (container.shadowRoot) {
  152. wrappedShadowRoot = new XPCNativeWrapper(container.shadowRoot);
  153. }
  154. let wrappedEl = new XPCNativeWrapper(el);
  155. let wrappedContainer = {
  156. frame: wrappedFrame,
  157. shadowRoot: wrappedShadowRoot,
  158. };
  159. if (!el ||
  160. !(wrappedEl.ownerDocument == wrappedFrame.document) ||
  161. element.isDisconnected(wrappedEl, wrappedContainer)) {
  162. throw new StaleElementReferenceError(
  163. error.pprint`The element reference of ${el} stale: ` +
  164. "either the element is no longer attached to the DOM " +
  165. "or the page has been refreshed");
  166. }
  167. return el;
  168. }
  169. };
  170. /**
  171. * Find a single element or a collection of elements starting at the
  172. * document root or a given node.
  173. *
  174. * If |timeout| is above 0, an implicit search technique is used.
  175. * This will wait for the duration of |timeout| for the element
  176. * to appear in the DOM.
  177. *
  178. * See the |element.Strategy| enum for a full list of supported
  179. * search strategies that can be passed to |strategy|.
  180. *
  181. * Available flags for |opts|:
  182. *
  183. * |all|
  184. * If true, a multi-element search selector is used and a sequence
  185. * of elements will be returned. Otherwise a single element.
  186. *
  187. * |timeout|
  188. * Duration to wait before timing out the search. If |all| is
  189. * false, a NoSuchElementError is thrown if unable to find
  190. * the element within the timeout duration.
  191. *
  192. * |startNode|
  193. * Element to use as the root of the search.
  194. *
  195. * @param {Object.<string, Window>} container
  196. * Window object and an optional shadow root that contains the
  197. * root shadow DOM element.
  198. * @param {string} strategy
  199. * Search strategy whereby to locate the element(s).
  200. * @param {string} selector
  201. * Selector search pattern. The selector must be compatible with
  202. * the chosen search |strategy|.
  203. * @param {Object.<string, ?>} opts
  204. * Options.
  205. *
  206. * @return {Promise: (nsIDOMElement|Array<nsIDOMElement>)}
  207. * Single element or a sequence of elements.
  208. *
  209. * @throws InvalidSelectorError
  210. * If |strategy| is unknown.
  211. * @throws InvalidSelectorError
  212. * If |selector| is malformed.
  213. * @throws NoSuchElementError
  214. * If a single element is requested, this error will throw if the
  215. * element is not found.
  216. */
  217. element.find = function (container, strategy, selector, opts = {}) {
  218. opts.all = !!opts.all;
  219. opts.timeout = opts.timeout || 0;
  220. let searchFn;
  221. if (opts.all) {
  222. searchFn = findElements.bind(this);
  223. } else {
  224. searchFn = findElement.bind(this);
  225. }
  226. return new Promise((resolve, reject) => {
  227. let findElements = implicitlyWaitFor(
  228. () => find_(container, strategy, selector, searchFn, opts),
  229. opts.timeout);
  230. findElements.then(foundEls => {
  231. // the following code ought to be moved into findElement
  232. // and findElements when bug 1254486 is addressed
  233. if (!opts.all && (!foundEls || foundEls.length == 0)) {
  234. let msg;
  235. switch (strategy) {
  236. case element.Strategy.AnonAttribute:
  237. msg = "Unable to locate anonymous element: " + JSON.stringify(selector);
  238. break;
  239. default:
  240. msg = "Unable to locate element: " + selector;
  241. }
  242. reject(new NoSuchElementError(msg));
  243. }
  244. if (opts.all) {
  245. resolve(foundEls);
  246. }
  247. resolve(foundEls[0]);
  248. }, reject);
  249. });
  250. };
  251. function find_(container, strategy, selector, searchFn, opts) {
  252. let rootNode = container.shadowRoot || container.frame.document;
  253. let startNode;
  254. if (opts.startNode) {
  255. startNode = opts.startNode;
  256. } else {
  257. switch (strategy) {
  258. // For anonymous nodes the start node needs to be of type DOMElement, which
  259. // will refer to :root in case of a DOMDocument.
  260. case element.Strategy.Anon:
  261. case element.Strategy.AnonAttribute:
  262. if (rootNode instanceof Ci.nsIDOMDocument) {
  263. startNode = rootNode.documentElement;
  264. }
  265. break;
  266. default:
  267. startNode = rootNode;
  268. }
  269. }
  270. let res;
  271. try {
  272. res = searchFn(strategy, selector, rootNode, startNode);
  273. } catch (e) {
  274. throw new InvalidSelectorError(
  275. `Given ${strategy} expression "${selector}" is invalid: ${e}`);
  276. }
  277. if (res) {
  278. if (opts.all) {
  279. return res;
  280. }
  281. return [res];
  282. }
  283. return [];
  284. }
  285. /**
  286. * Find a single element by XPath expression.
  287. *
  288. * @param {DOMElement} root
  289. * Document root
  290. * @param {DOMElement} startNode
  291. * Where in the DOM hiearchy to begin searching.
  292. * @param {string} expr
  293. * XPath search expression.
  294. *
  295. * @return {DOMElement}
  296. * First element matching expression.
  297. */
  298. element.findByXPath = function (root, startNode, expr) {
  299. let iter = root.evaluate(expr, startNode, null,
  300. Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
  301. return iter.singleNodeValue;
  302. };
  303. /**
  304. * Find elements by XPath expression.
  305. *
  306. * @param {DOMElement} root
  307. * Document root.
  308. * @param {DOMElement} startNode
  309. * Where in the DOM hierarchy to begin searching.
  310. * @param {string} expr
  311. * XPath search expression.
  312. *
  313. * @return {Array.<DOMElement>}
  314. * Sequence of found elements matching expression.
  315. */
  316. element.findByXPathAll = function (root, startNode, expr) {
  317. let rv = [];
  318. let iter = root.evaluate(expr, startNode, null,
  319. Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  320. let el = iter.iterateNext();
  321. while (el) {
  322. rv.push(el);
  323. el = iter.iterateNext();
  324. }
  325. return rv;
  326. };
  327. /**
  328. * Find all hyperlinks dscendant of |node| which link text is |s|.
  329. *
  330. * @param {DOMElement} node
  331. * Where in the DOM hierarchy to being searching.
  332. * @param {string} s
  333. * Link text to search for.
  334. *
  335. * @return {Array.<DOMAnchorElement>}
  336. * Sequence of link elements which text is |s|.
  337. */
  338. element.findByLinkText = function (node, s) {
  339. return filterLinks(node, link => link.text.trim() === s);
  340. };
  341. /**
  342. * Find all hyperlinks descendant of |node| which link text contains |s|.
  343. *
  344. * @param {DOMElement} node
  345. * Where in the DOM hierachy to begin searching.
  346. * @param {string} s
  347. * Link text to search for.
  348. *
  349. * @return {Array.<DOMAnchorElement>}
  350. * Sequence of link elements which text containins |s|.
  351. */
  352. element.findByPartialLinkText = function (node, s) {
  353. return filterLinks(node, link => link.text.indexOf(s) != -1);
  354. };
  355. /**
  356. * Filters all hyperlinks that are descendant of |node| by |predicate|.
  357. *
  358. * @param {DOMElement} node
  359. * Where in the DOM hierarchy to begin searching.
  360. * @param {function(DOMAnchorElement): boolean} predicate
  361. * Function that determines if given link should be included in
  362. * return value or filtered away.
  363. *
  364. * @return {Array.<DOMAnchorElement>}
  365. * Sequence of link elements matching |predicate|.
  366. */
  367. function filterLinks(node, predicate) {
  368. let rv = [];
  369. for (let link of node.getElementsByTagName("a")) {
  370. if (predicate(link)) {
  371. rv.push(link);
  372. }
  373. }
  374. return rv;
  375. }
  376. /**
  377. * Finds a single element.
  378. *
  379. * @param {element.Strategy} using
  380. * Selector strategy to use.
  381. * @param {string} value
  382. * Selector expression.
  383. * @param {DOMElement} rootNode
  384. * Document root.
  385. * @param {DOMElement=} startNode
  386. * Optional node from which to start searching.
  387. *
  388. * @return {DOMElement}
  389. * Found elements.
  390. *
  391. * @throws {InvalidSelectorError}
  392. * If strategy |using| is not recognised.
  393. * @throws {Error}
  394. * If selector expression |value| is malformed.
  395. */
  396. function findElement(using, value, rootNode, startNode) {
  397. switch (using) {
  398. case element.Strategy.ID:
  399. if (startNode.getElementById) {
  400. return startNode.getElementById(value);
  401. }
  402. return element.findByXPath(rootNode, startNode, `.//*[@id="${value}"]`);
  403. case element.Strategy.Name:
  404. if (startNode.getElementsByName) {
  405. return startNode.getElementsByName(value)[0];
  406. }
  407. return element.findByXPath(rootNode, startNode, `.//*[@name="${value}"]`);
  408. case element.Strategy.ClassName:
  409. // works for >= Firefox 3
  410. return startNode.getElementsByClassName(value)[0];
  411. case element.Strategy.TagName:
  412. // works for all elements
  413. return startNode.getElementsByTagName(value)[0];
  414. case element.Strategy.XPath:
  415. return element.findByXPath(rootNode, startNode, value);
  416. case element.Strategy.LinkText:
  417. for (let link of startNode.getElementsByTagName("a")) {
  418. if (link.text.trim() === value) {
  419. return link;
  420. }
  421. }
  422. break;
  423. case element.Strategy.PartialLinkText:
  424. for (let link of startNode.getElementsByTagName("a")) {
  425. if (link.text.indexOf(value) != -1) {
  426. return link;
  427. }
  428. }
  429. break;
  430. case element.Strategy.Selector:
  431. try {
  432. return startNode.querySelector(value);
  433. } catch (e) {
  434. throw new InvalidSelectorError(`${e.message}: "${value}"`);
  435. }
  436. break;
  437. case element.Strategy.Anon:
  438. return rootNode.getAnonymousNodes(startNode);
  439. case element.Strategy.AnonAttribute:
  440. let attr = Object.keys(value)[0];
  441. return rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
  442. default:
  443. throw new InvalidSelectorError(`No such strategy: ${using}`);
  444. }
  445. }
  446. /**
  447. * Find multiple elements.
  448. *
  449. * @param {element.Strategy} using
  450. * Selector strategy to use.
  451. * @param {string} value
  452. * Selector expression.
  453. * @param {DOMElement} rootNode
  454. * Document root.
  455. * @param {DOMElement=} startNode
  456. * Optional node from which to start searching.
  457. *
  458. * @return {DOMElement}
  459. * Found elements.
  460. *
  461. * @throws {InvalidSelectorError}
  462. * If strategy |using| is not recognised.
  463. * @throws {Error}
  464. * If selector expression |value| is malformed.
  465. */
  466. function findElements(using, value, rootNode, startNode) {
  467. switch (using) {
  468. case element.Strategy.ID:
  469. value = `.//*[@id="${value}"]`;
  470. // fall through
  471. case element.Strategy.XPath:
  472. return element.findByXPathAll(rootNode, startNode, value);
  473. case element.Strategy.Name:
  474. if (startNode.getElementsByName) {
  475. return startNode.getElementsByName(value);
  476. }
  477. return element.findByXPathAll(rootNode, startNode, `.//*[@name="${value}"]`);
  478. case element.Strategy.ClassName:
  479. return startNode.getElementsByClassName(value);
  480. case element.Strategy.TagName:
  481. return startNode.getElementsByTagName(value);
  482. case element.Strategy.LinkText:
  483. return element.findByLinkText(startNode, value);
  484. case element.Strategy.PartialLinkText:
  485. return element.findByPartialLinkText(startNode, value);
  486. case element.Strategy.Selector:
  487. return startNode.querySelectorAll(value);
  488. case element.Strategy.Anon:
  489. return rootNode.getAnonymousNodes(startNode);
  490. case element.Strategy.AnonAttribute:
  491. let attr = Object.keys(value)[0];
  492. let el = rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
  493. if (el) {
  494. return [el];
  495. }
  496. return [];
  497. default:
  498. throw new InvalidSelectorError(`No such strategy: ${using}`);
  499. }
  500. }
  501. /**
  502. * Runs function off the main thread until its return value is truthy
  503. * or the provided timeout is reached. The function is guaranteed to be
  504. * run at least once, irregardless of the timeout.
  505. *
  506. * A truthy return value constitutes a truthful boolean, positive number,
  507. * object, or non-empty array.
  508. *
  509. * The |func| is evaluated every |interval| for as long as its runtime
  510. * duration does not exceed |interval|. If the runtime evaluation duration
  511. * of |func| is greater than |interval|, evaluations of |func| are queued.
  512. *
  513. * @param {function(): ?} func
  514. * Function to run off the main thread.
  515. * @param {number} timeout
  516. * Desired timeout. If 0 or less than the runtime evaluation time
  517. * of |func|, |func| is guaranteed to run at least once.
  518. * @param {number=} interval
  519. * Duration between each poll of |func| in milliseconds. Defaults to
  520. * 100 milliseconds.
  521. *
  522. * @return {Promise}
  523. * Yields the return value from |func|. The promise is rejected if
  524. * |func| throws.
  525. */
  526. function implicitlyWaitFor(func, timeout, interval = 100) {
  527. let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  528. return new Promise((resolve, reject) => {
  529. let startTime = new Date().getTime();
  530. let endTime = startTime + timeout;
  531. let elementSearch = function() {
  532. let res;
  533. try {
  534. res = func();
  535. } catch (e) {
  536. reject(e);
  537. }
  538. if (
  539. // collections that might contain web elements
  540. // should be checked until they are not empty
  541. (element.isCollection(res) && res.length > 0)
  542. // !![] (ensuring boolean type on empty array) always returns true
  543. // and we can only use it on non-collections
  544. || (!element.isCollection(res) && !!res)
  545. // return immediately if timeout is 0,
  546. // allowing |func| to be evaluted at least once
  547. || startTime == endTime
  548. // return if timeout has elapsed
  549. || new Date().getTime() >= endTime
  550. ) {
  551. resolve(res);
  552. }
  553. };
  554. // the repeating slack timer waits |interval|
  555. // before invoking |elementSearch|
  556. elementSearch();
  557. timer.init(elementSearch, interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
  558. // cancel timer and propagate result
  559. }).then(res => {
  560. timer.cancel();
  561. return res;
  562. }, err => {
  563. timer.cancel();
  564. throw err;
  565. });
  566. }
  567. /** Determines if |obj| is an HTML or JS collection. */
  568. element.isCollection = function (seq) {
  569. switch (Object.prototype.toString.call(seq)) {
  570. case "[object Arguments]":
  571. case "[object Array]":
  572. case "[object FileList]":
  573. case "[object HTMLAllCollection]":
  574. case "[object HTMLCollection]":
  575. case "[object HTMLFormControlsCollection]":
  576. case "[object HTMLOptionsCollection]":
  577. case "[object NodeList]":
  578. return true;
  579. default:
  580. return false;
  581. }
  582. };
  583. element.makeWebElement = function (uuid) {
  584. return {
  585. [element.Key]: uuid,
  586. [element.LegacyKey]: uuid,
  587. };
  588. };
  589. /**
  590. * Checks if |ref| has either |element.Key| or |element.LegacyKey| as properties.
  591. *
  592. * @param {?} ref
  593. * Object that represents a web element reference.
  594. * @return {boolean}
  595. * True if |ref| has either expected property.
  596. */
  597. element.isWebElementReference = function (ref) {
  598. let properties = Object.getOwnPropertyNames(ref);
  599. return properties.includes(element.Key) || properties.includes(element.LegacyKey);
  600. };
  601. element.generateUUID = function() {
  602. let uuid = uuidGen.generateUUID().toString();
  603. return uuid.substring(1, uuid.length - 1);
  604. };
  605. /**
  606. * Convert any web elements in arbitrary objects to DOM elements by
  607. * looking them up in the seen element store.
  608. *
  609. * @param {?} obj
  610. * Arbitrary object containing web elements.
  611. * @param {element.Store} seenEls
  612. * Element store to use for lookup of web element references.
  613. * @param {Window} win
  614. * Window.
  615. * @param {ShadowRoot} shadowRoot
  616. * Shadow root.
  617. *
  618. * @return {?}
  619. * Same object as provided by |obj| with the web elements replaced
  620. * by DOM elements.
  621. */
  622. element.fromJson = function (
  623. obj, seenEls, win, shadowRoot = undefined) {
  624. switch (typeof obj) {
  625. case "boolean":
  626. case "number":
  627. case "string":
  628. return obj;
  629. case "object":
  630. if (obj === null) {
  631. return obj;
  632. }
  633. // arrays
  634. else if (Array.isArray(obj)) {
  635. return obj.map(e => element.fromJson(e, seenEls, win, shadowRoot));
  636. }
  637. // web elements
  638. else if (Object.keys(obj).includes(element.Key) ||
  639. Object.keys(obj).includes(element.LegacyKey)) {
  640. let uuid = obj[element.Key] || obj[element.LegacyKey];
  641. let el = seenEls.get(uuid, {frame: win, shadowRoot: shadowRoot});
  642. if (!el) {
  643. throw new WebDriverError(`Unknown element: ${uuid}`);
  644. }
  645. return el;
  646. }
  647. // arbitrary objects
  648. else {
  649. let rv = {};
  650. for (let prop in obj) {
  651. rv[prop] = element.fromJson(obj[prop], seenEls, win, shadowRoot);
  652. }
  653. return rv;
  654. }
  655. }
  656. };
  657. /**
  658. * Convert arbitrary objects to JSON-safe primitives that can be
  659. * transported over the Marionette protocol.
  660. *
  661. * Any DOM elements are converted to web elements by looking them up
  662. * and/or adding them to the element store provided.
  663. *
  664. * @param {?} obj
  665. * Object to be marshaled.
  666. * @param {element.Store} seenEls
  667. * Element store to use for lookup of web element references.
  668. *
  669. * @return {?}
  670. * Same object as provided by |obj| with the elements replaced by
  671. * web elements.
  672. */
  673. element.toJson = function (obj, seenEls) {
  674. let t = Object.prototype.toString.call(obj);
  675. // null
  676. if (t == "[object Undefined]" || t == "[object Null]") {
  677. return null;
  678. }
  679. // literals
  680. else if (t == "[object Boolean]" || t == "[object Number]" || t == "[object String]") {
  681. return obj;
  682. }
  683. // Array, NodeList, HTMLCollection, et al.
  684. else if (element.isCollection(obj)) {
  685. return [...obj].map(el => element.toJson(el, seenEls));
  686. }
  687. // HTMLElement
  688. else if ("nodeType" in obj && obj.nodeType == 1) {
  689. let uuid = seenEls.add(obj);
  690. return element.makeWebElement(uuid);
  691. }
  692. // arbitrary objects + files
  693. else {
  694. let rv = {};
  695. for (let prop in obj) {
  696. try {
  697. rv[prop] = element.toJson(obj[prop], seenEls);
  698. } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
  699. logger.debug(`Skipping ${prop}: ${e.message}`);
  700. }
  701. }
  702. return rv;
  703. }
  704. };
  705. /**
  706. * Check if the element is detached from the current frame as well as
  707. * the optional shadow root (when inside a Shadow DOM context).
  708. *
  709. * @param {nsIDOMElement} el
  710. * Element to be checked.
  711. * @param {Container} container
  712. * Container with |frame|, which is the window object that contains
  713. * the element, and an optional |shadowRoot|.
  714. *
  715. * @return {boolean}
  716. * Flag indicating that the element is disconnected.
  717. */
  718. element.isDisconnected = function (el, container = {}) {
  719. const {frame, shadowRoot} = container;
  720. assert.defined(frame);
  721. // shadow dom
  722. if (frame.ShadowRoot && shadowRoot) {
  723. if (el.compareDocumentPosition(shadowRoot) &
  724. DOCUMENT_POSITION_DISCONNECTED) {
  725. return true;
  726. }
  727. // looking for next possible ShadowRoot ancestor
  728. let parent = shadowRoot.host;
  729. while (parent && !(parent instanceof frame.ShadowRoot)) {
  730. parent = parent.parentNode;
  731. }
  732. return element.isDisconnected(
  733. shadowRoot.host,
  734. {frame: frame, shadowRoot: parent});
  735. // outside shadow dom
  736. } else {
  737. let docEl = frame.document.documentElement;
  738. return el.compareDocumentPosition(docEl) &
  739. DOCUMENT_POSITION_DISCONNECTED;
  740. }
  741. };
  742. /**
  743. * This function generates a pair of coordinates relative to the viewport
  744. * given a target element and coordinates relative to that element's
  745. * top-left corner.
  746. *
  747. * @param {Node} node
  748. * Target node.
  749. * @param {number=} xOffset
  750. * Horizontal offset relative to target's top-left corner.
  751. * Defaults to the centre of the target's bounding box.
  752. * @param {number=} yOffset
  753. * Vertical offset relative to target's top-left corner. Defaults to
  754. * the centre of the target's bounding box.
  755. *
  756. * @return {Object.<string, number>}
  757. * X- and Y coordinates.
  758. *
  759. * @throws TypeError
  760. * If |xOffset| or |yOffset| are not numbers.
  761. */
  762. element.coordinates = function (
  763. node, xOffset = undefined, yOffset = undefined) {
  764. let box = node.getBoundingClientRect();
  765. if (typeof xOffset == "undefined" || xOffset === null) {
  766. xOffset = box.width / 2.0;
  767. }
  768. if (typeof yOffset == "undefined" || yOffset === null) {
  769. yOffset = box.height / 2.0;
  770. }
  771. if (typeof yOffset != "number" || typeof xOffset != "number") {
  772. throw new TypeError("Offset must be a number");
  773. }
  774. return {
  775. x: box.left + xOffset,
  776. y: box.top + yOffset,
  777. };
  778. };
  779. /**
  780. * This function returns true if the node is in the viewport.
  781. *
  782. * @param {Element} el
  783. * Target element.
  784. * @param {number=} x
  785. * Horizontal offset relative to target. Defaults to the centre of
  786. * the target's bounding box.
  787. * @param {number=} y
  788. * Vertical offset relative to target. Defaults to the centre of
  789. * the target's bounding box.
  790. *
  791. * @return {boolean}
  792. * True if if |el| is in viewport, false otherwise.
  793. */
  794. element.inViewport = function (el, x = undefined, y = undefined) {
  795. let win = el.ownerDocument.defaultView;
  796. let c = element.coordinates(el, x, y);
  797. let vp = {
  798. top: win.pageYOffset,
  799. left: win.pageXOffset,
  800. bottom: (win.pageYOffset + win.innerHeight),
  801. right: (win.pageXOffset + win.innerWidth)
  802. };
  803. return (vp.left <= c.x + win.pageXOffset &&
  804. c.x + win.pageXOffset <= vp.right &&
  805. vp.top <= c.y + win.pageYOffset &&
  806. c.y + win.pageYOffset <= vp.bottom);
  807. };
  808. /**
  809. * Gets the element's container element.
  810. *
  811. * An element container is defined by the WebDriver
  812. * specification to be an <option> element in a valid element context
  813. * (https://html.spec.whatwg.org/#concept-element-contexts), meaning
  814. * that it has an ancestral element that is either <datalist> or <select>.
  815. *
  816. * If the element does not have a valid context, its container element
  817. * is itself.
  818. *
  819. * @param {Element} el
  820. * Element to get the container of.
  821. *
  822. * @return {Element}
  823. * Container element of |el|.
  824. */
  825. element.getContainer = function (el) {
  826. if (el.localName != "option") {
  827. return el;
  828. }
  829. function validContext(ctx) {
  830. return ctx.localName == "datalist" || ctx.localName == "select";
  831. }
  832. // does <option> have a valid context,
  833. // meaning is it a child of <datalist> or <select>?
  834. let parent = el;
  835. while (parent.parentNode && !validContext(parent)) {
  836. parent = parent.parentNode;
  837. }
  838. if (!validContext(parent)) {
  839. return el;
  840. }
  841. return parent;
  842. };
  843. /**
  844. * An element is in view if it is a member of its own pointer-interactable
  845. * paint tree.
  846. *
  847. * This means an element is considered to be in view, but not necessarily
  848. * pointer-interactable, if it is found somewhere in the
  849. * |elementsFromPoint| list at |el|'s in-view centre coordinates.
  850. *
  851. * Before running the check, we change |el|'s pointerEvents style property
  852. * to "auto", since elements without pointer events enabled do not turn
  853. * up in the paint tree we get from document.elementsFromPoint. This is
  854. * a specialisation that is only relevant when checking if the element is
  855. * in view.
  856. *
  857. * @param {Element} el
  858. * Element to check if is in view.
  859. *
  860. * @return {boolean}
  861. * True if |el| is inside the viewport, or false otherwise.
  862. */
  863. element.isInView = function (el) {
  864. let originalPointerEvents = el.style.pointerEvents;
  865. try {
  866. el.style.pointerEvents = "auto";
  867. const tree = element.getPointerInteractablePaintTree(el);
  868. return tree.includes(el);
  869. } finally {
  870. el.style.pointerEvents = originalPointerEvents;
  871. }
  872. };
  873. /**
  874. * This function throws the visibility of the element error if the element is
  875. * not displayed or the given coordinates are not within the viewport.
  876. *
  877. * @param {Element} el
  878. * Element to check if visible.
  879. * @param {number=} x
  880. * Horizontal offset relative to target. Defaults to the centre of
  881. * the target's bounding box.
  882. * @param {number=} y
  883. * Vertical offset relative to target. Defaults to the centre of
  884. * the target's bounding box.
  885. *
  886. * @return {boolean}
  887. * True if visible, false otherwise.
  888. */
  889. element.isVisible = function (el, x = undefined, y = undefined) {
  890. let win = el.ownerDocument.defaultView;
  891. // Bug 1094246: webdriver's isShown doesn't work with content xul
  892. if (!element.isXULElement(el) && !atom.isElementDisplayed(el, win)) {
  893. return false;
  894. }
  895. if (el.tagName.toLowerCase() == "body") {
  896. return true;
  897. }
  898. if (!element.inViewport(el, x, y)) {
  899. element.scrollIntoView(el);
  900. if (!element.inViewport(el)) {
  901. return false;
  902. }
  903. }
  904. return true;
  905. };
  906. /**
  907. * A pointer-interactable element is defined to be the first
  908. * non-transparent element, defined by the paint order found at the centre
  909. * point of its rectangle that is inside the viewport, excluding the size
  910. * of any rendered scrollbars.
  911. *
  912. * @param {DOMElement} el
  913. * Element determine if is pointer-interactable.
  914. *
  915. * @return {boolean}
  916. * True if interactable, false otherwise.
  917. */
  918. element.isPointerInteractable = function (el) {
  919. let tree = element.getPointerInteractablePaintTree(el);
  920. return tree[0] === el;
  921. };
  922. /**
  923. * Calculate the in-view centre point of the area of the given DOM client
  924. * rectangle that is inside the viewport.
  925. *
  926. * @param {DOMRect} rect
  927. * Element off a DOMRect sequence produced by calling |getClientRects|
  928. * on a |DOMElement|.
  929. * @param {nsIDOMWindow} win
  930. * Current browsing context.
  931. *
  932. * @return {Map.<string, number>}
  933. * X and Y coordinates that denotes the in-view centre point of |rect|.
  934. */
  935. element.getInViewCentrePoint = function (rect, win) {
  936. const {max, min} = Math;
  937. let x = {
  938. left: max(0, min(rect.x, rect.x + rect.width)),
  939. right: min(win.innerWidth, max(rect.x, rect.x + rect.width)),
  940. };
  941. let y = {
  942. top: max(0, min(rect.y, rect.y + rect.height)),
  943. bottom: min(win.innerHeight, max(rect.y, rect.y + rect.height)),
  944. };
  945. return {
  946. x: (x.left + x.right) / 2,
  947. y: (y.top + y.bottom) / 2,
  948. };
  949. };
  950. /**
  951. * Produces a pointer-interactable elements tree from a given element.
  952. *
  953. * The tree is defined by the paint order found at the centre point of
  954. * the element's rectangle that is inside the viewport, excluding the size
  955. * of any rendered scrollbars.
  956. *
  957. * @param {DOMElement} el
  958. * Element to determine if is pointer-interactable.
  959. *
  960. * @return {Array.<DOMElement>}
  961. * Sequence of elements in paint order.
  962. */
  963. element.getPointerInteractablePaintTree = function (el) {
  964. const doc = el.ownerDocument;
  965. const win = doc.defaultView;
  966. // pointer-interactable elements tree, step 1
  967. if (element.isDisconnected(el, {frame: win})) {
  968. return [];
  969. }
  970. // steps 2-3
  971. let rects = el.getClientRects();
  972. if (rects.length == 0) {
  973. return [];
  974. }
  975. // step 4
  976. let centre = element.getInViewCentrePoint(rects[0], win);
  977. // step 5
  978. return doc.elementsFromPoint(centre.x, centre.y);
  979. };
  980. // TODO(ato): Not implemented.
  981. // In fact, it's not defined in the spec.
  982. element.isKeyboardInteractable = function (el) {
  983. return true;
  984. };
  985. /**
  986. * Attempts to scroll into view |el|.
  987. *
  988. * @param {DOMElement} el
  989. * Element to scroll into view.
  990. */
  991. element.scrollIntoView = function (el) {
  992. if (el.scrollIntoView) {
  993. el.scrollIntoView({block: "end", inline: "nearest", behavior: "instant"});
  994. }
  995. };
  996. element.isXULElement = function (el) {
  997. let ns = atom.getElementAttribute(el, "namespaceURI");
  998. return ns.indexOf("there.is.only.xul") >= 0;
  999. };
  1000. const boolEls = {
  1001. audio: ["autoplay", "controls", "loop", "muted"],
  1002. button: ["autofocus", "disabled", "formnovalidate"],
  1003. details: ["open"],
  1004. dialog: ["open"],
  1005. fieldset: ["disabled"],
  1006. form: ["novalidate"],
  1007. iframe: ["allowfullscreen"],
  1008. img: ["ismap"],
  1009. input: ["autofocus", "checked", "disabled", "formnovalidate", "multiple", "readonly", "required"],
  1010. keygen: ["autofocus", "disabled"],
  1011. menuitem: ["checked", "default", "disabled"],
  1012. object: ["typemustmatch"],
  1013. ol: ["reversed"],
  1014. optgroup: ["disabled"],
  1015. option: ["disabled", "selected"],
  1016. script: ["async", "defer"],
  1017. select: ["autofocus", "disabled", "multiple", "required"],
  1018. textarea: ["autofocus", "disabled", "readonly", "required"],
  1019. track: ["default"],
  1020. video: ["autoplay", "controls", "loop", "muted"],
  1021. };
  1022. /**
  1023. * Tests if the attribute is a boolean attribute on element.
  1024. *
  1025. * @param {DOMElement} el
  1026. * Element to test if |attr| is a boolean attribute on.
  1027. * @param {string} attr
  1028. * Attribute to test is a boolean attribute.
  1029. *
  1030. * @return {boolean}
  1031. * True if the attribute is boolean, false otherwise.
  1032. */
  1033. element.isBooleanAttribute = function (el, attr) {
  1034. if (el.namespaceURI !== XMLNS) {
  1035. return false;
  1036. }
  1037. // global boolean attributes that apply to all HTML elements,
  1038. // except for custom elements
  1039. if ((attr == "hidden" || attr == "itemscope") && !el.localName.includes("-")) {
  1040. return true;
  1041. }
  1042. if (!boolEls.hasOwnProperty(el.localName)) {
  1043. return false;
  1044. }
  1045. return boolEls[el.localName].includes(attr)
  1046. };