summaryrefslogtreecommitdiffstats
path: root/remote/shared/webdriver
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--remote/shared/webdriver/Assert.sys.mjs489
-rw-r--r--remote/shared/webdriver/Capabilities.sys.mjs737
-rw-r--r--remote/shared/webdriver/Errors.sys.mjs559
-rw-r--r--remote/shared/webdriver/KeyData.sys.mjs340
-rw-r--r--remote/shared/webdriver/NodeCache.sys.mjs134
-rw-r--r--remote/shared/webdriver/Session.sys.mjs348
-rw-r--r--remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs95
-rw-r--r--remote/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs39
-rw-r--r--remote/shared/webdriver/test/xpcshell/head.js14
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Assert.js215
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Capabilities.js623
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Errors.js499
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_NodeCache.js123
-rw-r--r--remote/shared/webdriver/test/xpcshell/test_Session.js55
-rw-r--r--remote/shared/webdriver/test/xpcshell/xpcshell.ini12
15 files changed, 4282 insertions, 0 deletions
diff --git a/remote/shared/webdriver/Assert.sys.mjs b/remote/shared/webdriver/Assert.sys.mjs
new file mode 100644
index 0000000000..ed0278308a
--- /dev/null
+++ b/remote/shared/webdriver/Assert.sys.mjs
@@ -0,0 +1,489 @@
+/* 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",
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+});
+
+/**
+ * Shorthands for common assertions made in WebDriver.
+ *
+ * @namespace
+ */
+export const assert = {};
+
+/**
+ * Asserts that WebDriver has an active session.
+ *
+ * @param {WebDriverSession} session
+ * WebDriver session instance.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @throws {InvalidSessionIDError}
+ * If session does not exist, or has an invalid id.
+ */
+assert.session = function(session, msg = "") {
+ msg = msg || "WebDriver session does not exist, or is not active";
+ assert.that(
+ session => session && typeof session.id == "string",
+ msg,
+ lazy.error.InvalidSessionIDError
+ )(session);
+};
+
+/**
+ * Asserts that the current browser is Firefox Desktop.
+ *
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ * If current browser is not Firefox.
+ */
+assert.firefox = function(msg = "") {
+ msg = msg || "Only supported in Firefox";
+ assert.that(
+ isFirefox => isFirefox,
+ msg,
+ lazy.error.UnsupportedOperationError
+ )(lazy.AppInfo.isFirefox);
+};
+
+/**
+ * Asserts that the current application is Firefox Desktop or Thunderbird.
+ *
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ * If current application is not running on desktop.
+ */
+assert.desktop = function(msg = "") {
+ msg = msg || "Only supported in desktop applications";
+ assert.that(
+ isDesktop => isDesktop,
+ msg,
+ lazy.error.UnsupportedOperationError
+ )(!lazy.AppInfo.isAndroid);
+};
+
+/**
+ * Asserts that the current application runs on Android.
+ *
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ * If current application is not running on Android.
+ */
+assert.mobile = function(msg = "") {
+ msg = msg || "Only supported on Android";
+ assert.that(
+ isAndroid => isAndroid,
+ msg,
+ lazy.error.UnsupportedOperationError
+ )(lazy.AppInfo.isAndroid);
+};
+
+/**
+ * Asserts that the current <var>context</var> is content.
+ *
+ * @param {string} context
+ * Context to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {string}
+ * <var>context</var> is returned unaltered.
+ *
+ * @throws {UnsupportedOperationError}
+ * If <var>context</var> is not content.
+ */
+assert.content = function(context, msg = "") {
+ msg = msg || "Only supported in content context";
+ assert.that(
+ c => c.toString() == "content",
+ msg,
+ lazy.error.UnsupportedOperationError
+ )(context);
+};
+
+/**
+ * Asserts that the {@link CanonicalBrowsingContext} is open.
+ *
+ * @param {CanonicalBrowsingContext} browsingContext
+ * Canonical browsing context to check.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {CanonicalBrowsingContext}
+ * <var>browsingContext</var> is returned unaltered.
+ *
+ * @throws {NoSuchWindowError}
+ * If <var>browsingContext</var> is no longer open.
+ */
+assert.open = function(browsingContext, msg = "") {
+ msg = msg || "Browsing context has been discarded";
+ return assert.that(
+ browsingContext => {
+ if (!browsingContext?.currentWindowGlobal) {
+ return false;
+ }
+
+ if (browsingContext.isContent && !browsingContext.top.embedderElement) {
+ return false;
+ }
+
+ return true;
+ },
+ msg,
+ lazy.error.NoSuchWindowError
+ )(browsingContext);
+};
+
+/**
+ * Asserts that there is no current user prompt.
+ *
+ * @param {modal.Dialog} dialog
+ * Reference to current dialogue.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @throws {UnexpectedAlertOpenError}
+ * If there is a user prompt.
+ */
+assert.noUserPrompt = function(dialog, msg = "") {
+ assert.that(
+ d => d === null || typeof d == "undefined",
+ msg,
+ lazy.error.UnexpectedAlertOpenError
+ )(dialog);
+};
+
+/**
+ * Asserts that <var>obj</var> is defined.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {?}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not defined.
+ */
+assert.defined = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be defined`;
+ return assert.that(o => typeof o != "undefined", msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a finite number.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a number.
+ */
+assert.number = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be finite number`;
+ return assert.that(Number.isFinite, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a positive number.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a positive integer.
+ */
+assert.positiveNumber = function(obj, msg = "") {
+ assert.number(obj, msg);
+ msg = msg || lazy.pprint`Expected ${obj} to be >= 0`;
+ return assert.that(n => n >= 0, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a number in the inclusive range <var>lower</var> to <var>upper</var>.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {Array<number>} Range
+ * Array range [lower, upper]
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a number in the specified range.
+ */
+assert.numberInRange = function(obj, range, msg = "") {
+ const [lower, upper] = range;
+ assert.number(obj, msg);
+ msg = msg || lazy.pprint`Expected ${obj} to be >= ${lower} and <= ${upper}`;
+ return assert.that(n => n >= lower && n <= upper, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is callable.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {Function}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not callable.
+ */
+assert.callable = function(obj, msg = "") {
+ msg = msg || lazy.pprint`${obj} is not callable`;
+ return assert.that(o => typeof o == "function", msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is an unsigned short number.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not an unsigned short.
+ */
+assert.unsignedShort = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be >= 0 and < 65536`;
+ return assert.that(n => n >= 0 && n < 65536, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is an integer.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not an integer.
+ */
+assert.integer = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be an integer`;
+ return assert.that(Number.isSafeInteger, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a positive integer.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a positive integer.
+ */
+assert.positiveInteger = function(obj, msg = "") {
+ assert.integer(obj, msg);
+ msg = msg || lazy.pprint`Expected ${obj} to be >= 0`;
+ return assert.that(n => n >= 0, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is an integer in the inclusive range <var>lower</var> to <var>upper</var>.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {Array<number>} Range
+ * Array range [lower, upper]
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {number}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a number in the specified range.
+ */
+assert.integerInRange = function(obj, range, msg = "") {
+ const [lower, upper] = range;
+ assert.integer(obj, msg);
+ msg = msg || lazy.pprint`Expected ${obj} to be >= ${lower} and <= ${upper}`;
+ return assert.that(n => n >= lower && n <= upper, msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a boolean.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {boolean}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a boolean.
+ */
+assert.boolean = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be boolean`;
+ return assert.that(b => typeof b == "boolean", msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is a string.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {string}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not a string.
+ */
+assert.string = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be a string`;
+ return assert.that(s => typeof s == "string", msg)(obj);
+};
+
+/**
+ * Asserts that <var>obj</var> is an object.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {Object}
+ * obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not an object.
+ */
+assert.object = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be an object`;
+ return assert.that(o => {
+ // unable to use instanceof because LHS and RHS may come from
+ // different globals
+ let s = Object.prototype.toString.call(o);
+ return s == "[object Object]" || s == "[object nsJSIID]";
+ }, msg)(obj);
+};
+
+/**
+ * Asserts that <var>prop</var> is in <var>obj</var>.
+ *
+ * @param {?} prop
+ * An array element or own property to test if is in <var>obj</var>.
+ * @param {?} obj
+ * An array or an Object that is being tested.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {?}
+ * The array element, or the value of <var>obj</var>'s own property
+ * <var>prop</var>.
+ *
+ * @throws {InvalidArgumentError}
+ * If the <var>obj</var> was an array and did not contain <var>prop</var>.
+ * Otherwise if <var>prop</var> is not in <var>obj</var>, or <var>obj</var>
+ * is not an object.
+ */
+assert.in = function(prop, obj, msg = "") {
+ if (Array.isArray(obj)) {
+ assert.that(p => obj.includes(p), msg)(prop);
+ return prop;
+ }
+ assert.object(obj, msg);
+ msg = msg || lazy.pprint`Expected ${prop} in ${obj}`;
+ assert.that(p => obj.hasOwnProperty(p), msg)(prop);
+ return obj[prop];
+};
+
+/**
+ * Asserts that <var>obj</var> is an Array.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {Object}
+ * <var>obj</var> is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>obj</var> is not an Array.
+ */
+assert.array = function(obj, msg = "") {
+ msg = msg || lazy.pprint`Expected ${obj} to be an Array`;
+ return assert.that(Array.isArray, msg)(obj);
+};
+
+/**
+ * Returns a function that is used to assert the |predicate|.
+ *
+ * @param {function(?): boolean} predicate
+ * Evaluated on calling the return value of this function. If its
+ * return value of the inner function is false, <var>error</var>
+ * is thrown with <var>message</var>.
+ * @param {string=} message
+ * Custom error message.
+ * @param {Error=} error
+ * Custom error type by its class.
+ *
+ * @return {function(?): ?}
+ * Function that takes and returns the passed in value unaltered,
+ * and which may throw <var>error</var> with <var>message</var>
+ * if <var>predicate</var> evaluates to false.
+ */
+assert.that = function(
+ predicate,
+ message = "",
+ err = lazy.error.InvalidArgumentError
+) {
+ return obj => {
+ if (!predicate(obj)) {
+ throw new err(message);
+ }
+ return obj;
+ };
+};
diff --git a/remote/shared/webdriver/Capabilities.sys.mjs b/remote/shared/webdriver/Capabilities.sys.mjs
new file mode 100644
index 0000000000..7a1552746d
--- /dev/null
+++ b/remote/shared/webdriver/Capabilities.sys.mjs
@@ -0,0 +1,737 @@
+/* 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 = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+
+ AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+ RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "remoteAgent", () => {
+ return Cc["@mozilla.org/remote/agent;1"].createInstance(Ci.nsIRemoteAgent);
+});
+
+/** Representation of WebDriver session timeouts. */
+export class Timeouts {
+ constructor() {
+ // disabled
+ this.implicit = 0;
+ // five minutes
+ this.pageLoad = 300000;
+ // 30 seconds
+ this.script = 30000;
+ }
+
+ toString() {
+ return "[object Timeouts]";
+ }
+
+ /** Marshals timeout durations to a JSON Object. */
+ toJSON() {
+ return {
+ implicit: this.implicit,
+ pageLoad: this.pageLoad,
+ script: this.script,
+ };
+ }
+
+ static fromJSON(json) {
+ lazy.assert.object(
+ json,
+ lazy.pprint`Expected "timeouts" to be an object, got ${json}`
+ );
+ let t = new Timeouts();
+
+ for (let [type, ms] of Object.entries(json)) {
+ switch (type) {
+ case "implicit":
+ t.implicit = lazy.assert.positiveInteger(
+ ms,
+ lazy.pprint`Expected ${type} to be a positive integer, got ${ms}`
+ );
+ break;
+
+ case "script":
+ if (ms !== null) {
+ lazy.assert.positiveInteger(
+ ms,
+ lazy.pprint`Expected ${type} to be a positive integer, got ${ms}`
+ );
+ }
+ t.script = ms;
+ break;
+
+ case "pageLoad":
+ t.pageLoad = lazy.assert.positiveInteger(
+ ms,
+ lazy.pprint`Expected ${type} to be a positive integer, got ${ms}`
+ );
+ break;
+
+ default:
+ throw new lazy.error.InvalidArgumentError(
+ "Unrecognised timeout: " + type
+ );
+ }
+ }
+
+ return t;
+ }
+}
+
+/**
+ * Enum of page loading strategies.
+ *
+ * @enum
+ */
+export const PageLoadStrategy = {
+ /** No page load strategy. Navigation will return immediately. */
+ None: "none",
+ /**
+ * Eager, causing navigation to complete when the document reaches
+ * the <code>interactive</code> ready state.
+ */
+ Eager: "eager",
+ /**
+ * Normal, causing navigation to return when the document reaches the
+ * <code>complete</code> ready state.
+ */
+ Normal: "normal",
+};
+
+/** Proxy configuration object representation. */
+export class Proxy {
+ /** @class */
+ constructor() {
+ this.proxyType = null;
+ this.httpProxy = null;
+ this.httpProxyPort = null;
+ this.noProxy = null;
+ this.sslProxy = null;
+ this.sslProxyPort = null;
+ this.socksProxy = null;
+ this.socksProxyPort = null;
+ this.socksVersion = null;
+ this.proxyAutoconfigUrl = null;
+ }
+
+ /**
+ * Sets Firefox proxy settings.
+ *
+ * @return {boolean}
+ * True if proxy settings were updated as a result of calling this
+ * function, or false indicating that this function acted as
+ * a no-op.
+ */
+ init() {
+ switch (this.proxyType) {
+ case "autodetect":
+ lazy.Preferences.set("network.proxy.type", 4);
+ return true;
+
+ case "direct":
+ lazy.Preferences.set("network.proxy.type", 0);
+ return true;
+
+ case "manual":
+ lazy.Preferences.set("network.proxy.type", 1);
+
+ if (this.httpProxy) {
+ lazy.Preferences.set("network.proxy.http", this.httpProxy);
+ if (Number.isInteger(this.httpProxyPort)) {
+ lazy.Preferences.set("network.proxy.http_port", this.httpProxyPort);
+ }
+ }
+
+ if (this.sslProxy) {
+ lazy.Preferences.set("network.proxy.ssl", this.sslProxy);
+ if (Number.isInteger(this.sslProxyPort)) {
+ lazy.Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
+ }
+ }
+
+ if (this.socksProxy) {
+ lazy.Preferences.set("network.proxy.socks", this.socksProxy);
+ if (Number.isInteger(this.socksProxyPort)) {
+ lazy.Preferences.set(
+ "network.proxy.socks_port",
+ this.socksProxyPort
+ );
+ }
+ if (this.socksVersion) {
+ lazy.Preferences.set(
+ "network.proxy.socks_version",
+ this.socksVersion
+ );
+ }
+ }
+
+ if (this.noProxy) {
+ lazy.Preferences.set(
+ "network.proxy.no_proxies_on",
+ this.noProxy.join(", ")
+ );
+ }
+ return true;
+
+ case "pac":
+ lazy.Preferences.set("network.proxy.type", 2);
+ lazy.Preferences.set(
+ "network.proxy.autoconfig_url",
+ this.proxyAutoconfigUrl
+ );
+ return true;
+
+ case "system":
+ lazy.Preferences.set("network.proxy.type", 5);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * @param {Object.<string, ?>} json
+ * JSON Object to unmarshal.
+ *
+ * @throws {InvalidArgumentError}
+ * When proxy configuration is invalid.
+ */
+ static fromJSON(json) {
+ function stripBracketsFromIpv6Hostname(hostname) {
+ return hostname.includes(":")
+ ? hostname.replace(/[\[\]]/g, "")
+ : hostname;
+ }
+
+ // Parse hostname and optional port from host
+ function fromHost(scheme, host) {
+ lazy.assert.string(
+ host,
+ lazy.pprint`Expected proxy "host" to be a string, got ${host}`
+ );
+
+ if (host.includes("://")) {
+ throw new lazy.error.InvalidArgumentError(`${host} contains a scheme`);
+ }
+
+ let url;
+ try {
+ // To parse the host a scheme has to be added temporarily.
+ // If the returned value for the port is an empty string it
+ // could mean no port or the default port for this scheme was
+ // specified. In such a case parse again with a different
+ // scheme to ensure we filter out the default port.
+ url = new URL("http://" + host);
+ if (url.port == "") {
+ url = new URL("https://" + host);
+ }
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(e.message);
+ }
+
+ let hostname = stripBracketsFromIpv6Hostname(url.hostname);
+
+ // If the port hasn't been set, use the default port of
+ // the selected scheme (except for socks which doesn't have one).
+ let port = parseInt(url.port);
+ if (!Number.isInteger(port)) {
+ if (scheme === "socks") {
+ port = null;
+ } else {
+ port = Services.io.getDefaultPort(scheme);
+ }
+ }
+
+ if (
+ url.username != "" ||
+ url.password != "" ||
+ url.pathname != "/" ||
+ url.search != "" ||
+ url.hash != ""
+ ) {
+ throw new lazy.error.InvalidArgumentError(
+ `${host} was not of the form host[:port]`
+ );
+ }
+
+ return [hostname, port];
+ }
+
+ let p = new Proxy();
+ if (typeof json == "undefined" || json === null) {
+ return p;
+ }
+
+ lazy.assert.object(
+ json,
+ lazy.pprint`Expected "proxy" to be an object, got ${json}`
+ );
+
+ lazy.assert.in(
+ "proxyType",
+ json,
+ lazy.pprint`Expected "proxyType" in "proxy" object, got ${json}`
+ );
+ p.proxyType = lazy.assert.string(
+ json.proxyType,
+ lazy.pprint`Expected "proxyType" to be a string, got ${json.proxyType}`
+ );
+
+ switch (p.proxyType) {
+ case "autodetect":
+ case "direct":
+ case "system":
+ break;
+
+ case "pac":
+ p.proxyAutoconfigUrl = lazy.assert.string(
+ json.proxyAutoconfigUrl,
+ `Expected "proxyAutoconfigUrl" to be a string, ` +
+ lazy.pprint`got ${json.proxyAutoconfigUrl}`
+ );
+ break;
+
+ case "manual":
+ if (typeof json.ftpProxy != "undefined") {
+ throw new lazy.error.InvalidArgumentError(
+ "Since Firefox 90 'ftpProxy' is no longer supported"
+ );
+ }
+ if (typeof json.httpProxy != "undefined") {
+ [p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy);
+ }
+ if (typeof json.sslProxy != "undefined") {
+ [p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy);
+ }
+ if (typeof json.socksProxy != "undefined") {
+ [p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy);
+ p.socksVersion = lazy.assert.positiveInteger(
+ json.socksVersion,
+ lazy.pprint`Expected "socksVersion" to be a positive integer, got ${json.socksVersion}`
+ );
+ }
+ if (typeof json.noProxy != "undefined") {
+ let entries = lazy.assert.array(
+ json.noProxy,
+ lazy.pprint`Expected "noProxy" to be an array, got ${json.noProxy}`
+ );
+ p.noProxy = entries.map(entry => {
+ lazy.assert.string(
+ entry,
+ lazy.pprint`Expected "noProxy" entry to be a string, got ${entry}`
+ );
+ return stripBracketsFromIpv6Hostname(entry);
+ });
+ }
+ break;
+
+ default:
+ throw new lazy.error.InvalidArgumentError(
+ `Invalid type of proxy: ${p.proxyType}`
+ );
+ }
+
+ return p;
+ }
+
+ /**
+ * @return {Object.<string, (number|string)>}
+ * JSON serialisation of proxy object.
+ */
+ toJSON() {
+ function addBracketsToIpv6Hostname(hostname) {
+ return hostname.includes(":") ? `[${hostname}]` : hostname;
+ }
+
+ function toHost(hostname, port) {
+ if (!hostname) {
+ return null;
+ }
+
+ // Add brackets around IPv6 addresses
+ hostname = addBracketsToIpv6Hostname(hostname);
+
+ if (port != null) {
+ return `${hostname}:${port}`;
+ }
+
+ return hostname;
+ }
+
+ let excludes = this.noProxy;
+ if (excludes) {
+ excludes = excludes.map(addBracketsToIpv6Hostname);
+ }
+
+ return marshal({
+ proxyType: this.proxyType,
+ httpProxy: toHost(this.httpProxy, this.httpProxyPort),
+ noProxy: excludes,
+ sslProxy: toHost(this.sslProxy, this.sslProxyPort),
+ socksProxy: toHost(this.socksProxy, this.socksProxyPort),
+ socksVersion: this.socksVersion,
+ proxyAutoconfigUrl: this.proxyAutoconfigUrl,
+ });
+ }
+
+ toString() {
+ return "[object Proxy]";
+ }
+}
+
+/**
+ * Enum of unhandled prompt behavior.
+ *
+ * @enum
+ */
+export const UnhandledPromptBehavior = {
+ /** All simple dialogs encountered should be accepted. */
+ Accept: "accept",
+ /**
+ * All simple dialogs encountered should be accepted, and an error
+ * returned that the dialog was handled.
+ */
+ AcceptAndNotify: "accept and notify",
+ /** All simple dialogs encountered should be dismissed. */
+ Dismiss: "dismiss",
+ /**
+ * All simple dialogs encountered should be dismissed, and an error
+ * returned that the dialog was handled.
+ */
+ DismissAndNotify: "dismiss and notify",
+ /** All simple dialogs encountered should be left to the user to handle. */
+ Ignore: "ignore",
+};
+
+/** WebDriver session capabilities representation. */
+export class Capabilities extends Map {
+ /** @class */
+ constructor() {
+ super([
+ // webdriver
+ ["browserName", getWebDriverBrowserName()],
+ ["browserVersion", lazy.AppInfo.version],
+ ["platformName", getWebDriverPlatformName()],
+ ["acceptInsecureCerts", false],
+ ["pageLoadStrategy", PageLoadStrategy.Normal],
+ ["proxy", new Proxy()],
+ ["setWindowRect", !lazy.AppInfo.isAndroid],
+ ["timeouts", new Timeouts()],
+ ["strictFileInteractability", false],
+ ["unhandledPromptBehavior", UnhandledPromptBehavior.DismissAndNotify],
+ ["webSocketUrl", null],
+
+ // proprietary
+ ["moz:accessibilityChecks", false],
+ ["moz:buildID", lazy.AppInfo.appBuildID],
+ [
+ "moz:debuggerAddress",
+ // With bug 1715481 fixed always use the Remote Agent instance
+ lazy.RemoteAgent.running && lazy.RemoteAgent.cdp
+ ? lazy.remoteAgent.debuggerAddress
+ : null,
+ ],
+ [
+ "moz:headless",
+ Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless,
+ ],
+ ["moz:platformVersion", Services.sysinfo.getProperty("version")],
+ ["moz:processID", lazy.AppInfo.processID],
+ ["moz:profile", maybeProfile()],
+ [
+ "moz:shutdownTimeout",
+ Services.prefs.getIntPref("toolkit.asyncshutdown.crash_timeout"),
+ ],
+ ["moz:useNonSpecCompliantPointerOrigin", false],
+ ["moz:webdriverClick", true],
+ ["moz:windowless", false],
+ ]);
+ }
+
+ /**
+ * @param {string} key
+ * Capability key.
+ * @param {(string|number|boolean)} value
+ * JSON-safe capability value.
+ */
+ set(key, value) {
+ if (key === "timeouts" && !(value instanceof Timeouts)) {
+ throw new TypeError();
+ } else if (key === "proxy" && !(value instanceof Proxy)) {
+ throw new TypeError();
+ }
+
+ return super.set(key, value);
+ }
+
+ toString() {
+ return "[object Capabilities]";
+ }
+
+ /**
+ * JSON serialisation of capabilities object.
+ *
+ * @return {Object.<string, ?>}
+ */
+ toJSON() {
+ let marshalled = marshal(this);
+
+ // Always return the proxy capability even if it's empty
+ if (!("proxy" in marshalled)) {
+ marshalled.proxy = {};
+ }
+
+ marshalled.timeouts = super.get("timeouts");
+
+ return marshalled;
+ }
+
+ /**
+ * Unmarshal a JSON object representation of WebDriver capabilities.
+ *
+ * @param {Object.<string, *>=} json
+ * WebDriver capabilities.
+ *
+ * @return {Capabilities}
+ * Internal representation of WebDriver capabilities.
+ */
+ static fromJSON(json) {
+ if (typeof json == "undefined" || json === null) {
+ json = {};
+ }
+ lazy.assert.object(
+ json,
+ lazy.pprint`Expected "capabilities" to be an object, got ${json}"`
+ );
+
+ return Capabilities.match_(json);
+ }
+
+ // Matches capabilities as described by WebDriver.
+ static match_(json = {}) {
+ let matched = new Capabilities();
+
+ for (let [k, v] of Object.entries(json)) {
+ switch (k) {
+ case "acceptInsecureCerts":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be a boolean, got ${v}`
+ );
+ break;
+
+ case "pageLoadStrategy":
+ lazy.assert.string(
+ v,
+ lazy.pprint`Expected ${k} to be a string, got ${v}`
+ );
+ if (!Object.values(PageLoadStrategy).includes(v)) {
+ throw new lazy.error.InvalidArgumentError(
+ "Unknown page load strategy: " + v
+ );
+ }
+ break;
+
+ case "proxy":
+ v = Proxy.fromJSON(v);
+ break;
+
+ case "setWindowRect":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+ if (!lazy.AppInfo.isAndroid && !v) {
+ throw new lazy.error.InvalidArgumentError(
+ "setWindowRect cannot be disabled"
+ );
+ } else if (lazy.AppInfo.isAndroid && v) {
+ throw new lazy.error.InvalidArgumentError(
+ "setWindowRect is only supported on desktop"
+ );
+ }
+ break;
+
+ case "timeouts":
+ v = Timeouts.fromJSON(v);
+ break;
+
+ case "strictFileInteractability":
+ v = lazy.assert.boolean(v);
+ break;
+
+ case "unhandledPromptBehavior":
+ lazy.assert.string(
+ v,
+ lazy.pprint`Expected ${k} to be a string, got ${v}`
+ );
+ if (!Object.values(UnhandledPromptBehavior).includes(v)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Unknown unhandled prompt behavior: ${v}`
+ );
+ }
+ break;
+
+ case "webSocketUrl":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+
+ if (!v) {
+ throw new lazy.error.InvalidArgumentError(
+ lazy.pprint`Expected ${k} to be true, got ${v}`
+ );
+ }
+ break;
+
+ case "moz:accessibilityChecks":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+ break;
+
+ // Don't set the value because it's only used to return the address
+ // of the Remote Agent's debugger (HTTP server).
+ case "moz:debuggerAddress":
+ continue;
+
+ case "moz:useNonSpecCompliantPointerOrigin":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+ break;
+
+ case "moz:webdriverClick":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+ break;
+
+ case "moz:windowless":
+ lazy.assert.boolean(
+ v,
+ lazy.pprint`Expected ${k} to be boolean, got ${v}`
+ );
+
+ // Only supported on MacOS
+ if (v && !lazy.AppInfo.isMac) {
+ throw new lazy.error.InvalidArgumentError(
+ "moz:windowless only supported on MacOS"
+ );
+ }
+ break;
+ }
+
+ matched.set(k, v);
+ }
+
+ return matched;
+ }
+}
+
+function getWebDriverBrowserName() {
+ // Similar to chromedriver which reports "chrome" as browser name for all
+ // WebView apps, we will report "firefox" for all GeckoView apps.
+ if (lazy.AppInfo.isAndroid) {
+ return "firefox";
+ }
+
+ return lazy.AppInfo.name?.toLowerCase();
+}
+
+function getWebDriverPlatformName() {
+ let name = Services.sysinfo.getProperty("name");
+
+ if (lazy.AppInfo.isAndroid) {
+ return "android";
+ }
+
+ switch (name) {
+ case "Windows_NT":
+ return "windows";
+
+ case "Darwin":
+ return "mac";
+
+ default:
+ return name.toLowerCase();
+ }
+}
+
+// Specialisation of |JSON.stringify| that produces JSON-safe object
+// literals, dropping empty objects and entries which values are undefined
+// or null. Objects are allowed to produce their own JSON representations
+// by implementing a |toJSON| function.
+function marshal(obj) {
+ let rv = Object.create(null);
+
+ function* iter(mapOrObject) {
+ if (mapOrObject instanceof Map) {
+ for (const [k, v] of mapOrObject) {
+ yield [k, v];
+ }
+ } else {
+ for (const k of Object.keys(mapOrObject)) {
+ yield [k, mapOrObject[k]];
+ }
+ }
+ }
+
+ for (let [k, v] of iter(obj)) {
+ // Skip empty values when serialising to JSON.
+ if (typeof v == "undefined" || v === null) {
+ continue;
+ }
+
+ // Recursively marshal objects that are able to produce their own
+ // JSON representation.
+ if (typeof v.toJSON == "function") {
+ v = marshal(v.toJSON());
+
+ // Or do the same for object literals.
+ } else if (isObject(v)) {
+ v = marshal(v);
+ }
+
+ // And finally drop (possibly marshaled) objects which have no
+ // entries.
+ if (!isObjectEmpty(v)) {
+ rv[k] = v;
+ }
+ }
+
+ return rv;
+}
+
+function isObject(obj) {
+ return Object.prototype.toString.call(obj) == "[object Object]";
+}
+
+function isObjectEmpty(obj) {
+ return isObject(obj) && Object.keys(obj).length === 0;
+}
+
+// Services.dirsvc is not accessible from JSWindowActor child,
+// but we should not panic about that.
+function maybeProfile() {
+ try {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ } catch (e) {
+ return "<protected>";
+ }
+}
diff --git a/remote/shared/webdriver/Errors.sys.mjs b/remote/shared/webdriver/Errors.sys.mjs
new file mode 100644
index 0000000000..ddc88ae05f
--- /dev/null
+++ b/remote/shared/webdriver/Errors.sys.mjs
@@ -0,0 +1,559 @@
+/* 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 { RemoteError } from "chrome://remote/content/shared/RemoteError.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+});
+
+const ERRORS = new Set([
+ "DetachedShadowRootError",
+ "ElementClickInterceptedError",
+ "ElementNotAccessibleError",
+ "ElementNotInteractableError",
+ "InsecureCertificateError",
+ "InvalidArgumentError",
+ "InvalidCookieDomainError",
+ "InvalidElementStateError",
+ "InvalidSelectorError",
+ "InvalidSessionIDError",
+ "JavaScriptError",
+ "MoveTargetOutOfBoundsError",
+ "NoSuchAlertError",
+ "NoSuchElementError",
+ "NoSuchFrameError",
+ "NoSuchShadowRootError",
+ "NoSuchWindowError",
+ "ScriptTimeoutError",
+ "SessionNotCreatedError",
+ "StaleElementReferenceError",
+ "TimeoutError",
+ "UnableToSetCookieError",
+ "UnexpectedAlertOpenError",
+ "UnknownCommandError",
+ "UnknownError",
+ "UnsupportedOperationError",
+ "WebDriverError",
+]);
+
+const BUILTIN_ERRORS = new Set([
+ "Error",
+ "EvalError",
+ "InternalError",
+ "RangeError",
+ "ReferenceError",
+ "SyntaxError",
+ "TypeError",
+ "URIError",
+]);
+
+/** @namespace */
+export const error = {
+ /**
+ * Check if ``val`` is an instance of the ``Error`` prototype.
+ *
+ * Because error objects may originate from different globals, comparing
+ * the prototype of the left hand side with the prototype property from
+ * the right hand side, which is what ``instanceof`` does, will not work.
+ * If the LHS and RHS come from different globals, this check will always
+ * fail because the two objects will not have the same identity.
+ *
+ * Therefore it is not safe to use ``instanceof`` in any multi-global
+ * situation, e.g. in content across multiple ``Window`` objects or anywhere
+ * in chrome scope.
+ *
+ * This function also contains a special check if ``val`` is an XPCOM
+ * ``nsIException`` because they are special snowflakes and may indeed
+ * cause Firefox to crash if used with ``instanceof``.
+ *
+ * @param {*} val
+ * Any value that should be undergo the test for errorness.
+ * @return {boolean}
+ * True if error, false otherwise.
+ */
+ isError(val) {
+ if (val === null || typeof val != "object") {
+ return false;
+ } else if (val instanceof Ci.nsIException) {
+ return true;
+ }
+
+ // DOMRectList errors on string comparison
+ try {
+ let proto = Object.getPrototypeOf(val);
+ return BUILTIN_ERRORS.has(proto.toString());
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Checks if ``obj`` is an object in the :js:class:`WebDriverError`
+ * prototypal chain.
+ *
+ * @param {*} obj
+ * Arbitrary object to test.
+ *
+ * @return {boolean}
+ * True if ``obj`` is of the WebDriverError prototype chain,
+ * false otherwise.
+ */
+ isWebDriverError(obj) {
+ // Don't use "instanceof" to compare error objects because of possible
+ // problems when the other instance was created in a different global and
+ // as such won't have the same prototype object.
+ return error.isError(obj) && "name" in obj && ERRORS.has(obj.name);
+ },
+
+ /**
+ * Ensures error instance is a :js:class:`WebDriverError`.
+ *
+ * If the given error is already in the WebDriverError prototype
+ * chain, ``err`` is returned unmodified. If it is not, it is wrapped
+ * in :js:class:`UnknownError`.
+ *
+ * @param {Error} err
+ * Error to conditionally turn into a WebDriverError.
+ *
+ * @return {WebDriverError}
+ * If ``err`` is a WebDriverError, it is returned unmodified.
+ * Otherwise an UnknownError type is returned.
+ */
+ wrap(err) {
+ if (error.isWebDriverError(err)) {
+ return err;
+ }
+ return new UnknownError(err);
+ },
+
+ /**
+ * Unhandled error reporter. Dumps the error and its stacktrace to console,
+ * and reports error to the Browser Console.
+ */
+ report(err) {
+ let msg = "Marionette threw an error: " + error.stringify(err);
+ dump(msg + "\n");
+ console.error(msg);
+ },
+
+ /**
+ * Prettifies an instance of Error and its stacktrace to a string.
+ */
+ stringify(err) {
+ try {
+ let s = err.toString();
+ if ("stack" in err) {
+ s += "\n" + err.stack;
+ }
+ return s;
+ } catch (e) {
+ return "<unprintable error>";
+ }
+ },
+
+ /** Create a stacktrace to the current line in the program. */
+ stack() {
+ let trace = new Error().stack;
+ let sa = trace.split("\n");
+ sa = sa.slice(1);
+ let rv = "stacktrace:\n" + sa.join("\n");
+ return rv.trimEnd();
+ },
+};
+
+/**
+ * WebDriverError is the prototypal parent of all WebDriver errors.
+ * It should not be used directly, as it does not correspond to a real
+ * error in the specification.
+ */
+class WebDriverError extends RemoteError {
+ /**
+ * @param {(string|Error)=} x
+ * Optional string describing error situation or Error instance
+ * to propagate.
+ */
+ constructor(x) {
+ super(x);
+ this.name = this.constructor.name;
+ this.status = "webdriver error";
+
+ // Error's ctor does not preserve x' stack
+ if (error.isError(x)) {
+ this.stack = x.stack;
+ }
+ }
+
+ /**
+ * @return {Object.<string, string>}
+ * JSON serialisation of error prototype.
+ */
+ toJSON() {
+ return {
+ error: this.status,
+ message: this.message || "",
+ stacktrace: this.stack || "",
+ };
+ }
+
+ /**
+ * Unmarshals a JSON error representation to the appropriate Marionette
+ * error type.
+ *
+ * @param {Object.<string, string>} json
+ * Error object.
+ *
+ * @return {Error}
+ * Error prototype.
+ */
+ static fromJSON(json) {
+ if (typeof json.error == "undefined") {
+ let s = JSON.stringify(json);
+ throw new TypeError("Undeserialisable error type: " + s);
+ }
+ if (!STATUSES.has(json.error)) {
+ throw new TypeError("Not of WebDriverError descent: " + json.error);
+ }
+
+ let cls = STATUSES.get(json.error);
+ let err = new cls();
+ if ("message" in json) {
+ err.message = json.message;
+ }
+ if ("stacktrace" in json) {
+ err.stack = json.stacktrace;
+ }
+ return err;
+ }
+}
+
+/** The Gecko a11y API indicates that the element is not accessible. */
+class ElementNotAccessibleError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "element not accessible";
+ }
+}
+
+/**
+ * An element click could not be completed because the element receiving
+ * the events is obscuring the element that was requested clicked.
+ *
+ * @param {Element=} obscuredEl
+ * Element obscuring the element receiving the click. Providing this
+ * is not required, but will produce a nicer error message.
+ * @param {Map.<string, number>} coords
+ * Original click location. Providing this is not required, but
+ * will produce a nicer error message.
+ */
+class ElementClickInterceptedError extends WebDriverError {
+ constructor(obscuredEl = undefined, coords = undefined) {
+ let msg = "";
+ if (obscuredEl && coords) {
+ const doc = obscuredEl.ownerDocument;
+ const overlayingEl = doc.elementFromPoint(coords.x, coords.y);
+
+ switch (obscuredEl.style.pointerEvents) {
+ case "none":
+ msg =
+ lazy.pprint`Element ${obscuredEl} is not clickable ` +
+ `at point (${coords.x},${coords.y}) ` +
+ `because it does not have pointer events enabled, ` +
+ lazy.pprint`and element ${overlayingEl} ` +
+ `would receive the click instead`;
+ break;
+
+ default:
+ msg =
+ lazy.pprint`Element ${obscuredEl} is not clickable ` +
+ `at point (${coords.x},${coords.y}) ` +
+ lazy.pprint`because another element ${overlayingEl} ` +
+ `obscures it`;
+ break;
+ }
+ }
+
+ super(msg);
+ this.status = "element click intercepted";
+ }
+}
+
+/**
+ * A command could not be completed because the element is not pointer-
+ * or keyboard interactable.
+ */
+class ElementNotInteractableError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "element not interactable";
+ }
+}
+
+/**
+ * Navigation caused the user agent to hit a certificate warning, which
+ * is usually the result of an expired or invalid TLS certificate.
+ */
+class InsecureCertificateError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "insecure certificate";
+ }
+}
+
+/** The arguments passed to a command are either invalid or malformed. */
+class InvalidArgumentError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "invalid argument";
+ }
+}
+
+/**
+ * An illegal attempt was made to set a cookie under a different
+ * domain than the current page.
+ */
+class InvalidCookieDomainError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "invalid cookie domain";
+ }
+}
+
+/**
+ * A command could not be completed because the element is in an
+ * invalid state, e.g. attempting to clear an element that isn't both
+ * editable and resettable.
+ */
+class InvalidElementStateError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "invalid element state";
+ }
+}
+
+/** Argument was an invalid selector. */
+class InvalidSelectorError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "invalid selector";
+ }
+}
+
+/**
+ * Occurs if the given session ID is not in the list of active sessions,
+ * meaning the session either does not exist or that it's not active.
+ */
+class InvalidSessionIDError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "invalid session id";
+ }
+}
+
+/** An error occurred whilst executing JavaScript supplied by the user. */
+class JavaScriptError extends WebDriverError {
+ constructor(x) {
+ super(x);
+ this.status = "javascript error";
+ }
+}
+
+/**
+ * The target for mouse interaction is not in the browser's viewport
+ * and cannot be brought into that viewport.
+ */
+class MoveTargetOutOfBoundsError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "move target out of bounds";
+ }
+}
+
+/**
+ * An attempt was made to operate on a modal dialog when one was
+ * not open.
+ */
+class NoSuchAlertError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "no such alert";
+ }
+}
+
+/**
+ * An element could not be located on the page using the given
+ * search parameters.
+ */
+class NoSuchElementError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "no such element";
+ }
+}
+
+/**
+ * A shadow root was not attached to the element.
+ */
+class NoSuchShadowRootError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "no such shadow root";
+ }
+}
+
+/**
+ * A shadow root is no longer attached to the document
+ */
+class DetachedShadowRootError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "detached shadow root";
+ }
+}
+
+/**
+ * A command to switch to a frame could not be satisfied because
+ * the frame could not be found.
+ */
+class NoSuchFrameError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "no such frame";
+ }
+}
+
+/**
+ * A command to switch to a window could not be satisfied because
+ * the window could not be found.
+ */
+class NoSuchWindowError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "no such window";
+ }
+}
+
+/** A script did not complete before its timeout expired. */
+class ScriptTimeoutError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "script timeout";
+ }
+}
+
+/** A new session could not be created. */
+class SessionNotCreatedError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "session not created";
+ }
+}
+
+/**
+ * A command failed because the referenced element is no longer
+ * attached to the DOM.
+ */
+class StaleElementReferenceError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "stale element reference";
+ }
+}
+
+/** An operation did not complete before its timeout expired. */
+class TimeoutError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "timeout";
+ }
+}
+
+/** A command to set a cookie's value could not be satisfied. */
+class UnableToSetCookieError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "unable to set cookie";
+ }
+}
+
+/** A modal dialog was open, blocking this operation. */
+class UnexpectedAlertOpenError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "unexpected alert open";
+ }
+}
+
+/**
+ * A command could not be executed because the remote end is not
+ * aware of it.
+ */
+class UnknownCommandError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "unknown command";
+ }
+}
+
+/**
+ * An unknown error occurred in the remote end while processing
+ * the command.
+ */
+class UnknownError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "unknown error";
+ }
+}
+
+/**
+ * Indicates that a command that should have executed properly
+ * cannot be supported for some reason.
+ */
+class UnsupportedOperationError extends WebDriverError {
+ constructor(message) {
+ super(message);
+ this.status = "unsupported operation";
+ }
+}
+
+const STATUSES = new Map([
+ ["detached shadow root", DetachedShadowRootError],
+ ["element click intercepted", ElementClickInterceptedError],
+ ["element not accessible", ElementNotAccessibleError],
+ ["element not interactable", ElementNotInteractableError],
+ ["insecure certificate", InsecureCertificateError],
+ ["invalid argument", InvalidArgumentError],
+ ["invalid cookie domain", InvalidCookieDomainError],
+ ["invalid element state", InvalidElementStateError],
+ ["invalid selector", InvalidSelectorError],
+ ["invalid session id", InvalidSessionIDError],
+ ["javascript error", JavaScriptError],
+ ["move target out of bounds", MoveTargetOutOfBoundsError],
+ ["no such alert", NoSuchAlertError],
+ ["no such element", NoSuchElementError],
+ ["no such frame", NoSuchFrameError],
+ ["no such shadow root", NoSuchShadowRootError],
+ ["no such window", NoSuchWindowError],
+ ["script timeout", ScriptTimeoutError],
+ ["session not created", SessionNotCreatedError],
+ ["stale element reference", StaleElementReferenceError],
+ ["timeout", TimeoutError],
+ ["unable to set cookie", UnableToSetCookieError],
+ ["unexpected alert open", UnexpectedAlertOpenError],
+ ["unknown command", UnknownCommandError],
+ ["unknown error", UnknownError],
+ ["unsupported operation", UnsupportedOperationError],
+ ["webdriver error", WebDriverError],
+]);
+
+// Errors must be expored on the local this scope so that the
+// EXPORTED_SYMBOLS and the ChromeUtils.import("foo") machinery sees them.
+// We could assign each error definition directly to |this|, but
+// because they are Error prototypes this would mess up their names.
+for (let cls of STATUSES.values()) {
+ error[cls.name] = cls;
+}
diff --git a/remote/shared/webdriver/KeyData.sys.mjs b/remote/shared/webdriver/KeyData.sys.mjs
new file mode 100644
index 0000000000..5d2570686f
--- /dev/null
+++ b/remote/shared/webdriver/KeyData.sys.mjs
@@ -0,0 +1,340 @@
+/* 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 KEY_DATA = {
+ " ": { code: "Space" },
+ "!": { code: "Digit1", shifted: true },
+ "#": { code: "Digit3", shifted: true },
+ $: { code: "Digit4", shifted: true },
+ "%": { code: "Digit5", shifted: true },
+ "&": { code: "Digit7", shifted: true },
+ "'": { code: "Quote" },
+ "(": { code: "Digit9", shifted: true },
+ ")": { code: "Digit0", shifted: true },
+ "*": { code: "Digit8", shifted: true },
+ "+": { code: "Equal", shifted: true },
+ ",": { code: "Comma" },
+ "-": { code: "Minus" },
+ ".": { code: "Period" },
+ "/": { code: "Slash" },
+ "0": { code: "Digit0" },
+ "1": { code: "Digit1" },
+ "2": { code: "Digit2" },
+ "3": { code: "Digit3" },
+ "4": { code: "Digit4" },
+ "5": { code: "Digit5" },
+ "6": { code: "Digit6" },
+ "7": { code: "Digit7" },
+ "8": { code: "Digit8" },
+ "9": { code: "Digit9" },
+ ":": { code: "Semicolon", shifted: true },
+ ";": { code: "Semicolon" },
+ "<": { code: "Comma", shifted: true },
+ "=": { code: "Equal" },
+ ">": { code: "Period", shifted: true },
+ "?": { code: "Slash", shifted: true },
+ "@": { code: "Digit2", shifted: true },
+ A: { code: "KeyA", shifted: true },
+ B: { code: "KeyB", shifted: true },
+ C: { code: "KeyC", shifted: true },
+ D: { code: "KeyD", shifted: true },
+ E: { code: "KeyE", shifted: true },
+ F: { code: "KeyF", shifted: true },
+ G: { code: "KeyG", shifted: true },
+ H: { code: "KeyH", shifted: true },
+ I: { code: "KeyI", shifted: true },
+ J: { code: "KeyJ", shifted: true },
+ K: { code: "KeyK", shifted: true },
+ L: { code: "KeyL", shifted: true },
+ M: { code: "KeyM", shifted: true },
+ N: { code: "KeyN", shifted: true },
+ O: { code: "KeyO", shifted: true },
+ P: { code: "KeyP", shifted: true },
+ Q: { code: "KeyQ", shifted: true },
+ R: { code: "KeyR", shifted: true },
+ S: { code: "KeyS", shifted: true },
+ T: { code: "KeyT", shifted: true },
+ U: { code: "KeyU", shifted: true },
+ V: { code: "KeyV", shifted: true },
+ W: { code: "KeyW", shifted: true },
+ X: { code: "KeyX", shifted: true },
+ Y: { code: "KeyY", shifted: true },
+ Z: { code: "KeyZ", shifted: true },
+ "[": { code: "BracketLeft" },
+ '"': { code: "Quote", shifted: true },
+ "\\": { code: "Backslash" },
+ "]": { code: "BracketRight" },
+ "^": { code: "Digit6", shifted: true },
+ _: { code: "Minus", shifted: true },
+ "`": { code: "Backquote" },
+ a: { code: "KeyA" },
+ b: { code: "KeyB" },
+ c: { code: "KeyC" },
+ d: { code: "KeyD" },
+ e: { code: "KeyE" },
+ f: { code: "KeyF" },
+ g: { code: "KeyG" },
+ h: { code: "KeyH" },
+ i: { code: "KeyI" },
+ j: { code: "KeyJ" },
+ k: { code: "KeyK" },
+ l: { code: "KeyL" },
+ m: { code: "KeyM" },
+ n: { code: "KeyN" },
+ o: { code: "KeyO" },
+ p: { code: "KeyP" },
+ q: { code: "KeyQ" },
+ r: { code: "KeyR" },
+ s: { code: "KeyS" },
+ t: { code: "KeyT" },
+ u: { code: "KeyU" },
+ v: { code: "KeyV" },
+ w: { code: "KeyW" },
+ x: { code: "KeyX" },
+ y: { code: "KeyY" },
+ z: { code: "KeyZ" },
+ "{": { code: "BracketLeft", shifted: true },
+ "|": { code: "Backslash", shifted: true },
+ "}": { code: "BracketRight", shifted: true },
+ "~": { code: "Backquote", shifted: true },
+ "\uE000": { key: "Unidentified", printable: false },
+ "\uE001": { key: "Cancel", printable: false },
+ "\uE002": { code: "Help", key: "Help", printable: false },
+ "\uE003": { code: "Backspace", key: "Backspace", printable: false },
+ "\uE004": { code: "Tab", key: "Tab", printable: false },
+ "\uE005": { code: "", key: "Clear", printable: false },
+ "\uE006": { code: "Enter", key: "Enter", printable: false },
+ "\uE007": {
+ code: "NumpadEnter",
+ key: "Enter",
+ location: 1,
+ printable: false,
+ },
+ "\uE008": {
+ code: "ShiftLeft",
+ key: "Shift",
+ location: 1,
+ modifier: "shiftKey",
+ printable: false,
+ },
+ "\uE009": {
+ code: "ControlLeft",
+ key: "Control",
+ location: 1,
+ modifier: "ctrlKey",
+ printable: false,
+ },
+ "\uE00A": {
+ code: "AltLeft",
+ key: "Alt",
+ location: 1,
+ modifier: "altKey",
+ printable: false,
+ },
+ "\uE00B": { code: "", key: "Pause", printable: false },
+ "\uE00C": { code: "Escape", key: "Escape", printable: false },
+ "\uE00D": { code: "Space", key: " ", shifted: true },
+ "\uE00E": { code: "PageUp", key: "PageUp", printable: false },
+ "\uE00F": { code: "PageDown", key: "PageDown", printable: false },
+ "\uE010": { code: "End", key: "End", printable: false },
+ "\uE011": { code: "Home", key: "Home", printable: false },
+ "\uE012": { code: "ArrowLeft", key: "ArrowLeft", printable: false },
+ "\uE013": { code: "ArrowUp", key: "ArrowUp", printable: false },
+ "\uE014": { code: "ArrowRight", key: "ArrowRight", printable: false },
+ "\uE015": { code: "ArrowDown", key: "ArrowDown", printable: false },
+ "\uE016": { code: "Insert", key: "Insert", printable: false },
+ "\uE017": { code: "Delete", key: "Delete", printable: false },
+ "\uE018": { code: "", key: ";" },
+ "\uE019": { code: "", key: "=" },
+ "\uE01A": { code: "Numpad0", key: "0", location: 3 },
+ "\uE01B": { code: "Numpad1", key: "1", location: 3 },
+ "\uE01C": { code: "Numpad2", key: "2", location: 3 },
+ "\uE01D": { code: "Numpad3", key: "3", location: 3 },
+ "\uE01E": { code: "Numpad4", key: "4", location: 3 },
+ "\uE01F": { code: "Numpad5", key: "5", location: 3 },
+ "\uE020": { code: "Numpad6", key: "6", location: 3 },
+ "\uE021": { code: "Numpad7", key: "7", location: 3 },
+ "\uE022": { code: "Numpad8", key: "8", location: 3 },
+ "\uE023": { code: "Numpad9", key: "9", location: 3 },
+ "\uE024": { code: "NumpadMultiply", key: "*", location: 3 },
+ "\uE025": { code: "NumpadAdd", key: "+", location: 3 },
+ "\uE026": { code: "NumpadComma", key: ",", location: 3 },
+ "\uE027": { code: "NumpadSubtract", key: "-", location: 3 },
+ "\uE028": { code: "NumpadDecimal", key: ".", location: 3 },
+ "\uE029": { code: "NumpadDivide", key: "/", location: 3 },
+ "\uE031": { code: "F1", key: "F1", printable: false },
+ "\uE032": { code: "F2", key: "F2", printable: false },
+ "\uE033": { code: "F3", key: "F3", printable: false },
+ "\uE034": { code: "F4", key: "F4", printable: false },
+ "\uE035": { code: "F5", key: "F5", printable: false },
+ "\uE036": { code: "F6", key: "F6", printable: false },
+ "\uE037": { code: "F7", key: "F7", printable: false },
+ "\uE038": { code: "F8", key: "F8", printable: false },
+ "\uE039": { code: "F9", key: "F9", printable: false },
+ "\uE03A": { code: "F10", key: "F10", printable: false },
+ "\uE03B": { code: "F11", key: "F11", printable: false },
+ "\uE03C": { code: "F12", key: "F12", printable: false },
+ "\uE03D": {
+ code: "OSLeft",
+ key: "Meta",
+ location: 1,
+ modifier: "metaKey",
+ printable: false,
+ },
+ "\uE040": { code: "", key: "ZenkakuHankaku", printable: false },
+ "\uE050": {
+ code: "ShiftRight",
+ key: "Shift",
+ location: 2,
+ modifier: "shiftKey",
+ printable: false,
+ },
+ "\uE051": {
+ code: "ControlRight",
+ key: "Control",
+ location: 2,
+ modifier: "ctrlKey",
+ printable: false,
+ },
+ "\uE052": {
+ code: "AltRight",
+ key: "Alt",
+ location: 2,
+ modifier: "altKey",
+ printable: false,
+ },
+ "\uE053": {
+ code: "OSRight",
+ key: "Meta",
+ location: 2,
+ modifier: "metaKey",
+ printable: false,
+ },
+ "\uE054": {
+ code: "Numpad9",
+ key: "PageUp",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE055": {
+ code: "Numpad3",
+ key: "PageDown",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE056": {
+ code: "Numpad1",
+ key: "End",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE057": {
+ code: "Numpad7",
+ key: "Home",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE058": {
+ code: "Numpad4",
+ key: "ArrowLeft",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE059": {
+ code: "Numpad8",
+ key: "ArrowUp",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE05A": {
+ code: "Numpad6",
+ key: "ArrowRight",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE05B": {
+ code: "Numpad2",
+ key: "ArrowDown",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE05C": {
+ code: "Numpad0",
+ key: "Insert",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+ "\uE05D": {
+ code: "NumpadDecimal",
+ key: "Delete",
+ location: 3,
+ printable: false,
+ shifted: true,
+ },
+};
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "SHIFT_DATA", () => {
+ // Initalize the shift mapping
+ const shiftData = new Map();
+ const byCode = new Map();
+ for (let [key, props] of Object.entries(KEY_DATA)) {
+ if (props.code) {
+ if (!byCode.has(props.code)) {
+ byCode.set(props.code, [null, null]);
+ }
+ byCode.get(props.code)[props.shifted ? 1 : 0] = key;
+ }
+ }
+ for (let [unshifted, shifted] of byCode.values()) {
+ if (unshifted !== null && shifted !== null) {
+ shiftData.set(unshifted, shifted);
+ }
+ }
+ return shiftData;
+});
+
+export const keyData = {
+ /**
+ * Get key event data for a given key character.
+ *
+ * @param {string} key
+ * Key for which to get data. This can either be the key codepoint
+ * itself or one of the codepoints in the range U+E000-U+E05D that
+ * WebDriver uses to represent keys not corresponding directly to
+ * a codepoint.
+ * @returns {Object} Key event data object.
+ */
+ getData(rawKey) {
+ let keyData = { key: rawKey, location: 0, printable: true, shifted: false };
+ if (KEY_DATA.hasOwnProperty(rawKey)) {
+ keyData = { ...keyData, ...KEY_DATA[rawKey] };
+ }
+ return keyData;
+ },
+
+ /**
+ * Get shifted key character for a given key character.
+ *
+ * For characters unaffected by the shift key, this returns the input.
+ *
+ * @param {string} rawKey Key for which to get shifted key.
+ * @returns {string} Key string to use when the shift modifier is set.
+ */
+ getShiftedKey(rawKey) {
+ return lazy.SHIFT_DATA.get(rawKey) ?? rawKey;
+ },
+};
diff --git a/remote/shared/webdriver/NodeCache.sys.mjs b/remote/shared/webdriver/NodeCache.sys.mjs
new file mode 100644
index 0000000000..a8de59cccf
--- /dev/null
+++ b/remote/shared/webdriver/NodeCache.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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
+
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+});
+
+/**
+ * The class provides a mapping between DOM nodes and unique element
+ * references by using `ContentDOMReference` identifiers.
+ */
+export class NodeCache {
+ #domRefs;
+ #sharedIds;
+
+ constructor() {
+ // ContentDOMReference id => shared unique id
+ this.#sharedIds = new Map();
+
+ // shared unique id => ContentDOMReference
+ this.#domRefs = new Map();
+ }
+
+ /**
+ * Get the number of elements in the cache.
+ */
+ get size() {
+ return this.#sharedIds.size;
+ }
+
+ /**
+ * Add a DOM element to the cache if not known yet.
+ *
+ * @param {Element} el
+ * The DOM Element to be added.
+ *
+ * @return {string}
+ * The shared id to uniquely identify the DOM element.
+ */
+ add(el) {
+ let domRef, sharedId;
+
+ try {
+ // Evaluation of code will take place in mutable sandboxes, which are
+ // created to waive xrays by default. As such DOM elements have to be
+ // unwaived before accessing the ownerGlobal if possible, which is
+ // needed by ContentDOMReference.
+ domRef = lazy.ContentDOMReference.get(Cu.unwaiveXrays(el));
+ } catch (e) {
+ throw new lazy.error.UnknownError(
+ lazy.pprint`Failed to create element reference for ${el}: ${e.message}`
+ );
+ }
+
+ if (this.#sharedIds.has(domRef.id)) {
+ // For already known elements retrieve the cached shared id.
+ sharedId = this.#sharedIds.get(domRef.id);
+ } else {
+ // For new elements generate a unique id without curly braces.
+ sharedId = Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+
+ this.#sharedIds.set(domRef.id, sharedId);
+ this.#domRefs.set(sharedId, domRef);
+ }
+
+ return sharedId;
+ }
+
+ /**
+ * Clears all known DOM elements.
+ *
+ * @param {Object=} options
+ * @param {boolean=} options.all
+ * Clear all references from any browsing context. Defaults to false.
+ * @param {BrowsingContext=} browsingContext
+ * Clear all references living in that browsing context.
+ */
+ clear(options = {}) {
+ const { all = false, browsingContext } = options;
+
+ if (all) {
+ this.#sharedIds.clear();
+ this.#domRefs.clear();
+ return;
+ }
+
+ if (browsingContext) {
+ for (const [sharedId, domRef] of this.#domRefs.entries()) {
+ if (domRef.browsingContextId === browsingContext.id) {
+ this.#sharedIds.delete(domRef.id);
+ this.#domRefs.delete(sharedId);
+ }
+ }
+ return;
+ }
+
+ throw new Error(`Requires "browsingContext" or "all" to be set.`);
+ }
+
+ /**
+ * Wrapper around ContentDOMReference.resolve with additional error handling
+ * specific to WebDriver.
+ *
+ * @param {string} sharedId
+ * The unique identifier for the DOM element.
+ *
+ * @return {Element|null}
+ * The DOM element that the unique identifier was generated for or
+ * `null` if the element does not exist anymore.
+ *
+ * @throws {NoSuchElementError}
+ * If the DOM element as represented by the unique WebElement reference
+ * <var>sharedId</var> isn't known.
+ */
+ resolve(sharedId) {
+ const domRef = this.#domRefs.get(sharedId);
+ if (domRef == undefined) {
+ throw new lazy.error.NoSuchElementError(
+ `Unknown element with id ${sharedId}`
+ );
+ }
+
+ return lazy.ContentDOMReference.resolve(domRef);
+ }
+}
diff --git a/remote/shared/webdriver/Session.sys.mjs b/remote/shared/webdriver/Session.sys.mjs
new file mode 100644
index 0000000000..f37c5e5e92
--- /dev/null
+++ b/remote/shared/webdriver/Session.sys.mjs
@@ -0,0 +1,348 @@
+/* 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 = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs",
+ allowAllCerts: "chrome://remote/content/marionette/cert.sys.mjs",
+ Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ registerProcessDataActor:
+ "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
+ RootMessageHandler:
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
+ RootMessageHandlerRegistry:
+ "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
+ unregisterProcessDataActor:
+ "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
+ WebDriverBiDiConnection:
+ "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs",
+ WebSocketHandshake:
+ "chrome://remote/content/server/WebSocketHandshake.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+/**
+ * Representation of WebDriver session.
+ */
+export class WebDriverSession {
+ /**
+ * Construct a new WebDriver session.
+ *
+ * It is expected that the caller performs the necessary checks on
+ * the requested capabilities to be WebDriver conforming. The WebDriver
+ * service offered by Marionette does not match or negotiate capabilities
+ * beyond type- and bounds checks.
+ *
+ * <h3>Capabilities</h3>
+ *
+ * <dl>
+ * <dt><code>acceptInsecureCerts</code> (boolean)
+ * <dd>Indicates whether untrusted and self-signed TLS certificates
+ * are implicitly trusted on navigation for the duration of the session.
+ *
+ * <dt><code>pageLoadStrategy</code> (string)
+ * <dd>The page load strategy to use for the current session. Must be
+ * one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
+ *
+ * <dt><code>proxy</code> (Proxy object)
+ * <dd>Defines the proxy configuration.
+ *
+ * <dt><code>setWindowRect</code> (boolean)
+ * <dd>Indicates whether the remote end supports all of the resizing
+ * and repositioning commands.
+ *
+ * <dt><code>timeouts</code> (Timeouts object)
+ * <dd>Describes the timeouts imposed on certian session operations.
+ *
+ * <dt><code>strictFileInteractability</code> (boolean)
+ * <dd>Defines the current session’s strict file interactability.
+ *
+ * <dt><code>unhandledPromptBehavior</code> (string)
+ * <dd>Describes the current session’s user prompt handler. Must be one of
+ * "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>",
+ * "<tt>dismiss and notify</tt>", and "<tt>ignore</tt>". Defaults to the
+ * "<tt>dismiss and notify</tt>" state.
+ *
+ * <dt><code>moz:accessibilityChecks</code> (boolean)
+ * <dd>Run a11y checks when clicking elements.
+ *
+ * <dt><code>moz:debuggerAddress</code> (boolean)
+ * <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled.
+ *
+ * <dt><code>moz:useNonSpecCompliantPointerOrigin</code> (boolean)
+ * <dd>Use the not WebDriver conforming calculation of the pointer origin
+ * when the origin is an element, and the element center point is used.
+ *
+ * <dt><code>moz:webdriverClick</code> (boolean)
+ * <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
+ * </dl>
+ *
+ * <h4>Timeouts object</h4>
+ *
+ * <dl>
+ * <dt><code>script</code> (number)
+ * <dd>Determines when to interrupt a script that is being evaluates.
+ *
+ * <dt><code>pageLoad</code> (number)
+ * <dd>Provides the timeout limit used to interrupt navigation of the
+ * browsing context.
+ *
+ * <dt><code>implicit</code> (number)
+ * <dd>Gives the timeout of when to abort when locating an element.
+ * </dl>
+ *
+ * <h4>Proxy object</h4>
+ *
+ * <dl>
+ * <dt><code>proxyType</code> (string)
+ * <dd>Indicates the type of proxy configuration. Must be one
+ * of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
+ * "<tt>system</tt>", or "<tt>manual</tt>".
+ *
+ * <dt><code>proxyAutoconfigUrl</code> (string)
+ * <dd>Defines the URL for a proxy auto-config file if
+ * <code>proxyType</code> is equal to "<tt>pac</tt>".
+ *
+ * <dt><code>httpProxy</code> (string)
+ * <dd>Defines the proxy host for HTTP traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>noProxy</code> (string)
+ * <dd>Lists the adress for which the proxy should be bypassed when
+ * the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON
+ * List containing any number of any of domains, IPv4 addresses, or IPv6
+ * addresses.
+ *
+ * <dt><code>sslProxy</code> (string)
+ * <dd>Defines the proxy host for encrypted TLS traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>socksProxy</code> (string)
+ * <dd>Defines the proxy host for a SOCKS proxy traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>socksVersion</code> (string)
+ * <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
+ * "<tt>manual</tt>". It must be any integer between 0 and 255
+ * inclusive.
+ * </dl>
+ *
+ * <h3>Example</h3>
+ *
+ * Input:
+ *
+ * <pre><code>
+ * {"capabilities": {"acceptInsecureCerts": true}}
+ * </code></pre>
+ *
+ * @param {Object.<string, *>=} capabilities
+ * JSON Object containing any of the recognised capabilities listed
+ * above.
+ *
+ * @param {WebDriverBiDiConnection=} connection
+ * An optional existing WebDriver BiDi connection to associate with the
+ * new session.
+ *
+ * @throws {SessionNotCreatedError}
+ * If, for whatever reason, a session could not be created.
+ */
+ constructor(capabilities, connection) {
+ // WebSocket connections that use this session. This also accounts for
+ // possible disconnects due to network outages, which require clients
+ // to reconnect.
+ this._connections = new Set();
+
+ this.id = Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+
+ // Define the HTTP path to query this session via WebDriver BiDi
+ this.path = `/session/${this.id}`;
+
+ try {
+ this.capabilities = lazy.Capabilities.fromJSON(capabilities, this.path);
+ } catch (e) {
+ throw new lazy.error.SessionNotCreatedError(e);
+ }
+
+ if (this.capabilities.get("acceptInsecureCerts")) {
+ lazy.logger.warn(
+ "TLS certificate errors will be ignored for this session"
+ );
+ lazy.allowAllCerts.enable();
+ }
+
+ if (this.proxy.init()) {
+ lazy.logger.info(
+ `Proxy settings initialised: ${JSON.stringify(this.proxy)}`
+ );
+ }
+
+ // If we are testing accessibility with marionette, start a11y service in
+ // chrome first. This will ensure that we do not have any content-only
+ // services hanging around.
+ if (this.a11yChecks && lazy.accessibility.service) {
+ lazy.logger.info("Preemptively starting accessibility service in Chrome");
+ }
+
+ // If a connection without an associated session has been specified
+ // immediately register the newly created session for it.
+ if (connection) {
+ connection.registerSession(this);
+ this._connections.add(connection);
+ }
+
+ lazy.registerProcessDataActor();
+ }
+
+ destroy() {
+ lazy.allowAllCerts.disable();
+
+ // Close all open connections which unregister themselves.
+ this._connections.forEach(connection => connection.close());
+ if (this._connections.size > 0) {
+ lazy.logger.warn(
+ `Failed to close ${this._connections.size} WebSocket connections`
+ );
+ }
+
+ // Destroy the dedicated MessageHandler instance if we created one.
+ if (this._messageHandler) {
+ this._messageHandler.off(
+ "message-handler-protocol-event",
+ this._onMessageHandlerProtocolEvent
+ );
+ this._messageHandler.destroy();
+ }
+
+ lazy.unregisterProcessDataActor();
+ }
+
+ async execute(module, command, params) {
+ // XXX: At the moment, commands do not describe consistently their destination,
+ // so we will need a translation step based on a specific command and its params
+ // in order to extract a destination that can be understood by the MessageHandler.
+ //
+ // For now, an option is to send all commands to ROOT, and all BiDi MessageHandler
+ // modules will therefore need to implement this translation step in the root
+ // implementation of their module.
+ const destination = {
+ type: lazy.RootMessageHandler.type,
+ };
+ if (!this.messageHandler.supportsCommand(module, command, destination)) {
+ throw new lazy.error.UnknownCommandError(`${module}.${command}`);
+ }
+
+ return this.messageHandler.handleCommand({
+ moduleName: module,
+ commandName: command,
+ params,
+ destination,
+ });
+ }
+
+ get a11yChecks() {
+ return this.capabilities.get("moz:accessibilityChecks");
+ }
+
+ get messageHandler() {
+ if (!this._messageHandler) {
+ this._messageHandler = lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(
+ this.id
+ );
+ this._onMessageHandlerProtocolEvent = this._onMessageHandlerProtocolEvent.bind(
+ this
+ );
+ this._messageHandler.on(
+ "message-handler-protocol-event",
+ this._onMessageHandlerProtocolEvent
+ );
+ }
+
+ return this._messageHandler;
+ }
+
+ get pageLoadStrategy() {
+ return this.capabilities.get("pageLoadStrategy");
+ }
+
+ get proxy() {
+ return this.capabilities.get("proxy");
+ }
+
+ get strictFileInteractability() {
+ return this.capabilities.get("strictFileInteractability");
+ }
+
+ get timeouts() {
+ return this.capabilities.get("timeouts");
+ }
+
+ set timeouts(timeouts) {
+ this.capabilities.set("timeouts", timeouts);
+ }
+
+ get unhandledPromptBehavior() {
+ return this.capabilities.get("unhandledPromptBehavior");
+ }
+
+ /**
+ * Remove the specified WebDriver BiDi connection.
+ *
+ * @param {WebDriverBiDiConnection} connection
+ */
+ removeConnection(connection) {
+ if (this._connections.has(connection)) {
+ this._connections.delete(connection);
+ } else {
+ lazy.logger.warn("Trying to remove a connection that doesn't exist.");
+ }
+ }
+
+ toString() {
+ return `[object ${this.constructor.name} ${this.id}]`;
+ }
+
+ // nsIHttpRequestHandler
+
+ /**
+ * Handle new WebSocket connection requests.
+ *
+ * WebSocket clients will attempt to connect to this session at
+ * `/session/:id`. Hereby a WebSocket upgrade will automatically
+ * be performed.
+ *
+ * @param {Request} request
+ * HTTP request (httpd.js)
+ * @param {Response} response
+ * Response to an HTTP request (httpd.js)
+ */
+ async handle(request, response) {
+ const webSocket = await lazy.WebSocketHandshake.upgrade(request, response);
+ const conn = new lazy.WebDriverBiDiConnection(
+ webSocket,
+ response._connection
+ );
+ conn.registerSession(this);
+ this._connections.add(conn);
+ }
+
+ _onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) {
+ const { name, data } = messageHandlerEvent;
+ this._connections.forEach(connection => connection.sendEvent(name, data));
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
+ }
+}
diff --git a/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs b/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs
new file mode 100644
index 0000000000..b7c6f8a857
--- /dev/null
+++ b/remote/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs
@@ -0,0 +1,95 @@
+/* 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 = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ NodeCache: "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+// Observer to clean-up element references for closed browsing contexts.
+class BrowsingContextObserver {
+ constructor(actor) {
+ this.actor = actor;
+ }
+
+ async observe(subject, topic, data) {
+ if (topic === "browsing-context-discarded") {
+ this.actor.cleanUp({ browsingContext: subject });
+ }
+ }
+}
+
+export class WebDriverProcessDataChild extends JSProcessActorChild {
+ #browsingContextObserver;
+ #nodeCache;
+
+ constructor() {
+ super();
+
+ // For now have a single reference store only. Once multiple WebDriver
+ // sessions are supported, it needs to be hashed by the session id.
+ this.#nodeCache = new lazy.NodeCache();
+
+ // Register observer to cleanup element references when a browsing context
+ // gets destroyed.
+ this.#browsingContextObserver = new BrowsingContextObserver(this);
+ Services.obs.addObserver(
+ this.#browsingContextObserver,
+ "browsing-context-discarded"
+ );
+ }
+
+ actorCreated() {
+ lazy.logger.trace(
+ `WebDriverProcessData actor created for PID ${Services.appinfo.processID}`
+ );
+ }
+
+ didDestroy() {
+ Services.obs.removeObserver(
+ this.#browsingContextObserver,
+ "browsing-context-discarded"
+ );
+ }
+
+ /**
+ * Clean up all the process specific data.
+ *
+ * @param {Object=} options
+ * @param {BrowsingContext=} browsingContext
+ * If specified only clear data living in that browsing context.
+ */
+ cleanUp(options = {}) {
+ const { browsingContext = null } = options;
+
+ this.#nodeCache.clear({ browsingContext });
+ }
+
+ /**
+ * Get the node cache.
+ *
+ * @returns {NodeCache}
+ * The cache containing DOM node references.
+ */
+ getNodeCache() {
+ return this.#nodeCache;
+ }
+
+ async receiveMessage(msg) {
+ switch (msg.name) {
+ case "WebDriverProcessDataParent:CleanUp":
+ return this.cleanUp(msg.data);
+ default:
+ return Promise.reject(
+ new Error(`Unexpected message received: ${msg.name}`)
+ );
+ }
+ }
+}
diff --git a/remote/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs b/remote/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs
new file mode 100644
index 0000000000..43c48fbec4
--- /dev/null
+++ b/remote/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs
@@ -0,0 +1,39 @@
+/* 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 = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+/**
+ * Register the WebDriverProcessData actor that holds session data.
+ */
+export function registerProcessDataActor() {
+ try {
+ ChromeUtils.registerProcessActor("WebDriverProcessData", {
+ kind: "JSProcessActor",
+ child: {
+ esModuleURI:
+ "chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs",
+ },
+ includeParent: true,
+ });
+ } catch (e) {
+ if (e.name === "NotSupportedError") {
+ lazy.logger.warn(`WebDriverProcessData actor is already registered!`);
+ } else {
+ throw e;
+ }
+ }
+}
+
+export function unregisterProcessDataActor() {
+ ChromeUtils.unregisterProcessActor("WebDriverProcessData");
+}
diff --git a/remote/shared/webdriver/test/xpcshell/head.js b/remote/shared/webdriver/test/xpcshell/head.js
new file mode 100644
index 0000000000..81c0e3950a
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/head.js
@@ -0,0 +1,14 @@
+async function doGC() {
+ // Run GC and CC a few times to make sure that as much as possible is freed.
+ const numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
+ }
+
+ const MemoryReporter = Cc[
+ "@mozilla.org/memory-reporter-manager;1"
+ ].getService(Ci.nsIMemoryReporterManager);
+ await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
+}
diff --git a/remote/shared/webdriver/test/xpcshell/test_Assert.js b/remote/shared/webdriver/test/xpcshell/test_Assert.js
new file mode 100644
index 0000000000..a16ff77fb0
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Assert.js
@@ -0,0 +1,215 @@
+/* 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";
+/* eslint-disable no-array-constructor, no-new-object */
+
+const { assert } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Assert.sys.mjs"
+);
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+
+add_test(function test_session() {
+ assert.session({ id: "foo" });
+
+ const invalidTypes = [
+ null,
+ undefined,
+ [],
+ {},
+ { id: undefined },
+ { id: null },
+ { id: true },
+ { id: 1 },
+ { id: [] },
+ { id: {} },
+ ];
+
+ for (const invalidType of invalidTypes) {
+ Assert.throws(() => assert.session(invalidType), /InvalidSessionIDError/);
+ }
+
+ Assert.throws(() => assert.session({ id: null }, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_platforms() {
+ // at least one will fail
+ let raised;
+ for (let fn of [assert.desktop, assert.mobile]) {
+ try {
+ fn();
+ } catch (e) {
+ raised = e;
+ }
+ }
+ ok(raised instanceof error.UnsupportedOperationError);
+
+ run_next_test();
+});
+
+add_test(function test_noUserPrompt() {
+ assert.noUserPrompt(null);
+ assert.noUserPrompt(undefined);
+ Assert.throws(() => assert.noUserPrompt({}), /UnexpectedAlertOpenError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_defined() {
+ assert.defined({});
+ Assert.throws(() => assert.defined(undefined), /InvalidArgumentError/);
+ Assert.throws(() => assert.noUserPrompt({}, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_number() {
+ assert.number(1);
+ assert.number(0);
+ assert.number(-1);
+ assert.number(1.2);
+ for (let i of ["foo", "1", {}, [], NaN, Infinity, undefined]) {
+ Assert.throws(() => assert.number(i), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.number("foo", "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_callable() {
+ assert.callable(function() {});
+ assert.callable(() => {});
+
+ for (let typ of [undefined, "", true, {}, []]) {
+ Assert.throws(() => assert.callable(typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.callable("foo", "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_integer() {
+ assert.integer(1);
+ assert.integer(0);
+ assert.integer(-1);
+ Assert.throws(() => assert.integer("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.integer(1.2), /InvalidArgumentError/);
+
+ Assert.throws(() => assert.integer("foo", "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_positiveInteger() {
+ assert.positiveInteger(1);
+ assert.positiveInteger(0);
+ Assert.throws(() => assert.positiveInteger(-1), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveInteger("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveInteger("foo", "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_positiveNumber() {
+ assert.positiveNumber(1);
+ assert.positiveNumber(0);
+ assert.positiveNumber(1.1);
+ assert.positiveNumber(Number.MAX_VALUE);
+ // eslint-disable-next-line no-loss-of-precision
+ Assert.throws(() => assert.positiveNumber(1.8e308), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber(-1), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber(Infinity), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber("foo"), /InvalidArgumentError/);
+ Assert.throws(() => assert.positiveNumber("foo", "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_boolean() {
+ assert.boolean(true);
+ assert.boolean(false);
+ Assert.throws(() => assert.boolean("false"), /InvalidArgumentError/);
+ Assert.throws(() => assert.boolean(undefined), /InvalidArgumentError/);
+ Assert.throws(() => assert.boolean(undefined, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_string() {
+ assert.string("foo");
+ assert.string(`bar`);
+ Assert.throws(() => assert.string(42), /InvalidArgumentError/);
+ Assert.throws(() => assert.string(42, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_open() {
+ assert.open({ currentWindowGlobal: {} });
+
+ for (let typ of [null, undefined, { currentWindowGlobal: null }]) {
+ Assert.throws(() => assert.open(typ), /NoSuchWindowError/);
+ }
+
+ Assert.throws(() => assert.open(null, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_object() {
+ assert.object({});
+ assert.object(new Object());
+ for (let typ of [42, "foo", true, null, undefined]) {
+ Assert.throws(() => assert.object(typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.object(null, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_in() {
+ assert.in("foo", { foo: 42 });
+ for (let typ of [{}, 42, true, null, undefined]) {
+ Assert.throws(() => assert.in("foo", typ), /InvalidArgumentError/);
+ }
+
+ Assert.throws(() => assert.in("foo", { bar: 42 }, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_array() {
+ assert.array([]);
+ assert.array(new Array());
+ Assert.throws(() => assert.array(42), /InvalidArgumentError/);
+ Assert.throws(() => assert.array({}), /InvalidArgumentError/);
+
+ Assert.throws(() => assert.array(42, "custom"), /custom/);
+
+ run_next_test();
+});
+
+add_test(function test_that() {
+ equal(1, assert.that(n => n + 1)(1));
+ Assert.throws(() => assert.that(() => false)(), /InvalidArgumentError/);
+ Assert.throws(() => assert.that(val => val)(false), /InvalidArgumentError/);
+ Assert.throws(
+ () => assert.that(val => val, "foo", error.SessionNotCreatedError)(false),
+ /SessionNotCreatedError/
+ );
+
+ Assert.throws(() => assert.that(() => false, "custom")(), /custom/);
+
+ run_next_test();
+});
+
+/* eslint-enable no-array-constructor, no-new-object */
diff --git a/remote/shared/webdriver/test/xpcshell/test_Capabilities.js b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js
new file mode 100644
index 0000000000..4e655b63e2
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Capabilities.js
@@ -0,0 +1,623 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const { AppInfo } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/AppInfo.sys.mjs"
+);
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+const {
+ Capabilities,
+ PageLoadStrategy,
+ Proxy,
+ Timeouts,
+ UnhandledPromptBehavior,
+} = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
+);
+
+add_test(function test_Timeouts_ctor() {
+ let ts = new Timeouts();
+ equal(ts.implicit, 0);
+ equal(ts.pageLoad, 300000);
+ equal(ts.script, 30000);
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toString() {
+ equal(new Timeouts().toString(), "[object Timeouts]");
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toJSON() {
+ let ts = new Timeouts();
+ deepEqual(ts.toJSON(), { implicit: 0, pageLoad: 300000, script: 30000 });
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON() {
+ let json = {
+ implicit: 0,
+ pageLoad: 2.0,
+ script: Number.MAX_SAFE_INTEGER,
+ };
+ let ts = Timeouts.fromJSON(json);
+ equal(ts.implicit, json.implicit);
+ equal(ts.pageLoad, json.pageLoad);
+ equal(ts.script, json.script);
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON_unrecognised_field() {
+ let json = {
+ sessionId: "foobar",
+ };
+ try {
+ Timeouts.fromJSON(json);
+ } catch (e) {
+ equal(e.name, error.InvalidArgumentError.name);
+ equal(e.message, "Unrecognised timeout: sessionId");
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON_invalid_types() {
+ for (let value of [null, [], {}, false, "10", 2.5]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ implicit: value }),
+ /InvalidArgumentError/
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON_bounds() {
+ for (let value of [-1, Number.MAX_SAFE_INTEGER + 1]) {
+ Assert.throws(
+ () => Timeouts.fromJSON({ script: value }),
+ /InvalidArgumentError/
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_PageLoadStrategy() {
+ equal(PageLoadStrategy.None, "none");
+ equal(PageLoadStrategy.Eager, "eager");
+ equal(PageLoadStrategy.Normal, "normal");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_ctor() {
+ let p = new Proxy();
+ let props = [
+ "proxyType",
+ "httpProxy",
+ "sslProxy",
+ "socksProxy",
+ "socksVersion",
+ "proxyAutoconfigUrl",
+ ];
+ for (let prop of props) {
+ ok(prop in p, `${prop} in ${JSON.stringify(props)}`);
+ equal(p[prop], null);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_init() {
+ let p = new Proxy();
+
+ // no changed made, and 5 (system) is default
+ equal(p.init(), false);
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // pac
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "http://localhost:1234";
+ ok(p.init());
+
+ equal(Preferences.get("network.proxy.type"), 2);
+ equal(
+ Preferences.get("network.proxy.autoconfig_url"),
+ "http://localhost:1234"
+ );
+
+ // direct
+ p = new Proxy();
+ p.proxyType = "direct";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 0);
+
+ // autodetect
+ p = new Proxy();
+ p.proxyType = "autodetect";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 4);
+
+ // system
+ p = new Proxy();
+ p.proxyType = "system";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // manual
+ for (let proxy of ["http", "ssl", "socks"]) {
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["foo", "bar"];
+ p[`${proxy}Proxy`] = "foo";
+ p[`${proxy}ProxyPort`] = 42;
+ if (proxy === "socks") {
+ p[`${proxy}Version`] = 4;
+ }
+
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 1);
+ equal(Preferences.get("network.proxy.no_proxies_on"), "foo, bar");
+ equal(Preferences.get(`network.proxy.${proxy}`), "foo");
+ equal(Preferences.get(`network.proxy.${proxy}_port`), 42);
+ if (proxy === "socks") {
+ equal(Preferences.get(`network.proxy.${proxy}_version`), 4);
+ }
+ }
+
+ // empty no proxy should reset default exclustions
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = [];
+ ok(p.init());
+ equal(Preferences.get("network.proxy.no_proxies_on"), "");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_toString() {
+ equal(new Proxy().toString(), "[object Proxy]");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_toJSON() {
+ let p = new Proxy();
+ deepEqual(p.toJSON(), {});
+
+ // autoconfig url
+ p = new Proxy();
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "foo";
+ deepEqual(p.toJSON(), { proxyType: "pac", proxyAutoconfigUrl: "foo" });
+
+ // manual proxy
+ p = new Proxy();
+ p.proxyType = "manual";
+ deepEqual(p.toJSON(), { proxyType: "manual" });
+
+ for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) {
+ let expected = { proxyType: "manual" };
+
+ p = new Proxy();
+ p.proxyType = "manual";
+
+ if (proxy == "socksProxy") {
+ p.socksVersion = 5;
+ expected.socksVersion = 5;
+ }
+
+ // without port
+ p[proxy] = "foo";
+ expected[proxy] = "foo";
+ deepEqual(p.toJSON(), expected);
+
+ // with port
+ p[proxy] = "foo";
+ p[`${proxy}Port`] = 0;
+ expected[proxy] = "foo:0";
+ deepEqual(p.toJSON(), expected);
+
+ p[`${proxy}Port`] = 42;
+ expected[proxy] = "foo:42";
+ deepEqual(p.toJSON(), expected);
+
+ // add brackets for IPv6 address as proxy hostname
+ p[proxy] = "2001:db8::1";
+ p[`${proxy}Port`] = 42;
+ expected[proxy] = "foo:42";
+ expected[proxy] = "[2001:db8::1]:42";
+ deepEqual(p.toJSON(), expected);
+ }
+
+ // noProxy: add brackets for IPv6 address
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["2001:db8::1"];
+ let expected = { proxyType: "manual", noProxy: "[2001:db8::1]" };
+ deepEqual(p.toJSON(), expected);
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_fromJSON() {
+ let p = new Proxy();
+ deepEqual(p, Proxy.fromJSON(undefined));
+ deepEqual(p, Proxy.fromJSON(null));
+
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => Proxy.fromJSON(typ), /InvalidArgumentError/);
+ }
+
+ // must contain a valid proxyType
+ Assert.throws(() => Proxy.fromJSON({}), /InvalidArgumentError/);
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "foo" }),
+ /InvalidArgumentError/
+ );
+
+ // autoconfig url
+ for (let url of [true, 42, [], {}]) {
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: url }),
+ /InvalidArgumentError/
+ );
+ }
+
+ p = new Proxy();
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "foo";
+ deepEqual(p, Proxy.fromJSON({ proxyType: "pac", proxyAutoconfigUrl: "foo" }));
+
+ // manual proxy
+ p = new Proxy();
+ p.proxyType = "manual";
+ deepEqual(p, Proxy.fromJSON({ proxyType: "manual" }));
+
+ for (let proxy of ["httpProxy", "sslProxy", "socksProxy"]) {
+ let manual = { proxyType: "manual" };
+
+ // invalid hosts
+ for (let host of [
+ true,
+ 42,
+ [],
+ {},
+ null,
+ "http://foo",
+ "foo:-1",
+ "foo:65536",
+ "foo/test",
+ "foo#42",
+ "foo?foo=bar",
+ "2001:db8::1",
+ ]) {
+ manual[proxy] = host;
+ Assert.throws(() => Proxy.fromJSON(manual), /InvalidArgumentError/);
+ }
+
+ p = new Proxy();
+ p.proxyType = "manual";
+ if (proxy == "socksProxy") {
+ manual.socksVersion = 5;
+ p.socksVersion = 5;
+ }
+
+ let host_map = {
+ "foo:1": { hostname: "foo", port: 1 },
+ "foo:21": { hostname: "foo", port: 21 },
+ "foo:80": { hostname: "foo", port: 80 },
+ "foo:443": { hostname: "foo", port: 443 },
+ "foo:65535": { hostname: "foo", port: 65535 },
+ "127.0.0.1:42": { hostname: "127.0.0.1", port: 42 },
+ "[2001:db8::1]:42": { hostname: "2001:db8::1", port: "42" },
+ };
+
+ // valid proxy hosts with port
+ for (let host in host_map) {
+ manual[proxy] = host;
+
+ p[`${proxy}`] = host_map[host].hostname;
+ p[`${proxy}Port`] = host_map[host].port;
+
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+
+ // Without a port the default port of the scheme is used
+ for (let host of ["foo", "foo:"]) {
+ manual[proxy] = host;
+
+ // For socks no default port is available
+ p[proxy] = `foo`;
+ if (proxy === "socksProxy") {
+ p[`${proxy}Port`] = null;
+ } else {
+ let default_ports = { httpProxy: 80, sslProxy: 443 };
+
+ p[`${proxy}Port`] = default_ports[proxy];
+ }
+
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+ }
+
+ // missing required socks version
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", socksProxy: "foo:1234" }),
+ /InvalidArgumentError/
+ );
+
+ // Bug 1703805: Since Firefox 90 ftpProxy is no longer supported
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", ftpProxy: "foo:21" }),
+ /InvalidArgumentError/
+ );
+
+ // noProxy: invalid settings
+ for (let noProxy of [true, 42, {}, null, "foo", [true], [42], [{}], [null]]) {
+ Assert.throws(
+ () => Proxy.fromJSON({ proxyType: "manual", noProxy }),
+ /InvalidArgumentError/
+ );
+ }
+
+ // noProxy: valid settings
+ p = new Proxy();
+ p.proxyType = "manual";
+ for (let noProxy of [[], ["foo"], ["foo", "bar"], ["127.0.0.1"]]) {
+ let manual = { proxyType: "manual", noProxy };
+ p.noProxy = noProxy;
+ deepEqual(p, Proxy.fromJSON(manual));
+ }
+
+ // noProxy: IPv6 needs brackets removed
+ p = new Proxy();
+ p.proxyType = "manual";
+ p.noProxy = ["2001:db8::1"];
+ let manual = { proxyType: "manual", noProxy: ["[2001:db8::1]"] };
+ deepEqual(p, Proxy.fromJSON(manual));
+
+ run_next_test();
+});
+
+add_test(function test_UnhandledPromptBehavior() {
+ equal(UnhandledPromptBehavior.Accept, "accept");
+ equal(UnhandledPromptBehavior.AcceptAndNotify, "accept and notify");
+ equal(UnhandledPromptBehavior.Dismiss, "dismiss");
+ equal(UnhandledPromptBehavior.DismissAndNotify, "dismiss and notify");
+ equal(UnhandledPromptBehavior.Ignore, "ignore");
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_ctor() {
+ let caps = new Capabilities();
+ ok(caps.has("browserName"));
+ ok(caps.has("browserVersion"));
+ ok(caps.has("platformName"));
+ ok(["linux", "mac", "windows", "android"].includes(caps.get("platformName")));
+ equal(PageLoadStrategy.Normal, caps.get("pageLoadStrategy"));
+ equal(false, caps.get("acceptInsecureCerts"));
+ ok(caps.get("timeouts") instanceof Timeouts);
+ ok(caps.get("proxy") instanceof Proxy);
+ equal(caps.get("setWindowRect"), !AppInfo.isAndroid);
+ equal(caps.get("strictFileInteractability"), false);
+ equal(caps.get("webSocketUrl"), null);
+
+ equal(false, caps.get("moz:accessibilityChecks"));
+ ok(caps.has("moz:buildID"));
+ ok(caps.has("moz:debuggerAddress"));
+ ok(caps.has("moz:platformVersion"));
+ ok(caps.has("moz:processID"));
+ ok(caps.has("moz:profile"));
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ equal(true, caps.get("moz:webdriverClick"));
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_toString() {
+ equal("[object Capabilities]", new Capabilities().toString());
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_toJSON() {
+ let caps = new Capabilities();
+ let json = caps.toJSON();
+
+ equal(caps.get("browserName"), json.browserName);
+ equal(caps.get("browserVersion"), json.browserVersion);
+ equal(caps.get("platformName"), json.platformName);
+ equal(caps.get("pageLoadStrategy"), json.pageLoadStrategy);
+ equal(caps.get("acceptInsecureCerts"), json.acceptInsecureCerts);
+ deepEqual(caps.get("proxy").toJSON(), json.proxy);
+ deepEqual(caps.get("timeouts").toJSON(), json.timeouts);
+ equal(caps.get("setWindowRect"), json.setWindowRect);
+ equal(caps.get("strictFileInteractability"), json.strictFileInteractability);
+ equal(caps.get("webSocketUrl"), json.webSocketUrl);
+
+ equal(caps.get("moz:accessibilityChecks"), json["moz:accessibilityChecks"]);
+ equal(caps.get("moz:buildID"), json["moz:buildID"]);
+ equal(caps.get("moz:debuggerAddress"), json["moz:debuggerAddress"]);
+ equal(caps.get("moz:platformVersion"), json["moz:platformVersion"]);
+ equal(caps.get("moz:processID"), json["moz:processID"]);
+ equal(caps.get("moz:profile"), json["moz:profile"]);
+ equal(
+ caps.get("moz:useNonSpecCompliantPointerOrigin"),
+ json["moz:useNonSpecCompliantPointerOrigin"]
+ );
+ equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]);
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_fromJSON() {
+ const { fromJSON } = Capabilities;
+
+ // plain
+ for (let typ of [{}, null, undefined]) {
+ ok(fromJSON(typ).has("browserName"));
+ }
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => fromJSON(typ), /InvalidArgumentError/);
+ }
+
+ // matching
+ let caps = new Capabilities();
+
+ caps = fromJSON({ acceptInsecureCerts: true });
+ equal(true, caps.get("acceptInsecureCerts"));
+ caps = fromJSON({ acceptInsecureCerts: false });
+ equal(false, caps.get("acceptInsecureCerts"));
+ Assert.throws(
+ () => fromJSON({ acceptInsecureCerts: "foo" }),
+ /InvalidArgumentError/
+ );
+
+ for (let strategy of Object.values(PageLoadStrategy)) {
+ caps = fromJSON({ pageLoadStrategy: strategy });
+ equal(strategy, caps.get("pageLoadStrategy"));
+ }
+ Assert.throws(
+ () => fromJSON({ pageLoadStrategy: "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ pageLoadStrategy: null }),
+ /InvalidArgumentError/
+ );
+
+ let proxyConfig = { proxyType: "manual" };
+ caps = fromJSON({ proxy: proxyConfig });
+ equal("manual", caps.get("proxy").proxyType);
+
+ let timeoutsConfig = { implicit: 123 };
+ caps = fromJSON({ timeouts: timeoutsConfig });
+ equal(123, caps.get("timeouts").implicit);
+
+ if (!AppInfo.isAndroid) {
+ caps = fromJSON({ setWindowRect: true });
+ equal(true, caps.get("setWindowRect"));
+ Assert.throws(
+ () => fromJSON({ setWindowRect: false }),
+ /InvalidArgumentError/
+ );
+ } else {
+ Assert.throws(
+ () => fromJSON({ setWindowRect: true }),
+ /InvalidArgumentError/
+ );
+ }
+
+ caps = fromJSON({ strictFileInteractability: false });
+ equal(false, caps.get("strictFileInteractability"));
+ caps = fromJSON({ strictFileInteractability: true });
+ equal(true, caps.get("strictFileInteractability"));
+
+ caps = fromJSON({ webSocketUrl: true });
+ equal(true, caps.get("webSocketUrl"));
+ Assert.throws(
+ () => fromJSON({ webSocketUrl: false }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ webSocketUrl: "foo" }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "moz:accessibilityChecks": true });
+ equal(true, caps.get("moz:accessibilityChecks"));
+ caps = fromJSON({ "moz:accessibilityChecks": false });
+ equal(false, caps.get("moz:accessibilityChecks"));
+ Assert.throws(
+ () => fromJSON({ "moz:accessibilityChecks": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:accessibilityChecks": 1 }),
+ /InvalidArgumentError/
+ );
+
+ // capability is always populated with null if remote agent is not listening
+ caps = fromJSON({});
+ equal(null, caps.get("moz:debuggerAddress"));
+ caps = fromJSON({ "moz:debuggerAddress": "foo" });
+ equal(null, caps.get("moz:debuggerAddress"));
+ caps = fromJSON({ "moz:debuggerAddress": true });
+ equal(null, caps.get("moz:debuggerAddress"));
+
+ caps = fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false });
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ caps = fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true });
+ equal(true, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": 1 }),
+ /InvalidArgumentError/
+ );
+
+ caps = fromJSON({ "moz:webdriverClick": true });
+ equal(true, caps.get("moz:webdriverClick"));
+ caps = fromJSON({ "moz:webdriverClick": false });
+ equal(false, caps.get("moz:webdriverClick"));
+ Assert.throws(
+ () => fromJSON({ "moz:webdriverClick": "foo" }),
+ /InvalidArgumentError/
+ );
+ Assert.throws(
+ () => fromJSON({ "moz:webdriverClick": 1 }),
+ /InvalidArgumentError/
+ );
+
+ run_next_test();
+});
+
+// use Proxy.toJSON to test marshal
+add_test(function test_marshal() {
+ let proxy = new Proxy();
+
+ // drop empty fields
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = "manual";
+ deepEqual({ proxyType: "manual" }, proxy.toJSON());
+ proxy.proxyType = null;
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = undefined;
+ deepEqual({}, proxy.toJSON());
+
+ // iterate over object literals
+ proxy.proxyType = { foo: "bar" };
+ deepEqual({ proxyType: { foo: "bar" } }, proxy.toJSON());
+
+ // iterate over complex object that implement toJSON
+ proxy.proxyType = new Proxy();
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType.proxyType = "manual";
+ deepEqual({ proxyType: { proxyType: "manual" } }, proxy.toJSON());
+
+ // drop objects with no entries
+ proxy.proxyType = { foo: {} };
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = { foo: new Proxy() };
+ deepEqual({}, proxy.toJSON());
+
+ run_next_test();
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_Errors.js b/remote/shared/webdriver/test/xpcshell/test_Errors.js
new file mode 100644
index 0000000000..1510198afe
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Errors.js
@@ -0,0 +1,499 @@
+/* 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 { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+
+function notok(condition) {
+ ok(!condition);
+}
+
+add_test(function test_isError() {
+ notok(error.isError(null));
+ notok(error.isError([]));
+ notok(error.isError(new Date()));
+
+ ok(error.isError(new Components.Exception()));
+ ok(error.isError(new Error()));
+ ok(error.isError(new EvalError()));
+ ok(error.isError(new InternalError()));
+ ok(error.isError(new RangeError()));
+ ok(error.isError(new ReferenceError()));
+ ok(error.isError(new SyntaxError()));
+ ok(error.isError(new TypeError()));
+ ok(error.isError(new URIError()));
+ ok(error.isError(new error.WebDriverError()));
+ ok(error.isError(new error.InvalidArgumentError()));
+
+ run_next_test();
+});
+
+add_test(function test_isWebDriverError() {
+ notok(error.isWebDriverError(new Components.Exception()));
+ notok(error.isWebDriverError(new Error()));
+ notok(error.isWebDriverError(new EvalError()));
+ notok(error.isWebDriverError(new InternalError()));
+ notok(error.isWebDriverError(new RangeError()));
+ notok(error.isWebDriverError(new ReferenceError()));
+ notok(error.isWebDriverError(new SyntaxError()));
+ notok(error.isWebDriverError(new TypeError()));
+ notok(error.isWebDriverError(new URIError()));
+
+ ok(error.isWebDriverError(new error.WebDriverError()));
+ ok(error.isWebDriverError(new error.InvalidArgumentError()));
+ ok(error.isWebDriverError(new error.JavaScriptError()));
+
+ run_next_test();
+});
+
+add_test(function test_wrap() {
+ // webdriver-derived errors should not be wrapped
+ equal(error.wrap(new error.WebDriverError()).name, "WebDriverError");
+ ok(error.wrap(new error.WebDriverError()) instanceof error.WebDriverError);
+ equal(
+ error.wrap(new error.InvalidArgumentError()).name,
+ "InvalidArgumentError"
+ );
+ ok(
+ error.wrap(new error.InvalidArgumentError()) instanceof error.WebDriverError
+ );
+ ok(
+ error.wrap(new error.InvalidArgumentError()) instanceof
+ error.InvalidArgumentError
+ );
+
+ // JS errors should be wrapped in UnknownError
+ equal(error.wrap(new Error()).name, "UnknownError");
+ ok(error.wrap(new Error()) instanceof error.UnknownError);
+ equal(error.wrap(new EvalError()).name, "UnknownError");
+ equal(error.wrap(new InternalError()).name, "UnknownError");
+ equal(error.wrap(new RangeError()).name, "UnknownError");
+ equal(error.wrap(new ReferenceError()).name, "UnknownError");
+ equal(error.wrap(new SyntaxError()).name, "UnknownError");
+ equal(error.wrap(new TypeError()).name, "UnknownError");
+ equal(error.wrap(new URIError()).name, "UnknownError");
+
+ // wrapped JS errors should retain their type
+ // as part of the message field
+ equal(error.wrap(new error.WebDriverError("foo")).message, "foo");
+ equal(error.wrap(new TypeError("foo")).message, "TypeError: foo");
+
+ run_next_test();
+});
+
+add_test(function test_stringify() {
+ equal("<unprintable error>", error.stringify());
+ equal("<unprintable error>", error.stringify("foo"));
+ equal("[object Object]", error.stringify({}));
+ equal("[object Object]\nfoo", error.stringify({ stack: "foo" }));
+ equal("Error: foo", error.stringify(new Error("foo")).split("\n")[0]);
+ equal(
+ "WebDriverError: foo",
+ error.stringify(new error.WebDriverError("foo")).split("\n")[0]
+ );
+ equal(
+ "InvalidArgumentError: foo",
+ error.stringify(new error.InvalidArgumentError("foo")).split("\n")[0]
+ );
+
+ run_next_test();
+});
+
+add_test(function test_stack() {
+ equal("string", typeof error.stack());
+ ok(error.stack().includes("test_stack"));
+ ok(!error.stack().includes("add_test"));
+
+ run_next_test();
+});
+
+add_test(function test_toJSON() {
+ let e0 = new error.WebDriverError();
+ let e0s = e0.toJSON();
+ equal(e0s.error, "webdriver error");
+ equal(e0s.message, "");
+ equal(e0s.stacktrace, e0.stack);
+
+ let e1 = new error.WebDriverError("a");
+ let e1s = e1.toJSON();
+ equal(e1s.message, e1.message);
+ equal(e1s.stacktrace, e1.stack);
+
+ let e2 = new error.JavaScriptError("foo");
+ let e2s = e2.toJSON();
+ equal(e2.status, e2s.error);
+ equal(e2.message, e2s.message);
+
+ run_next_test();
+});
+
+add_test(function test_fromJSON() {
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({ error: "foo" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({ error: "Error" }),
+ /Not of WebDriverError descent/
+ );
+ Assert.throws(
+ () => error.WebDriverError.fromJSON({}),
+ /Undeserialisable error type/
+ );
+ Assert.throws(() => error.WebDriverError.fromJSON(undefined), /TypeError/);
+
+ // stacks will be different
+ let e1 = new error.WebDriverError("1");
+ let e1r = error.WebDriverError.fromJSON({
+ error: "webdriver error",
+ message: "1",
+ });
+ ok(e1r instanceof error.WebDriverError);
+ equal(e1r.name, e1.name);
+ equal(e1r.status, e1.status);
+ equal(e1r.message, e1.message);
+
+ // stacks will be different
+ let e2 = new error.InvalidArgumentError("2");
+ let e2r = error.WebDriverError.fromJSON({
+ error: "invalid argument",
+ message: "2",
+ });
+ ok(e2r instanceof error.WebDriverError);
+ ok(e2r instanceof error.InvalidArgumentError);
+ equal(e2r.name, e2.name);
+ equal(e2r.status, e2.status);
+ equal(e2r.message, e2.message);
+
+ // test stacks
+ let e3j = { error: "no such element", message: "3", stacktrace: "4" };
+ let e3r = error.WebDriverError.fromJSON(e3j);
+ ok(e3r instanceof error.WebDriverError);
+ ok(e3r instanceof error.NoSuchElementError);
+ equal(e3r.name, "NoSuchElementError");
+ equal(e3r.status, e3j.error);
+ equal(e3r.message, e3j.message);
+ equal(e3r.stack, e3j.stacktrace);
+
+ // parity with toJSON
+ let e4j = new error.JavaScriptError("foo").toJSON();
+ let e4 = error.WebDriverError.fromJSON(e4j);
+ equal(e4j.error, e4.status);
+ equal(e4j.message, e4.message);
+ equal(e4j.stacktrace, e4.stack);
+
+ run_next_test();
+});
+
+add_test(function test_WebDriverError() {
+ let err = new error.WebDriverError("foo");
+ equal("WebDriverError", err.name);
+ equal("foo", err.message);
+ equal("webdriver error", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_DetachedShadowRootError() {
+ let err = new error.DetachedShadowRootError("foo");
+ equal("DetachedShadowRootError", err.name);
+ equal("foo", err.message);
+ equal("detached shadow root", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_ElementClickInterceptedError() {
+ let otherEl = {
+ hasAttribute: attr => attr in otherEl,
+ getAttribute: attr => (attr in otherEl ? otherEl[attr] : null),
+ nodeType: 1,
+ localName: "a",
+ };
+ let obscuredEl = {
+ hasAttribute: attr => attr in obscuredEl,
+ getAttribute: attr => (attr in obscuredEl ? obscuredEl[attr] : null),
+ nodeType: 1,
+ localName: "b",
+ ownerDocument: {
+ elementFromPoint() {
+ return otherEl;
+ },
+ },
+ style: {
+ pointerEvents: "auto",
+ },
+ };
+
+ let err1 = new error.ElementClickInterceptedError(obscuredEl, { x: 1, y: 2 });
+ equal("ElementClickInterceptedError", err1.name);
+ equal(
+ "Element <b> is not clickable at point (1,2) " +
+ "because another element <a> obscures it",
+ err1.message
+ );
+ equal("element click intercepted", err1.status);
+ ok(err1 instanceof error.WebDriverError);
+
+ obscuredEl.style.pointerEvents = "none";
+ let err2 = new error.ElementClickInterceptedError(obscuredEl, { x: 1, y: 2 });
+ equal(
+ "Element <b> is not clickable at point (1,2) " +
+ "because it does not have pointer events enabled, " +
+ "and element <a> would receive the click instead",
+ err2.message
+ );
+
+ run_next_test();
+});
+
+add_test(function test_ElementNotAccessibleError() {
+ let err = new error.ElementNotAccessibleError("foo");
+ equal("ElementNotAccessibleError", err.name);
+ equal("foo", err.message);
+ equal("element not accessible", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_ElementNotInteractableError() {
+ let err = new error.ElementNotInteractableError("foo");
+ equal("ElementNotInteractableError", err.name);
+ equal("foo", err.message);
+ equal("element not interactable", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InsecureCertificateError() {
+ let err = new error.InsecureCertificateError("foo");
+ equal("InsecureCertificateError", err.name);
+ equal("foo", err.message);
+ equal("insecure certificate", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InvalidArgumentError() {
+ let err = new error.InvalidArgumentError("foo");
+ equal("InvalidArgumentError", err.name);
+ equal("foo", err.message);
+ equal("invalid argument", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InvalidCookieDomainError() {
+ let err = new error.InvalidCookieDomainError("foo");
+ equal("InvalidCookieDomainError", err.name);
+ equal("foo", err.message);
+ equal("invalid cookie domain", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InvalidElementStateError() {
+ let err = new error.InvalidElementStateError("foo");
+ equal("InvalidElementStateError", err.name);
+ equal("foo", err.message);
+ equal("invalid element state", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InvalidSelectorError() {
+ let err = new error.InvalidSelectorError("foo");
+ equal("InvalidSelectorError", err.name);
+ equal("foo", err.message);
+ equal("invalid selector", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_InvalidSessionIDError() {
+ let err = new error.InvalidSessionIDError("foo");
+ equal("InvalidSessionIDError", err.name);
+ equal("foo", err.message);
+ equal("invalid session id", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_JavaScriptError() {
+ let err = new error.JavaScriptError("foo");
+ equal("JavaScriptError", err.name);
+ equal("foo", err.message);
+ equal("javascript error", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ equal("", new error.JavaScriptError(undefined).message);
+
+ let superErr = new RangeError("foo");
+ let inheritedErr = new error.JavaScriptError(superErr);
+ equal("RangeError: foo", inheritedErr.message);
+ equal(superErr.stack, inheritedErr.stack);
+
+ run_next_test();
+});
+
+add_test(function test_MoveTargetOutOfBoundsError() {
+ let err = new error.MoveTargetOutOfBoundsError("foo");
+ equal("MoveTargetOutOfBoundsError", err.name);
+ equal("foo", err.message);
+ equal("move target out of bounds", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_NoSuchAlertError() {
+ let err = new error.NoSuchAlertError("foo");
+ equal("NoSuchAlertError", err.name);
+ equal("foo", err.message);
+ equal("no such alert", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_NoSuchElementError() {
+ let err = new error.NoSuchElementError("foo");
+ equal("NoSuchElementError", err.name);
+ equal("foo", err.message);
+ equal("no such element", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_NoSuchFrameError() {
+ let err = new error.NoSuchFrameError("foo");
+ equal("NoSuchFrameError", err.name);
+ equal("foo", err.message);
+ equal("no such frame", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_NoSuchShadowRootError() {
+ let err = new error.NoSuchShadowRootError("foo");
+ equal("NoSuchShadowRootError", err.name);
+ equal("foo", err.message);
+ equal("no such shadow root", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_NoSuchWindowError() {
+ let err = new error.NoSuchWindowError("foo");
+ equal("NoSuchWindowError", err.name);
+ equal("foo", err.message);
+ equal("no such window", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_ScriptTimeoutError() {
+ let err = new error.ScriptTimeoutError("foo");
+ equal("ScriptTimeoutError", err.name);
+ equal("foo", err.message);
+ equal("script timeout", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_SessionNotCreatedError() {
+ let err = new error.SessionNotCreatedError("foo");
+ equal("SessionNotCreatedError", err.name);
+ equal("foo", err.message);
+ equal("session not created", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_StaleElementReferenceError() {
+ let err = new error.StaleElementReferenceError("foo");
+ equal("StaleElementReferenceError", err.name);
+ equal("foo", err.message);
+ equal("stale element reference", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_TimeoutError() {
+ let err = new error.TimeoutError("foo");
+ equal("TimeoutError", err.name);
+ equal("foo", err.message);
+ equal("timeout", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_UnableToSetCookieError() {
+ let err = new error.UnableToSetCookieError("foo");
+ equal("UnableToSetCookieError", err.name);
+ equal("foo", err.message);
+ equal("unable to set cookie", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_UnexpectedAlertOpenError() {
+ let err = new error.UnexpectedAlertOpenError("foo");
+ equal("UnexpectedAlertOpenError", err.name);
+ equal("foo", err.message);
+ equal("unexpected alert open", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_UnknownCommandError() {
+ let err = new error.UnknownCommandError("foo");
+ equal("UnknownCommandError", err.name);
+ equal("foo", err.message);
+ equal("unknown command", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_UnknownError() {
+ let err = new error.UnknownError("foo");
+ equal("UnknownError", err.name);
+ equal("foo", err.message);
+ equal("unknown error", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
+
+add_test(function test_UnsupportedOperationError() {
+ let err = new error.UnsupportedOperationError("foo");
+ equal("UnsupportedOperationError", err.name);
+ equal("foo", err.message);
+ equal("unsupported operation", err.status);
+ ok(err instanceof error.WebDriverError);
+
+ run_next_test();
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_NodeCache.js b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js
new file mode 100644
index 0000000000..8111cd0bc7
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_NodeCache.js
@@ -0,0 +1,123 @@
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+
+const nodeCache = new NodeCache();
+
+const SVG_NS = "http://www.w3.org/2000/svg";
+
+const browser = Services.appShell.createWindowlessBrowser(false);
+
+const domEl = browser.document.createElement("div");
+browser.document.body.appendChild(domEl);
+
+const svgEl = browser.document.createElementNS(SVG_NS, "rect");
+browser.document.body.appendChild(svgEl);
+
+registerCleanupFunction(() => {
+ nodeCache.clear({ all: true });
+});
+
+add_test(function addElement() {
+ const domElRef = nodeCache.add(domEl);
+ equal(nodeCache.size, 1);
+
+ const domElRefOther = nodeCache.add(domEl);
+ equal(nodeCache.size, 1);
+ equal(domElRefOther, domElRef);
+
+ nodeCache.add(svgEl);
+ equal(nodeCache.size, 2);
+
+ run_next_test();
+});
+
+add_test(function addInvalidElement() {
+ Assert.throws(() => nodeCache.add("foo"), /UnknownError/);
+
+ run_next_test();
+});
+
+add_test(function clear() {
+ nodeCache.add(domEl);
+ nodeCache.add(svgEl);
+ equal(nodeCache.size, 2);
+
+ // Clear requires explicit arguments.
+ Assert.throws(() => nodeCache.clear(), /Error/);
+
+ // Clear references for a different browsing context
+ const browser2 = Services.appShell.createWindowlessBrowser(false);
+ let imgEl = browser2.document.createElement("img");
+ browser2.document.body.appendChild(imgEl);
+
+ nodeCache.add(imgEl);
+ nodeCache.clear({ browsingContext: browser.browsingContext });
+ equal(nodeCache.size, 1);
+
+ // Clear all references
+ nodeCache.add(domEl);
+ equal(nodeCache.size, 2);
+
+ nodeCache.clear({ all: true });
+ equal(nodeCache.size, 0);
+
+ run_next_test();
+});
+
+add_test(function resolveElement() {
+ const domElSharedId = nodeCache.add(domEl);
+ deepEqual(nodeCache.resolve(domElSharedId), domEl);
+
+ const svgElSharedId = nodeCache.add(svgEl);
+ deepEqual(nodeCache.resolve(svgElSharedId), svgEl);
+ deepEqual(nodeCache.resolve(domElSharedId), domEl);
+
+ run_next_test();
+});
+
+add_test(function resolveUnknownElement() {
+ Assert.throws(() => nodeCache.resolve("foo"), /NoSuchElementError/);
+
+ run_next_test();
+});
+
+add_test(function resolveElementNotAttachedToDOM() {
+ const imgEl = browser.document.createElement("img");
+
+ const imgElSharedId = nodeCache.add(imgEl);
+ deepEqual(nodeCache.resolve(imgElSharedId), imgEl);
+
+ run_next_test();
+});
+
+add_test(async function resolveElementRemoved() {
+ let imgEl = browser.document.createElement("img");
+ const imgElSharedId = nodeCache.add(imgEl);
+
+ // Delete element and force a garbage collection
+ imgEl = null;
+
+ await doGC();
+
+ const el = nodeCache.resolve(imgElSharedId);
+ deepEqual(el, null);
+
+ run_next_test();
+});
+
+add_test(function elementReferencesDifferentPerNodeCache() {
+ const sharedId = nodeCache.add(domEl);
+
+ const nodeCache2 = new NodeCache();
+ const sharedId2 = nodeCache2.add(domEl);
+
+ notEqual(sharedId, sharedId2);
+ equal(nodeCache.resolve(sharedId), nodeCache2.resolve(sharedId2));
+
+ Assert.throws(() => nodeCache.resolve(sharedId2), /NoSuchElementError/);
+
+ nodeCache2.clear({ all: true });
+
+ run_next_test();
+});
diff --git a/remote/shared/webdriver/test/xpcshell/test_Session.js b/remote/shared/webdriver/test/xpcshell/test_Session.js
new file mode 100644
index 0000000000..76ab59f82a
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/test_Session.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Capabilities, Timeouts } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
+);
+const { WebDriverSession } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Session.sys.mjs"
+);
+
+add_test(function test_WebDriverSession_ctor() {
+ const session = new WebDriverSession();
+
+ equal(typeof session.id, "string");
+ ok(session.capabilities instanceof Capabilities);
+
+ run_next_test();
+});
+
+add_test(function test_WebDriverSession_getters() {
+ const session = new WebDriverSession();
+
+ equal(
+ session.a11yChecks,
+ session.capabilities.get("moz:accessibilityChecks")
+ );
+ equal(session.pageLoadStrategy, session.capabilities.get("pageLoadStrategy"));
+ equal(session.proxy, session.capabilities.get("proxy"));
+ equal(
+ session.strictFileInteractability,
+ session.capabilities.get("strictFileInteractability")
+ );
+ equal(session.timeouts, session.capabilities.get("timeouts"));
+ equal(
+ session.unhandledPromptBehavior,
+ session.capabilities.get("unhandledPromptBehavior")
+ );
+
+ run_next_test();
+});
+
+add_test(function test_WebDriverSession_setters() {
+ const session = new WebDriverSession();
+
+ const timeouts = new Timeouts();
+ timeouts.pageLoad = 45;
+
+ session.timeouts = timeouts;
+ equal(session.timeouts, session.capabilities.get("timeouts"));
+
+ run_next_test();
+});
diff --git a/remote/shared/webdriver/test/xpcshell/xpcshell.ini b/remote/shared/webdriver/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..6279d13325
--- /dev/null
+++ b/remote/shared/webdriver/test/xpcshell/xpcshell.ini
@@ -0,0 +1,12 @@
+# 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/.
+
+[DEFAULT]
+head = head.js
+
+[test_Assert.js]
+[test_Capabilities.js]
+[test_Errors.js]
+[test_NodeCache.js]
+[test_Session.js]