From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- remote/marionette/.eslintrc.js | 14 + remote/marionette/README | 20 + remote/marionette/accessibility.sys.mjs | 479 +++ .../actors/MarionetteCommandsChild.sys.mjs | 595 ++++ .../actors/MarionetteCommandsParent.sys.mjs | 436 +++ .../actors/MarionetteEventsChild.sys.mjs | 71 + .../actors/MarionetteEventsParent.sys.mjs | 113 + .../actors/MarionetteReftestChild.sys.mjs | 239 ++ .../actors/MarionetteReftestParent.sys.mjs | 90 + remote/marionette/addon.sys.mjs | 142 + remote/marionette/atom.sys.mjs | 55 + remote/marionette/browser.sys.mjs | 385 +++ remote/marionette/cert.sys.mjs | 57 + remote/marionette/chrome/reftest.xhtml | 8 + remote/marionette/chrome/test.xhtml | 59 + remote/marionette/chrome/test2.xhtml | 36 + remote/marionette/chrome/test_dialog.dtd | 7 + remote/marionette/chrome/test_dialog.properties | 7 + remote/marionette/chrome/test_dialog.xhtml | 38 + remote/marionette/chrome/test_menupopup.xhtml | 34 + remote/marionette/chrome/test_nested_iframe.xhtml | 8 + remote/marionette/chrome/test_no_xul.xhtml | 58 + remote/marionette/cookie.sys.mjs | 296 ++ remote/marionette/driver.sys.mjs | 3575 ++++++++++++++++++++ remote/marionette/evaluate.sys.mjs | 356 ++ remote/marionette/event.sys.mjs | 291 ++ remote/marionette/interaction.sys.mjs | 819 +++++ remote/marionette/jar.mn | 51 + remote/marionette/json.sys.mjs | 491 +++ remote/marionette/l10n.sys.mjs | 101 + remote/marionette/message.sys.mjs | 329 ++ remote/marionette/moz.build | 10 + remote/marionette/navigate.sys.mjs | 429 +++ remote/marionette/packets.sys.mjs | 424 +++ remote/marionette/permissions.sys.mjs | 95 + remote/marionette/prefs.sys.mjs | 175 + remote/marionette/reftest-content.js | 65 + remote/marionette/reftest.sys.mjs | 921 +++++ remote/marionette/server.sys.mjs | 460 +++ remote/marionette/stream-utils.sys.mjs | 256 ++ remote/marionette/sync.sys.mjs | 538 +++ remote/marionette/test/README | 1 + remote/marionette/test/xpcshell/.eslintrc.js | 7 + remote/marionette/test/xpcshell/README | 16 + remote/marionette/test/xpcshell/test_actors.js | 55 + remote/marionette/test/xpcshell/test_browser.js | 21 + remote/marionette/test/xpcshell/test_cookie.js | 362 ++ remote/marionette/test/xpcshell/test_json.js | 472 +++ remote/marionette/test/xpcshell/test_message.js | 245 ++ remote/marionette/test/xpcshell/test_navigate.js | 90 + remote/marionette/test/xpcshell/test_prefs.js | 98 + remote/marionette/test/xpcshell/test_sync.js | 419 +++ .../marionette/test/xpcshell/test_web-reference.js | 293 ++ remote/marionette/test/xpcshell/xpcshell.toml | 20 + remote/marionette/transport.sys.mjs | 527 +++ remote/marionette/web-reference.sys.mjs | 297 ++ remote/marionette/webauthn.sys.mjs | 134 + 57 files changed, 15690 insertions(+) create mode 100644 remote/marionette/.eslintrc.js create mode 100644 remote/marionette/README create mode 100644 remote/marionette/accessibility.sys.mjs create mode 100644 remote/marionette/actors/MarionetteCommandsChild.sys.mjs create mode 100644 remote/marionette/actors/MarionetteCommandsParent.sys.mjs create mode 100644 remote/marionette/actors/MarionetteEventsChild.sys.mjs create mode 100644 remote/marionette/actors/MarionetteEventsParent.sys.mjs create mode 100644 remote/marionette/actors/MarionetteReftestChild.sys.mjs create mode 100644 remote/marionette/actors/MarionetteReftestParent.sys.mjs create mode 100644 remote/marionette/addon.sys.mjs create mode 100644 remote/marionette/atom.sys.mjs create mode 100644 remote/marionette/browser.sys.mjs create mode 100644 remote/marionette/cert.sys.mjs create mode 100644 remote/marionette/chrome/reftest.xhtml create mode 100644 remote/marionette/chrome/test.xhtml create mode 100644 remote/marionette/chrome/test2.xhtml create mode 100644 remote/marionette/chrome/test_dialog.dtd create mode 100644 remote/marionette/chrome/test_dialog.properties create mode 100644 remote/marionette/chrome/test_dialog.xhtml create mode 100644 remote/marionette/chrome/test_menupopup.xhtml create mode 100644 remote/marionette/chrome/test_nested_iframe.xhtml create mode 100644 remote/marionette/chrome/test_no_xul.xhtml create mode 100644 remote/marionette/cookie.sys.mjs create mode 100644 remote/marionette/driver.sys.mjs create mode 100644 remote/marionette/evaluate.sys.mjs create mode 100644 remote/marionette/event.sys.mjs create mode 100644 remote/marionette/interaction.sys.mjs create mode 100644 remote/marionette/jar.mn create mode 100644 remote/marionette/json.sys.mjs create mode 100644 remote/marionette/l10n.sys.mjs create mode 100644 remote/marionette/message.sys.mjs create mode 100644 remote/marionette/moz.build create mode 100644 remote/marionette/navigate.sys.mjs create mode 100644 remote/marionette/packets.sys.mjs create mode 100644 remote/marionette/permissions.sys.mjs create mode 100644 remote/marionette/prefs.sys.mjs create mode 100644 remote/marionette/reftest-content.js create mode 100644 remote/marionette/reftest.sys.mjs create mode 100644 remote/marionette/server.sys.mjs create mode 100644 remote/marionette/stream-utils.sys.mjs create mode 100644 remote/marionette/sync.sys.mjs create mode 100644 remote/marionette/test/README create mode 100644 remote/marionette/test/xpcshell/.eslintrc.js create mode 100644 remote/marionette/test/xpcshell/README create mode 100644 remote/marionette/test/xpcshell/test_actors.js create mode 100644 remote/marionette/test/xpcshell/test_browser.js create mode 100644 remote/marionette/test/xpcshell/test_cookie.js create mode 100644 remote/marionette/test/xpcshell/test_json.js create mode 100644 remote/marionette/test/xpcshell/test_message.js create mode 100644 remote/marionette/test/xpcshell/test_navigate.js create mode 100644 remote/marionette/test/xpcshell/test_prefs.js create mode 100644 remote/marionette/test/xpcshell/test_sync.js create mode 100644 remote/marionette/test/xpcshell/test_web-reference.js create mode 100644 remote/marionette/test/xpcshell/xpcshell.toml create mode 100644 remote/marionette/transport.sys.mjs create mode 100644 remote/marionette/web-reference.sys.mjs create mode 100644 remote/marionette/webauthn.sys.mjs (limited to 'remote/marionette') diff --git a/remote/marionette/.eslintrc.js b/remote/marionette/.eslintrc.js new file mode 100644 index 0000000000..64a8883c43 --- /dev/null +++ b/remote/marionette/.eslintrc.js @@ -0,0 +1,14 @@ +/* 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"; + +// inherits from ../../tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js + +module.exports = { + rules: { + camelcase: ["error", { properties: "never" }], + "no-var": "error", + }, +}; diff --git a/remote/marionette/README b/remote/marionette/README new file mode 100644 index 0000000000..d077a5136c --- /dev/null +++ b/remote/marionette/README @@ -0,0 +1,20 @@ +Marionette [ ˌmarɪəˈnɛt] is + + * a puppet worked by strings: the bird bobs up and down like + a marionette; + + * a person who is easily manipulated or controlled: many officers + dismissed him as the mayor’s marionette; + + * the remote protocol that lets out-of-process programs communicate + with, instrument, and control Gecko-based browsers. + +Marionette provides interfaces for interacting with both the internal +JavaScript runtime and UI elements of Gecko-based browsers, such +as Firefox on desktop and mobile. It can control both the chrome- and content +documents, giving a high level of control and ability to replicate, +or emulate, user interaction. + +Head on to the Marionette documentation to find out more: + + https://firefox-source-docs.mozilla.org/testing/marionette/ diff --git a/remote/marionette/accessibility.sys.mjs b/remote/marionette/accessibility.sys.mjs new file mode 100644 index 0000000000..c500f2121e --- /dev/null +++ b/remote/marionette/accessibility.sys.mjs @@ -0,0 +1,479 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + waitForObserverTopic: "chrome://remote/content/marionette/sync.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +ChromeUtils.defineLazyGetter(lazy, "service", () => { + try { + return Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + } catch (e) { + lazy.logger.warn("Accessibility module is not present"); + return undefined; + } +}); + +/** @namespace */ +export const accessibility = { + get service() { + return lazy.service; + }, +}; + +/** + * Accessible states used to check element"s state from the accessiblity API + * perspective. + * + * Note: if gecko is built with --disable-accessibility, the interfaces + * are not defined. This is why we use getters instead to be able to use + * these statically. + */ +accessibility.State = { + get Unavailable() { + return Ci.nsIAccessibleStates.STATE_UNAVAILABLE; + }, + get Focusable() { + return Ci.nsIAccessibleStates.STATE_FOCUSABLE; + }, + get Selectable() { + return Ci.nsIAccessibleStates.STATE_SELECTABLE; + }, + get Selected() { + return Ci.nsIAccessibleStates.STATE_SELECTED; + }, +}; + +/** + * Accessible object roles that support some action. + */ +accessibility.ActionableRoles = new Set([ + "checkbutton", + "check menu item", + "check rich option", + "combobox", + "combobox option", + "entry", + "key", + "link", + "listbox option", + "listbox rich option", + "menuitem", + "option", + "outlineitem", + "pagetab", + "pushbutton", + "radiobutton", + "radio menu item", + "rowheader", + "slider", + "spinbutton", + "switch", +]); + +/** + * Factory function that constructs a new {@code accessibility.Checks} + * object with enforced strictness or not. + */ +accessibility.get = function (strict = false) { + return new accessibility.Checks(!!strict); +}; + +/** + * Wait for the document accessibility state to be different from STATE_BUSY. + * + * @param {Document} doc + * The document to wait for. + * @returns {Promise} + * A promise which resolves when the document's accessibility state is no + * longer busy. + */ +function waitForDocumentAccessibility(doc) { + const documentAccessible = accessibility.service.getAccessibleFor(doc); + const state = {}; + documentAccessible.getState(state, {}); + if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) { + return Promise.resolve(); + } + + // Accessibility for the doc is busy, so wait for the state to change. + return lazy.waitForObserverTopic("accessible-event", { + checkFn: subject => { + // If event type does not match expected type, skip the event. + // If event's accessible does not match expected accessible, + // skip the event. + const event = subject.QueryInterface(Ci.nsIAccessibleEvent); + return ( + event.eventType === Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE && + event.accessible === documentAccessible + ); + }, + }); +} + +/** + * Retrieve the Accessible for the provided element. + * + * @param {Element} element + * The element for which we need to retrieve the accessible. + * + * @returns {nsIAccessible|null} + * The Accessible object corresponding to the provided element or null if + * the accessibility service is not available. + */ +accessibility.getAccessible = async function (element) { + if (!accessibility.service) { + return null; + } + + // First, wait for accessibility to be ready for the element's document. + await waitForDocumentAccessibility(element.ownerDocument); + + const acc = accessibility.service.getAccessibleFor(element); + if (acc) { + return acc; + } + + // The Accessible doesn't exist yet. This can happen because a11y tree + // mutations happen during refresh driver ticks. Stop the refresh driver from + // doing its regular ticks and force two refresh driver ticks: the first to + // let layout update and notify a11y, and the second to let a11y process + // updates. + const windowUtils = element.ownerGlobal.windowUtils; + windowUtils.advanceTimeAndRefresh(0); + windowUtils.advanceTimeAndRefresh(0); + // Go back to normal refresh driver ticks. + windowUtils.restoreNormalRefresh(); + return accessibility.service.getAccessibleFor(element); +}; + +/** + * Component responsible for interacting with platform accessibility + * API. + * + * Its methods serve as wrappers for testing content and chrome + * accessibility as well as accessibility of user interactions. + */ +accessibility.Checks = class { + /** + * @param {boolean} strict + * Flag indicating whether the accessibility issue should be logged + * or cause an error to be thrown. Default is to log to stdout. + */ + constructor(strict) { + this.strict = strict; + } + + /** + * Assert that the element has a corresponding accessible object, and retrieve + * this accessible. Note that if the accessibility.Checks component was + * created in non-strict mode, this helper will not attempt to resolve the + * accessible at all and will simply return null. + * + * @param {DOMElement|XULElement} element + * Element to get the accessible object for. + * @param {boolean=} mustHaveAccessible + * Flag indicating that the element must have an accessible object. + * Defaults to not require this. + * + * @returns {Promise.} + * Promise with an accessibility object for the given element. + */ + async assertAccessible(element, mustHaveAccessible = false) { + if (!this.strict) { + return null; + } + + const accessible = await accessibility.getAccessible(element); + if (!accessible && mustHaveAccessible) { + this.error("Element does not have an accessible object", element); + } + + return accessible; + } + + /** + * Test if the accessible has a role that supports some arbitrary + * action. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @returns {boolean} + * True if an actionable role is found on the accessible, false + * otherwise. + */ + isActionableRole(accessible) { + return accessibility.ActionableRoles.has( + accessibility.service.getStringRole(accessible.role) + ); + } + + /** + * Test if an accessible has at least one action that it supports. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @returns {boolean} + * True if the accessible has at least one supported action, + * false otherwise. + */ + hasActionCount(accessible) { + return accessible.actionCount > 0; + } + + /** + * Test if an accessible has a valid name. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @returns {boolean} + * True if the accessible has a non-empty valid name, or false if + * this is not the case. + */ + hasValidName(accessible) { + return accessible.name && accessible.name.trim(); + } + + /** + * Test if an accessible has a {@code hidden} attribute. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @returns {boolean} + * True if the accessible object has a {@code hidden} attribute, + * false otherwise. + */ + hasHiddenAttribute(accessible) { + let hidden = false; + try { + hidden = accessible.attributes.getStringProperty("hidden"); + } catch (e) {} + // if the property is missing, error will be thrown + return hidden && hidden === "true"; + } + + /** + * Verify if an accessible has a given state. + * Test if an accessible has a given state. + * + * @param {nsIAccessible} accessible + * Accessible object to test. + * @param {number} stateToMatch + * State to match. + * + * @returns {boolean} + * True if |accessible| has |stateToMatch|, false otherwise. + */ + matchState(accessible, stateToMatch) { + let state = {}; + accessible.getState(state, {}); + return !!(state.value & stateToMatch); + } + + /** + * Test if an accessible is hidden from the user. + * + * @param {nsIAccessible} accessible + * Accessible object. + * + * @returns {boolean} + * True if element is hidden from user, false otherwise. + */ + isHidden(accessible) { + if (!accessible) { + return true; + } + + while (accessible) { + if (this.hasHiddenAttribute(accessible)) { + return true; + } + accessible = accessible.parent; + } + return false; + } + + /** + * Test if the element's visible state corresponds to its accessibility + * API visibility. + * + * @param {nsIAccessible} accessible + * Accessible object. + * @param {DOMElement|XULElement} element + * Element associated with |accessible|. + * @param {boolean} visible + * Visibility state of |element|. + * + * @throws ElementNotAccessibleError + * If |element|'s visibility state does not correspond to + * |accessible|'s. + */ + assertVisible(accessible, element, visible) { + let hiddenAccessibility = this.isHidden(accessible); + + let message; + if (visible && hiddenAccessibility) { + message = + "Element is not currently visible via the accessibility API " + + "and may not be manipulated by it"; + } else if (!visible && !hiddenAccessibility) { + message = + "Element is currently only visible via the accessibility API " + + "and can be manipulated by it"; + } + this.error(message, element); + } + + /** + * Test if the element's unavailable accessibility state matches the + * enabled state. + * + * @param {nsIAccessible} accessible + * Accessible object. + * @param {DOMElement|XULElement} element + * Element associated with |accessible|. + * @param {boolean} enabled + * Enabled state of |element|. + * + * @throws ElementNotAccessibleError + * If |element|'s enabled state does not match |accessible|'s. + */ + assertEnabled(accessible, element, enabled) { + if (!accessible) { + return; + } + + let win = element.ownerGlobal; + let disabledAccessibility = this.matchState( + accessible, + accessibility.State.Unavailable + ); + let explorable = + win.getComputedStyle(element).getPropertyValue("pointer-events") !== + "none"; + + let message; + if (!explorable && !disabledAccessibility) { + message = + "Element is enabled but is not explorable via the " + + "accessibility API"; + } else if (enabled && disabledAccessibility) { + message = "Element is enabled but disabled via the accessibility API"; + } else if (!enabled && !disabledAccessibility) { + message = "Element is disabled but enabled via the accessibility API"; + } + this.error(message, element); + } + + /** + * Test if it is possible to activate an element with the accessibility + * API. + * + * @param {nsIAccessible} accessible + * Accessible object. + * @param {DOMElement|XULElement} element + * Element associated with |accessible|. + * + * @throws ElementNotAccessibleError + * If it is impossible to activate |element| with |accessible|. + */ + assertActionable(accessible, element) { + if (!accessible) { + return; + } + + let message; + if (!this.hasActionCount(accessible)) { + message = "Element does not support any accessible actions"; + } else if (!this.isActionableRole(accessible)) { + message = + "Element does not have a correct accessibility role " + + "and may not be manipulated via the accessibility API"; + } else if (!this.hasValidName(accessible)) { + message = "Element is missing an accessible name"; + } else if (!this.matchState(accessible, accessibility.State.Focusable)) { + message = "Element is not focusable via the accessibility API"; + } + + this.error(message, element); + } + + /** + * Test that an element's selected state corresponds to its + * accessibility API selected state. + * + * @param {nsIAccessible} accessible + * Accessible object. + * @param {DOMElement|XULElement} element + * Element associated with |accessible|. + * @param {boolean} selected + * The |element|s selected state. + * + * @throws ElementNotAccessibleError + * If |element|'s selected state does not correspond to + * |accessible|'s. + */ + assertSelected(accessible, element, selected) { + if (!accessible) { + return; + } + + // element is not selectable via the accessibility API + if (!this.matchState(accessible, accessibility.State.Selectable)) { + return; + } + + let selectedAccessibility = this.matchState( + accessible, + accessibility.State.Selected + ); + + let message; + if (selected && !selectedAccessibility) { + message = + "Element is selected but not selected via the accessibility API"; + } else if (!selected && selectedAccessibility) { + message = + "Element is not selected but selected via the accessibility API"; + } + this.error(message, element); + } + + /** + * Throw an error if strict accessibility checks are enforced and log + * the error to the log. + * + * @param {string} message + * @param {DOMElement|XULElement} element + * Element that caused an error. + * + * @throws ElementNotAccessibleError + * If |strict| is true. + */ + error(message, element) { + if (!message || !this.strict) { + return; + } + if (element) { + let { id, tagName, className } = element; + message += `: id: ${id}, tagName: ${tagName}, className: ${className}`; + } + + throw new lazy.error.ElementNotAccessibleError(message); + } +}; diff --git a/remote/marionette/actors/MarionetteCommandsChild.sys.mjs b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs new file mode 100644 index 0000000000..078612da56 --- /dev/null +++ b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs @@ -0,0 +1,595 @@ +/* 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/. */ + +/* eslint-disable no-restricted-globals */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs", + action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs", + atom: "chrome://remote/content/marionette/atom.sys.mjs", + dom: "chrome://remote/content/shared/DOM.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs", + interaction: "chrome://remote/content/marionette/interaction.sys.mjs", + json: "chrome://remote/content/marionette/json.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs", + Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +export class MarionetteCommandsChild extends JSWindowActorChild { + #processActor; + + constructor() { + super(); + + this.#processActor = ChromeUtils.domProcessChild.getActor( + "WebDriverProcessData" + ); + + // sandbox storage and name of the current sandbox + this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView); + // State of the input actions. This is specific to contexts and sessions + this.actionState = null; + } + + get innerWindowId() { + return this.manager.innerWindowId; + } + + actorCreated() { + lazy.logger.trace( + `[${this.browsingContext.id}] MarionetteCommands actor created ` + + `for window id ${this.innerWindowId}` + ); + } + + didDestroy() { + lazy.logger.trace( + `[${this.browsingContext.id}] MarionetteCommands actor destroyed ` + + `for window id ${this.innerWindowId}` + ); + } + + async receiveMessage(msg) { + if (!this.contentWindow) { + throw new DOMException("Actor is no longer active", "InactiveActor"); + } + + try { + let result; + let waitForNextTick = false; + + const { name, data: serializedData } = msg; + + const data = lazy.json.deserialize( + serializedData, + this.#processActor.getNodeCache(), + this.contentWindow.browsingContext + ); + + switch (name) { + case "MarionetteCommandsParent:clearElement": + this.clearElement(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:clickElement": + result = await this.clickElement(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:executeScript": + result = await this.executeScript(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:findElement": + result = await this.findElement(data); + break; + case "MarionetteCommandsParent:findElements": + result = await this.findElements(data); + break; + case "MarionetteCommandsParent:getActiveElement": + result = await this.getActiveElement(); + break; + case "MarionetteCommandsParent:getComputedLabel": + result = await this.getComputedLabel(data); + break; + case "MarionetteCommandsParent:getComputedRole": + result = await this.getComputedRole(data); + break; + case "MarionetteCommandsParent:getElementAttribute": + result = await this.getElementAttribute(data); + break; + case "MarionetteCommandsParent:getElementProperty": + result = await this.getElementProperty(data); + break; + case "MarionetteCommandsParent:getElementRect": + result = await this.getElementRect(data); + break; + case "MarionetteCommandsParent:getElementTagName": + result = await this.getElementTagName(data); + break; + case "MarionetteCommandsParent:getElementText": + result = await this.getElementText(data); + break; + case "MarionetteCommandsParent:getElementValueOfCssProperty": + result = await this.getElementValueOfCssProperty(data); + break; + case "MarionetteCommandsParent:getPageSource": + result = await this.getPageSource(); + break; + case "MarionetteCommandsParent:getScreenshotRect": + result = await this.getScreenshotRect(data); + break; + case "MarionetteCommandsParent:getShadowRoot": + result = await this.getShadowRoot(data); + break; + case "MarionetteCommandsParent:isElementDisplayed": + result = await this.isElementDisplayed(data); + break; + case "MarionetteCommandsParent:isElementEnabled": + result = await this.isElementEnabled(data); + break; + case "MarionetteCommandsParent:isElementSelected": + result = await this.isElementSelected(data); + break; + case "MarionetteCommandsParent:performActions": + result = await this.performActions(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:releaseActions": + result = await this.releaseActions(); + break; + case "MarionetteCommandsParent:sendKeysToElement": + result = await this.sendKeysToElement(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:switchToFrame": + result = await this.switchToFrame(data); + waitForNextTick = true; + break; + case "MarionetteCommandsParent:switchToParentFrame": + result = await this.switchToParentFrame(); + waitForNextTick = true; + break; + } + + // Inform the content process that the command has completed. It allows + // it to process async follow-up tasks before the reply is sent. + if (waitForNextTick) { + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + } + + const { seenNodeIds, serializedValue, hasSerializedWindows } = + lazy.json.clone(result, this.#processActor.getNodeCache()); + + // Because in WebDriver classic nodes can only be returned from the same + // browsing context, we only need the seen unique ids as flat array. + return { + seenNodeIds: [...seenNodeIds.values()].flat(), + serializedValue, + hasSerializedWindows, + }; + } catch (e) { + // Always wrap errors as WebDriverError + return { error: lazy.error.wrap(e).toJSON() }; + } + } + + // Implementation of WebDriver commands + + /** Clear the text of an element. + * + * @param {object} options + * @param {Element} options.elem + */ + clearElement(options = {}) { + const { elem } = options; + + lazy.interaction.clearElement(elem); + } + + /** + * Click an element. + */ + async clickElement(options = {}) { + const { capabilities, elem } = options; + + return lazy.interaction.clickElement( + elem, + capabilities["moz:accessibilityChecks"], + capabilities["moz:webdriverClick"] + ); + } + + /** + * Executes a JavaScript function. + */ + async executeScript(options = {}) { + const { args, opts = {}, script } = options; + + let sb; + if (opts.sandboxName) { + sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox); + } else { + sb = lazy.sandbox.createMutable(this.document.defaultView); + } + + return lazy.evaluate.sandbox(sb, script, args, opts); + } + + /** + * Find an element in the current browsing context's document using the + * given search strategy. + * + * @param {object=} options + * @param {string} options.strategy + * @param {string} options.selector + * @param {object} options.opts + * @param {Element} options.opts.startNode + * + */ + async findElement(options = {}) { + const { strategy, selector, opts } = options; + + opts.all = false; + + const container = { frame: this.document.defaultView }; + return lazy.dom.find(container, strategy, selector, opts); + } + + /** + * Find elements in the current browsing context's document using the + * given search strategy. + * + * @param {object=} options + * @param {string} options.strategy + * @param {string} options.selector + * @param {object} options.opts + * @param {Element} options.opts.startNode + * + */ + async findElements(options = {}) { + const { strategy, selector, opts } = options; + + opts.all = true; + + const container = { frame: this.document.defaultView }; + return lazy.dom.find(container, strategy, selector, opts); + } + + /** + * Return the active element in the document. + */ + async getActiveElement() { + let elem = this.document.activeElement; + if (!elem) { + throw new lazy.error.NoSuchElementError(); + } + + return elem; + } + + /** + * Return the accessible label for a given element. + */ + async getComputedLabel(options = {}) { + const { elem } = options; + + const accessible = await lazy.accessibility.getAccessible(elem); + if (!accessible) { + return null; + } + + // If name is null (absent), expose the empty string. + if (accessible.name === null) { + return ""; + } + + return accessible.name; + } + + /** + * Return the accessible role for a given element. + */ + async getComputedRole(options = {}) { + const { elem } = options; + + const accessible = await lazy.accessibility.getAccessible(elem); + if (!accessible) { + // If it's not in the a11y tree, it's probably presentational. + return "none"; + } + + return accessible.computedARIARole; + } + + /** + * Get the value of an attribute for the given element. + */ + async getElementAttribute(options = {}) { + const { name, elem } = options; + + if (lazy.dom.isBooleanAttribute(elem, name)) { + if (elem.hasAttribute(name)) { + return "true"; + } + return null; + } + return elem.getAttribute(name); + } + + /** + * Get the value of a property for the given element. + */ + async getElementProperty(options = {}) { + const { name, elem } = options; + + // Waive Xrays to get unfiltered access to the untrusted element. + const el = Cu.waiveXrays(elem); + return typeof el[name] != "undefined" ? el[name] : null; + } + + /** + * Get the position and dimensions of the element. + */ + async getElementRect(options = {}) { + const { elem } = options; + + const rect = elem.getBoundingClientRect(); + return { + x: rect.x + this.document.defaultView.pageXOffset, + y: rect.y + this.document.defaultView.pageYOffset, + width: rect.width, + height: rect.height, + }; + } + + /** + * Get the tagName for the given element. + */ + async getElementTagName(options = {}) { + const { elem } = options; + + return elem.tagName.toLowerCase(); + } + + /** + * Get the text content for the given element. + */ + async getElementText(options = {}) { + const { elem } = options; + + try { + return await lazy.atom.getVisibleText(elem, this.document.defaultView); + } catch (e) { + lazy.logger.warn(`Atom getVisibleText failed: "${e.message}"`); + + // Fallback in case the atom implementation is broken. + // As known so far this only happens for XML documents (bug 1794099). + return elem.textContent; + } + } + + /** + * Get the value of a css property for the given element. + */ + async getElementValueOfCssProperty(options = {}) { + const { name, elem } = options; + + const style = this.document.defaultView.getComputedStyle(elem); + return style.getPropertyValue(name); + } + + /** + * Get the source of the current browsing context's document. + */ + async getPageSource() { + return this.document.documentElement.outerHTML; + } + + /** + * Returns the rect of the element to screenshot. + * + * Because the screen capture takes place in the parent process the dimensions + * for the screenshot have to be determined in the appropriate child process. + * + * Also it takes care of scrolling an element into view if requested. + * + * @param {object} options + * @param {Element} options.elem + * Optional element to take a screenshot of. + * @param {boolean=} options.full + * True to take a screenshot of the entire document element. + * Defaults to true. + * @param {boolean=} options.scroll + * When elem is given, scroll it into view. + * Defaults to true. + * + * @returns {DOMRect} + * The area to take a snapshot from. + */ + async getScreenshotRect(options = {}) { + const { elem, full = true, scroll = true } = options; + const win = elem + ? this.document.defaultView + : this.browsingContext.top.window; + + let rect; + + if (elem) { + if (scroll) { + lazy.dom.scrollIntoView(elem); + } + rect = this.getElementRect({ elem }); + } else if (full) { + const docEl = win.document.documentElement; + rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight); + } else { + // viewport + rect = new DOMRect( + win.pageXOffset, + win.pageYOffset, + win.innerWidth, + win.innerHeight + ); + } + + return rect; + } + + /** + * Return the shadowRoot attached to an element + */ + async getShadowRoot(options = {}) { + const { elem } = options; + + return lazy.dom.getShadowRoot(elem); + } + + /** + * Determine the element displayedness of the given web element. + */ + async isElementDisplayed(options = {}) { + const { capabilities, elem } = options; + + return lazy.interaction.isElementDisplayed( + elem, + capabilities["moz:accessibilityChecks"] + ); + } + + /** + * Check if element is enabled. + */ + async isElementEnabled(options = {}) { + const { capabilities, elem } = options; + + return lazy.interaction.isElementEnabled( + elem, + capabilities["moz:accessibilityChecks"] + ); + } + + /** + * Determine whether the referenced element is selected or not. + */ + async isElementSelected(options = {}) { + const { capabilities, elem } = options; + + return lazy.interaction.isElementSelected( + elem, + capabilities["moz:accessibilityChecks"] + ); + } + + /** + * Perform a series of grouped actions at the specified points in time. + * + * @param {object} options + * @param {object} options.actions + * Array of objects with each representing an action sequence. + * @param {object} options.capabilities + * Object with a list of WebDriver session capabilities. + */ + async performActions(options = {}) { + const { actions } = options; + if (this.actionState === null) { + this.actionState = new lazy.action.State(); + } + let actionChain = lazy.action.Chain.fromJSON(this.actionState, actions); + + await actionChain.dispatch(this.actionState, this.document.defaultView); + // Terminate the current wheel transaction if there is one. Wheel + // transactions should not live longer than a single action chain. + ChromeUtils.endWheelTransaction(); + } + + /** + * The release actions command is used to release all the keys and pointer + * buttons that are currently depressed. This causes events to be fired + * as if the state was released by an explicit series of actions. It also + * clears all the internal state of the virtual devices. + */ + async releaseActions() { + if (this.actionState === null) { + return; + } + await this.actionState.release(this.document.defaultView); + this.actionState = null; + } + + /* + * Send key presses to element after focusing on it. + */ + async sendKeysToElement(options = {}) { + const { capabilities, elem, text } = options; + + const opts = { + strictFileInteractability: capabilities.strictFileInteractability, + accessibilityChecks: capabilities["moz:accessibilityChecks"], + webdriverClick: capabilities["moz:webdriverClick"], + }; + + return lazy.interaction.sendKeysToElement(elem, text, opts); + } + + /** + * Switch to the specified frame. + * + * @param {object=} options + * @param {(number|Element)=} options.id + * If it's a number treat it as the index for all the existing frames. + * If it's an Element switch to this specific frame. + * If not specified or `null` switch to the top-level browsing context. + */ + async switchToFrame(options = {}) { + const { id } = options; + + const childContexts = this.browsingContext.children; + let browsingContext; + + if (id == null) { + browsingContext = this.browsingContext.top; + } else if (typeof id == "number") { + if (id < 0 || id >= childContexts.length) { + throw new lazy.error.NoSuchFrameError( + `Unable to locate frame with index: ${id}` + ); + } + browsingContext = childContexts[id]; + } else { + const context = childContexts.find(context => { + return context.embedderElement === id; + }); + if (!context) { + throw new lazy.error.NoSuchFrameError( + `Unable to locate frame for element: ${id}` + ); + } + browsingContext = context; + } + + // For in-process iframes the window global is lazy-loaded for optimization + // reasons. As such force the currentWindowGlobal to be created so we always + // have a window (bug 1691348). + browsingContext.window; + + return { browsingContextId: browsingContext.id }; + } + + /** + * Switch to the parent frame. + */ + async switchToParentFrame() { + const browsingContext = this.browsingContext.parent || this.browsingContext; + + return { browsingContextId: browsingContext.id }; + } +} diff --git a/remote/marionette/actors/MarionetteCommandsParent.sys.mjs b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs new file mode 100644 index 0000000000..970c927eab --- /dev/null +++ b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs @@ -0,0 +1,436 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + capture: "chrome://remote/content/shared/Capture.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + getSeenNodesForBrowsingContext: + "chrome://remote/content/shared/webdriver/Session.sys.mjs", + json: "chrome://remote/content/marionette/json.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +// Because Marionette supports a single session only we store its id +// globally so that the parent actor can access it. +let webDriverSessionId = null; + +export class MarionetteCommandsParent extends JSWindowActorParent { + #deferredDialogOpened; + + actorCreated() { + this.#deferredDialogOpened = null; + } + + async sendQuery(name, serializedValue) { + const seenNodes = lazy.getSeenNodesForBrowsingContext( + webDriverSessionId, + this.manager.browsingContext + ); + + // return early if a dialog is opened + this.#deferredDialogOpened = Promise.withResolvers(); + let { + error, + seenNodeIds, + serializedValue: serializedResult, + hasSerializedWindows, + } = await Promise.race([ + super.sendQuery(name, serializedValue), + this.#deferredDialogOpened.promise, + ]).finally(() => { + this.#deferredDialogOpened = null; + }); + + if (error) { + const err = lazy.error.WebDriverError.fromJSON(error); + this.#handleError(err, seenNodes); + } + + // Update seen nodes for serialized element and shadow root nodes. + seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId)); + + if (hasSerializedWindows) { + // The serialized data contains WebWindow references that need to be + // converted to unique identifiers. + serializedResult = lazy.json.mapToNavigableIds(serializedResult); + } + + return serializedResult; + } + + /** + * Handle WebDriver error and replace error type if necessary. + * + * @param {WebDriverError} error + * The WebDriver error to handle. + * @param {Set} seenNodes + * List of node ids already seen in this navigable. + * + * @throws {WebDriverError} + * The original or replaced WebDriver error. + */ + #handleError(error, seenNodes) { + // If an element hasn't been found during deserialization check if it + // may be a stale reference. + if ( + error instanceof lazy.error.NoSuchElementError && + error.data.elementId !== undefined && + seenNodes.has(error.data.elementId) + ) { + throw new lazy.error.StaleElementReferenceError(error); + } + + // If a shadow root hasn't been found during deserialization check if it + // may be a detached reference. + if ( + error instanceof lazy.error.NoSuchShadowRootError && + error.data.shadowId !== undefined && + seenNodes.has(error.data.shadowId) + ) { + throw new lazy.error.DetachedShadowRootError(error); + } + + throw error; + } + + notifyDialogOpened() { + if (this.#deferredDialogOpened) { + this.#deferredDialogOpened.resolve({ data: null }); + } + } + + // Proxying methods for WebDriver commands + + clearElement(webEl) { + return this.sendQuery("MarionetteCommandsParent:clearElement", { + elem: webEl, + }); + } + + clickElement(webEl, capabilities) { + return this.sendQuery("MarionetteCommandsParent:clickElement", { + elem: webEl, + capabilities: capabilities.toJSON(), + }); + } + + async executeScript(script, args, opts) { + return this.sendQuery("MarionetteCommandsParent:executeScript", { + script, + args: lazy.json.mapFromNavigableIds(args), + opts, + }); + } + + findElement(strategy, selector, opts) { + return this.sendQuery("MarionetteCommandsParent:findElement", { + strategy, + selector, + opts, + }); + } + + findElements(strategy, selector, opts) { + return this.sendQuery("MarionetteCommandsParent:findElements", { + strategy, + selector, + opts, + }); + } + + async getShadowRoot(webEl) { + return this.sendQuery("MarionetteCommandsParent:getShadowRoot", { + elem: webEl, + }); + } + + async getActiveElement() { + return this.sendQuery("MarionetteCommandsParent:getActiveElement"); + } + + async getComputedLabel(webEl) { + return this.sendQuery("MarionetteCommandsParent:getComputedLabel", { + elem: webEl, + }); + } + + async getComputedRole(webEl) { + return this.sendQuery("MarionetteCommandsParent:getComputedRole", { + elem: webEl, + }); + } + + async getElementAttribute(webEl, name) { + return this.sendQuery("MarionetteCommandsParent:getElementAttribute", { + elem: webEl, + name, + }); + } + + async getElementProperty(webEl, name) { + return this.sendQuery("MarionetteCommandsParent:getElementProperty", { + elem: webEl, + name, + }); + } + + async getElementRect(webEl) { + return this.sendQuery("MarionetteCommandsParent:getElementRect", { + elem: webEl, + }); + } + + async getElementTagName(webEl) { + return this.sendQuery("MarionetteCommandsParent:getElementTagName", { + elem: webEl, + }); + } + + async getElementText(webEl) { + return this.sendQuery("MarionetteCommandsParent:getElementText", { + elem: webEl, + }); + } + + async getElementValueOfCssProperty(webEl, name) { + return this.sendQuery( + "MarionetteCommandsParent:getElementValueOfCssProperty", + { + elem: webEl, + name, + } + ); + } + + async getPageSource() { + return this.sendQuery("MarionetteCommandsParent:getPageSource"); + } + + async isElementDisplayed(webEl, capabilities) { + return this.sendQuery("MarionetteCommandsParent:isElementDisplayed", { + capabilities: capabilities.toJSON(), + elem: webEl, + }); + } + + async isElementEnabled(webEl, capabilities) { + return this.sendQuery("MarionetteCommandsParent:isElementEnabled", { + capabilities: capabilities.toJSON(), + elem: webEl, + }); + } + + async isElementSelected(webEl, capabilities) { + return this.sendQuery("MarionetteCommandsParent:isElementSelected", { + capabilities: capabilities.toJSON(), + elem: webEl, + }); + } + + async sendKeysToElement(webEl, text, capabilities) { + return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", { + capabilities: capabilities.toJSON(), + elem: webEl, + text, + }); + } + + async performActions(actions) { + return this.sendQuery("MarionetteCommandsParent:performActions", { + actions, + }); + } + + async releaseActions() { + return this.sendQuery("MarionetteCommandsParent:releaseActions"); + } + + async switchToFrame(id) { + const { browsingContextId } = await this.sendQuery( + "MarionetteCommandsParent:switchToFrame", + { id } + ); + + return { + browsingContext: BrowsingContext.get(browsingContextId), + }; + } + + async switchToParentFrame() { + const { browsingContextId } = await this.sendQuery( + "MarionetteCommandsParent:switchToParentFrame" + ); + + return { + browsingContext: BrowsingContext.get(browsingContextId), + }; + } + + async takeScreenshot(webEl, format, full, scroll) { + const rect = await this.sendQuery( + "MarionetteCommandsParent:getScreenshotRect", + { + elem: webEl, + full, + scroll, + } + ); + + // If no element has been specified use the top-level browsing context. + // Otherwise use the browsing context from the currently selected frame. + const browsingContext = webEl + ? this.browsingContext + : this.browsingContext.top; + + let canvas = await lazy.capture.canvas( + browsingContext.topChromeWindow, + browsingContext, + rect.x, + rect.y, + rect.width, + rect.height + ); + + switch (format) { + case lazy.capture.Format.Hash: + return lazy.capture.toHash(canvas); + + case lazy.capture.Format.Base64: + return lazy.capture.toBase64(canvas); + + default: + throw new TypeError(`Invalid capture format: ${format}`); + } + } +} + +/** + * Proxy that will dynamically create MarionetteCommands actors for a dynamically + * provided browsing context until the method can be fully executed by the + * JSWindowActor pair. + * + * @param {function(): BrowsingContext} browsingContextFn + * A function that returns the reference to the browsing context for which + * the query should run. + */ +export function getMarionetteCommandsActorProxy(browsingContextFn) { + const MAX_ATTEMPTS = 10; + + /** + * Methods which modify the content page cannot be retried safely. + * See Bug 1673345. + */ + const NO_RETRY_METHODS = [ + "clickElement", + "executeScript", + "performActions", + "releaseActions", + "sendKeysToElement", + ]; + + return new Proxy( + {}, + { + get(target, methodName) { + return async (...args) => { + let attempts = 0; + while (true) { + try { + const browsingContext = browsingContextFn(); + if (!browsingContext) { + throw new DOMException( + "No BrowsingContext found", + "NoBrowsingContext" + ); + } + + // TODO: Scenarios where the window/tab got closed and + // currentWindowGlobal is null will be handled in Bug 1662808. + const actor = + browsingContext.currentWindowGlobal.getActor( + "MarionetteCommands" + ); + + const result = await actor[methodName](...args); + return result; + } catch (e) { + if (!["AbortError", "InactiveActor"].includes(e.name)) { + // Only retry when the JSWindowActor pair gets destroyed, or + // gets inactive eg. when the page is moved into bfcache. + throw e; + } + + if (NO_RETRY_METHODS.includes(methodName)) { + const browsingContextId = browsingContextFn()?.id; + lazy.logger.trace( + `[${browsingContextId}] Querying "${methodName}" failed with` + + ` ${e.name}, returning "null" as fallback` + ); + return null; + } + + if (++attempts > MAX_ATTEMPTS) { + const browsingContextId = browsingContextFn()?.id; + lazy.logger.trace( + `[${browsingContextId}] Querying "${methodName} "` + + `reached the limit of retry attempts (${MAX_ATTEMPTS})` + ); + throw e; + } + + lazy.logger.trace( + `Retrying "${methodName}", attempt: ${attempts}` + ); + } + } + }; + }, + } + ); +} + +/** + * Register the MarionetteCommands actor that holds all the commands. + * + * @param {string} sessionId + * The id of the current WebDriver session. + */ +export function registerCommandsActor(sessionId) { + try { + ChromeUtils.registerWindowActor("MarionetteCommands", { + kind: "JSWindowActor", + parent: { + esModuleURI: + "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs", + }, + child: { + esModuleURI: + "chrome://remote/content/marionette/actors/MarionetteCommandsChild.sys.mjs", + }, + + allFrames: true, + includeChrome: true, + }); + } catch (e) { + if (e.name === "NotSupportedError") { + lazy.logger.warn(`MarionetteCommands actor is already registered!`); + } else { + throw e; + } + } + + webDriverSessionId = sessionId; +} + +export function unregisterCommandsActor() { + webDriverSessionId = null; + + ChromeUtils.unregisterWindowActor("MarionetteCommands"); +} diff --git a/remote/marionette/actors/MarionetteEventsChild.sys.mjs b/remote/marionette/actors/MarionetteEventsChild.sys.mjs new file mode 100644 index 0000000000..b59b4d00e7 --- /dev/null +++ b/remote/marionette/actors/MarionetteEventsChild.sys.mjs @@ -0,0 +1,71 @@ +/* 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/. */ + +/* eslint-disable no-restricted-globals */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +export class MarionetteEventsChild extends JSWindowActorChild { + get innerWindowId() { + return this.manager.innerWindowId; + } + + actorCreated() { + // Prevent the logger from being created if the current log level + // isn't set to 'trace'. This is important for a faster content process + // creation when Marionette is running. + if (lazy.Log.isTraceLevelOrOrMore) { + lazy.logger.trace( + `[${this.browsingContext.id}] MarionetteEvents actor created ` + + `for window id ${this.innerWindowId}` + ); + } + } + + handleEvent({ target, type }) { + if (!Services.cpmm.sharedData.get("MARIONETTE_EVENTS_ENABLED")) { + // The parent process will set MARIONETTE_EVENTS_ENABLED to false when + // the Marionette session ends to avoid unnecessary inter process + // communications + return; + } + + // Ignore invalid combinations of load events and document's readyState. + if ( + (type === "DOMContentLoaded" && target.readyState != "interactive") || + (type === "pageshow" && target.readyState != "complete") + ) { + lazy.logger.warn( + `Ignoring event '${type}' because document has an invalid ` + + `readyState of '${target.readyState}'.` + ); + return; + } + + switch (type) { + case "beforeunload": + case "DOMContentLoaded": + case "hashchange": + case "pagehide": + case "pageshow": + case "popstate": + this.sendAsyncMessage("MarionetteEventsChild:PageLoadEvent", { + browsingContext: this.browsingContext, + documentURI: target.documentURI, + readyState: target.readyState, + type, + windowId: this.innerWindowId, + }); + break; + } + } +} diff --git a/remote/marionette/actors/MarionetteEventsParent.sys.mjs b/remote/marionette/actors/MarionetteEventsParent.sys.mjs new file mode 100644 index 0000000000..c051fb2b1f --- /dev/null +++ b/remote/marionette/actors/MarionetteEventsParent.sys.mjs @@ -0,0 +1,113 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +// Singleton to allow forwarding events to registered listeners. +export const EventDispatcher = { + init() { + lazy.EventEmitter.decorate(this); + }, +}; + +EventDispatcher.init(); + +export class MarionetteEventsParent extends JSWindowActorParent { + async receiveMessage(msg) { + const { name, data } = msg; + + let rv; + switch (name) { + case "MarionetteEventsChild:PageLoadEvent": + EventDispatcher.emit("page-load", data); + break; + } + + return rv; + } +} + +// Flag to check if the MarionetteEvents actors have already been registed. +let eventsActorRegistered = false; + +/** + * Register Events actors to listen for page load events via EventDispatcher. + */ +function registerEventsActor() { + if (eventsActorRegistered) { + return; + } + + try { + // Register the JSWindowActor pair for events as used by Marionette + ChromeUtils.registerWindowActor("MarionetteEvents", { + kind: "JSWindowActor", + parent: { + esModuleURI: + "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs", + }, + child: { + esModuleURI: + "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs", + events: { + beforeunload: { capture: true }, + DOMContentLoaded: { mozSystemGroup: true }, + hashchange: { mozSystemGroup: true }, + pagehide: { mozSystemGroup: true }, + pageshow: { mozSystemGroup: true }, + // popstate doesn't bubble, as such use capturing phase + popstate: { capture: true, mozSystemGroup: true }, + + click: {}, + dblclick: {}, + unload: { capture: true, createActor: false }, + }, + }, + + allFrames: true, + includeChrome: true, + }); + + eventsActorRegistered = true; + } catch (e) { + if (e.name === "NotSupportedError") { + lazy.logger.warn(`MarionetteEvents actor is already registered!`); + } else { + throw e; + } + } +} + +/** + * Enable MarionetteEvents actors to start forwarding page load events from the + * child actor to the parent actor. Register the MarionetteEvents actor if necessary. + */ +export function enableEventsActor() { + // sharedData is replicated across processes and will be checked by + // MarionetteEventsChild before forward events to the parent actor. + Services.ppmm.sharedData.set("MARIONETTE_EVENTS_ENABLED", true); + // Request to immediately flush the data to the content processes to avoid races. + Services.ppmm.sharedData.flush(); + + registerEventsActor(); +} + +/** + * Disable MarionetteEvents actors to stop forwarding page load events from the + * child actor to the parent actor. + */ +export function disableEventsActor() { + Services.ppmm.sharedData.set("MARIONETTE_EVENTS_ENABLED", false); + Services.ppmm.sharedData.flush(); +} diff --git a/remote/marionette/actors/MarionetteReftestChild.sys.mjs b/remote/marionette/actors/MarionetteReftestChild.sys.mjs new file mode 100644 index 0000000000..fd73f88a88 --- /dev/null +++ b/remote/marionette/actors/MarionetteReftestChild.sys.mjs @@ -0,0 +1,239 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", + + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +/** + * Child JSWindowActor to handle navigation for reftests relying on marionette. + */ +export class MarionetteReftestChild extends JSWindowActorChild { + constructor() { + super(); + + // This promise will resolve with the URL recorded in the "load" event + // handler. This URL will not be impacted by any hash modification that + // might be performed by the test script. + // The harness should be loaded before loading any test page, so the actors + // should be registered before the "load" event is received for a test page. + this._loadedURLPromise = new Promise( + r => (this._resolveLoadedURLPromise = r) + ); + } + + handleEvent(event) { + if (event.type == "load") { + const url = event.target.location.href; + lazy.logger.debug(`Handle load event with URL ${url}`); + this._resolveLoadedURLPromise(url); + } + } + + actorCreated() { + lazy.logger.trace( + `[${this.browsingContext.id}] Reftest actor created ` + + `for window id ${this.manager.innerWindowId}` + ); + } + + async receiveMessage(msg) { + const { name, data } = msg; + + let result; + switch (name) { + case "MarionetteReftestParent:flushRendering": + result = await this.flushRendering(data); + break; + case "MarionetteReftestParent:reftestWait": + result = await this.reftestWait(data); + break; + } + return result; + } + + /** + * Wait for a reftest page to be ready for screenshots: + * - wait for the loadedURL to be available (see handleEvent) + * - check if the URL matches the expected URL + * - if present, wait for the "reftest-wait" classname to be removed from the + * document element + * + * @param {object} options + * @param {string} options.url + * The expected test page URL + * @param {boolean} options.useRemote + * True when using e10s + * @param {boolean} options.warnOnOverflow + * True if we should check the content fits in the viewport. + * This isn't necessary for print reftests where we will render the full + * size of the paginated content. + * @returns {boolean} + * Returns true when the correct page is loaded and ready for + * screenshots. Returns false if the page loaded bug does not have the + * expected URL. + */ + async reftestWait(options = {}) { + const { url, useRemote } = options; + const loadedURL = await this._loadedURLPromise; + if (loadedURL !== url) { + lazy.logger.debug( + `Window URL does not match the expected URL "${loadedURL}" !== "${url}"` + ); + return false; + } + + const documentElement = this.document.documentElement; + const hasReftestWait = documentElement.classList.contains("reftest-wait"); + + lazy.logger.debug("Waiting for event loop to spin"); + await new Promise(resolve => lazy.setTimeout(resolve, 0)); + + await this.paintComplete({ useRemote, ignoreThrottledAnimations: true }); + + if (hasReftestWait) { + const event = new this.document.defaultView.Event("TestRendered", { + bubbles: true, + }); + documentElement.dispatchEvent(event); + lazy.logger.info("Emitted TestRendered event"); + await this.reftestWaitRemoved(); + await this.paintComplete({ useRemote, ignoreThrottledAnimations: false }); + } + if ( + options.warnOnOverflow && + (this.document.defaultView.innerWidth < documentElement.scrollWidth || + this.document.defaultView.innerHeight < documentElement.scrollHeight) + ) { + lazy.logger.warn( + `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})` + ); + } + return true; + } + + paintComplete({ useRemote, ignoreThrottledAnimations }) { + lazy.logger.debug("Waiting for rendering"); + let windowUtils = this.document.defaultView.windowUtils; + return new Promise(resolve => { + let maybeResolve = () => { + this.flushRendering({ ignoreThrottledAnimations }); + if (useRemote) { + // Flush display (paint) + lazy.logger.debug("Force update of layer tree"); + windowUtils.updateLayerTree(); + } + + if (windowUtils.isMozAfterPaintPending) { + lazy.logger.debug("isMozAfterPaintPending: true"); + this.document.defaultView.addEventListener( + "MozAfterPaint", + maybeResolve, + { + once: true, + } + ); + } else { + // resolve at the start of the next frame in case of leftover paints + lazy.logger.debug("isMozAfterPaintPending: false"); + this.document.defaultView.requestAnimationFrame(() => { + this.document.defaultView.requestAnimationFrame(resolve); + }); + } + }; + maybeResolve(); + }); + } + + reftestWaitRemoved() { + lazy.logger.debug("Waiting for reftest-wait removal"); + return new Promise(resolve => { + const documentElement = this.document.documentElement; + let observer = new this.document.defaultView.MutationObserver(() => { + if (!documentElement.classList.contains("reftest-wait")) { + observer.disconnect(); + lazy.logger.debug("reftest-wait removed"); + lazy.setTimeout(resolve, 0); + } + }); + if (documentElement.classList.contains("reftest-wait")) { + observer.observe(documentElement, { attributes: true }); + } else { + lazy.setTimeout(resolve, 0); + } + }); + } + + /** + * Ensure layout is flushed in each frame + * + * @param {object} options + * @param {boolean} options.ignoreThrottledAnimations Don't flush + * the layout of throttled animations. We can end up in a + * situation where flushing a throttled animation causes + * mozAfterPaint events even when all rendering we care about + * should have ceased. See + * https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729 + * for more detail. + */ + flushRendering(options = {}) { + let { ignoreThrottledAnimations } = options; + lazy.logger.debug( + `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}` + ); + let anyPendingPaintsGeneratedInDescendants = false; + + let windowUtils = this.document.defaultView.windowUtils; + + function flushWindow(win) { + let utils = win.windowUtils; + let afterPaintWasPending = utils.isMozAfterPaintPending; + + let root = win.document.documentElement; + if (root) { + try { + if (ignoreThrottledAnimations) { + utils.flushLayoutWithoutThrottledAnimations(); + } else { + root.getBoundingClientRect(); + } + } catch (e) { + lazy.logger.error("flushWindow failed", e); + } + } + + if (!afterPaintWasPending && utils.isMozAfterPaintPending) { + anyPendingPaintsGeneratedInDescendants = true; + } + + for (let i = 0; i < win.frames.length; ++i) { + // Skip remote frames, flushRendering will be called on their individual + // MarionetteReftest actor via _recursiveFlushRendering performed from + // the topmost MarionetteReftest actor. + if (!Cu.isRemoteProxy(win.frames[i])) { + flushWindow(win.frames[i]); + } + } + } + flushWindow(this.document.defaultView); + + if ( + anyPendingPaintsGeneratedInDescendants && + !windowUtils.isMozAfterPaintPending + ) { + lazy.logger.error( + "Descendant frame generated a MozAfterPaint event, " + + "but the root document doesn't have one!" + ); + } + } +} diff --git a/remote/marionette/actors/MarionetteReftestParent.sys.mjs b/remote/marionette/actors/MarionetteReftestParent.sys.mjs new file mode 100644 index 0000000000..327806ebbf --- /dev/null +++ b/remote/marionette/actors/MarionetteReftestParent.sys.mjs @@ -0,0 +1,90 @@ +/* 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/. */ + +/** + * Parent JSWindowActor to handle navigation for reftests relying on marionette. + */ +export class MarionetteReftestParent extends JSWindowActorParent { + /** + * Wait for the expected URL to be loaded. + * + * @param {string} url + * The expected url. + * @param {boolean} useRemote + * True if tests are running with e10s. + * @param {boolean} warnOnOverflow + * True if we should check the content fits in the viewport. + * This isn't necessary for print reftests where we will render the full + * size of the paginated content. + * @returns {boolean} true if the page is fully loaded with the expected url, + * false otherwise. + */ + async reftestWait(url, useRemote, warnOnOverflow) { + try { + const isCorrectUrl = await this.sendQuery( + "MarionetteReftestParent:reftestWait", + { + url, + useRemote, + warnOnOverflow, + } + ); + + if (isCorrectUrl) { + // Trigger flush rendering for all remote frames. + await this._flushRenderingInSubtree({ + ignoreThrottledAnimations: false, + }); + } + + return isCorrectUrl; + } catch (e) { + if (e.name === "AbortError") { + // If the query is aborted, the window global is being destroyed, most + // likely because a navigation happened. + return false; + } + + // Other errors should not be swallowed. + throw e; + } + } + + /** + * Call flushRendering on all browsing contexts in the subtree. + * Each actor will flush rendering in all the same process frames. + */ + async _flushRenderingInSubtree({ ignoreThrottledAnimations }) { + const browsingContext = this.manager.browsingContext; + const contexts = browsingContext.getAllBrowsingContextsInSubtree(); + + await Promise.all( + contexts.map(async context => { + if (context === browsingContext) { + // Skip the top browsing context, for which flushRendering is + // already performed via the initial reftestWait call. + return; + } + + const windowGlobal = context.currentWindowGlobal; + if (!windowGlobal) { + // Bail out if there is no window attached to the current context. + return; + } + + if (!windowGlobal.isProcessRoot) { + // Bail out if this window global is not a process root. + // MarionetteReftestChild::flushRendering will flush all same process + // frames, so we only need to call flushRendering on process roots. + return; + } + + const reftestActor = windowGlobal.getActor("MarionetteReftest"); + await reftestActor.sendQuery("MarionetteReftestParent:flushRendering", { + ignoreThrottledAnimations, + }); + }) + ); + } +} diff --git a/remote/marionette/addon.sys.mjs b/remote/marionette/addon.sys.mjs new file mode 100644 index 0000000000..f83671694b --- /dev/null +++ b/remote/marionette/addon.sys.mjs @@ -0,0 +1,142 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", +}); + +// from https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#AddonInstall_errors +const ERRORS = { + [-1]: "ERROR_NETWORK_FAILURE: A network error occured.", + [-2]: "ERROR_INCORRECT_HASH: The downloaded file did not match the expected hash.", + [-3]: "ERROR_CORRUPT_FILE: The file appears to be corrupt.", + [-4]: "ERROR_FILE_ACCESS: There was an error accessing the filesystem.", + [-5]: "ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't.", + [-6]: "ERROR_UNEXPECTED_ADDON_TYPE: The downloaded add-on had a different type than expected (during an update).", + [-7]: "ERROR_INCORRECT_ID: The addon did not have the expected ID (during an update).", + [-8]: "ERROR_INVALID_DOMAIN: The addon install_origins does not list the 3rd party domain.", + [-9]: "ERROR_UNEXPECTED_ADDON_VERSION: The downloaded add-on had a different version than expected (during an update).", + [-10]: "ERROR_BLOCKLISTED: The add-on is blocklisted.", + [-11]: + "ERROR_INCOMPATIBLE: The add-on is incompatible (w.r.t. the compatibility range).", + [-12]: + "ERROR_UNSUPPORTED_ADDON_TYPE: The add-on type is not supported by the platform.", +}; + +async function installAddon(file) { + let install = await lazy.AddonManager.getInstallForFile(file, null, { + source: "internal", + }); + + if (install.error) { + throw new lazy.error.UnknownError(ERRORS[install.error]); + } + + return install.install().catch(err => { + throw new lazy.error.UnknownError(ERRORS[install.error]); + }); +} + +/** Installs addons by path and uninstalls by ID. */ +export class Addon { + /** + * Install a Firefox addon. + * + * If the addon is restartless, it can be used right away. Otherwise a + * restart is required. + * + * Temporary addons will automatically be uninstalled on shutdown and + * do not need to be signed, though they must be restartless. + * + * @param {string} path + * Full path to the extension package archive. + * @param {boolean=} temporary + * True to install the addon temporarily, false (default) otherwise. + * + * @returns {Promise.} + * Addon ID. + * + * @throws {UnknownError} + * If there is a problem installing the addon. + */ + static async install(path, temporary = false) { + let addon; + let file; + + try { + file = new lazy.FileUtils.File(path); + } catch (e) { + throw new lazy.error.UnknownError(`Expected absolute path: ${e}`, e); + } + + if (!file.exists()) { + throw new lazy.error.UnknownError(`No such file or directory: ${path}`); + } + + try { + if (temporary) { + addon = await lazy.AddonManager.installTemporaryAddon(file); + } else { + addon = await installAddon(file); + } + } catch (e) { + throw new lazy.error.UnknownError( + `Could not install add-on: ${path}: ${e.message}`, + e + ); + } + + return addon.id; + } + + /** + * Uninstall a Firefox addon. + * + * If the addon is restartless it will be uninstalled right away. + * Otherwise, Firefox must be restarted for the change to take effect. + * + * @param {string} id + * ID of the addon to uninstall. + * + * @returns {Promise} + * + * @throws {UnknownError} + * If there is a problem uninstalling the addon. + */ + static async uninstall(id) { + let candidate = await lazy.AddonManager.getAddonByID(id); + if (candidate === null) { + // `AddonManager.getAddonByID` never rejects but instead + // returns `null` if the requested addon cannot be found. + throw new lazy.error.UnknownError(`Addon ${id} is not installed`); + } + + return new Promise(resolve => { + let listener = { + onOperationCancelled: addon => { + if (addon.id === candidate.id) { + lazy.AddonManager.removeAddonListener(listener); + throw new lazy.error.UnknownError( + `Uninstall of ${candidate.id} has been canceled` + ); + } + }, + + onUninstalled: addon => { + if (addon.id === candidate.id) { + lazy.AddonManager.removeAddonListener(listener); + resolve(); + } + }, + }; + + lazy.AddonManager.addAddonListener(listener); + candidate.uninstall(); + }); + } +} diff --git a/remote/marionette/atom.sys.mjs b/remote/marionette/atom.sys.mjs new file mode 100644 index 0000000000..d065849d43 --- /dev/null +++ b/remote/marionette/atom.sys.mjs @@ -0,0 +1,55 @@ +// Copyright 2011-2017 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs", + sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs", +}); + +/** @namespace */ +export const atom = {}; + +// Follow the instructions to export all the atoms: +// https://firefox-source-docs.mozilla.org/testing/marionette/SeleniumAtoms.html +// +// Built from SHA1: bd5cbe5b3a3e60b5970d8168474dd69a996c392c +const ATOMS = { + getVisibleText: "function(){return (function(){var k=this||self;function aa(a){return\"string\"==typeof a}function ba(a,b){a=a.split(\".\");var c=k;a[0]in c||\"undefined\"==typeof c.execScript||c.execScript(\"var \"+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b}\nfunction ca(a){var b=typeof a;if(\"object\"==b)if(a){if(a instanceof Array)return\"array\";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if(\"[object Window]\"==c)return\"object\";if(\"[object Array]\"==c||\"number\"==typeof a.length&&\"undefined\"!=typeof a.splice&&\"undefined\"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable(\"splice\"))return\"array\";if(\"[object Function]\"==c||\"undefined\"!=typeof a.call&&\"undefined\"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable(\"call\"))return\"function\"}else return\"null\";\nelse if(\"function\"==b&&\"undefined\"==typeof a.call)return\"object\";return b}function da(a,b,c){return a.call.apply(a.bind,arguments)}function ea(a,b,c){if(!a)throw Error();if(2b?null:\"string\"===typeof a?a.charAt(b):a[b]}function qa(a){return Array.prototype.concat.apply([],arguments)}function ra(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};function sa(a){var b=a.length-1;return 0<=b&&a.indexOf(\" \",b)==b}var ta=String.prototype.trim?function(a){return a.trim()}:function(a){return/^[\\s\\xa0]*([\\s\\S]*?)[\\s\\xa0]*$/.exec(a)[1]};function ua(a,b){return ab?1:0};var r;a:{var va=k.navigator;if(va){var wa=va.userAgent;if(wa){r=wa;break a}}r=\"\"}function u(a){return-1!=r.indexOf(a)};function xa(){return u(\"Firefox\")||u(\"FxiOS\")}function ya(){return(u(\"Chrome\")||u(\"CriOS\"))&&!u(\"Edge\")};function za(a){return String(a).replace(/\\-([a-z])/g,function(b,c){return c.toUpperCase()})};function Aa(){return u(\"iPhone\")&&!u(\"iPod\")&&!u(\"iPad\")};function Ba(a,b){var c=Ca;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var Da=u(\"Opera\"),w=u(\"Trident\")||u(\"MSIE\"),Ea=u(\"Edge\"),Fa=u(\"Gecko\")&&!(-1!=r.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\"))&&!(u(\"Trident\")||u(\"MSIE\"))&&!u(\"Edge\"),Ga=-1!=r.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\");function Ha(){var a=k.document;return a?a.documentMode:void 0}var Ia;\na:{var Ja=\"\",Ka=function(){var a=r;if(Fa)return/rv:([^\\);]+)(\\)|;)/.exec(a);if(Ea)return/Edge\\/([\\d\\.]+)/.exec(a);if(w)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(Ga)return/WebKit\\/(\\S+)/.exec(a);if(Da)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();Ka&&(Ja=Ka?Ka[1]:\"\");if(w){var La=Ha();if(null!=La&&La>parseFloat(Ja)){Ia=String(La);break a}}Ia=Ja}var Ca={};\nfunction Ma(a){return Ba(a,function(){for(var b=0,c=ta(String(Ia)).split(\".\"),d=ta(String(a)).split(\".\"),e=Math.max(c.length,d.length),f=0;0==b&&f]=|\\s+|./g,Ua=/^\\s/;function y(a,b){return a.b[a.a+(b||0)]}function z(a){return a.b[a.a++]}function Va(a){return a.b.length<=a.a};function Wa(a,b){this.x=void 0!==a?a:0;this.y=void 0!==b?b:0}Wa.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};Wa.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};Wa.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};function Xa(a,b){this.width=a;this.height=b}Xa.prototype.aspectRatio=function(){return this.width/this.height};Xa.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};Xa.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};Xa.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function Ya(a){for(;a&&1!=a.nodeType;)a=a.previousSibling;return a}function Za(a,b){if(!a||!b)return!1;if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if(\"undefined\"!=typeof a.compareDocumentPosition)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a}\nfunction $a(a,b){if(a==b)return 0;if(a.compareDocumentPosition)return a.compareDocumentPosition(b)&2?1:-1;if(w&&!(9<=Number(Na))){if(9==a.nodeType)return-1;if(9==b.nodeType)return 1}if(\"sourceIndex\"in a||a.parentNode&&\"sourceIndex\"in a.parentNode){var c=1==a.nodeType,d=1==b.nodeType;if(c&&d)return a.sourceIndex-b.sourceIndex;var e=a.parentNode,f=b.parentNode;return e==f?ab(a,b):!c&&Za(e,b)?-1*bb(a,b):!d&&Za(f,a)?bb(b,a):(c?a.sourceIndex:e.sourceIndex)-(d?b.sourceIndex:f.sourceIndex)}d=A(a);c=d.createRange();\nc.selectNode(a);c.collapse(!0);a=d.createRange();a.selectNode(b);a.collapse(!0);return c.compareBoundaryPoints(k.Range.START_TO_END,a)}function bb(a,b){var c=a.parentNode;if(c==b)return-1;for(;b.parentNode!=c;)b=b.parentNode;return ab(b,a)}function ab(a,b){for(;b=b.previousSibling;)if(b==a)return-1;return 1}function A(a){return 9==a.nodeType?a:a.ownerDocument||a.document}function cb(a,b){a&&(a=a.parentNode);for(var c=0;a;){if(b(a))return a;a=a.parentNode;c++}return null}\nfunction db(a){this.a=a||k.document||document}db.prototype.getElementsByTagName=function(a,b){return(b||this.a).getElementsByTagName(String(a))};function B(a){var b=null,c=a.nodeType;1==c&&(b=a.textContent,b=void 0==b||null==b?a.innerText:b,b=void 0==b||null==b?\"\":b);if(\"string\"!=typeof b)if(x&&\"title\"==a.nodeName.toLowerCase()&&1==c)b=a.text;else if(9==c||1==c){a=9==c?a.documentElement:a.firstChild;c=0;var d=[];for(b=\"\";a;){do 1!=a.nodeType&&(b+=a.nodeValue),x&&\"title\"==a.nodeName.toLowerCase()&&(b+=a.text),d[c++]=a;while(a=a.firstChild);for(;c&&!(a=d[--c].nextSibling););}}else b=a.nodeValue;return b}\nfunction C(a,b,c){if(null===b)return!0;try{if(!a.getAttribute)return!1}catch(d){return!1}Oa&&\"class\"==b&&(b=\"className\");return null==c?!!a.getAttribute(b):a.getAttribute(b,2)==c}function eb(a,b,c,d,e){return(x?fb:gb).call(null,a,b,aa(c)?c:null,aa(d)?d:null,e||new E)}\nfunction fb(a,b,c,d,e){if(a instanceof F||8==a.b||c&&null===a.b){var f=b.all;if(!f)return e;a=ib(a);if(\"*\"!=a&&(f=b.getElementsByTagName(a),!f))return e;if(c){for(var g=[],h=0;b=f[h++];)C(b,c,d)&&g.push(b);f=g}for(h=0;b=f[h++];)\"*\"==a&&\"!\"==b.tagName||e.add(b);return e}jb(a,b,c,d,e);return e}\nfunction gb(a,b,c,d,e){b.getElementsByName&&d&&\"name\"==c&&!w?(b=b.getElementsByName(d),p(b,function(f){a.a(f)&&e.add(f)})):b.getElementsByClassName&&d&&\"class\"==c?(b=b.getElementsByClassName(d),p(b,function(f){f.className==d&&a.a(f)&&e.add(f)})):a instanceof G?jb(a,b,c,d,e):b.getElementsByTagName&&(b=b.getElementsByTagName(a.f()),p(b,function(f){C(f,c,d)&&e.add(f)}));return e}\nfunction kb(a,b,c,d,e){var f;if((a instanceof F||8==a.b||c&&null===a.b)&&(f=b.childNodes)){var g=ib(a);if(\"*\"!=g&&(f=ka(f,function(h){return h.tagName&&h.tagName.toLowerCase()==g}),!f))return e;c&&(f=ka(f,function(h){return C(h,c,d)}));p(f,function(h){\"*\"==g&&(\"!\"==h.tagName||\"*\"==g&&1!=h.nodeType)||e.add(h)});return e}return lb(a,b,c,d,e)}function lb(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b);return e}\nfunction jb(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b),jb(a,b,c,d,e)}function ib(a){if(a instanceof G){if(8==a.b)return\"!\";if(null===a.b)return\"*\"}return a.f()};function E(){this.b=this.a=null;this.l=0}function mb(a){this.f=a;this.a=this.b=null}function nb(a,b){if(!a.a)return b;if(!b.a)return a;var c=a.a;b=b.a;for(var d=null,e,f=0;c&&b;){e=c.f;var g=b.f;e==g||e instanceof Pa&&g instanceof Pa&&e.a==g.a?(e=c,c=c.a,b=b.a):0<$a(c.f,b.f)?(e=b,b=b.a):(e=c,c=c.a);(e.b=d)?d.a=e:a.a=e;d=e;f++}for(e=c||b;e;)e.b=d,d=d.a=e,f++,e=e.a;a.b=d;a.l=f;return a}function ob(a,b){b=new mb(b);b.a=a.a;a.b?a.a.b=b:a.a=a.b=b;a.a=b;a.l++}\nE.prototype.add=function(a){a=new mb(a);a.b=this.b;this.a?this.b.a=a:this.a=this.b=a;this.b=a;this.l++};function pb(a){return(a=a.a)?a.f:null}function qb(a){return(a=pb(a))?B(a):\"\"}function H(a,b){return new rb(a,!!b)}function rb(a,b){this.f=a;this.b=(this.s=b)?a.b:a.a;this.a=null}function I(a){var b=a.b;if(null==b)return null;var c=a.a=b;a.b=a.s?b.b:b.a;return c.f};function J(a){this.i=a;this.b=this.g=!1;this.f=null}function K(a){return\"\\n \"+a.toString().split(\"\\n\").join(\"\\n \")}function sb(a,b){a.g=b}function tb(a,b){a.b=b}function L(a,b){a=a.a(b);return a instanceof E?+qb(a):+a}function O(a,b){a=a.a(b);return a instanceof E?qb(a):\"\"+a}function ub(a,b){a=a.a(b);return a instanceof E?!!a.l:!!a};function vb(a,b,c){J.call(this,a.i);this.c=a;this.h=b;this.o=c;this.g=b.g||c.g;this.b=b.b||c.b;this.c==wb&&(c.b||c.g||4==c.i||0==c.i||!b.f?b.b||b.g||4==b.i||0==b.i||!c.f||(this.f={name:c.f.name,u:b}):this.f={name:b.f.name,u:c})}m(vb,J);\nfunction xb(a,b,c,d,e){b=b.a(d);c=c.a(d);var f;if(b instanceof E&&c instanceof E){b=H(b);for(d=I(b);d;d=I(b))for(e=H(c),f=I(e);f;f=I(e))if(a(B(d),B(f)))return!0;return!1}if(b instanceof E||c instanceof E){b instanceof E?(e=b,d=c):(e=c,d=b);f=H(e);for(var g=typeof d,h=I(f);h;h=I(f)){switch(g){case \"number\":h=+B(h);break;case \"boolean\":h=!!B(h);break;case \"string\":h=B(h);break;default:throw Error(\"Illegal primitive type for comparison.\");}if(e==b&&a(h,d)||e==c&&a(d,h))return!0}return!1}return e?\"boolean\"==\ntypeof b||\"boolean\"==typeof c?a(!!b,!!c):\"number\"==typeof b||\"number\"==typeof c?a(+b,+c):a(b,c):a(+b,+c)}vb.prototype.a=function(a){return this.c.m(this.h,this.o,a)};vb.prototype.toString=function(){var a=\"Binary Expression: \"+this.c;a+=K(this.h);return a+=K(this.o)};function yb(a,b,c,d){this.I=a;this.D=b;this.i=c;this.m=d}yb.prototype.toString=function(){return this.I};var zb={};\nfunction P(a,b,c,d){if(zb.hasOwnProperty(a))throw Error(\"Binary operator already created: \"+a);a=new yb(a,b,c,d);return zb[a.toString()]=a}P(\"div\",6,1,function(a,b,c){return L(a,c)/L(b,c)});P(\"mod\",6,1,function(a,b,c){return L(a,c)%L(b,c)});P(\"*\",6,1,function(a,b,c){return L(a,c)*L(b,c)});P(\"+\",5,1,function(a,b,c){return L(a,c)+L(b,c)});P(\"-\",5,1,function(a,b,c){return L(a,c)-L(b,c)});P(\"<\",4,2,function(a,b,c){return xb(function(d,e){return d\",4,2,function(a,b,c){return xb(function(d,e){return d>e},a,b,c)});P(\"<=\",4,2,function(a,b,c){return xb(function(d,e){return d<=e},a,b,c)});P(\">=\",4,2,function(a,b,c){return xb(function(d,e){return d>=e},a,b,c)});var wb=P(\"=\",3,2,function(a,b,c){return xb(function(d,e){return d==e},a,b,c,!0)});P(\"!=\",3,2,function(a,b,c){return xb(function(d,e){return d!=e},a,b,c,!0)});P(\"and\",2,2,function(a,b,c){return ub(a,c)&&ub(b,c)});P(\"or\",1,2,function(a,b,c){return ub(a,c)||ub(b,c)});function Ab(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");J.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}m(Ab,J);Ab.prototype.a=function(a){a=this.c.a(a);return Bb(this.h,a)};Ab.prototype.toString=function(){var a=\"Filter:\"+K(this.c);return a+=K(this.h)};function Cb(a,b){if(b.lengtha.B)throw Error(\"Function \"+a.j+\" expects at most \"+a.B+\" arguments, \"+b.length+\" given\");a.H&&p(b,function(c,d){if(4!=c.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+c);});J.call(this,a.i);this.v=a;this.c=b;sb(this,a.g||na(b,function(c){return c.g}));tb(this,a.G&&!b.length||a.F&&!!b.length||na(b,function(c){return c.b}))}\nm(Cb,J);Cb.prototype.a=function(a){return this.v.m.apply(null,qa(a,this.c))};Cb.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length){var b=ma(this.c,function(c,d){return c+K(d)},\"Arguments:\");a+=K(b)}return a};function Db(a,b,c,d,e,f,g,h){this.j=a;this.i=b;this.g=c;this.G=d;this.F=!1;this.m=e;this.C=f;this.B=void 0!==g?g:f;this.H=!!h}Db.prototype.toString=function(){return this.j};var Eb={};\nfunction Q(a,b,c,d,e,f,g,h){if(Eb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");Eb[a]=new Db(a,b,c,d,e,f,g,h)}Q(\"boolean\",2,!1,!1,function(a,b){return ub(b,a)},1);Q(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(L(b,a))},1);Q(\"concat\",3,!1,!1,function(a,b){return ma(ra(arguments,1),function(c,d){return c+O(d,a)},\"\")},2,null);Q(\"contains\",2,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);return-1!=b.indexOf(a)},2);Q(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nQ(\"false\",2,!1,!1,function(){return!1},0);Q(\"floor\",1,!1,!1,function(a,b){return Math.floor(L(b,a))},1);Q(\"id\",4,!1,!1,function(a,b){function c(h){if(x){var l=e.all[h];if(l){if(l.nodeType&&h==l.id)return l;if(l.length)return pa(l,function(v){return h==v.id})}return null}return e.getElementById(h)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=O(b,a).split(/\\s+/);var f=[];p(a,function(h){h=c(h);!h||0<=ja(f,h)||f.push(h)});f.sort($a);var g=new E;p(f,function(h){g.add(h)});return g},1);\nQ(\"lang\",2,!1,!1,function(){return!1},1);Q(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);Q(\"local-name\",3,!1,!0,function(a,b){return(a=b?pb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);Q(\"name\",3,!1,!0,function(a,b){return(a=b?pb(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);Q(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nQ(\"normalize-space\",3,!1,!0,function(a,b){return(b?O(b,a):B(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);Q(\"not\",2,!1,!1,function(a,b){return!ub(b,a)},1);Q(\"number\",1,!1,!0,function(a,b){return b?L(b,a):+B(a.a)},0,1);Q(\"position\",1,!0,!1,function(a){return a.b},0);Q(\"round\",1,!1,!1,function(a,b){return Math.round(L(b,a))},1);Q(\"starts-with\",2,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);return 0==b.lastIndexOf(a,0)},2);Q(\"string\",3,!1,!0,function(a,b){return b?O(b,a):B(a.a)},0,1);\nQ(\"string-length\",1,!1,!0,function(a,b){return(b?O(b,a):B(a.a)).length},0,1);Q(\"substring\",3,!1,!1,function(a,b,c,d){c=L(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?L(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=O(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);Q(\"substring-after\",3,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nQ(\"substring-before\",3,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);Q(\"sum\",1,!1,!1,function(a,b){a=H(b.a(a));b=0;for(var c=I(a);c;c=I(a))b+=+B(c);return b},1,1,!0);Q(\"translate\",3,!1,!1,function(a,b,c,d){b=O(b,a);c=O(c,a);var e=O(d,a);a={};for(d=0;da.length)throw Error(\"Unclosed literal string\");return new Gb(a)}\nfunction fc(a){var b=[];if(Mb(y(a.a))){var c=z(a.a);var d=y(a.a);if(\"/\"==c&&(Va(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new Kb;d=new Kb;T(a,\"Missing next location step.\");c=gc(a,c);b.push(c)}else{a:{c=y(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":z(a.a);c=Zb(a);T(a,'unclosed \"(\"');cc(a,\")\");break;case '\"':case \"'\":c=ec(a);break;default:if(isNaN(+c))if(!Fb(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==y(a.a,1)){c=z(a.a);\nc=Eb[c]||null;z(a.a);for(d=[];\")\"!=y(a.a);){T(a,\"Missing function argument list.\");d.push(Zb(a));if(\",\"!=y(a.a))break;z(a.a)}T(a,\"Unclosed function argument list.\");dc(a);c=new Cb(c,d)}else{c=null;break a}else c=new Hb(+z(a.a))}\"[\"==y(a.a)&&(d=new Pb(hc(a)),c=new Ab(c,d))}if(c)if(Mb(y(a.a)))d=c;else return c;else c=gc(a,\"/\"),d=new Lb,b.push(c)}for(;Mb(y(a.a));)c=z(a.a),T(a,\"Missing next location step.\"),c=gc(a,c),b.push(c);return new Ib(d,b)}\nfunction gc(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==y(a.a)){var c=new R(Vb,new G(\"node\"));z(a.a);return c}if(\"..\"==y(a.a))return c=new R(Ub,new G(\"node\")),z(a.a),c;if(\"@\"==y(a.a)){var d=Jb;z(a.a);T(a,\"Missing attribute name\")}else if(\"::\"==y(a.a,1)){if(!/(?![0-9])[\\w]/.test(y(a.a).charAt(0)))throw Error(\"Bad token: \"+z(a.a));var e=z(a.a);d=Tb[e]||null;if(!d)throw Error(\"No axis with name: \"+e);z(a.a);T(a,\"Missing node name\")}else d=Qb;e=y(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\ny(a.a,1)){if(!Fb(e))throw Error(\"Invalid node type: \"+e);e=z(a.a);if(!Fb(e))throw Error(\"Invalid type name: \"+e);cc(a,\"(\");T(a,\"Bad nodetype\");var f=y(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=ec(a);T(a,\"Bad nodetype\");dc(a);e=new G(e,g)}else if(e=z(a.a),f=e.indexOf(\":\"),-1==f)e=new F(e);else{g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new F(e,h)}else throw Error(\"Bad token: \"+z(a.a));a=new Pb(hc(a),d.s);return c||new R(d,\ne,a,\"//\"==b)}function hc(a){for(var b=[];\"[\"==y(a.a);){z(a.a);T(a,\"Missing predicate expression.\");var c=Zb(a);b.push(c);T(a,\"Unclosed predicate expression.\");cc(a,\"]\")}return b}function bc(a){if(\"-\"==y(a.a))return z(a.a),new Wb(bc(a));var b=fc(a);if(\"|\"!=y(a.a))a=b;else{for(b=[b];\"|\"==z(a.a);)T(a,\"Missing next union location path.\"),b.push(fc(a));a.a.a--;a=new Xb(b)}return a};function ic(a){switch(a.nodeType){case 1:return ha(jc,a);case 9:return ic(a.documentElement);case 11:case 10:case 6:case 12:return kc;default:return a.parentNode?ic(a.parentNode):kc}}function kc(){return null}function jc(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?jc(a.parentNode,b):null};function lc(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=Sa(a);if(Va(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ca(b)||(b=fa(b.lookupNamespaceURI,b)):b=function(){return null};var c=Zb(new Yb(a,b));if(!Va(a))throw Error(\"Bad token: \"+z(a));this.evaluate=function(d,e){d=c.a(new ia(d));return new U(d,e)}}\nfunction U(a,b){if(0==b)if(a instanceof E)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof E))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof E?qb(a):\"\"+a;break;case 1:this.numberValue=a instanceof E?+qb(a):+a;break;case 3:this.booleanValue=a instanceof E?0=d.length?null:d[f++]};this.snapshotItem=function(g){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return g>=d.length||\n0>g?null:d[g]}}U.ANY_TYPE=0;U.NUMBER_TYPE=1;U.STRING_TYPE=2;U.BOOLEAN_TYPE=3;U.UNORDERED_NODE_ITERATOR_TYPE=4;U.ORDERED_NODE_ITERATOR_TYPE=5;U.UNORDERED_NODE_SNAPSHOT_TYPE=6;U.ORDERED_NODE_SNAPSHOT_TYPE=7;U.ANY_UNORDERED_NODE_TYPE=8;U.FIRST_ORDERED_NODE_TYPE=9;function mc(a){this.lookupNamespaceURI=ic(a)}\nfunction nc(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=U,c.evaluate=function(d,e,f,g){return(new lc(d,f)).evaluate(e,g)},c.createExpression=function(d,e){return new lc(d,e)},c.createNSResolver=function(d){return new mc(d)}}ba(\"wgxpath.install\",nc);ba(\"wgxpath.install\",nc);var oc={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",\ndarkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",\nghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",\nlightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",\nmoccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",\nseashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"};var pc=\"backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor\".split(\" \"),qc=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,rc=/^#(?:[0-9a-f]{3}){1,2}$/i,sc=/^(?:rgba)?\\((\\d{1,3}),\\s?(\\d{1,3}),\\s?(\\d{1,3}),\\s?(0|1|0\\.\\d*)\\)$/i,tc=/^(?:rgb)?\\((0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2})\\)$/i;function uc(a,b){this.code=a;this.a=V[a]||vc;this.message=b||\"\";a=this.a.replace(/((?:^|\\s+)[a-z])/g,function(c){return c.toUpperCase().replace(/^[\\s\\xa0]+/g,\"\")});b=a.length-5;if(0>b||a.indexOf(\"Error\",b)!=b)a+=\"Error\";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||\"\"}m(uc,Error);var vc=\"unknown error\",V={15:\"element not selectable\",11:\"element not visible\"};V[31]=vc;V[30]=vc;V[24]=\"invalid cookie domain\";V[29]=\"invalid element coordinates\";V[12]=\"invalid element state\";\nV[32]=\"invalid selector\";V[51]=\"invalid selector\";V[52]=\"invalid selector\";V[17]=\"javascript error\";V[405]=\"unsupported operation\";V[34]=\"move target out of bounds\";V[27]=\"no such alert\";V[7]=\"no such element\";V[8]=\"no such frame\";V[23]=\"no such window\";V[28]=\"script timeout\";V[33]=\"session not created\";V[10]=\"stale element reference\";V[21]=\"timeout\";V[25]=\"unable to set cookie\";V[26]=\"unexpected alert open\";V[13]=vc;V[9]=\"unknown command\";var wc=xa(),xc=Aa()||u(\"iPod\"),yc=u(\"iPad\"),zc=u(\"Android\")&&!(ya()||xa()||u(\"Opera\")||u(\"Silk\")),Ac=ya(),Bc=u(\"Safari\")&&!(ya()||u(\"Coast\")||u(\"Opera\")||u(\"Edge\")||u(\"Edg/\")||u(\"OPR\")||xa()||u(\"Silk\")||u(\"Android\"))&&!(Aa()||u(\"iPad\")||u(\"iPod\"));function Cc(a){return(a=a.exec(r))?a[1]:\"\"}(function(){if(wc)return Cc(/Firefox\\/([0-9.]+)/);if(w||Ea||Da)return Ia;if(Ac)return Aa()||u(\"iPad\")||u(\"iPod\")?Cc(/CriOS\\/([0-9.]+)/):Cc(/Chrome\\/([0-9.]+)/);if(Bc&&!(Aa()||u(\"iPad\")||u(\"iPod\")))return Cc(/Version\\/([0-9.]+)/);if(xc||yc){var a=/Version\\/(\\S+).*Mobile\\/(\\S+)/.exec(r);if(a)return a[1]+\".\"+a[2]}else if(zc)return(a=Cc(/Android\\s+([0-9.]+)/))?a:Cc(/Version\\/([0-9.]+)/);return\"\"})();var Dc=w&&!(9<=Number(Na));function W(a,b){b&&\"string\"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)};var Ec=function(){var a={K:\"http://www.w3.org/2000/svg\"};return function(b){return a[b]||null}}();\nfunction Fc(a,b){var c=A(a);if(!c.documentElement)return null;(w||zc)&&nc(c?c.parentWindow||c.defaultView:window);try{var d=c.createNSResolver?c.createNSResolver(c.documentElement):Ec;if(w&&!Ma(7))return c.evaluate.call(c,b,a,d,9,null);if(!w||9<=Number(Na)){for(var e={},f=c.getElementsByTagName(\"*\"),g=0;g=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(tc))if(b=\nNumber(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=oc[b.toLowerCase()];if(!c&&(c=\"#\"==b.charAt(0)?b:\"#\"+b,4==c.length&&(c=c.replace(qc,\"#$1$1$2$2$3$3\")),!rc.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?\"rgba(\"+b.join(\", \")+\")\":a}return a}\nfunction Kc(a,b){var c=a.currentStyle||a.style,d=c[b];void 0===d&&\"function\"==ca(c.getPropertyValue)&&(d=c.getPropertyValue(b));return\"inherit\"!=d?void 0!==d?d:null:(a=Jc(a))?Kc(a,b):null}\nfunction Lc(a,b,c){function d(g){var h=Mc(g);return 0=D.a+D.width;D=e.c>=D.b+D.height;if(M&&\"hidden\"==n.x||D&&\"hidden\"==n.y)return Z;if(M&&\"visible\"!=n.x||D&&\"visible\"!=n.y){if(v&&(n=d(a),e.f>=g.scrollWidth-n.x||e.a>=g.scrollHeight-n.y))return Z;e=Nc(a);return e==Z?Z:\"scroll\"}}}return\"none\"}\nfunction Mc(a){var b=Oc(a);if(b)return b.rect;if(W(a,\"HTML\"))return a=A(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a=\"CSS1Compat\"==a.compatMode?a.documentElement:a.body,a=new Xa(a.clientWidth,a.clientHeight),new X(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new X(0,0,0,0)}b=new X(c.left,c.top,c.right-c.left,c.bottom-c.top);w&&a.ownerDocument.body&&(a=A(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);\nreturn b}function Oc(a){var b=W(a,\"MAP\");if(!b&&!W(a,\"AREA\"))return null;var c=b?a:W(a.parentNode,\"MAP\")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Gc('/descendant::*[@usemap = \"#'+c.name+'\"]',A(c)))&&(e=Mc(d),b||\"default\"==a.shape.toLowerCase()||(a=Sc(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new X(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{image:d,rect:e||new X(0,0,0,0)}}\nfunction Sc(a){var b=a.shape.toLowerCase();a=a.coords.split(\",\");if(\"rect\"==b&&4==a.length){b=a[0];var c=a[1];return new X(b,c,a[2]-b,a[3]-c)}if(\"circle\"==b&&3==a.length)return b=a[2],new X(a[0]-b,a[1]-b,2*b,2*b);if(\"poly\"==b&&2b?null:\"string\"===typeof a?a.charAt(b):a[b]}\nfunction pa(a){return Array.prototype.concat.apply([],arguments)}function qa(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};var ra=String.prototype.trim?function(a){return a.trim()}:function(a){return/^[\\s\\xa0]*([\\s\\S]*?)[\\s\\xa0]*$/.exec(a)[1]};function sa(a,b){return ab?1:0};var t;a:{var ta=k.navigator;if(ta){var ua=ta.userAgent;if(ua){t=ua;break a}}t=\"\"}function u(a){return-1!=t.indexOf(a)};function va(){return u(\"Firefox\")||u(\"FxiOS\")}function wa(){return(u(\"Chrome\")||u(\"CriOS\"))&&!u(\"Edge\")};function xa(a){return String(a).replace(/\\-([a-z])/g,function(b,c){return c.toUpperCase()})};function ya(){return u(\"iPhone\")&&!u(\"iPod\")&&!u(\"iPad\")};function za(a,b){var c=Aa;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var Ba=u(\"Opera\"),v=u(\"Trident\")||u(\"MSIE\"),Ca=u(\"Edge\"),Da=u(\"Gecko\")&&!(-1!=t.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\"))&&!(u(\"Trident\")||u(\"MSIE\"))&&!u(\"Edge\"),Ea=-1!=t.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\");function Fa(){var a=k.document;return a?a.documentMode:void 0}var Ga;\na:{var Ha=\"\",Ia=function(){var a=t;if(Da)return/rv:([^\\);]+)(\\)|;)/.exec(a);if(Ca)return/Edge\\/([\\d\\.]+)/.exec(a);if(v)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(Ea)return/WebKit\\/(\\S+)/.exec(a);if(Ba)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();Ia&&(Ha=Ia?Ia[1]:\"\");if(v){var Ja=Fa();if(null!=Ja&&Ja>parseFloat(Ha)){Ga=String(Ja);break a}}Ga=Ha}var Aa={};\nfunction Ka(a){return za(a,function(){for(var b=0,c=ra(String(Ga)).split(\".\"),d=ra(String(a)).split(\".\"),e=Math.max(c.length,d.length),f=0;0==b&&f]=|\\s+|./g,Sa=/^\\s/;function y(a,b){return a.b[a.a+(b||0)]}function z(a){return a.b[a.a++]}function Ta(a){return a.b.length<=a.a};function Ua(a,b){this.x=void 0!==a?a:0;this.y=void 0!==b?b:0}Ua.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};Ua.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};Ua.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};function Va(a,b){this.width=a;this.height=b}Va.prototype.aspectRatio=function(){return this.width/this.height};Va.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};Va.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};Va.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function Wa(a,b){if(!a||!b)return!1;if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if(\"undefined\"!=typeof a.compareDocumentPosition)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a}\nfunction Xa(a,b){if(a==b)return 0;if(a.compareDocumentPosition)return a.compareDocumentPosition(b)&2?1:-1;if(v&&!(9<=Number(La))){if(9==a.nodeType)return-1;if(9==b.nodeType)return 1}if(\"sourceIndex\"in a||a.parentNode&&\"sourceIndex\"in a.parentNode){var c=1==a.nodeType,d=1==b.nodeType;if(c&&d)return a.sourceIndex-b.sourceIndex;var e=a.parentNode,f=b.parentNode;return e==f?Ya(a,b):!c&&Wa(e,b)?-1*Za(a,b):!d&&Wa(f,a)?Za(b,a):(c?a.sourceIndex:e.sourceIndex)-(d?b.sourceIndex:f.sourceIndex)}d=A(a);c=d.createRange();\nc.selectNode(a);c.collapse(!0);a=d.createRange();a.selectNode(b);a.collapse(!0);return c.compareBoundaryPoints(k.Range.START_TO_END,a)}function Za(a,b){var c=a.parentNode;if(c==b)return-1;for(;b.parentNode!=c;)b=b.parentNode;return Ya(b,a)}function Ya(a,b){for(;b=b.previousSibling;)if(b==a)return-1;return 1}function A(a){return 9==a.nodeType?a:a.ownerDocument||a.document}function $a(a,b){a&&(a=a.parentNode);for(var c=0;a;){if(b(a))return a;a=a.parentNode;c++}return null}\nfunction ab(a){this.a=a||k.document||document}ab.prototype.getElementsByTagName=function(a,b){return(b||this.a).getElementsByTagName(String(a))};function B(a){var b=null,c=a.nodeType;1==c&&(b=a.textContent,b=void 0==b||null==b?a.innerText:b,b=void 0==b||null==b?\"\":b);if(\"string\"!=typeof b)if(x&&\"title\"==a.nodeName.toLowerCase()&&1==c)b=a.text;else if(9==c||1==c){a=9==c?a.documentElement:a.firstChild;c=0;var d=[];for(b=\"\";a;){do 1!=a.nodeType&&(b+=a.nodeValue),x&&\"title\"==a.nodeName.toLowerCase()&&(b+=a.text),d[c++]=a;while(a=a.firstChild);for(;c&&!(a=d[--c].nextSibling););}}else b=a.nodeValue;return b}\nfunction C(a,b,c){if(null===b)return!0;try{if(!a.getAttribute)return!1}catch(d){return!1}Ma&&\"class\"==b&&(b=\"className\");return null==c?!!a.getAttribute(b):a.getAttribute(b,2)==c}function bb(a,b,c,d,e){return(x?cb:db).call(null,a,b,aa(c)?c:null,aa(d)?d:null,e||new E)}\nfunction cb(a,b,c,d,e){if(a instanceof F||8==a.b||c&&null===a.b){var f=b.all;if(!f)return e;a=eb(a);if(\"*\"!=a&&(f=b.getElementsByTagName(a),!f))return e;if(c){for(var g=[],h=0;b=f[h++];)C(b,c,d)&&g.push(b);f=g}for(h=0;b=f[h++];)\"*\"==a&&\"!\"==b.tagName||e.add(b);return e}gb(a,b,c,d,e);return e}\nfunction db(a,b,c,d,e){b.getElementsByName&&d&&\"name\"==c&&!v?(b=b.getElementsByName(d),n(b,function(f){a.a(f)&&e.add(f)})):b.getElementsByClassName&&d&&\"class\"==c?(b=b.getElementsByClassName(d),n(b,function(f){f.className==d&&a.a(f)&&e.add(f)})):a instanceof G?gb(a,b,c,d,e):b.getElementsByTagName&&(b=b.getElementsByTagName(a.f()),n(b,function(f){C(f,c,d)&&e.add(f)}));return e}\nfunction hb(a,b,c,d,e){var f;if((a instanceof F||8==a.b||c&&null===a.b)&&(f=b.childNodes)){var g=eb(a);if(\"*\"!=g&&(f=ka(f,function(h){return h.tagName&&h.tagName.toLowerCase()==g}),!f))return e;c&&(f=ka(f,function(h){return C(h,c,d)}));n(f,function(h){\"*\"==g&&(\"!\"==h.tagName||\"*\"==g&&1!=h.nodeType)||e.add(h)});return e}return ib(a,b,c,d,e)}function ib(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b);return e}\nfunction gb(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b),gb(a,b,c,d,e)}function eb(a){if(a instanceof G){if(8==a.b)return\"!\";if(null===a.b)return\"*\"}return a.f()};function E(){this.b=this.a=null;this.l=0}function jb(a){this.f=a;this.a=this.b=null}function kb(a,b){if(!a.a)return b;if(!b.a)return a;var c=a.a;b=b.a;for(var d=null,e,f=0;c&&b;){e=c.f;var g=b.f;e==g||e instanceof Na&&g instanceof Na&&e.a==g.a?(e=c,c=c.a,b=b.a):0\",4,2,function(a,b,c){return ub(function(d,e){return d>e},a,b,c)});P(\"<=\",4,2,function(a,b,c){return ub(function(d,e){return d<=e},a,b,c)});P(\">=\",4,2,function(a,b,c){return ub(function(d,e){return d>=e},a,b,c)});var tb=P(\"=\",3,2,function(a,b,c){return ub(function(d,e){return d==e},a,b,c,!0)});P(\"!=\",3,2,function(a,b,c){return ub(function(d,e){return d!=e},a,b,c,!0)});P(\"and\",2,2,function(a,b,c){return rb(a,c)&&rb(b,c)});P(\"or\",1,2,function(a,b,c){return rb(a,c)||rb(b,c)});function xb(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");J.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}l(xb,J);xb.prototype.a=function(a){a=this.c.a(a);return yb(this.h,a)};xb.prototype.toString=function(){var a=\"Filter:\"+K(this.c);return a+=K(this.h)};function zb(a,b){if(b.lengtha.B)throw Error(\"Function \"+a.j+\" expects at most \"+a.B+\" arguments, \"+b.length+\" given\");a.H&&n(b,function(c,d){if(4!=c.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+c);});J.call(this,a.i);this.v=a;this.c=b;pb(this,a.g||ma(b,function(c){return c.g}));qb(this,a.G&&!b.length||a.F&&!!b.length||ma(b,function(c){return c.b}))}\nl(zb,J);zb.prototype.a=function(a){return this.v.m.apply(null,pa(a,this.c))};zb.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length){var b=la(this.c,function(c,d){return c+K(d)},\"Arguments:\");a+=K(b)}return a};function Ab(a,b,c,d,e,f,g,h){this.j=a;this.i=b;this.g=c;this.G=d;this.F=!1;this.m=e;this.C=f;this.B=void 0!==g?g:f;this.H=!!h}Ab.prototype.toString=function(){return this.j};var Bb={};\nfunction Q(a,b,c,d,e,f,g,h){if(Bb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");Bb[a]=new Ab(a,b,c,d,e,f,g,h)}Q(\"boolean\",2,!1,!1,function(a,b){return rb(b,a)},1);Q(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(N(b,a))},1);Q(\"concat\",3,!1,!1,function(a,b){return la(qa(arguments,1),function(c,d){return c+O(d,a)},\"\")},2,null);Q(\"contains\",2,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);return-1!=b.indexOf(a)},2);Q(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nQ(\"false\",2,!1,!1,function(){return!1},0);Q(\"floor\",1,!1,!1,function(a,b){return Math.floor(N(b,a))},1);Q(\"id\",4,!1,!1,function(a,b){function c(h){if(x){var m=e.all[h];if(m){if(m.nodeType&&h==m.id)return m;if(m.length)return oa(m,function(w){return h==w.id})}return null}return e.getElementById(h)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=O(b,a).split(/\\s+/);var f=[];n(a,function(h){h=c(h);!h||0<=ja(f,h)||f.push(h)});f.sort(Xa);var g=new E;n(f,function(h){g.add(h)});return g},1);\nQ(\"lang\",2,!1,!1,function(){return!1},1);Q(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);Q(\"local-name\",3,!1,!0,function(a,b){return(a=b?mb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);Q(\"name\",3,!1,!0,function(a,b){return(a=b?mb(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);Q(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nQ(\"normalize-space\",3,!1,!0,function(a,b){return(b?O(b,a):B(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);Q(\"not\",2,!1,!1,function(a,b){return!rb(b,a)},1);Q(\"number\",1,!1,!0,function(a,b){return b?N(b,a):+B(a.a)},0,1);Q(\"position\",1,!0,!1,function(a){return a.b},0);Q(\"round\",1,!1,!1,function(a,b){return Math.round(N(b,a))},1);Q(\"starts-with\",2,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);return 0==b.lastIndexOf(a,0)},2);Q(\"string\",3,!1,!0,function(a,b){return b?O(b,a):B(a.a)},0,1);\nQ(\"string-length\",1,!1,!0,function(a,b){return(b?O(b,a):B(a.a)).length},0,1);Q(\"substring\",3,!1,!1,function(a,b,c,d){c=N(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?N(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=O(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);Q(\"substring-after\",3,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nQ(\"substring-before\",3,!1,!1,function(a,b,c){b=O(b,a);a=O(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);Q(\"sum\",1,!1,!1,function(a,b){a=H(b.a(a));b=0;for(var c=I(a);c;c=I(a))b+=+B(c);return b},1,1,!0);Q(\"translate\",3,!1,!1,function(a,b,c,d){b=O(b,a);c=O(c,a);var e=O(d,a);a={};for(d=0;da.length)throw Error(\"Unclosed literal string\");return new Db(a)}\nfunction cc(a){var b=[];if(Jb(y(a.a))){var c=z(a.a);var d=y(a.a);if(\"/\"==c&&(Ta(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new Hb;d=new Hb;T(a,\"Missing next location step.\");c=dc(a,c);b.push(c)}else{a:{c=y(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":z(a.a);c=Yb(a);T(a,'unclosed \"(\"');$b(a,\")\");break;case '\"':case \"'\":c=bc(a);break;default:if(isNaN(+c))if(!Cb(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==y(a.a,1)){c=z(a.a);\nc=Bb[c]||null;z(a.a);for(d=[];\")\"!=y(a.a);){T(a,\"Missing function argument list.\");d.push(Yb(a));if(\",\"!=y(a.a))break;z(a.a)}T(a,\"Unclosed function argument list.\");ac(a);c=new zb(c,d)}else{c=null;break a}else c=new Eb(+z(a.a))}\"[\"==y(a.a)&&(d=new Mb(ec(a)),c=new xb(c,d))}if(c)if(Jb(y(a.a)))d=c;else return c;else c=dc(a,\"/\"),d=new Ib,b.push(c)}for(;Jb(y(a.a));)c=z(a.a),T(a,\"Missing next location step.\"),c=dc(a,c),b.push(c);return new Fb(d,b)}\nfunction dc(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==y(a.a)){var c=new R(Sb,new G(\"node\"));z(a.a);return c}if(\"..\"==y(a.a))return c=new R(Rb,new G(\"node\")),z(a.a),c;if(\"@\"==y(a.a)){var d=Gb;z(a.a);T(a,\"Missing attribute name\")}else if(\"::\"==y(a.a,1)){if(!/(?![0-9])[\\w]/.test(y(a.a).charAt(0)))throw Error(\"Bad token: \"+z(a.a));var e=z(a.a);d=Qb[e]||null;if(!d)throw Error(\"No axis with name: \"+e);z(a.a);T(a,\"Missing node name\")}else d=Nb;e=y(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\ny(a.a,1)){if(!Cb(e))throw Error(\"Invalid node type: \"+e);e=z(a.a);if(!Cb(e))throw Error(\"Invalid type name: \"+e);$b(a,\"(\");T(a,\"Bad nodetype\");var f=y(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=bc(a);T(a,\"Bad nodetype\");ac(a);e=new G(e,g)}else if(e=z(a.a),f=e.indexOf(\":\"),-1==f)e=new F(e);else{g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new F(e,h)}else throw Error(\"Bad token: \"+z(a.a));a=new Mb(ec(a),d.s);return c||new R(d,\ne,a,\"//\"==b)}function ec(a){for(var b=[];\"[\"==y(a.a);){z(a.a);T(a,\"Missing predicate expression.\");var c=Yb(a);b.push(c);T(a,\"Unclosed predicate expression.\");$b(a,\"]\")}return b}function Zb(a){if(\"-\"==y(a.a))return z(a.a),new Tb(Zb(a));var b=cc(a);if(\"|\"!=y(a.a))a=b;else{for(b=[b];\"|\"==z(a.a);)T(a,\"Missing next union location path.\"),b.push(cc(a));a.a.a--;a=new Ub(b)}return a};function fc(a){switch(a.nodeType){case 1:return ha(gc,a);case 9:return fc(a.documentElement);case 11:case 10:case 6:case 12:return hc;default:return a.parentNode?fc(a.parentNode):hc}}function hc(){return null}function gc(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?gc(a.parentNode,b):null};function ic(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=Qa(a);if(Ta(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ca(b)||(b=fa(b.lookupNamespaceURI,b)):b=function(){return null};var c=Yb(new Vb(a,b));if(!Ta(a))throw Error(\"Bad token: \"+z(a));this.evaluate=function(d,e){d=c.a(new ia(d));return new U(d,e)}}\nfunction U(a,b){if(0==b)if(a instanceof E)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof E))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof E?nb(a):\"\"+a;break;case 1:this.numberValue=a instanceof E?+nb(a):+a;break;case 3:this.booleanValue=a instanceof E?0=d.length?null:d[f++]};this.snapshotItem=function(g){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return g>=d.length||\n0>g?null:d[g]}}U.ANY_TYPE=0;U.NUMBER_TYPE=1;U.STRING_TYPE=2;U.BOOLEAN_TYPE=3;U.UNORDERED_NODE_ITERATOR_TYPE=4;U.ORDERED_NODE_ITERATOR_TYPE=5;U.UNORDERED_NODE_SNAPSHOT_TYPE=6;U.ORDERED_NODE_SNAPSHOT_TYPE=7;U.ANY_UNORDERED_NODE_TYPE=8;U.FIRST_ORDERED_NODE_TYPE=9;function jc(a){this.lookupNamespaceURI=fc(a)}\nfunction kc(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=U,c.evaluate=function(d,e,f,g){return(new ic(d,f)).evaluate(e,g)},c.createExpression=function(d,e){return new ic(d,e)},c.createNSResolver=function(d){return new jc(d)}}ba(\"wgxpath.install\",kc);ba(\"wgxpath.install\",kc);var lc={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",\ndarkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",\nghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",\nlightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",\nmoccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",\nseashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"};var mc=\"backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor\".split(\" \"),nc=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,oc=/^#(?:[0-9a-f]{3}){1,2}$/i,pc=/^(?:rgba)?\\((\\d{1,3}),\\s?(\\d{1,3}),\\s?(\\d{1,3}),\\s?(0|1|0\\.\\d*)\\)$/i,qc=/^(?:rgb)?\\((0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2})\\)$/i;function rc(a,b){this.code=a;this.a=V[a]||sc;this.message=b||\"\";a=this.a.replace(/((?:^|\\s+)[a-z])/g,function(c){return c.toUpperCase().replace(/^[\\s\\xa0]+/g,\"\")});b=a.length-5;if(0>b||a.indexOf(\"Error\",b)!=b)a+=\"Error\";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||\"\"}l(rc,Error);var sc=\"unknown error\",V={15:\"element not selectable\",11:\"element not visible\"};V[31]=sc;V[30]=sc;V[24]=\"invalid cookie domain\";V[29]=\"invalid element coordinates\";V[12]=\"invalid element state\";\nV[32]=\"invalid selector\";V[51]=\"invalid selector\";V[52]=\"invalid selector\";V[17]=\"javascript error\";V[405]=\"unsupported operation\";V[34]=\"move target out of bounds\";V[27]=\"no such alert\";V[7]=\"no such element\";V[8]=\"no such frame\";V[23]=\"no such window\";V[28]=\"script timeout\";V[33]=\"session not created\";V[10]=\"stale element reference\";V[21]=\"timeout\";V[25]=\"unable to set cookie\";V[26]=\"unexpected alert open\";V[13]=sc;V[9]=\"unknown command\";var tc=va(),uc=ya()||u(\"iPod\"),vc=u(\"iPad\"),wc=u(\"Android\")&&!(wa()||va()||u(\"Opera\")||u(\"Silk\")),xc=wa(),yc=u(\"Safari\")&&!(wa()||u(\"Coast\")||u(\"Opera\")||u(\"Edge\")||u(\"Edg/\")||u(\"OPR\")||va()||u(\"Silk\")||u(\"Android\"))&&!(ya()||u(\"iPad\")||u(\"iPod\"));function zc(a){return(a=a.exec(t))?a[1]:\"\"}(function(){if(tc)return zc(/Firefox\\/([0-9.]+)/);if(v||Ca||Ba)return Ga;if(xc)return ya()||u(\"iPad\")||u(\"iPod\")?zc(/CriOS\\/([0-9.]+)/):zc(/Chrome\\/([0-9.]+)/);if(yc&&!(ya()||u(\"iPad\")||u(\"iPod\")))return zc(/Version\\/([0-9.]+)/);if(uc||vc){var a=/Version\\/(\\S+).*Mobile\\/(\\S+)/.exec(t);if(a)return a[1]+\".\"+a[2]}else if(wc)return(a=zc(/Android\\s+([0-9.]+)/))?a:zc(/Version\\/([0-9.]+)/);return\"\"})();var Ac=v&&!(9<=Number(La));function W(a,b){b&&\"string\"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)};var Bc=function(){var a={K:\"http://www.w3.org/2000/svg\"};return function(b){return a[b]||null}}();\nfunction Cc(a,b){var c=A(a);if(!c.documentElement)return null;(v||wc)&&kc(c?c.parentWindow||c.defaultView:window);try{var d=c.createNSResolver?c.createNSResolver(c.documentElement):Bc;if(v&&!Ka(7))return c.evaluate.call(c,b,a,d,9,null);if(!v||9<=Number(La)){for(var e={},f=c.getElementsByTagName(\"*\"),g=0;g=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(qc))if(b=\nNumber(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=lc[b.toLowerCase()];if(!c&&(c=\"#\"==b.charAt(0)?b:\"#\"+b,4==c.length&&(c=c.replace(nc,\"#$1$1$2$2$3$3\")),!oc.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?\"rgba(\"+b.join(\", \")+\")\":a}return a}\nfunction Hc(a,b){var c=a.currentStyle||a.style,d=c[b];void 0===d&&\"function\"==ca(c.getPropertyValue)&&(d=c.getPropertyValue(b));return\"inherit\"!=d?void 0!==d?d:null:(a=Gc(a))?Hc(a,b):null}\nfunction Ic(a,b,c){function d(g){var h=Jc(g);return 0=D.a+D.width;D=e.c>=D.b+D.height;if(L&&\"hidden\"==r.x||D&&\"hidden\"==r.y)return Z;if(L&&\"visible\"!=r.x||D&&\"visible\"!=r.y){if(w&&(r=d(a),e.f>=g.scrollWidth-r.x||e.a>=g.scrollHeight-r.y))return Z;e=Kc(a);return e==Z?Z:\"scroll\"}}}return\"none\"}\nfunction Jc(a){var b=Lc(a);if(b)return b.rect;if(W(a,\"HTML\"))return a=A(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a=\"CSS1Compat\"==a.compatMode?a.documentElement:a.body,a=new Va(a.clientWidth,a.clientHeight),new X(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new X(0,0,0,0)}b=new X(c.left,c.top,c.right-c.left,c.bottom-c.top);v&&a.ownerDocument.body&&(a=A(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);\nreturn b}function Lc(a){var b=W(a,\"MAP\");if(!b&&!W(a,\"AREA\"))return null;var c=b?a:W(a.parentNode,\"MAP\")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Dc('/descendant::*[@usemap = \"#'+c.name+'\"]',A(c)))&&(e=Jc(d),b||\"default\"==a.shape.toLowerCase()||(a=Oc(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new X(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{image:d,rect:e||new X(0,0,0,0)}}\nfunction Oc(a){var b=a.shape.toLowerCase();a=a.coords.split(\",\");if(\"rect\"==b&&4==a.length){b=a[0];var c=a[1];return new X(b,c,a[2]-b,a[3]-c)}if(\"circle\"==b&&3==a.length)return b=a[2],new X(a[0]-b,a[1]-b,2*b,2*b);if(\"poly\"==b&&2b?null:\"string\"===typeof a?a.charAt(b):a[b]}function la(a){return Array.prototype.concat.apply([],arguments)}function ma(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};var t;a:{var na=k.navigator;if(na){var oa=na.userAgent;if(oa){t=oa;break a}}t=\"\"}function u(a){return-1!=t.indexOf(a)};function pa(){return u(\"Firefox\")||u(\"FxiOS\")}function qa(){return(u(\"Chrome\")||u(\"CriOS\"))&&!u(\"Edge\")};function ra(){return u(\"iPhone\")&&!u(\"iPod\")&&!u(\"iPad\")};var sa=u(\"Opera\"),v=u(\"Trident\")||u(\"MSIE\"),ta=u(\"Edge\"),ua=u(\"Gecko\")&&!(-1!=t.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\"))&&!(u(\"Trident\")||u(\"MSIE\"))&&!u(\"Edge\"),va=-1!=t.toLowerCase().indexOf(\"webkit\")&&!u(\"Edge\");function wa(){var a=k.document;return a?a.documentMode:void 0}var xa;\na:{var ya=\"\",za=function(){var a=t;if(ua)return/rv:([^\\);]+)(\\)|;)/.exec(a);if(ta)return/Edge\\/([\\d\\.]+)/.exec(a);if(v)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(va)return/WebKit\\/(\\S+)/.exec(a);if(sa)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();za&&(ya=za?za[1]:\"\");if(v){var Aa=wa();if(null!=Aa&&Aa>parseFloat(ya)){xa=String(Aa);break a}}xa=ya}var Ba;Ba=k.document&&v?wa():void 0;var w=v&&!(9<=Number(Ba)),Ca=v&&!(8<=Number(Ba));function y(a,b,c,d){this.a=a;this.nodeName=c;this.nodeValue=d;this.nodeType=2;this.parentNode=this.ownerElement=b}function Da(a,b){var c=Ca&&\"href\"==b.nodeName?a.getAttribute(b.nodeName,2):b.nodeValue;return new y(b,a,b.nodeName,c)};function Ea(a){this.b=a;this.a=0}function Fa(a){a=a.match(Ga);for(var b=0;b]=|\\s+|./g,Ha=/^\\s/;function z(a,b){return a.b[a.a+(b||0)]}function A(a){return a.b[a.a++]}function Ia(a){return a.b.length<=a.a};function Ja(a){for(;a&&1!=a.nodeType;)a=a.previousSibling;return a}function Ka(a,b){if(!a||!b)return!1;if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if(\"undefined\"!=typeof a.compareDocumentPosition)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a}\nfunction La(a,b){if(a==b)return 0;if(a.compareDocumentPosition)return a.compareDocumentPosition(b)&2?1:-1;if(v&&!(9<=Number(Ba))){if(9==a.nodeType)return-1;if(9==b.nodeType)return 1}if(\"sourceIndex\"in a||a.parentNode&&\"sourceIndex\"in a.parentNode){var c=1==a.nodeType,d=1==b.nodeType;if(c&&d)return a.sourceIndex-b.sourceIndex;var e=a.parentNode,f=b.parentNode;return e==f?Ma(a,b):!c&&Ka(e,b)?-1*Na(a,b):!d&&Ka(f,a)?Na(b,a):(c?a.sourceIndex:e.sourceIndex)-(d?b.sourceIndex:f.sourceIndex)}d=9==a.nodeType?\na:a.ownerDocument||a.document;c=d.createRange();c.selectNode(a);c.collapse(!0);a=d.createRange();a.selectNode(b);a.collapse(!0);return c.compareBoundaryPoints(k.Range.START_TO_END,a)}function Na(a,b){var c=a.parentNode;if(c==b)return-1;for(;b.parentNode!=c;)b=b.parentNode;return Ma(b,a)}function Ma(a,b){for(;b=b.previousSibling;)if(b==a)return-1;return 1}function Oa(a,b){for(var c=0;a;){if(b(a))return a;a=a.parentNode;c++}return null};function B(a){var b=null,c=a.nodeType;1==c&&(b=a.textContent,b=void 0==b||null==b?a.innerText:b,b=void 0==b||null==b?\"\":b);if(\"string\"!=typeof b)if(w&&\"title\"==a.nodeName.toLowerCase()&&1==c)b=a.text;else if(9==c||1==c){a=9==c?a.documentElement:a.firstChild;c=0;var d=[];for(b=\"\";a;){do 1!=a.nodeType&&(b+=a.nodeValue),w&&\"title\"==a.nodeName.toLowerCase()&&(b+=a.text),d[c++]=a;while(a=a.firstChild);for(;c&&!(a=d[--c].nextSibling););}}else b=a.nodeValue;return b}\nfunction C(a,b,c){if(null===b)return!0;try{if(!a.getAttribute)return!1}catch(d){return!1}Ca&&\"class\"==b&&(b=\"className\");return null==c?!!a.getAttribute(b):a.getAttribute(b,2)==c}function D(a,b,c,d,e){return(w?Pa:Qa).call(null,a,b,aa(c)?c:null,aa(d)?d:null,e||new E)}\nfunction Pa(a,b,c,d,e){if(a instanceof F||8==a.b||c&&null===a.b){var f=b.all;if(!f)return e;a=Ra(a);if(\"*\"!=a&&(f=b.getElementsByTagName(a),!f))return e;if(c){for(var g=[],h=0;b=f[h++];)C(b,c,d)&&g.push(b);f=g}for(h=0;b=f[h++];)\"*\"==a&&\"!\"==b.tagName||e.add(b);return e}Sa(a,b,c,d,e);return e}\nfunction Qa(a,b,c,d,e){b.getElementsByName&&d&&\"name\"==c&&!v?(b=b.getElementsByName(d),n(b,function(f){a.a(f)&&e.add(f)})):b.getElementsByClassName&&d&&\"class\"==c?(b=b.getElementsByClassName(d),n(b,function(f){f.className==d&&a.a(f)&&e.add(f)})):a instanceof G?Sa(a,b,c,d,e):b.getElementsByTagName&&(b=b.getElementsByTagName(a.f()),n(b,function(f){C(f,c,d)&&e.add(f)}));return e}\nfunction Ta(a,b,c,d,e){var f;if((a instanceof F||8==a.b||c&&null===a.b)&&(f=b.childNodes)){var g=Ra(a);if(\"*\"!=g&&(f=ja(f,function(h){return h.tagName&&h.tagName.toLowerCase()==g}),!f))return e;c&&(f=ja(f,function(h){return C(h,c,d)}));n(f,function(h){\"*\"==g&&(\"!\"==h.tagName||\"*\"==g&&1!=h.nodeType)||e.add(h)});return e}return Ua(a,b,c,d,e)}function Ua(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b);return e}\nfunction Sa(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)C(b,c,d)&&a.a(b)&&e.add(b),Sa(a,b,c,d,e)}function Ra(a){if(a instanceof G){if(8==a.b)return\"!\";if(null===a.b)return\"*\"}return a.f()};function E(){this.b=this.a=null;this.l=0}function Va(a){this.f=a;this.a=this.b=null}function Wa(a,b){if(!a.a)return b;if(!b.a)return a;var c=a.a;b=b.a;for(var d=null,e,f=0;c&&b;){e=c.f;var g=b.f;e==g||e instanceof y&&g instanceof y&&e.a==g.a?(e=c,c=c.a,b=b.a):0\",4,2,function(a,b,c){return P(function(d,e){return d>e},a,b,c)});Q(\"<=\",4,2,function(a,b,c){return P(function(d,e){return d<=e},a,b,c)});Q(\">=\",4,2,function(a,b,c){return P(function(d,e){return d>=e},a,b,c)});var cb=Q(\"=\",3,2,function(a,b,c){return P(function(d,e){return d==e},a,b,c,!0)});Q(\"!=\",3,2,function(a,b,c){return P(function(d,e){return d!=e},a,b,c,!0)});Q(\"and\",2,2,function(a,b,c){return N(a,c)&&N(b,c)});Q(\"or\",1,2,function(a,b,c){return N(a,c)||N(b,c)});function fb(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");J.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}l(fb,J);fb.prototype.a=function(a){a=this.c.a(a);return gb(this.h,a)};fb.prototype.toString=function(){var a=\"Filter:\"+K(this.c);return a+=K(this.h)};function hb(a,b){if(b.lengtha.B)throw Error(\"Function \"+a.j+\" expects at most \"+a.B+\" arguments, \"+b.length+\" given\");a.H&&n(b,function(c,d){if(4!=c.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+c);});J.call(this,a.i);this.v=a;this.c=b;ab(this,a.g||r(b,function(c){return c.g}));bb(this,a.G&&!b.length||a.F&&!!b.length||r(b,function(c){return c.b}))}l(hb,J);\nhb.prototype.a=function(a){return this.v.m.apply(null,la(a,this.c))};hb.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length){var b=p(this.c,function(c,d){return c+K(d)},\"Arguments:\");a+=K(b)}return a};function ib(a,b,c,d,e,f,g,h){this.j=a;this.i=b;this.g=c;this.G=d;this.F=!1;this.m=e;this.C=f;this.B=void 0!==g?g:f;this.H=!!h}ib.prototype.toString=function(){return this.j};var jb={};\nfunction R(a,b,c,d,e,f,g,h){if(jb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");jb[a]=new ib(a,b,c,d,e,f,g,h)}R(\"boolean\",2,!1,!1,function(a,b){return N(b,a)},1);R(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(L(b,a))},1);R(\"concat\",3,!1,!1,function(a,b){return p(ma(arguments,1),function(c,d){return c+M(d,a)},\"\")},2,null);R(\"contains\",2,!1,!1,function(a,b,c){b=M(b,a);a=M(c,a);return-1!=b.indexOf(a)},2);R(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nR(\"false\",2,!1,!1,function(){return!1},0);R(\"floor\",1,!1,!1,function(a,b){return Math.floor(L(b,a))},1);R(\"id\",4,!1,!1,function(a,b){function c(h){if(w){var q=e.all[h];if(q){if(q.nodeType&&h==q.id)return q;if(q.length)return ka(q,function(x){return h==x.id})}return null}return e.getElementById(h)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=M(b,a).split(/\\s+/);var f=[];n(a,function(h){h=c(h);!h||0<=ia(f,h)||f.push(h)});f.sort(La);var g=new E;n(f,function(h){g.add(h)});return g},1);\nR(\"lang\",2,!1,!1,function(){return!1},1);R(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);R(\"local-name\",3,!1,!0,function(a,b){return(a=b?Ya(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);R(\"name\",3,!1,!0,function(a,b){return(a=b?Ya(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);R(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nR(\"normalize-space\",3,!1,!0,function(a,b){return(b?M(b,a):B(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);R(\"not\",2,!1,!1,function(a,b){return!N(b,a)},1);R(\"number\",1,!1,!0,function(a,b){return b?L(b,a):+B(a.a)},0,1);R(\"position\",1,!0,!1,function(a){return a.b},0);R(\"round\",1,!1,!1,function(a,b){return Math.round(L(b,a))},1);R(\"starts-with\",2,!1,!1,function(a,b,c){b=M(b,a);a=M(c,a);return 0==b.lastIndexOf(a,0)},2);R(\"string\",3,!1,!0,function(a,b){return b?M(b,a):B(a.a)},0,1);\nR(\"string-length\",1,!1,!0,function(a,b){return(b?M(b,a):B(a.a)).length},0,1);R(\"substring\",3,!1,!1,function(a,b,c,d){c=L(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?L(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=M(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);R(\"substring-after\",3,!1,!1,function(a,b,c){b=M(b,a);a=M(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nR(\"substring-before\",3,!1,!1,function(a,b,c){b=M(b,a);a=M(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);R(\"sum\",1,!1,!1,function(a,b){a=H(b.a(a));b=0;for(var c=I(a);c;c=I(a))b+=+B(c);return b},1,1,!0);R(\"translate\",3,!1,!1,function(a,b,c,d){b=M(b,a);c=M(c,a);var e=M(d,a);a={};for(d=0;da.length)throw Error(\"Unclosed literal string\");return new lb(a)}\nfunction Ib(a){var b=[];if(qb(z(a.a))){var c=A(a.a);var d=z(a.a);if(\"/\"==c&&(Ia(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new S;d=new S;W(a,\"Missing next location step.\");c=Jb(a,c);b.push(c)}else{a:{c=z(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":A(a.a);c=Db(a);W(a,'unclosed \"(\"');Fb(a,\")\");break;case '\"':case \"'\":c=Hb(a);break;default:if(isNaN(+c))if(!kb(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==z(a.a,1)){c=A(a.a);\nc=jb[c]||null;A(a.a);for(d=[];\")\"!=z(a.a);){W(a,\"Missing function argument list.\");d.push(Db(a));if(\",\"!=z(a.a))break;A(a.a)}W(a,\"Unclosed function argument list.\");Gb(a);c=new hb(c,d)}else{c=null;break a}else c=new mb(+A(a.a))}\"[\"==z(a.a)&&(d=new tb(Kb(a)),c=new fb(c,d))}if(c)if(qb(z(a.a)))d=c;else return c;else c=Jb(a,\"/\"),d=new pb,b.push(c)}for(;qb(z(a.a));)c=A(a.a),W(a,\"Missing next location step.\"),c=Jb(a,c),b.push(c);return new nb(d,b)}\nfunction Jb(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==z(a.a)){var c=new U(zb,new G(\"node\"));A(a.a);return c}if(\"..\"==z(a.a))return c=new U(yb,new G(\"node\")),A(a.a),c;if(\"@\"==z(a.a)){var d=ob;A(a.a);W(a,\"Missing attribute name\")}else if(\"::\"==z(a.a,1)){if(!/(?![0-9])[\\w]/.test(z(a.a).charAt(0)))throw Error(\"Bad token: \"+A(a.a));var e=A(a.a);d=xb[e]||null;if(!d)throw Error(\"No axis with name: \"+e);A(a.a);W(a,\"Missing node name\")}else d=ub;e=z(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\nz(a.a,1)){if(!kb(e))throw Error(\"Invalid node type: \"+e);e=A(a.a);if(!kb(e))throw Error(\"Invalid type name: \"+e);Fb(a,\"(\");W(a,\"Bad nodetype\");var f=z(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=Hb(a);W(a,\"Bad nodetype\");Gb(a);e=new G(e,g)}else if(e=A(a.a),f=e.indexOf(\":\"),-1==f)e=new F(e);else{g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new F(e,h)}else throw Error(\"Bad token: \"+A(a.a));a=new tb(Kb(a),d.s);return c||new U(d,\ne,a,\"//\"==b)}function Kb(a){for(var b=[];\"[\"==z(a.a);){A(a.a);W(a,\"Missing predicate expression.\");var c=Db(a);b.push(c);W(a,\"Unclosed predicate expression.\");Fb(a,\"]\")}return b}function Eb(a){if(\"-\"==z(a.a))return A(a.a),new Ab(Eb(a));var b=Ib(a);if(\"|\"!=z(a.a))a=b;else{for(b=[b];\"|\"==A(a.a);)W(a,\"Missing next union location path.\"),b.push(Ib(a));a.a.a--;a=new Bb(b)}return a};function Lb(a){switch(a.nodeType){case 1:return ha(Mb,a);case 9:return Lb(a.documentElement);case 11:case 10:case 6:case 12:return Nb;default:return a.parentNode?Lb(a.parentNode):Nb}}function Nb(){return null}function Mb(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?Mb(a.parentNode,b):null};function Ob(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=Fa(a);if(Ia(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ca(b)||(b=fa(b.lookupNamespaceURI,b)):b=function(){return null};var c=Db(new Cb(a,b));if(!Ia(a))throw Error(\"Bad token: \"+A(a));this.evaluate=function(d,e){d=c.a(new m(d));return new X(d,e)}}\nfunction X(a,b){if(0==b)if(a instanceof E)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof E))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof E?Za(a):\"\"+a;break;case 1:this.numberValue=a instanceof E?+Za(a):+a;break;case 3:this.booleanValue=a instanceof E?0=d.length?null:d[f++]};this.snapshotItem=function(g){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return g>=d.length||\n0>g?null:d[g]}}X.ANY_TYPE=0;X.NUMBER_TYPE=1;X.STRING_TYPE=2;X.BOOLEAN_TYPE=3;X.UNORDERED_NODE_ITERATOR_TYPE=4;X.ORDERED_NODE_ITERATOR_TYPE=5;X.UNORDERED_NODE_SNAPSHOT_TYPE=6;X.ORDERED_NODE_SNAPSHOT_TYPE=7;X.ANY_UNORDERED_NODE_TYPE=8;X.FIRST_ORDERED_NODE_TYPE=9;function Pb(a){this.lookupNamespaceURI=Lb(a)}\nfunction Qb(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=X,c.evaluate=function(d,e,f,g){return(new Ob(d,f)).evaluate(e,g)},c.createExpression=function(d,e){return new Ob(d,e)},c.createNSResolver=function(d){return new Pb(d)}}ba(\"wgxpath.install\",Qb);ba(\"wgxpath.install\",Qb);var Rb=pa(),Sb=ra()||u(\"iPod\"),Tb=u(\"iPad\"),Ub=u(\"Android\")&&!(qa()||pa()||u(\"Opera\")||u(\"Silk\")),Vb=qa(),Wb=u(\"Safari\")&&!(qa()||u(\"Coast\")||u(\"Opera\")||u(\"Edge\")||u(\"Edg/\")||u(\"OPR\")||pa()||u(\"Silk\")||u(\"Android\"))&&!(ra()||u(\"iPad\")||u(\"iPod\"));function Y(a){return(a=a.exec(t))?a[1]:\"\"}(function(){if(Rb)return Y(/Firefox\\/([0-9.]+)/);if(v||ta||sa)return xa;if(Vb)return ra()||u(\"iPad\")||u(\"iPod\")?Y(/CriOS\\/([0-9.]+)/):Y(/Chrome\\/([0-9.]+)/);if(Wb&&!(ra()||u(\"iPad\")||u(\"iPod\")))return Y(/Version\\/([0-9.]+)/);if(Sb||Tb){var a=/Version\\/(\\S+).*Mobile\\/(\\S+)/.exec(t);if(a)return a[1]+\".\"+a[2]}else if(Ub)return(a=Y(/Android\\s+([0-9.]+)/))?a:Y(/Version\\/([0-9.]+)/);return\"\"})();function Z(a,b){b&&\"string\"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)};var Xb=\"BUTTON INPUT OPTGROUP OPTION SELECT TEXTAREA\".split(\" \");function Yb(a){return r(Xb,function(b){return Z(a,b)})?a.disabled?!1:a.parentNode&&1==a.parentNode.nodeType&&Z(a,\"OPTGROUP\")||Z(a,\"OPTION\")?Yb(a.parentNode):!Oa(a,function(b){var c=b.parentNode;if(c&&Z(c,\"FIELDSET\")&&c.disabled){if(!Z(b,\"LEGEND\"))return!0;for(;b=void 0!==b.previousElementSibling?b.previousElementSibling:Ja(b.previousSibling);)if(Z(b,\"LEGEND\"))return!0}return!1}):!0};ba(\"_\",Yb);; return this._.apply(null,arguments);}).apply({navigator:typeof window!='undefined'?window.navigator:null,document:typeof window!='undefined'?window.document:null}, arguments);}\n", +}; + +atom.getVisibleText = async function (element, window) { + return executeInContent("getVisibleText", element, window); +} + +atom.isElementDisplayed = function (element, window) { + return executeInContent("isElementDisplayed", element, window); +} + +atom.isElementEnabled = function (element, window) { + return executeInContent("isElementEnabled", element, window); +} + +function executeInContent(name, element, window) { + const sandbox = lazy.sandbox.createMutable(window); + + return lazy.evaluate.sandbox( + sandbox, + `return (${ATOMS[name]})(arguments[0]);`, + [element] + ); +} diff --git a/remote/marionette/browser.sys.mjs b/remote/marionette/browser.sys.mjs new file mode 100644 index 0000000000..fd5aac21a3 --- /dev/null +++ b/remote/marionette/browser.sys.mjs @@ -0,0 +1,385 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + EventPromise: "chrome://remote/content/shared/Sync.sys.mjs", + MessageManagerDestroyedPromise: + "chrome://remote/content/marionette/sync.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", +}); + +/** @namespace */ +export const browser = {}; + +/** + * Variations of Marionette contexts. + * + * Choosing a context through the Marionette:SetContext + * command directs all subsequent browsing context scoped commands + * to that context. + * + * @class Marionette.Context + */ +export class Context { + /** + * Gets the correct context from a string. + * + * @param {string} s + * Context string serialisation. + * + * @returns {Context} + * Context. + * + * @throws {TypeError} + * If s is not a context. + */ + static fromString(s) { + switch (s) { + case "chrome": + return Context.Chrome; + + case "content": + return Context.Content; + + default: + throw new TypeError(`Unknown context: ${s}`); + } + } +} + +Context.Chrome = "chrome"; +Context.Content = "content"; + +/** + * Creates a browsing context wrapper. + * + * Browsing contexts handle interactions with the browser, according to + * the current environment. + */ +browser.Context = class { + /** + * @param {ChromeWindow} window + * ChromeWindow that contains the top-level browsing context. + * @param {GeckoDriver} driver + * Reference to driver instance. + */ + constructor(window, driver) { + this.window = window; + this.driver = driver; + + // In Firefox this is (not !) + // and MobileTabBrowser in GeckoView. + this.tabBrowser = lazy.TabManager.getTabBrowser(this.window); + + // Used to set curFrameId upon new session + this.newSession = true; + + // A reference to the tab corresponding to the current window handle, + // if any. Specifically, this.tab refers to the last tab that Marionette + // switched to in this browser window. Note that this may not equal the + // currently selected tab. For example, if Marionette switches to tab + // A, and then clicks on a button that opens a new tab B in the same + // browser window, this.tab will still point to tab A, despite tab B + // being the currently selected tab. + this.tab = null; + } + + /** + * Returns the content browser for the currently selected tab. + * If there is no tab selected, null will be returned. + */ + get contentBrowser() { + if (this.tab) { + return lazy.TabManager.getBrowserForTab(this.tab); + } else if ( + this.tabBrowser && + this.driver.isReftestBrowser(this.tabBrowser) + ) { + return this.tabBrowser; + } + + return null; + } + + get messageManager() { + if (this.contentBrowser) { + return this.contentBrowser.messageManager; + } + + return null; + } + + /** + * Checks if the browsing context has been discarded. + * + * The browsing context will have been discarded if the content + * browser, represented by the <xul:browser>, + * has been detached. + * + * @returns {boolean} + * True if browsing context has been discarded, false otherwise. + */ + get closed() { + return this.contentBrowser === null; + } + + /** + * Gets the position and dimensions of the top-level browsing context. + * + * @returns {Map.} + * Object with |x|, |y|, |width|, and |height| properties. + */ + get rect() { + return { + x: this.window.screenX, + y: this.window.screenY, + width: this.window.outerWidth, + height: this.window.outerHeight, + }; + } + + /** + * Retrieves the current tabmodal UI object. According to the browser + * associated with the currently selected tab. + */ + getTabModal() { + let br = this.contentBrowser; + if (!br.hasAttribute("tabmodalPromptShowing")) { + return null; + } + + // The modal is a direct sibling of the browser element. + // See tabbrowser.xml's getTabModalPromptBox. + let modalElements = br.parentNode.getElementsByTagName("tabmodalprompt"); + + return br.tabModalPromptBox.getPrompt(modalElements[0]); + } + + /** + * Close the current window. + * + * @returns {Promise} + * A promise which is resolved when the current window has been closed. + */ + async closeWindow() { + return lazy.windowManager.closeWindow(this.window); + } + + /** + * Focus the current window. + * + * @returns {Promise} + * A promise which is resolved when the current window has been focused. + */ + async focusWindow() { + await lazy.windowManager.focusWindow(this.window); + + // Also focus the currently selected tab if present. + this.contentBrowser?.focus(); + } + + /** + * Open a new browser window. + * + * @returns {Promise} + * A promise resolving to the newly created chrome window. + */ + openBrowserWindow(focus = false, isPrivate = false) { + return lazy.windowManager.openBrowserWindow({ + openerWindow: this.window, + focus, + isPrivate, + }); + } + + /** + * Close the current tab. + * + * @returns {Promise} + * A promise which is resolved when the current tab has been closed. + * + * @throws UnsupportedOperationError + * If tab handling for the current application isn't supported. + */ + async closeTab() { + // If the current window is not a browser then close it directly. Do the + // same if only one remaining tab is open, or no tab selected at all. + // + // Note: For GeckoView there will always be a single tab only. But for + // consistency with other platforms a specific condition has been added + // below as well even it's not really used. + if ( + !this.tabBrowser || + !this.tabBrowser.tabs || + this.tabBrowser.tabs.length === 1 || + !this.tab + ) { + return this.closeWindow(); + } + + let destroyed = new lazy.MessageManagerDestroyedPromise( + this.messageManager + ); + let tabClosed; + + if (lazy.AppInfo.isAndroid) { + await lazy.TabManager.removeTab(this.tab); + } else if (lazy.AppInfo.isFirefox) { + tabClosed = new lazy.EventPromise(this.tab, "TabClose"); + await this.tabBrowser.removeTab(this.tab); + } else { + throw new lazy.error.UnsupportedOperationError( + `closeTab() not supported for ${lazy.AppInfo.name}` + ); + } + + return Promise.all([destroyed, tabClosed]); + } + + /** + * Open a new tab in the currently selected chrome window. + */ + async openTab(focus = false) { + let tab = null; + + // Bug 1795841 - For Firefox the TabManager cannot be used yet. As such + // handle opening a tab differently for Android. + if (lazy.AppInfo.isAndroid) { + tab = await lazy.TabManager.addTab({ focus, window: this.window }); + } else if (lazy.AppInfo.isFirefox) { + const opened = new lazy.EventPromise(this.window, "TabOpen"); + this.window.BrowserOpenTab({ url: "about:blank" }); + await opened; + + tab = this.tabBrowser.selectedTab; + + // The new tab is always selected by default. If focus is not wanted, + // the previously tab needs to be selected again. + if (!focus) { + await lazy.TabManager.selectTab(this.tab); + } + } else { + throw new lazy.error.UnsupportedOperationError( + `openTab() not supported for ${lazy.AppInfo.name}` + ); + } + + return tab; + } + + /** + * Set the current tab. + * + * @param {number=} index + * Tab index to switch to. If the parameter is undefined, + * the currently selected tab will be used. + * @param {ChromeWindow=} window + * Switch to this window before selecting the tab. + * @param {boolean=} focus + * A boolean value which determins whether to focus + * the window. Defaults to true. + * + * @returns {Tab} + * The selected tab. + * + * @throws UnsupportedOperationError + * If tab handling for the current application isn't supported. + */ + async switchToTab(index, window = undefined, focus = true) { + if (window) { + this.window = window; + this.tabBrowser = lazy.TabManager.getTabBrowser(this.window); + } + + if (!this.tabBrowser || this.driver.isReftestBrowser(this.tabBrowser)) { + return null; + } + + if (typeof index == "undefined") { + this.tab = this.tabBrowser.selectedTab; + } else { + this.tab = this.tabBrowser.tabs[index]; + } + + if (focus) { + await lazy.TabManager.selectTab(this.tab); + } + + // By accessing the content browser's message manager a new browsing + // context is created for browserless tabs, which is needed to successfully + // run the WebDriver's is browsing context open step. This is temporary + // until we find a better solution on bug 1812258. + this.messageManager; + + return this.tab; + } + + /** + * Registers a new frame, and sets its current frame id to this frame + * if it is not already assigned, and if a) we already have a session + * or b) we're starting a new session and it is the right start frame. + * + * @param {XULBrowser} target + * The that was the target of the originating message. + */ + register(target) { + if (!this.tabBrowser) { + return; + } + + // If we're setting up a new session on Firefox, we only process the + // registration for this frame if it belongs to the current tab. + if (!this.tab) { + this.switchToTab(); + } + } +}; + +/** + * Marionette representation of the {@link ChromeWindow} window state. + * + * @enum {string} + */ +export const WindowState = { + Maximized: "maximized", + Minimized: "minimized", + Normal: "normal", + Fullscreen: "fullscreen", + + /** + * Converts {@link Window.windowState} to WindowState. + * + * @param {number} windowState + * Attribute from {@link Window.windowState}. + * + * @returns {WindowState} + * JSON representation. + * + * @throws {TypeError} + * If windowState was unknown. + */ + from(windowState) { + switch (windowState) { + case 1: + return WindowState.Maximized; + + case 2: + return WindowState.Minimized; + + case 3: + return WindowState.Normal; + + case 4: + return WindowState.Fullscreen; + + default: + throw new TypeError(`Unknown window state: ${windowState}`); + } + }, +}; diff --git a/remote/marionette/cert.sys.mjs b/remote/marionette/cert.sys.mjs new file mode 100644 index 0000000000..c2cdf7b748 --- /dev/null +++ b/remote/marionette/cert.sys.mjs @@ -0,0 +1,57 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "sss", + "@mozilla.org/ssservice;1", + "nsISiteSecurityService" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "certOverrideService", + "@mozilla.org/security/certoverride;1", + "nsICertOverrideService" +); + +const CERT_PINNING_ENFORCEMENT_PREF = "security.cert_pinning.enforcement_level"; +const HSTS_PRELOAD_LIST_PREF = "network.stricttransportsecurity.preloadlist"; + +/** @namespace */ +export const allowAllCerts = {}; + +/** + * Disable all security check and allow all certs. + */ +allowAllCerts.enable = function () { + // make it possible to register certificate overrides for domains + // that use HSTS or HPKP + Services.prefs.setBoolPref(HSTS_PRELOAD_LIST_PREF, false); + Services.prefs.setIntPref(CERT_PINNING_ENFORCEMENT_PREF, 0); + + lazy.certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); +}; + +/** + * Enable all security check. + */ +allowAllCerts.disable = function () { + lazy.certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + + Services.prefs.clearUserPref(HSTS_PRELOAD_LIST_PREF); + Services.prefs.clearUserPref(CERT_PINNING_ENFORCEMENT_PREF); + + // clear collected HSTS and HPKP state + // through the site security service + lazy.sss.clearAll(); +}; diff --git a/remote/marionette/chrome/reftest.xhtml b/remote/marionette/chrome/reftest.xhtml new file mode 100644 index 0000000000..ec4145832d --- /dev/null +++ b/remote/marionette/chrome/reftest.xhtml @@ -0,0 +1,8 @@ + + + diff --git a/remote/marionette/chrome/test.xhtml b/remote/marionette/chrome/test.xhtml new file mode 100644 index 0000000000..1a94c69617 --- /dev/null +++ b/remote/marionette/chrome/test.xhtml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + `; + + const divEl = browser.document.querySelector("div"); + const svgEl = browser.document.querySelector("svg"); + const textareaEl = browser.document.querySelector("textarea"); + const videoEl = browser.document.querySelector("video"); + + const iframeEl = browser.document.querySelector("iframe"); + const childEl = iframeEl.contentDocument.createElement("div"); + iframeEl.contentDocument.body.appendChild(childEl); + + const shadowRoot = videoEl.openOrClosedShadowRoot; + + return { + browser, + childEl, + divEl, + iframeEl, + nodeCache: new NodeCache(), + shadowRoot, + svgEl, + textareaEl, + videoEl, + }; +} + +add_task(function test_WebReference_ctor() { + const el = new WebReference("foo"); + equal(el.uuid, "foo"); + + for (let t of [42, true, [], {}, null, undefined]) { + Assert.throws(() => new WebReference(t), /to be a string/); + } +}); + +add_task(function test_WebReference_from() { + const { divEl, iframeEl } = setupTest(); + + ok(WebReference.from(divEl) instanceof WebElement); + ok(WebReference.from(xulEl) instanceof WebElement); + ok(WebReference.from(divEl.ownerGlobal) instanceof WebWindow); + ok(WebReference.from(iframeEl.contentWindow) instanceof WebFrame); + ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement); + ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement); + + Assert.throws(() => WebReference.from({}), /InvalidArgumentError/); +}); + +add_task(function test_WebReference_fromJSON_malformed() { + Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/); + Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/); +}); + +add_task(function test_WebReference_fromJSON_ShadowRoot() { + const { Identifier } = ShadowRoot; + + const ref = { [Identifier]: "foo" }; + const shadowRootEl = WebReference.fromJSON(ref); + ok(shadowRootEl instanceof ShadowRoot); + equal(shadowRootEl.uuid, "foo"); + + let identifierPrecedence = { + [Identifier]: "identifier-uuid", + }; + const precedenceShadowRoot = WebReference.fromJSON(identifierPrecedence); + ok(precedenceShadowRoot instanceof ShadowRoot); + equal(precedenceShadowRoot.uuid, "identifier-uuid"); +}); + +add_task(function test_WebReference_fromJSON_WebElement() { + const { Identifier } = WebElement; + + const ref = { [Identifier]: "foo" }; + const webEl = WebReference.fromJSON(ref); + ok(webEl instanceof WebElement); + equal(webEl.uuid, "foo"); + + let identifierPrecedence = { + [Identifier]: "identifier-uuid", + }; + const precedenceEl = WebReference.fromJSON(identifierPrecedence); + ok(precedenceEl instanceof WebElement); + equal(precedenceEl.uuid, "identifier-uuid"); +}); + +add_task(function test_WebReference_fromJSON_WebFrame() { + const ref = { [WebFrame.Identifier]: "foo" }; + const frame = WebReference.fromJSON(ref); + ok(frame instanceof WebFrame); + equal(frame.uuid, "foo"); +}); + +add_task(function test_WebReference_fromJSON_WebWindow() { + const ref = { [WebWindow.Identifier]: "foo" }; + const win = WebReference.fromJSON(ref); + + ok(win instanceof WebWindow); + equal(win.uuid, "foo"); +}); + +add_task(function test_WebReference_is() { + const a = new WebReference("a"); + const b = new WebReference("b"); + + ok(a.is(a)); + ok(b.is(b)); + ok(!a.is(b)); + ok(!b.is(a)); + + ok(!a.is({})); +}); + +add_task(function test_WebReference_isReference() { + for (let t of [42, true, "foo", [], {}]) { + ok(!WebReference.isReference(t)); + } + + ok(WebReference.isReference({ [WebElement.Identifier]: "foo" })); + ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" })); + ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" })); +}); + +add_task(function test_ShadowRoot_fromJSON() { + const { Identifier } = ShadowRoot; + + const shadowRoot = ShadowRoot.fromJSON({ [Identifier]: "foo" }); + ok(shadowRoot instanceof ShadowRoot); + equal(shadowRoot.uuid, "foo"); + + Assert.throws(() => ShadowRoot.fromJSON({}), /InvalidArgumentError/); +}); + +add_task(function test_ShadowRoot_fromUUID() { + const shadowRoot = ShadowRoot.fromUUID("baz"); + + ok(shadowRoot instanceof ShadowRoot); + equal(shadowRoot.uuid, "baz"); + + Assert.throws(() => ShadowRoot.fromUUID(), /InvalidArgumentError/); +}); + +add_task(function test_ShadowRoot_toJSON() { + const { Identifier } = ShadowRoot; + + const shadowRoot = new ShadowRoot("foo"); + const json = shadowRoot.toJSON(); + + ok(Identifier in json); + equal(json[Identifier], "foo"); +}); + +add_task(function test_WebElement_fromJSON() { + const { Identifier } = WebElement; + + const el = WebElement.fromJSON({ [Identifier]: "foo" }); + ok(el instanceof WebElement); + equal(el.uuid, "foo"); + + Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/); +}); + +add_task(function test_WebElement_fromUUID() { + const domWebEl = WebElement.fromUUID("bar"); + + ok(domWebEl instanceof WebElement); + equal(domWebEl.uuid, "bar"); + + Assert.throws(() => WebElement.fromUUID(), /InvalidArgumentError/); +}); + +add_task(function test_WebElement_toJSON() { + const { Identifier } = WebElement; + + const el = new WebElement("foo"); + const json = el.toJSON(); + + ok(Identifier in json); + equal(json[Identifier], "foo"); +}); + +add_task(function test_WebFrame_fromJSON() { + const ref = { [WebFrame.Identifier]: "foo" }; + const win = WebFrame.fromJSON(ref); + + ok(win instanceof WebFrame); + equal(win.uuid, "foo"); +}); + +add_task(function test_WebFrame_toJSON() { + const frame = new WebFrame("foo"); + const json = frame.toJSON(); + + ok(WebFrame.Identifier in json); + equal(json[WebFrame.Identifier], "foo"); +}); + +add_task(function test_WebWindow_fromJSON() { + const ref = { [WebWindow.Identifier]: "foo" }; + const win = WebWindow.fromJSON(ref); + + ok(win instanceof WebWindow); + equal(win.uuid, "foo"); +}); + +add_task(function test_WebWindow_toJSON() { + const win = new WebWindow("foo"); + const json = win.toJSON(); + + ok(WebWindow.Identifier in json); + equal(json[WebWindow.Identifier], "foo"); +}); diff --git a/remote/marionette/test/xpcshell/xpcshell.toml b/remote/marionette/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..46ee8581fd --- /dev/null +++ b/remote/marionette/test/xpcshell/xpcshell.toml @@ -0,0 +1,20 @@ +[DEFAULT] +skip-if = ["appname == 'thunderbird'"] + +["test_actors.js"] + +["test_browser.js"] + +["test_cookie.js"] + +["test_json.js"] + +["test_message.js"] + +["test_navigate.js"] + +["test_prefs.js"] + +["test_sync.js"] + +["test_web-reference.js"] diff --git a/remote/marionette/transport.sys.mjs b/remote/marionette/transport.sys.mjs new file mode 100644 index 0000000000..3c05c8603e --- /dev/null +++ b/remote/marionette/transport.sys.mjs @@ -0,0 +1,527 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + BulkPacket: "chrome://remote/content/marionette/packets.sys.mjs", + executeSoon: "chrome://remote/content/marionette/sync.sys.mjs", + JSONPacket: "chrome://remote/content/marionette/packets.sys.mjs", + Packet: "chrome://remote/content/marionette/packets.sys.mjs", + StreamUtils: "chrome://remote/content/marionette/stream-utils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "ScriptableInputStream", () => { + return Components.Constructor( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" + ); +}); + +const flags = { wantVerbose: false, wantLogging: false }; + +const dumpv = flags.wantVerbose + ? function (msg) { + dump(msg + "\n"); + } + : function () {}; + +const PACKET_HEADER_MAX = 200; + +/** + * An adapter that handles data transfers between the debugger client + * and server. It can work with both nsIPipe and nsIServerSocket + * transports so long as the properly created input and output streams + * are specified. (However, for intra-process connections, + * LocalDebuggerTransport, below, is more efficient than using an nsIPipe + * pair with DebuggerTransport.) + * + * @param {nsIAsyncInputStream} input + * The input stream. + * @param {nsIAsyncOutputStream} output + * The output stream. + * + * Given a DebuggerTransport instance dt: + * 1) Set dt.hooks to a packet handler object (described below). + * 2) Call dt.ready() to begin watching for input packets. + * 3) Call dt.send() / dt.startBulkSend() to send packets. + * 4) Call dt.close() to close the connection, and disengage from + * the event loop. + * + * A packet handler is an object with the following methods: + * + * - onPacket(packet) - called when we have received a complete packet. + * |packet| is the parsed form of the packet --- a JavaScript value, not + * a JSON-syntax string. + * + * - onBulkPacket(packet) - called when we have switched to bulk packet + * receiving mode. |packet| is an object containing: + * actor: Name of actor that will receive the packet + * type: Name of actor's method that should be called on receipt + * length: Size of the data to be read + * stream: This input stream should only be used directly if you + * can ensure that you will read exactly |length| bytes and + * will not close the stream when reading is complete + * done: If you use the stream directly (instead of |copyTo| + * below), you must signal completion by resolving/rejecting + * this deferred. If it's rejected, the transport will + * be closed. If an Error is supplied as a rejection value, + * it will be logged via |dump|. If you do use |copyTo|, + * resolving is taken care of for you when copying completes. + * copyTo: A helper function for getting your data out of the + * stream that meets the stream handling requirements above, + * and has the following signature: + * + * - params + * {nsIAsyncOutputStream} output + * The stream to copy to. + * - returns {Promise} + * The promise is resolved when copying completes or + * rejected if any (unexpected) errors occur. This object + * also emits "progress" events for each chunk that is + * copied. See stream-utils.js. + * + * - onClosed(reason) - called when the connection is closed. |reason| + * is an optional nsresult or object, typically passed when the + * transport is closed due to some error in a underlying stream. + * + * See ./packets.js and the Remote Debugging Protocol specification for + * more details on the format of these packets. + * + * @class + */ +export function DebuggerTransport(input, output) { + lazy.EventEmitter.decorate(this); + + this._input = input; + this._scriptableInput = new lazy.ScriptableInputStream(input); + this._output = output; + + // The current incoming (possibly partial) header, which will determine + // which type of Packet |_incoming| below will become. + this._incomingHeader = ""; + // The current incoming Packet object + this._incoming = null; + // A queue of outgoing Packet objects + this._outgoing = []; + + this.hooks = null; + this.active = false; + + this._incomingEnabled = true; + this._outgoingEnabled = true; + + this.close = this.close.bind(this); +} + +DebuggerTransport.prototype = { + /** + * Transmit an object as a JSON packet. + * + * This method returns immediately, without waiting for the entire + * packet to be transmitted, registering event handlers as needed to + * transmit the entire packet. Packets are transmitted in the order they + * are passed to this method. + */ + send(object) { + this.emit("send", object); + + let packet = new lazy.JSONPacket(this); + packet.object = object; + this._outgoing.push(packet); + this._flushOutgoing(); + }, + + /** + * Transmit streaming data via a bulk packet. + * + * This method initiates the bulk send process by queuing up the header + * data. The caller receives eventual access to a stream for writing. + * + * N.B.: Do *not* attempt to close the stream handed to you, as it + * will continue to be used by this transport afterwards. Most users + * should instead use the provided |copyFrom| function instead. + * + * @param {object} header + * This is modeled after the format of JSON packets above, but does + * not actually contain the data, but is instead just a routing + * header: + * + * - actor: Name of actor that will receive the packet + * - type: Name of actor's method that should be called on receipt + * - length: Size of the data to be sent + * + * @returns {Promise} + * The promise will be resolved when you are allowed to write to + * the stream with an object containing: + * + * - stream: This output stream should only be used directly + * if you can ensure that you will write exactly + * |length| bytes and will not close the stream when + * writing is complete. + * - done: If you use the stream directly (instead of + * |copyFrom| below), you must signal completion by + * resolving/rejecting this deferred. If it's + * rejected, the transport will be closed. If an + * Error is supplied as a rejection value, it will + * be logged via |dump|. If you do use |copyFrom|, + * resolving is taken care of for you when copying + * completes. + * - copyFrom: A helper function for getting your data onto the + * stream that meets the stream handling requirements + * above, and has the following signature: + * + * - params + * {nsIAsyncInputStream} input + * The stream to copy from. + * - returns {Promise} + * The promise is resolved when copying completes + * or rejected if any (unexpected) errors occur. + * This object also emits "progress" events for + * each chunkthat is copied. See stream-utils.js. + */ + startBulkSend(header) { + this.emit("startbulksend", header); + + let packet = new lazy.BulkPacket(this); + packet.header = header; + this._outgoing.push(packet); + this._flushOutgoing(); + return packet.streamReadyForWriting; + }, + + /** + * Close the transport. + * + * @param {(nsresult|object)=} reason + * The status code or error message that corresponds to the reason + * for closing the transport (likely because a stream closed + * or failed). + */ + close(reason) { + this.emit("close", reason); + + this.active = false; + this._input.close(); + this._scriptableInput.close(); + this._output.close(); + this._destroyIncoming(); + this._destroyAllOutgoing(); + if (this.hooks) { + this.hooks.onClosed(reason); + this.hooks = null; + } + if (reason) { + dumpv("Transport closed: " + reason); + } else { + dumpv("Transport closed."); + } + }, + + /** + * The currently outgoing packet (at the top of the queue). + */ + get _currentOutgoing() { + return this._outgoing[0]; + }, + + /** + * Flush data to the outgoing stream. Waits until the output + * stream notifies us that it is ready to be written to (via + * onOutputStreamReady). + */ + _flushOutgoing() { + if (!this._outgoingEnabled || this._outgoing.length === 0) { + return; + } + + // If the top of the packet queue has nothing more to send, remove it. + if (this._currentOutgoing.done) { + this._finishCurrentOutgoing(); + } + + if (this._outgoing.length) { + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + this._output.asyncWait(this, 0, 0, threadManager.currentThread); + } + }, + + /** + * Pause this transport's attempts to write to the output stream. + * This is used when we've temporarily handed off our output stream for + * writing bulk data. + */ + pauseOutgoing() { + this._outgoingEnabled = false; + }, + + /** + * Resume this transport's attempts to write to the output stream. + */ + resumeOutgoing() { + this._outgoingEnabled = true; + this._flushOutgoing(); + }, + + // nsIOutputStreamCallback + /** + * This is called when the output stream is ready for more data to + * be written. The current outgoing packet will attempt to write some + * amount of data, but may not complete. + */ + onOutputStreamReady(stream) { + if (!this._outgoingEnabled || this._outgoing.length === 0) { + return; + } + + try { + this._currentOutgoing.write(stream); + } catch (e) { + if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this.close(e.result); + return; + } + throw e; + } + + this._flushOutgoing(); + }, + + /** + * Remove the current outgoing packet from the queue upon completion. + */ + _finishCurrentOutgoing() { + if (this._currentOutgoing) { + this._currentOutgoing.destroy(); + this._outgoing.shift(); + } + }, + + /** + * Clear the entire outgoing queue. + */ + _destroyAllOutgoing() { + for (let packet of this._outgoing) { + packet.destroy(); + } + this._outgoing = []; + }, + + /** + * Initialize the input stream for reading. Once this method has been + * called, we watch for packets on the input stream, and pass them to + * the appropriate handlers via this.hooks. + */ + ready() { + this.active = true; + this._waitForIncoming(); + }, + + /** + * Asks the input stream to notify us (via onInputStreamReady) when it is + * ready for reading. + */ + _waitForIncoming() { + if (this._incomingEnabled) { + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + this._input.asyncWait(this, 0, 0, threadManager.currentThread); + } + }, + + /** + * Pause this transport's attempts to read from the input stream. + * This is used when we've temporarily handed off our input stream for + * reading bulk data. + */ + pauseIncoming() { + this._incomingEnabled = false; + }, + + /** + * Resume this transport's attempts to read from the input stream. + */ + resumeIncoming() { + this._incomingEnabled = true; + this._flushIncoming(); + this._waitForIncoming(); + }, + + // nsIInputStreamCallback + /** + * Called when the stream is either readable or closed. + */ + onInputStreamReady(stream) { + try { + while ( + stream.available() && + this._incomingEnabled && + this._processIncoming(stream, stream.available()) + ) { + // Loop until there is nothing more to process + } + this._waitForIncoming(); + } catch (e) { + if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this.close(e.result); + } else { + throw e; + } + } + }, + + /** + * Process the incoming data. Will create a new currently incoming + * Packet if needed. Tells the incoming Packet to read as much data + * as it can, but reading may not complete. The Packet signals that + * its data is ready for delivery by calling one of this transport's + * _on*Ready methods (see ./packets.js and the _on*Ready methods below). + * + * @returns {boolean} + * Whether incoming stream processing should continue for any + * remaining data. + */ + _processIncoming(stream, count) { + dumpv("Data available: " + count); + + if (!count) { + dumpv("Nothing to read, skipping"); + return false; + } + + try { + if (!this._incoming) { + dumpv("Creating a new packet from incoming"); + + if (!this._readHeader(stream)) { + // Not enough data to read packet type + return false; + } + + // Attempt to create a new Packet by trying to parse each possible + // header pattern. + this._incoming = lazy.Packet.fromHeader(this._incomingHeader, this); + if (!this._incoming) { + throw new Error( + "No packet types for header: " + this._incomingHeader + ); + } + } + + if (!this._incoming.done) { + // We have an incomplete packet, keep reading it. + dumpv("Existing packet incomplete, keep reading"); + this._incoming.read(stream, this._scriptableInput); + } + } catch (e) { + dump(`Error reading incoming packet: (${e} - ${e.stack})\n`); + + // Now in an invalid state, shut down the transport. + this.close(); + return false; + } + + if (!this._incoming.done) { + // Still not complete, we'll wait for more data. + dumpv("Packet not done, wait for more"); + return true; + } + + // Ready for next packet + this._flushIncoming(); + return true; + }, + + /** + * Read as far as we can into the incoming data, attempting to build + * up a complete packet header (which terminates with ":"). We'll only + * read up to PACKET_HEADER_MAX characters. + * + * @returns {boolean} + * True if we now have a complete header. + */ + _readHeader() { + let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length; + this._incomingHeader += lazy.StreamUtils.delimitedRead( + this._scriptableInput, + ":", + amountToRead + ); + if (flags.wantVerbose) { + dumpv("Header read: " + this._incomingHeader); + } + + if (this._incomingHeader.endsWith(":")) { + if (flags.wantVerbose) { + dumpv("Found packet header successfully: " + this._incomingHeader); + } + return true; + } + + if (this._incomingHeader.length >= PACKET_HEADER_MAX) { + throw new Error("Failed to parse packet header!"); + } + + // Not enough data yet. + return false; + }, + + /** + * If the incoming packet is done, log it as needed and clear the buffer. + */ + _flushIncoming() { + if (!this._incoming.done) { + return; + } + if (flags.wantLogging) { + dumpv("Got: " + this._incoming); + } + this._destroyIncoming(); + }, + + /** + * Handler triggered by an incoming JSONPacket completing it's |read| + * method. Delivers the packet to this.hooks.onPacket. + */ + _onJSONObjectReady(object) { + lazy.executeSoon(() => { + // Ensure the transport is still alive by the time this runs. + if (this.active) { + this.emit("packet", object); + this.hooks.onPacket(object); + } + }); + }, + + /** + * Handler triggered by an incoming BulkPacket entering the |read| + * phase for the stream portion of the packet. Delivers info about the + * incoming streaming data to this.hooks.onBulkPacket. See the main + * comment on the transport at the top of this file for more details. + */ + _onBulkReadReady(...args) { + lazy.executeSoon(() => { + // Ensure the transport is still alive by the time this runs. + if (this.active) { + this.emit("bulkpacket", ...args); + this.hooks.onBulkPacket(...args); + } + }); + }, + + /** + * Remove all handlers and references related to the current incoming + * packet, either because it is now complete or because the transport + * is closing. + */ + _destroyIncoming() { + if (this._incoming) { + this._incoming.destroy(); + } + this._incomingHeader = ""; + this._incoming = null; + }, +}; diff --git a/remote/marionette/web-reference.sys.mjs b/remote/marionette/web-reference.sys.mjs new file mode 100644 index 0000000000..5d2d510265 --- /dev/null +++ b/remote/marionette/web-reference.sys.mjs @@ -0,0 +1,297 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + dom: "chrome://remote/content/shared/DOM.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + pprint: "chrome://remote/content/shared/Format.sys.mjs", +}); + +/** + * A web reference is an abstraction used to identify an element when + * it is transported via the protocol, between remote- and local ends. + * + * In Marionette this abstraction can represent DOM elements, + * WindowProxies, and XUL elements. + */ +export class WebReference { + /** + * @param {string} uuid + * Identifier that must be unique across all browsing contexts + * for the contract to be upheld. + */ + constructor(uuid) { + this.uuid = lazy.assert.string(uuid); + } + + /** + * Performs an equality check between this web element and + * other. + * + * @param {WebReference} other + * Web element to compare with this. + * + * @returns {boolean} + * True if this and other are the same. False + * otherwise. + */ + is(other) { + return other instanceof WebReference && this.uuid === other.uuid; + } + + toString() { + return `[object ${this.constructor.name} uuid=${this.uuid}]`; + } + + /** + * Returns a new {@link WebReference} reference for a DOM or XUL element, + * WindowProxy, or ShadowRoot. + * + * @param {(Element|ShadowRoot|WindowProxy|MockXULElement)} node + * Node to construct a web element reference for. + * @param {string=} uuid + * Optional unique identifier of the WebReference if already known. + * If not defined a new unique identifier will be created. + * + * @returns {WebReference} + * Web reference for node. + * + * @throws {InvalidArgumentError} + * If node is neither a WindowProxy, + * DOM or XUL element, or ShadowRoot. + */ + static from(node, uuid) { + if (uuid === undefined) { + uuid = lazy.generateUUID(); + } + + if (lazy.dom.isShadowRoot(node) && !lazy.dom.isInPrivilegedDocument(node)) { + // When we support Chrome Shadowroots we will need to + // do a check here of shadowroot.host being in a privileged document + // See Bug 1743541 + return new ShadowRoot(uuid); + } else if (lazy.dom.isElement(node)) { + return new WebElement(uuid); + } else if (lazy.dom.isDOMWindow(node)) { + if (node.parent === node) { + return new WebWindow(uuid); + } + return new WebFrame(uuid); + } + + throw new lazy.error.InvalidArgumentError( + "Expected DOM window/element " + lazy.pprint`or XUL element, got: ${node}` + ); + } + + /** + * Unmarshals a JSON Object to one of {@link ShadowRoot}, {@link WebElement}, + * {@link WebFrame}, or {@link WebWindow}. + * + * @param {Object} json + * Web reference, which is supposed to be a JSON Object + * where the key is one of the {@link WebReference} concrete + * classes' UUID identifiers. + * + * @returns {WebReference} + * Web reference for the JSON object. + * + * @throws {InvalidArgumentError} + * If json is not a web reference. + */ + static fromJSON(json) { + lazy.assert.object(json); + if (json instanceof WebReference) { + return json; + } + let keys = Object.keys(json); + + for (let key of keys) { + switch (key) { + case ShadowRoot.Identifier: + return ShadowRoot.fromJSON(json); + + case WebElement.Identifier: + return WebElement.fromJSON(json); + + case WebFrame.Identifier: + return WebFrame.fromJSON(json); + + case WebWindow.Identifier: + return WebWindow.fromJSON(json); + } + } + + throw new lazy.error.InvalidArgumentError( + lazy.pprint`Expected web reference, got: ${json}` + ); + } + + /** + * Checks if obj is a {@link WebReference} reference. + * + * @param {Object} obj + * Object that represents a {@link WebReference}. + * + * @returns {boolean} + * True if obj is a {@link WebReference}, false otherwise. + */ + static isReference(obj) { + if (Object.prototype.toString.call(obj) != "[object Object]") { + return false; + } + + if ( + ShadowRoot.Identifier in obj || + WebElement.Identifier in obj || + WebFrame.Identifier in obj || + WebWindow.Identifier in obj + ) { + return true; + } + return false; + } +} + +/** + * Shadow Root elements are represented as shadow root references when they are + * transported over the wire protocol + */ +export class ShadowRoot extends WebReference { + toJSON() { + return { [ShadowRoot.Identifier]: this.uuid }; + } + + static fromJSON(json) { + const { Identifier } = ShadowRoot; + + if (!(Identifier in json)) { + throw new lazy.error.InvalidArgumentError( + lazy.pprint`Expected shadow root reference, got: ${json}` + ); + } + + let uuid = json[Identifier]; + return new ShadowRoot(uuid); + } + + /** + * Constructs a {@link ShadowRoot} from a string uuid. + * + * This whole function is a workaround for the fact that clients + * to Marionette occasionally pass {id: } JSON + * Objects instead of shadow root representations. + * + * @param {string} uuid + * UUID to be associated with the web reference. + * + * @returns {ShadowRoot} + * The shadow root reference. + * + * @throws {InvalidArgumentError} + * If uuid is not a string. + */ + static fromUUID(uuid) { + lazy.assert.string(uuid); + + return new ShadowRoot(uuid); + } +} + +ShadowRoot.Identifier = "shadow-6066-11e4-a52e-4f735466cecf"; + +/** + * DOM elements are represented as web elements when they are + * transported over the wire protocol. + */ +export class WebElement extends WebReference { + toJSON() { + return { [WebElement.Identifier]: this.uuid }; + } + + static fromJSON(json) { + const { Identifier } = WebElement; + + if (!(Identifier in json)) { + throw new lazy.error.InvalidArgumentError( + lazy.pprint`Expected web element reference, got: ${json}` + ); + } + + let uuid = json[Identifier]; + return new WebElement(uuid); + } + + /** + * Constructs a {@link WebElement} from a string uuid. + * + * This whole function is a workaround for the fact that clients + * to Marionette occasionally pass {id: } JSON + * Objects instead of web element representations. + * + * @param {string} uuid + * UUID to be associated with the web reference. + * + * @returns {WebElement} + * The web element reference. + * + * @throws {InvalidArgumentError} + * If uuid is not a string. + */ + static fromUUID(uuid) { + return new WebElement(uuid); + } +} + +WebElement.Identifier = "element-6066-11e4-a52e-4f735466cecf"; + +/** + * Nested browsing contexts, such as the WindowProxy + * associated with <frame> and <iframe>, + * are represented as web frames over the wire protocol. + */ +export class WebFrame extends WebReference { + toJSON() { + return { [WebFrame.Identifier]: this.uuid }; + } + + static fromJSON(json) { + if (!(WebFrame.Identifier in json)) { + throw new lazy.error.InvalidArgumentError( + lazy.pprint`Expected web frame reference, got: ${json}` + ); + } + let uuid = json[WebFrame.Identifier]; + return new WebFrame(uuid); + } +} + +WebFrame.Identifier = "frame-075b-4da1-b6ba-e579c2d3230a"; + +/** + * Top-level browsing contexts, such as WindowProxy + * whose opener is null, are represented as web windows + * over the wire protocol. + */ +export class WebWindow extends WebReference { + toJSON() { + return { [WebWindow.Identifier]: this.uuid }; + } + + static fromJSON(json) { + if (!(WebWindow.Identifier in json)) { + throw new lazy.error.InvalidArgumentError( + lazy.pprint`Expected web window reference, got: ${json}` + ); + } + let uuid = json[WebWindow.Identifier]; + return new WebWindow(uuid); + } +} + +WebWindow.Identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"; diff --git a/remote/marionette/webauthn.sys.mjs b/remote/marionette/webauthn.sys.mjs new file mode 100644 index 0000000000..c52bf6cb5c --- /dev/null +++ b/remote/marionette/webauthn.sys.mjs @@ -0,0 +1,134 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "webauthnService", + "@mozilla.org/webauthn/service;1", + "nsIWebAuthnService" +); + +/** @namespace */ +export const webauthn = {}; + +/** + * Add a virtual authenticator. + * + * @param {string} protocol one of "ctap1/u2f", "ctap2", "ctap2_1" + * @param {string} transport one of "usb", "nfc", "ble", "smart-card", + * "hybrid", "internal" + * @param {boolean} hasResidentKey + * @param {boolean} hasUserVerification + * @param {boolean} isUserConsenting + * @param {boolean} isUserVerified + * @returns {id} the id of the added authenticator + */ +webauthn.addVirtualAuthenticator = function ( + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified +) { + return lazy.webauthnService.addVirtualAuthenticator( + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified + ); +}; + +/** + * Removes a virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + */ +webauthn.removeVirtualAuthenticator = function (authenticatorId) { + lazy.webauthnService.removeVirtualAuthenticator(authenticatorId); +}; + +/** + * Adds a credential to a previously-added virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + * @param {string} credentialId a probabilistically-unique byte sequence + * identifying a public key credential source and its + * authentication assertions (encoded using Base64url + * Encoding). + * @param {boolean} isResidentCredential if set to true, a client-side + * discoverable credential is created. If set to false, a + * server-side credential is created instead. + * @param {string} rpId The Relying Party ID the credential is scoped to. + * @param {string} privateKey An asymmetric key package containing a single + * private key per RFC5958, encoded using Base64url Encoding. + * @param {string} userHandle The userHandle associated to the credential + * encoded using Base64url Encoding. + * @param {number} signCount The initial value for a signature counter + * associated to the public key credential source. + */ +webauthn.addCredential = function ( + authenticatorId, + credentialId, + isResidentCredential, + rpId, + privateKey, + userHandle, + signCount +) { + lazy.webauthnService.addCredential( + authenticatorId, + credentialId, + isResidentCredential, + rpId, + privateKey, + userHandle, + signCount + ); +}; + +/** + * Gets all credentials from a virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + * @returns {object} the credentials on the authenticator + */ +webauthn.getCredentials = function (authenticatorId) { + return lazy.webauthnService.getCredentials(authenticatorId); +}; + +/** + * Removes a credential from a virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + * @param {string} credentialId the id of the credential + */ +webauthn.removeCredential = function (authenticatorId, credentialId) { + lazy.webauthnService.removeCredential(authenticatorId, credentialId); +}; + +/** + * Removes all credentials from a virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + */ +webauthn.removeAllCredentials = function (authenticatorId) { + lazy.webauthnService.removeAllCredentials(authenticatorId); +}; + +/** + * Sets the "isUserVerified" bit on a virtual authenticator. + * + * @param {id} authenticatorId the id of the virtual authenticator + * @param {bool} isUserVerified the value to set the "isUserVerified" bit to + */ +webauthn.setUserVerified = function (authenticatorId, isUserVerified) { + lazy.webauthnService.setUserVerified(authenticatorId, isUserVerified); +}; -- cgit v1.2.3