summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /remote/webdriver-bidi
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/webdriver-bidi')
-rw-r--r--remote/webdriver-bidi/NewSessionHandler.sys.mjs57
-rw-r--r--remote/webdriver-bidi/Realm.sys.mjs306
-rw-r--r--remote/webdriver-bidi/RemoteValue.sys.mjs772
-rw-r--r--remote/webdriver-bidi/WebDriverBiDi.sys.mjs240
-rw-r--r--remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs200
-rw-r--r--remote/webdriver-bidi/jar.mn31
-rw-r--r--remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs69
-rw-r--r--remote/webdriver-bidi/modules/root/browsingContext.sys.mjs663
-rw-r--r--remote/webdriver-bidi/modules/root/log.sys.mjs15
-rw-r--r--remote/webdriver-bidi/modules/root/network.sys.mjs342
-rw-r--r--remote/webdriver-bidi/modules/root/script.sys.mjs532
-rw-r--r--remote/webdriver-bidi/modules/root/session.sys.mjs406
-rw-r--r--remote/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs31
-rw-r--r--remote/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs28
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs159
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/log.sys.mjs242
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/script.sys.mjs344
-rw-r--r--remote/webdriver-bidi/moz.build10
-rw-r--r--remote/webdriver-bidi/test/xpcshell/test_Realm.js90
-rw-r--r--remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js1016
-rw-r--r--remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js27
-rw-r--r--remote/webdriver-bidi/test/xpcshell/xpcshell.ini7
22 files changed, 5587 insertions, 0 deletions
diff --git a/remote/webdriver-bidi/NewSessionHandler.sys.mjs b/remote/webdriver-bidi/NewSessionHandler.sys.mjs
new file mode 100644
index 0000000000..342419033f
--- /dev/null
+++ b/remote/webdriver-bidi/NewSessionHandler.sys.mjs
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ WebDriverBiDiConnection:
+ "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs",
+ WebSocketHandshake:
+ "chrome://remote/content/server/WebSocketHandshake.sys.mjs",
+});
+
+/**
+ * httpd.js JSON handler for direct BiDi connections.
+ */
+export class WebDriverNewSessionHandler {
+ /**
+ * Construct a new JSON handler.
+ *
+ * @param {WebDriverBiDi} webDriverBiDi
+ * Reference to the WebDriver BiDi protocol implementation.
+ */
+ constructor(webDriverBiDi) {
+ this.webDriverBiDi = webDriverBiDi;
+ }
+
+ // nsIHttpRequestHandler
+
+ /**
+ * Handle new direct WebSocket connection requests.
+ *
+ * WebSocket clients not using the WebDriver BiDi opt-in mechanism via the
+ * WebDriver HTTP implementation will attempt to directly connect at
+ * `/session`. 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
+ );
+
+ this.webDriverBiDi.addSessionlessConnection(conn);
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
+ }
+}
diff --git a/remote/webdriver-bidi/Realm.sys.mjs b/remote/webdriver-bidi/Realm.sys.mjs
new file mode 100644
index 0000000000..e4924cecdd
--- /dev/null
+++ b/remote/webdriver-bidi/Realm.sys.mjs
@@ -0,0 +1,306 @@
+/* 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, {
+ addDebuggerToGlobal: "resource://gre/modules/jsdebugger.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "dbg", () => {
+ // eslint-disable-next-line mozilla/reject-globalThis-modification
+ lazy.addDebuggerToGlobal(globalThis);
+ return new Debugger();
+});
+
+/**
+ * @typedef {string} RealmType
+ **/
+
+/**
+ * Enum of realm types.
+ *
+ * @readonly
+ * @enum {RealmType}
+ **/
+export const RealmType = {
+ AudioWorklet: "audio-worklet",
+ DedicatedWorker: "dedicated-worker",
+ PaintWorklet: "paint-worklet",
+ ServiceWorker: "service-worker",
+ SharedWorker: "shared-worker",
+ Window: "window",
+ Worker: "worker",
+ Worklet: "worklet",
+};
+
+function getUUID() {
+ return Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+}
+
+/**
+ * Base class that wraps any kind of WebDriver BiDi realm.
+ */
+export class Realm {
+ #handleObjectMap;
+ #id;
+
+ constructor() {
+ this.#id = getUUID();
+
+ // Map of unique handles (UUIDs) to objects belonging to this realm.
+ this.#handleObjectMap = new Map();
+ }
+
+ destroy() {
+ this.#handleObjectMap = null;
+ }
+
+ /**
+ * Get the unique identifier of the realm instance.
+ *
+ * @return {string} The unique identifier.
+ */
+ get id() {
+ return this.#id;
+ }
+
+ /**
+ * A getter to get a realm origin.
+ *
+ * It's required to be implemented in the sub class.
+ */
+ get origin() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Ensure the provided object can be used within this realm.
+
+ * @param {Object} object
+ * Any non-primitive object.
+
+ * @return {Object}
+ * An object usable in the current realm.
+ */
+ cloneIntoRealm(obj) {
+ return obj;
+ }
+
+ /**
+ * Remove the reference corresponding to the provided unique handle.
+ *
+ * @param {string} handle
+ * The unique handle of an object reference tracked in this realm.
+ */
+ removeObjectHandle(handle) {
+ this.#handleObjectMap.delete(handle);
+ }
+
+ /**
+ * Get a new unique handle for the provided object, creating a strong
+ * reference on the object.
+ *
+ * @param {Object} object
+ * Any non-primitive object.
+ * @return {string} The unique handle created for this strong reference.
+ */
+ getHandleForObject(object) {
+ const handle = getUUID();
+ this.#handleObjectMap.set(handle, object);
+ return handle;
+ }
+
+ /**
+ * Get the basic realm information.
+ *
+ * @return {BaseRealmInfo}
+ */
+ getInfo() {
+ return {
+ realm: this.#id,
+ origin: this.origin,
+ };
+ }
+
+ /**
+ * Retrieve the object corresponding to the provided unique handle.
+ *
+ * @param {string} handle
+ * The unique handle of an object reference tracked in this realm.
+ * @return {Object} object
+ * Any non-primitive object.
+ */
+ getObjectForHandle(handle) {
+ return this.#handleObjectMap.get(handle);
+ }
+}
+
+/**
+ * Wrapper for Window realms including sandbox objects.
+ */
+export class WindowRealm extends Realm {
+ #globalObject;
+ #globalObjectReference;
+ #sandboxName;
+ #window;
+
+ static type = RealmType.Window;
+
+ /**
+ *
+ * @param {Window} window
+ * The window global to wrap.
+ * @param {Object} options
+ * @param {string=} options.sandboxName
+ * Name of the sandbox to create if specified. Defaults to `null`.
+ */
+ constructor(window, options = {}) {
+ const { sandboxName = null } = options;
+
+ super();
+
+ this.#sandboxName = sandboxName;
+ this.#window = window;
+ this.#globalObject =
+ sandboxName === null ? this.#window : this.#createSandbox();
+ this.#globalObjectReference = lazy.dbg.makeGlobalObjectReference(
+ this.#globalObject
+ );
+
+ lazy.dbg.enableAsyncStack(this.#globalObject);
+ }
+
+ destroy() {
+ lazy.dbg.disableAsyncStack(this.#globalObject);
+
+ this.#globalObjectReference = null;
+ this.#globalObject = null;
+ this.#window = null;
+
+ super.destroy();
+ }
+
+ get globalObjectReference() {
+ return this.#globalObjectReference;
+ }
+
+ get origin() {
+ return this.#window.origin;
+ }
+
+ #createDebuggerObject(obj) {
+ return this.#globalObjectReference.makeDebuggeeValue(obj);
+ }
+
+ #createSandbox() {
+ const win = this.#window;
+ const opts = {
+ sameZoneAs: win,
+ sandboxPrototype: win,
+ wantComponents: false,
+ wantXrays: true,
+ };
+
+ return new Cu.Sandbox(win, opts);
+ }
+
+ /**
+ * Clone the provided object into the scope of this Realm (either a window
+ * global, or a sandbox).
+ *
+ * @param {Object} obj
+ * Any non-primitive object.
+ *
+ * @return {Object}
+ * The cloned object.
+ */
+ cloneIntoRealm(obj) {
+ return Cu.cloneInto(obj, this.#globalObject);
+ }
+
+ /**
+ * Evaluates a provided expression in the context of the current realm.
+ *
+ * @param {string} expression
+ * The expression to evaluate.
+ *
+ * @return {Object}
+ * - evaluationStatus {EvaluationStatus} One of "normal", "throw".
+ * - exceptionDetails {ExceptionDetails=} the details of the exception if
+ * the evaluation status was "throw".
+ * - result {RemoteValue=} the result of the evaluation serialized as a
+ * RemoteValue if the evaluation status was "normal".
+ */
+ executeInGlobal(expression) {
+ return this.#globalObjectReference.executeInGlobal(expression, {
+ url: this.#window.document.baseURI,
+ });
+ }
+
+ /**
+ * Call a function in the context of the current realm.
+ *
+ * @param {string} functionDeclaration
+ * The body of the function to call.
+ * @param {Array<Object>} functionArguments
+ * The arguments to pass to the function call.
+ * @param {Object} thisParameter
+ * The value of the `this` keyword for the function call.
+ *
+ * @return {Object}
+ * - evaluationStatus {EvaluationStatus} One of "normal", "throw".
+ * - exceptionDetails {ExceptionDetails=} the details of the exception if
+ * the evaluation status was "throw".
+ * - result {RemoteValue=} the result of the evaluation serialized as a
+ * RemoteValue if the evaluation status was "normal".
+ */
+ executeInGlobalWithBindings(
+ functionDeclaration,
+ functionArguments,
+ thisParameter
+ ) {
+ const expression = `(${functionDeclaration}).apply(__bidi_this, __bidi_args)`;
+
+ const args = this.cloneIntoRealm([]);
+ for (const arg of functionArguments) {
+ args.push(arg);
+ }
+
+ return this.#globalObjectReference.executeInGlobalWithBindings(
+ expression,
+ {
+ __bidi_args: this.#createDebuggerObject(args),
+ __bidi_this: this.#createDebuggerObject(thisParameter),
+ },
+ {
+ url: this.#window.document.baseURI,
+ }
+ );
+ }
+
+ /**
+ * Get the realm information.
+ *
+ * @return {Object}
+ * - context {BrowsingContext} The browsing context, associated with the realm.
+ * - id {string} The realm unique identifier.
+ * - origin {string} The serialization of an origin.
+ * - sandbox {string|null} The name of the sandbox.
+ * - type {RealmType.Window} The window realm type.
+ */
+ getInfo() {
+ const baseInfo = super.getInfo();
+ return {
+ ...baseInfo,
+ context: this.#window.browsingContext,
+ sandbox: this.#sandboxName,
+ type: WindowRealm.type,
+ };
+ }
+}
diff --git a/remote/webdriver-bidi/RemoteValue.sys.mjs b/remote/webdriver-bidi/RemoteValue.sys.mjs
new file mode 100644
index 0000000000..a45f679dc2
--- /dev/null
+++ b/remote/webdriver-bidi/RemoteValue.sys.mjs
@@ -0,0 +1,772 @@
+/* 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, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
+);
+
+/**
+ * @typedef {Object} OwnershipModel
+ **/
+
+/**
+ * Enum of ownership models supported by the serialization.
+ *
+ * @readonly
+ * @enum {OwnershipModel}
+ **/
+export const OwnershipModel = {
+ None: "none",
+ Root: "root",
+};
+
+function getUUID() {
+ return Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+}
+
+const TYPED_ARRAY_CLASSES = [
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Uint16Array",
+ "Uint32Array",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Float32Array",
+ "Float64Array",
+ "BigInt64Array",
+ "BigUint64Array",
+];
+
+/**
+ * Build the serialized RemoteValue.
+ *
+ * @return {Object}
+ * An object with a mandatory `type` property, and optional `handle`,
+ * depending on the OwnershipModel, used for the serialization and
+ * on the value's type.
+ */
+function buildSerialized(type, handle = null) {
+ const serialized = { type };
+
+ if (handle !== null) {
+ serialized.handle = handle;
+ }
+
+ return serialized;
+}
+
+/**
+ * Helper to validate if a date string follows Date Time String format.
+ *
+ * @see https://tc39.es/ecma262/#sec-date-time-string-format
+ *
+ * @param {string} dateString
+ * String which needs to be validated.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>dateString</var> doesn't follow the format.
+ */
+function checkDateTimeString(dateString) {
+ // Check if a date string follows a simplification of
+ // the ISO 8601 calendar date extended format.
+ const expandedYear = "[+-]\\d{6}";
+ const year = "\\d{4}";
+ const YYYY = `${expandedYear}|${year}`;
+ const MM = "\\d{2}";
+ const DD = "\\d{2}";
+ const date = `${YYYY}(?:-${MM})?(?:-${DD})?`;
+ const HH_mm = "\\d{2}:\\d{2}";
+ const SS = "\\d{2}";
+ const sss = "\\d{3}";
+ const TZ = `Z|[+-]${HH_mm}`;
+ const time = `T${HH_mm}(?::${SS}(?:\\.${sss})?(?:${TZ})?)?`;
+ const iso8601Format = new RegExp(`^${date}(?:${time})?$`);
+
+ // Check also if a date string is a valid date.
+ if (Number.isNaN(Date.parse(dateString)) || !iso8601Format.test(dateString)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "value" for Date to be a Date Time string, got ${dateString}`
+ );
+ }
+}
+
+/**
+ * Helper to deserialize value list.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#deserialize-value-list
+ *
+ * @param {Realm} realm
+ * The Realm in which the value is deserialized.
+ * @param {Array} serializedValueList
+ * List of serialized values.
+ *
+ * @return {Array} List of deserialized values.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>serializedValueList</var> is not an array.
+ */
+function deserializeValueList(realm, serializedValueList) {
+ lazy.assert.array(
+ serializedValueList,
+ `Expected "serializedValueList" to be an array, got ${serializedValueList}`
+ );
+
+ const deserializedValues = [];
+
+ for (const item of serializedValueList) {
+ deserializedValues.push(deserialize(realm, item));
+ }
+
+ return deserializedValues;
+}
+
+/**
+ * Helper to deserialize key-value list.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#deserialize-key-value-list
+ *
+ * @param {Realm} realm
+ * The Realm in which the value is deserialized.
+ * @param {Array} serializedKeyValueList
+ * List of serialized key-value.
+ *
+ * @return {Array} List of deserialized key-value.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>serializedKeyValueList</var> is not an array or
+ * not an array of key-value arrays.
+ */
+function deserializeKeyValueList(realm, serializedKeyValueList) {
+ lazy.assert.array(
+ serializedKeyValueList,
+ `Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
+ );
+
+ const deserializedKeyValueList = [];
+
+ for (const serializedKeyValue of serializedKeyValueList) {
+ if (!Array.isArray(serializedKeyValue) || serializedKeyValue.length != 2) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected key-value pair to be an array with 2 elements, got ${serializedKeyValue}`
+ );
+ }
+ const [serializedKey, serializedValue] = serializedKeyValue;
+ const deserializedKey =
+ typeof serializedKey == "string"
+ ? serializedKey
+ : deserialize(realm, serializedKey);
+ const deserializedValue = deserialize(realm, serializedValue);
+
+ deserializedKeyValueList.push([deserializedKey, deserializedValue]);
+ }
+
+ return deserializedKeyValueList;
+}
+
+/**
+ * Deserialize a local value.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#deserialize-local-value
+ *
+ * @param {Realm} realm
+ * The Realm in which the value is deserialized.
+ * @param {Object} serializedValue
+ * Value of any type to be deserialized.
+ *
+ * @return {Object} Deserialized representation of the value.
+ */
+export function deserialize(realm, serializedValue) {
+ const { handle, type, value } = serializedValue;
+
+ // With a handle present deserialize as remote reference.
+ if (handle !== undefined) {
+ lazy.assert.string(
+ handle,
+ `Expected "handle" to be a string, got ${handle}`
+ );
+
+ const object = realm.getObjectForHandle(handle);
+ if (!object) {
+ throw new lazy.error.InvalidArgumentError(
+ `Unable to find an object reference for "handle" ${handle}`
+ );
+ }
+
+ return object;
+ }
+
+ lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
+
+ // Primitive protocol values
+ switch (type) {
+ case "undefined":
+ return undefined;
+ case "null":
+ return null;
+ case "string":
+ lazy.assert.string(
+ value,
+ `Expected "value" to be a string, got ${value}`
+ );
+ return value;
+ case "number":
+ // If value is already a number return its value.
+ if (typeof value === "number") {
+ return value;
+ }
+
+ // Otherwise it has to be one of the special strings
+ lazy.assert.in(
+ value,
+ ["NaN", "-0", "Infinity", "-Infinity"],
+ `Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
+ );
+ return Number(value);
+ case "boolean":
+ lazy.assert.boolean(
+ value,
+ `Expected "value" to be a boolean, got ${value}`
+ );
+ return value;
+ case "bigint":
+ lazy.assert.string(
+ value,
+ `Expected "value" to be a string, got ${value}`
+ );
+ try {
+ return BigInt(value);
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to deserialize value as BigInt: ${value}`
+ );
+ }
+
+ // Non-primitive protocol values
+ case "array":
+ const array = realm.cloneIntoRealm([]);
+ deserializeValueList(realm, value).forEach(v => array.push(v));
+ return array;
+ case "date":
+ // We want to support only Date Time String format,
+ // check if the value follows it.
+ checkDateTimeString(value);
+
+ return realm.cloneIntoRealm(new Date(value));
+ case "map":
+ const map = realm.cloneIntoRealm(new Map());
+ deserializeKeyValueList(realm, value).forEach(([k, v]) => map.set(k, v));
+ return map;
+ case "object":
+ const object = realm.cloneIntoRealm({});
+ deserializeKeyValueList(realm, value).forEach(
+ ([k, v]) => (object[k] = v)
+ );
+ return object;
+ case "regexp":
+ lazy.assert.object(
+ value,
+ `Expected "value" for RegExp to be an object, got ${value}`
+ );
+ const { pattern, flags } = value;
+ lazy.assert.string(
+ pattern,
+ `Expected "pattern" for RegExp to be a string, got ${pattern}`
+ );
+ if (flags !== undefined) {
+ lazy.assert.string(
+ flags,
+ `Expected "flags" for RegExp to be a string, got ${flags}`
+ );
+ }
+ try {
+ return realm.cloneIntoRealm(new RegExp(pattern, flags));
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to deserialize value as RegExp: ${value}`
+ );
+ }
+ case "set":
+ const set = realm.cloneIntoRealm(new Set());
+ deserializeValueList(realm, value).forEach(v => set.add(v));
+ return set;
+ }
+
+ lazy.logger.warn(`Unsupported type for local value ${type}`);
+ return undefined;
+}
+
+/**
+ * Helper to retrieve the handle id for a given object, for the provided realm
+ * and ownership type.
+ *
+ * See https://w3c.github.io/webdriver-bidi/#handle-for-an-object
+ *
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Object} object
+ * The object being serialized.
+ *
+ * @return {string} The unique handle id for the object. Will be null if the
+ * Ownership type is "none".
+ */
+function getHandleForObject(realm, ownershipType, object) {
+ if (ownershipType === OwnershipModel.None) {
+ return null;
+ }
+ return realm.getHandleForObject(object);
+}
+
+/**
+ * Helper to serialize an Array-like object.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#serialize-an-array-like
+ *
+ * @param {string} production
+ * Type of object
+ * @param {string} handleId
+ * The unique id of the <var>value</var>.
+ * @param {boolean} knownObject
+ * Indicates if the <var>value</var> has already been serialized.
+ * @param {Object} value
+ * The Array-like object to serialize.
+ * @param {number|null} maxDepth
+ * Depth of a serialization.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Map} serializationInternalMap
+ * Map of internal ids.
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ *
+ * @return {Object} Object for serialized values.
+ */
+function serializeArrayLike(
+ production,
+ handleId,
+ knownObject,
+ value,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+) {
+ const serialized = buildSerialized(production, handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && maxDepth !== null && maxDepth > 0) {
+ serialized.value = serializeList(
+ value,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+
+ return serialized;
+}
+
+/**
+ * Helper to serialize as a list.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-list
+ *
+ * @param {Iterable} iterable
+ * List of values to be serialized.
+ * @param {number|null} maxDepth
+ * Depth of a serialization.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Map} serializationInternalMap
+ * Map of internal ids.
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ *
+ * @return {Array} List of serialized values.
+ */
+function serializeList(
+ iterable,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+) {
+ const serialized = [];
+ const childDepth = maxDepth !== null ? maxDepth - 1 : null;
+
+ for (const item of iterable) {
+ serialized.push(
+ serialize(
+ item,
+ childDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ )
+ );
+ }
+
+ return serialized;
+}
+
+/**
+ * Helper to serialize as a mapping.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-mapping
+ *
+ * @param {Iterable} iterable
+ * List of values to be serialized.
+ * @param {number|null} maxDepth
+ * Depth of a serialization.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Map} serializationInternalMap
+ * Map of internal ids.
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ *
+ * @return {Array} List of serialized values.
+ */
+function serializeMapping(
+ iterable,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+) {
+ const serialized = [];
+ const childDepth = maxDepth !== null ? maxDepth - 1 : null;
+
+ for (const [key, item] of iterable) {
+ const serializedKey =
+ typeof key == "string"
+ ? key
+ : serialize(
+ key,
+ childDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ const serializedValue = serialize(
+ item,
+ childDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+
+ serialized.push([serializedKey, serializedValue]);
+ }
+
+ return serialized;
+}
+
+/**
+ * Helper to serialize as a Node.
+ *
+ * @param {Node} node
+ * Node to be serialized.
+ * @param {number|null} maxDepth
+ * Depth of a serialization.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Map} serializationInternalMap
+ * Map of internal ids.
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ *
+ * @return {Object} Serialized value.
+ */
+function serializeNode(
+ node,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+) {
+ const isAttribute = Attr.isInstance(node);
+ const isElement = Element.isInstance(node);
+
+ const serialized = {
+ nodeType: node.nodeType,
+ };
+
+ if (node.nodeValue !== null) {
+ serialized.nodeValue = node.nodeValue;
+ }
+
+ if (isElement || isAttribute) {
+ serialized.localName = node.localName;
+ serialized.namespaceURI = node.namespaceURI;
+ }
+
+ serialized.childNodeCount = node.childNodes.length;
+
+ let children = null;
+ if (maxDepth !== 0) {
+ children = [];
+ const childDepth = maxDepth !== null ? maxDepth - 1 : null;
+ for (const child of node.childNodes) {
+ children.push(
+ serialize(
+ child,
+ childDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ )
+ );
+ }
+ }
+ serialized.children = children;
+
+ if (isElement) {
+ serialized.attributes = [...node.attributes].reduce((map, attr) => {
+ map[attr.name] = attr.value;
+ return map;
+ }, {});
+
+ // TODO: Bug 1802137 - Add support for shadowRoot
+ }
+
+ return serialized;
+}
+
+/**
+ * Serialize a value as a remote value.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-remote-value
+ *
+ * @param {Object} value
+ * Value of any type to be serialized.
+ * @param {number|null} maxDepth
+ * Depth of a serialization.
+ * @param {OwnershipModel} ownershipType
+ * The ownership model to use for this serialization.
+ * @param {Map} serializationInternalMap
+ * Map of internal ids.
+ * @param {Realm} realm
+ * The Realm from which comes the value being serialized.
+ *
+ * @return {Object} Serialized representation of the value.
+ */
+export function serialize(
+ value,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+) {
+ const type = typeof value;
+
+ // Primitive protocol values
+ if (type == "undefined") {
+ return { type };
+ } else if (Object.is(value, null)) {
+ return { type: "null" };
+ } else if (Object.is(value, NaN)) {
+ return { type: "number", value: "NaN" };
+ } else if (Object.is(value, -0)) {
+ return { type: "number", value: "-0" };
+ } else if (Object.is(value, Infinity)) {
+ return { type: "number", value: "Infinity" };
+ } else if (Object.is(value, -Infinity)) {
+ return { type: "number", value: "-Infinity" };
+ } else if (type == "bigint") {
+ return { type, value: value.toString() };
+ } else if (["boolean", "number", "string"].includes(type)) {
+ return { type, value };
+ }
+
+ const handleId = getHandleForObject(realm, ownershipType, value);
+ const knownObject = serializationInternalMap.has(value);
+
+ // Set the OwnershipModel to use for all complex object serializations.
+ ownershipType = OwnershipModel.None;
+
+ // Remote values
+
+ // symbols are primitive JS values which can only be serialized
+ // as remote values.
+ if (type == "symbol") {
+ return buildSerialized("symbol", handleId);
+ }
+
+ // All other remote values are non-primitives and their
+ // className can be extracted with ChromeUtils.getClassName
+ const className = ChromeUtils.getClassName(value);
+ if (["Array", "HTMLCollection", "NodeList"].includes(className)) {
+ return serializeArrayLike(
+ className.toLowerCase(),
+ handleId,
+ knownObject,
+ value,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ } else if (className == "RegExp") {
+ const serialized = buildSerialized("regexp", handleId);
+ serialized.value = { pattern: value.source, flags: value.flags };
+ return serialized;
+ } else if (className == "Date") {
+ const serialized = buildSerialized("date", handleId);
+ serialized.value = value.toISOString();
+ return serialized;
+ } else if (className == "Map") {
+ const serialized = buildSerialized("map", handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && maxDepth !== null && maxDepth > 0) {
+ serialized.value = serializeMapping(
+ value.entries(),
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+ return serialized;
+ } else if (className == "Set") {
+ const serialized = buildSerialized("set", handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && maxDepth !== null && maxDepth > 0) {
+ serialized.value = serializeList(
+ value.values(),
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+ return serialized;
+ } else if (
+ [
+ "ArrayBuffer",
+ "Function",
+ "Promise",
+ "WeakMap",
+ "WeakSet",
+ "Window",
+ ].includes(className)
+ ) {
+ return buildSerialized(className.toLowerCase(), handleId);
+ } else if (lazy.error.isError(value)) {
+ return buildSerialized("error", handleId);
+ } else if (TYPED_ARRAY_CLASSES.includes(className)) {
+ return buildSerialized("typedarray", handleId);
+ } else if (Node.isInstance(value)) {
+ const serialized = buildSerialized("node", handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject) {
+ serialized.value = serializeNode(
+ value,
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+
+ return serialized;
+ } else if (ChromeUtils.isDOMObject(value)) {
+ const serialized = buildSerialized("object", handleId);
+ return serialized;
+ }
+
+ // Otherwise serialize the JavaScript object as generic object.
+ const serialized = buildSerialized("object", handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && maxDepth !== null && maxDepth > 0) {
+ serialized.value = serializeMapping(
+ Object.entries(value),
+ maxDepth,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+ return serialized;
+}
+
+/**
+ * Set the internalId property of a provided serialized RemoteValue,
+ * and potentially of a previously created serialized RemoteValue,
+ * corresponding to the same provided object.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#set-internal-ids-if-needed
+ *
+ * @param {Map} serializationInternalMap
+ * Map of objects to remote values.
+ * @param {Object} remoteValue
+ * A serialized RemoteValue for the provided object.
+ * @param {Object} object
+ * Object of any type to be serialized.
+ */
+function setInternalIdsIfNeeded(serializationInternalMap, remoteValue, object) {
+ if (!serializationInternalMap.has(object)) {
+ // If the object was not tracked yet in the current serialization, add
+ // a new entry in the serialization internal map. An internal id will only
+ // be generated if the same object is encountered again.
+ serializationInternalMap.set(object, remoteValue);
+ } else {
+ // This is at least the second time this object is encountered, retrieve the
+ // original remote value stored for this object.
+ const previousRemoteValue = serializationInternalMap.get(object);
+
+ if (!previousRemoteValue.internalId) {
+ // If the original remote value has no internal id yet, generate a uuid
+ // and update the internalId of the original remote value with it.
+ previousRemoteValue.internalId = getUUID();
+ }
+
+ // Copy the internalId of the original remote value to the new remote value.
+ remoteValue.internalId = previousRemoteValue.internalId;
+ }
+}
+
+/**
+ * Safely stringify a value.
+ *
+ * @param {Object} value
+ * Value of any type to be stringified.
+ *
+ * @return {string} String representation of the value.
+ */
+export function stringify(obj) {
+ let text;
+ try {
+ text =
+ obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
+ } catch (e) {
+ // The error-case will also be handled in `finally {}`.
+ } finally {
+ if (typeof text != "string") {
+ text = Object.prototype.toString.apply(obj);
+ }
+ }
+
+ return text;
+}
diff --git a/remote/webdriver-bidi/WebDriverBiDi.sys.mjs b/remote/webdriver-bidi/WebDriverBiDi.sys.mjs
new file mode 100644
index 0000000000..a00e9c5788
--- /dev/null
+++ b/remote/webdriver-bidi/WebDriverBiDi.sys.mjs
@@ -0,0 +1,240 @@
+/* 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, {
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ WebDriverNewSessionHandler:
+ "chrome://remote/content/webdriver-bidi/NewSessionHandler.sys.mjs",
+ WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
+);
+XPCOMUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
+
+/**
+ * Entry class for the WebDriver BiDi support.
+ *
+ * @see https://w3c.github.io/webdriver-bidi
+ */
+export class WebDriverBiDi {
+ /**
+ * Creates a new instance of the WebDriverBiDi class.
+ *
+ * @param {RemoteAgent} agent
+ * Reference to the Remote Agent instance.
+ */
+ constructor(agent) {
+ this.agent = agent;
+ this._running = false;
+
+ this._session = null;
+ this._sessionlessConnections = new Set();
+ }
+
+ get address() {
+ return `ws://${this.agent.host}:${this.agent.port}`;
+ }
+
+ get session() {
+ return this._session;
+ }
+
+ /**
+ * Add a new connection that is not yet attached to a WebDriver session.
+ *
+ * @param {WebDriverBiDiConnection} connection
+ * The connection without an accociated WebDriver session.
+ */
+ addSessionlessConnection(connection) {
+ this._sessionlessConnections.add(connection);
+ }
+
+ /**
+ * Create a new WebDriver session.
+ *
+ * @param {Object.<string, *>=} capabilities
+ * JSON Object containing any of the recognised capabilities as listed
+ * on the `WebDriverSession` class.
+ *
+ * @param {WebDriverBiDiConnection=} sessionlessConnection
+ * Optional connection that is not yet accociated with a WebDriver
+ * session, and has to be associated with the new WebDriver session.
+ *
+ * @return {Object<String, Capabilities>}
+ * Object containing the current session ID, and all its capabilities.
+ *
+ * @throws {SessionNotCreatedError}
+ * If, for whatever reason, a session could not be created.
+ */
+ async createSession(capabilities, sessionlessConnection) {
+ if (this.session) {
+ throw new lazy.error.SessionNotCreatedError(
+ "Maximum number of active sessions"
+ );
+ }
+
+ const session = new lazy.WebDriverSession(
+ capabilities,
+ sessionlessConnection
+ );
+
+ // When the Remote Agent is listening, and a BiDi WebSocket connection
+ // has been requested, register a path handler for the session.
+ let webSocketUrl = null;
+ if (
+ this.agent.running &&
+ (session.capabilities.get("webSocketUrl") || sessionlessConnection)
+ ) {
+ // Creating a WebDriver BiDi session too early can cause issues with
+ // clients in not being able to find any available browsing context.
+ // Also when closing the application while it's still starting up can
+ // cause shutdown hangs. As such WebDriver BiDi will return a new session
+ // once the initial application window has finished initializing.
+ lazy.logger.debug(`Waiting for initial application window`);
+ await this.agent.browserStartupFinished;
+
+ this.agent.server.registerPathHandler(session.path, session);
+ webSocketUrl = `${this.address}${session.path}`;
+
+ lazy.logger.debug(`Registered session handler: ${session.path}`);
+
+ if (sessionlessConnection) {
+ // Remove temporary session-less connection
+ this._sessionlessConnections.delete(sessionlessConnection);
+ }
+ }
+
+ // Also update the webSocketUrl capability to contain the session URL if
+ // a path handler has been registered. Otherwise set its value to null.
+ session.capabilities.set("webSocketUrl", webSocketUrl);
+
+ this._session = session;
+
+ return {
+ sessionId: this.session.id,
+ capabilities: this.session.capabilities,
+ };
+ }
+
+ /**
+ * Delete the current WebDriver session.
+ */
+ deleteSession() {
+ if (!this.session) {
+ return;
+ }
+
+ // When the Remote Agent is listening, and a BiDi WebSocket is active,
+ // unregister the path handler for the session.
+ if (this.agent.running && this.session.capabilities.get("webSocketUrl")) {
+ this.agent.server.registerPathHandler(this.session.path, null);
+ lazy.logger.debug(`Unregistered session handler: ${this.session.path}`);
+ }
+
+ this.session.destroy();
+ this._session = null;
+ }
+
+ /**
+ * Retrieve the readiness state of the remote end, regarding the creation of
+ * new WebDriverBiDi sessions.
+ *
+ * See https://w3c.github.io/webdriver-bidi/#command-session-status
+ *
+ * @return {Object}
+ * The readiness state.
+ */
+ getSessionReadinessStatus() {
+ if (this.session) {
+ // We currently only support one session, see Bug 1720707.
+ return {
+ ready: false,
+ message: "Session already started",
+ };
+ }
+
+ return {
+ ready: true,
+ message: "",
+ };
+ }
+
+ /**
+ * Starts the WebDriver BiDi support.
+ */
+ async start() {
+ if (this._running) {
+ return;
+ }
+
+ this._running = true;
+
+ // Install a HTTP handler for direct WebDriver BiDi connection requests.
+ this.agent.server.registerPathHandler(
+ "/session",
+ new lazy.WebDriverNewSessionHandler(this)
+ );
+
+ Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
+
+ // Write WebSocket connection details to the WebDriverBiDiServer.json file
+ // located within the application's profile.
+ this._bidiServerPath = PathUtils.join(
+ PathUtils.profileDir,
+ "WebDriverBiDiServer.json"
+ );
+
+ const data = {
+ ws_host: this.agent.host,
+ ws_port: this.agent.port,
+ };
+
+ try {
+ await IOUtils.write(
+ this._bidiServerPath,
+ lazy.textEncoder.encode(JSON.stringify(data, undefined, " "))
+ );
+ } catch (e) {
+ lazy.logger.warn(
+ `Failed to create ${this._bidiServerPath} (${e.message})`
+ );
+ }
+ }
+
+ /**
+ * Stops the WebDriver BiDi support.
+ */
+ async stop() {
+ if (!this._running) {
+ return;
+ }
+
+ try {
+ try {
+ await IOUtils.remove(this._bidiServerPath);
+ } catch (e) {
+ lazy.logger.warn(
+ `Failed to remove ${this._bidiServerPath} (${e.message})`
+ );
+ }
+
+ this.deleteSession();
+
+ this.agent.server.registerPathHandler("/session", null);
+
+ // Close all open session-less connections
+ this._sessionlessConnections.forEach(connection => connection.close());
+ this._sessionlessConnections.clear();
+ } finally {
+ this._running = false;
+ }
+ }
+}
diff --git a/remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs b/remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs
new file mode 100644
index 0000000000..07819c775a
--- /dev/null
+++ b/remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs
@@ -0,0 +1,200 @@
+/* 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";
+
+import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
+);
+
+export class WebDriverBiDiConnection extends WebSocketConnection {
+ /**
+ * @param {WebSocket} webSocket
+ * The WebSocket server connection to wrap.
+ * @param {Connection} httpdConnection
+ * Reference to the httpd.js's connection needed for clean-up.
+ */
+ constructor(webSocket, httpdConnection) {
+ super(webSocket, httpdConnection);
+
+ // Each connection has only a single associated WebDriver session.
+ this.session = null;
+ }
+
+ /**
+ * Register a new WebDriver Session to forward the messages to.
+ *
+ * @param {Session} session
+ * The WebDriverSession to register.
+ */
+ registerSession(session) {
+ if (this.session) {
+ throw new lazy.error.UnknownError(
+ "A WebDriver session has already been set"
+ );
+ }
+
+ this.session = session;
+ lazy.logger.debug(
+ `Connection ${this.id} attached to session ${session.id}`
+ );
+ }
+
+ /**
+ * Unregister the already set WebDriver session.
+ *
+ * @param {Session} session
+ * The WebDriverSession to register.
+ */
+ unregisterSession() {
+ if (!this.session) {
+ return;
+ }
+
+ this.session.removeConnection(this);
+ this.session = null;
+ }
+
+ /**
+ * Send an error back to the WebDriver BiDi client.
+ *
+ * @param {Number} id
+ * Id of the packet which lead to an error.
+ * @param {Error} err
+ * Error object with `status`, `message` and `stack` attributes.
+ */
+ sendError(id, err) {
+ const webDriverError = lazy.error.wrap(err);
+
+ this.send({
+ id,
+ error: webDriverError.status,
+ message: webDriverError.message,
+ stacktrace: webDriverError.stack,
+ });
+ }
+
+ /**
+ * Send an event coming from a module to the WebDriver BiDi client.
+ *
+ * @param {String} method
+ * The event name. This is composed by a module name, a dot character
+ * followed by the event name, e.g. `log.entryAdded`.
+ * @param {Object} params
+ * A JSON-serializable object, which is the payload of this event.
+ */
+ sendEvent(method, params) {
+ this.send({ method, params });
+ }
+
+ /**
+ * Send the result of a call to a module's method back to the
+ * WebDriver BiDi client.
+ *
+ * @param {Number} id
+ * The request id being sent by the client to call the module's method.
+ * @param {Object} result
+ * A JSON-serializable object, which is the actual result.
+ */
+ sendResult(id, result) {
+ result = typeof result !== "undefined" ? result : {};
+ this.send({ id, result });
+ }
+
+ // Transport hooks
+
+ /**
+ * Called by the `transport` when the connection is closed.
+ */
+ onClosed() {
+ this.unregisterSession();
+
+ super.onClosed();
+ }
+
+ /**
+ * Receive a packet from the WebSocket layer.
+ *
+ * This packet is sent by a WebDriver BiDi client and is meant to execute
+ * a particular method on a given module.
+ *
+ * @param Object packet
+ * JSON-serializable object sent by the client
+ */
+ async onPacket(packet) {
+ super.onPacket(packet);
+
+ const { id, method, params } = packet;
+
+ try {
+ // First check for mandatory field in the command packet
+ lazy.assert.positiveInteger(id, "id: unsigned integer value expected");
+ lazy.assert.string(method, "method: string value expected");
+ lazy.assert.object(params, "params: object value expected");
+
+ // Extract the module and the command name out of `method` attribute
+ const { module, command } = splitMethod(method);
+ let result;
+
+ // Handle static commands first
+ if (module === "session" && command === "new") {
+ // TODO: Needs capability matching code
+ result = await lazy.RemoteAgent.webDriverBiDi.createSession(
+ params,
+ this
+ );
+ } else if (module === "session" && command === "status") {
+ result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus();
+ } else {
+ lazy.assert.session(this.session);
+
+ // Bug 1741854 - Workaround to deny internal methods to be called
+ if (command.startsWith("_")) {
+ throw new lazy.error.UnknownCommandError(method);
+ }
+
+ // Finally, instruct the session to execute the command
+ result = await this.session.execute(module, command, params);
+ }
+
+ this.sendResult(id, result);
+ } catch (e) {
+ this.sendError(packet.id, e);
+ }
+ }
+}
+
+/**
+ * Splits a WebDriver BiDi method into module and command components.
+ *
+ * @param {String} method
+ * Name of the method to split, e.g. "session.subscribe".
+ *
+ * @returns {Object<String, String>}
+ * Object with the module ("session") and command ("subscribe")
+ * as properties.
+ */
+export function splitMethod(method) {
+ const parts = method.split(".");
+
+ if (parts.length != 2 || !parts[0].length || !parts[1].length) {
+ throw new TypeError(`Invalid method format: '${method}'`);
+ }
+
+ return {
+ module: parts[0],
+ command: parts[1],
+ };
+}
diff --git a/remote/webdriver-bidi/jar.mn b/remote/webdriver-bidi/jar.mn
new file mode 100644
index 0000000000..44fc24c56f
--- /dev/null
+++ b/remote/webdriver-bidi/jar.mn
@@ -0,0 +1,31 @@
+# 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/.
+
+remote.jar:
+% content remote %content/
+
+ content/webdriver-bidi/NewSessionHandler.sys.mjs (NewSessionHandler.sys.mjs)
+ content/webdriver-bidi/Realm.sys.mjs (Realm.sys.mjs)
+ content/webdriver-bidi/RemoteValue.sys.mjs (RemoteValue.sys.mjs)
+ content/webdriver-bidi/WebDriverBiDi.sys.mjs (WebDriverBiDi.sys.mjs)
+ content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs (WebDriverBiDiConnection.sys.mjs)
+
+ # WebDriver BiDi modules
+ content/webdriver-bidi/modules/ModuleRegistry.sys.mjs (modules/ModuleRegistry.sys.mjs)
+
+ # WebDriver BiDi root modules
+ content/webdriver-bidi/modules/root/browsingContext.sys.mjs (modules/root/browsingContext.sys.mjs)
+ content/webdriver-bidi/modules/root/log.sys.mjs (modules/root/log.sys.mjs)
+ content/webdriver-bidi/modules/root/network.sys.mjs (modules/root/network.sys.mjs)
+ content/webdriver-bidi/modules/root/script.sys.mjs (modules/root/script.sys.mjs)
+ content/webdriver-bidi/modules/root/session.sys.mjs (modules/root/session.sys.mjs)
+
+ # WebDriver BiDi windowglobal modules
+ content/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs (modules/windowglobal/browsingContext.sys.mjs)
+ content/webdriver-bidi/modules/windowglobal/log.sys.mjs (modules/windowglobal/log.sys.mjs)
+ content/webdriver-bidi/modules/windowglobal/script.sys.mjs (modules/windowglobal/script.sys.mjs)
+
+ # WebDriver BiDi windowglobal-in-root modules
+ content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs (modules/windowglobal-in-root/browsingContext.sys.mjs)
+ content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs (modules/windowglobal-in-root/log.sys.mjs)
diff --git a/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs
new file mode 100644
index 0000000000..5bb1a908bf
--- /dev/null
+++ b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs
@@ -0,0 +1,69 @@
+/* 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 modules = {
+ root: {},
+ "windowglobal-in-root": {},
+ windowglobal: {},
+};
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules.root, {
+ browsingContext:
+ "chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs",
+ log: "chrome://remote/content/webdriver-bidi/modules/root/log.sys.mjs",
+ network:
+ "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
+ script: "chrome://remote/content/webdriver-bidi/modules/root/script.sys.mjs",
+ session:
+ "chrome://remote/content/webdriver-bidi/modules/root/session.sys.mjs",
+});
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules["windowglobal-in-root"], {
+ browsingContext:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs",
+ log:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs",
+});
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules.windowglobal, {
+ browsingContext:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs",
+ log:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal/log.sys.mjs",
+ script:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal/script.sys.mjs",
+});
+
+/**
+ * Retrieve the WebDriver BiDi module class matching the provided module name
+ * and folder.
+ *
+ * @param {String} moduleName
+ * The name of the module to get the class for.
+ * @param {String} moduleFolder
+ * A valid folder name for modules.
+ * @return {Class=}
+ * The class corresponding to the module name and folder, null if no match
+ * was found.
+ * @throws {Error}
+ * If the provided module folder is unexpected.
+ **/
+export const getModuleClass = function(moduleName, moduleFolder) {
+ if (!modules[moduleFolder]) {
+ throw new Error(
+ `Invalid module folder "${moduleFolder}", expected one of "${Object.keys(
+ modules
+ )}"`
+ );
+ }
+
+ if (!modules[moduleFolder][moduleName]) {
+ return null;
+ }
+
+ return modules[moduleFolder][moduleName];
+};
diff --git a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
new file mode 100644
index 0000000000..cd8dff6ca0
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
@@ -0,0 +1,663 @@
+/* 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";
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ BrowsingContextListener:
+ "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
+ capture: "chrome://remote/content/shared/Capture.sys.mjs",
+ ContextDescriptorType:
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+ ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ waitForInitialNavigationCompleted:
+ "chrome://remote/content/shared/Navigate.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+ windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
+);
+
+/**
+ * @typedef {Object} CreateType
+ **/
+
+/**
+ * Enum of types supported by the browsingContext.create command.
+ *
+ * @readonly
+ * @enum {CreateType}
+ **/
+const CreateType = {
+ tab: "tab",
+ window: "window",
+};
+
+/**
+ * @typedef {string} WaitCondition
+ **/
+
+/**
+ * Wait conditions supported by WebDriver BiDi for navigation.
+ *
+ * @enum {WaitCondition}
+ */
+const WaitCondition = {
+ None: "none",
+ Interactive: "interactive",
+ Complete: "complete",
+};
+
+class BrowsingContextModule extends Module {
+ #contextListener;
+ #subscribedEvents;
+
+ /**
+ * Create a new module instance.
+ *
+ * @param {MessageHandler} messageHandler
+ * The MessageHandler instance which owns this Module instance.
+ */
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Create the console-api listener and listen on "message" events.
+ this.#contextListener = new lazy.BrowsingContextListener();
+ this.#contextListener.on("attached", this.#onContextAttached);
+
+ // Set of event names which have active subscriptions.
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {
+ this.#contextListener.off("attached", this.#onContextAttached);
+ this.#contextListener.destroy();
+
+ this.#subscribedEvents = null;
+ }
+
+ /**
+ * Capture a base64-encoded screenshot of the provided browsing context.
+ *
+ * @param {Object=} options
+ * @param {string} context
+ * Id of the browsing context to screenshot.
+ *
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async captureScreenshot(options = {}) {
+ const { context: contextId } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+ const context = this.#getBrowsingContext(contextId);
+
+ const rect = await this.messageHandler.handleCommand({
+ moduleName: "browsingContext",
+ commandName: "_getScreenshotRect",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ retryOnAbort: true,
+ });
+
+ const canvas = await lazy.capture.canvas(
+ context.topChromeWindow,
+ context,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height
+ );
+
+ return {
+ data: lazy.capture.toBase64(canvas),
+ };
+ }
+
+ /**
+ * Close the provided browsing context.
+ *
+ * @param {Object=} options
+ * @param {string} context
+ * Id of the browsing context to close.
+ *
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ * @throws {InvalidArgumentError}
+ * If the browsing context is not a top-level one.
+ */
+ async close(options = {}) {
+ const { context: contextId } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (!context) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${contextId} not found`
+ );
+ }
+
+ if (context.parent) {
+ throw new lazy.error.InvalidArgumentError(
+ `Browsing Context with id ${contextId} is not top-level`
+ );
+ }
+
+ if (lazy.TabManager.getTabCount() === 1) {
+ // The behavior when closing the last tab is currently unspecified.
+ // Warn the consumer about potential issues
+ lazy.logger.warn(
+ `Closing the last open tab (Browsing Context id ${contextId}), expect inconsistent behavior across platforms`
+ );
+ }
+
+ const tab = lazy.TabManager.getTabForBrowsingContext(context);
+ await lazy.TabManager.removeTab(tab);
+ }
+
+ /**
+ * Create a new browsing context using the provided type "tab" or "window".
+ *
+ * @param {Object=} options
+ * @param {string=} options.referenceContext
+ * Id of the top-level browsing context to use as reference.
+ * If options.type is "tab", the new tab will open in the same window as
+ * the reference context, and will be added next to the reference context.
+ * If options.type is "window", the reference context is ignored.
+ * @param {CreateType} options.type
+ * Type of browsing context to create.
+ *
+ * @throws {InvalidArgumentError}
+ * If the browsing context is not a top-level one.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async create(options = {}) {
+ const { referenceContext: referenceContextId = null, type } = options;
+ if (type !== CreateType.tab && type !== CreateType.window) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "type" to be one of ${Object.values(CreateType)}, got ${type}`
+ );
+ }
+
+ let browser;
+ switch (type) {
+ case "window":
+ let newWindow = await lazy.windowManager.openBrowserWindow();
+ browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser;
+ break;
+
+ case "tab":
+ if (!lazy.TabManager.supportsTabs()) {
+ throw new lazy.error.UnsupportedOperationError(
+ `browsingContext.create with type "tab" not supported in ${lazy.AppInfo.name}`
+ );
+ }
+
+ let referenceTab;
+ if (referenceContextId !== null) {
+ lazy.assert.string(
+ referenceContextId,
+ lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}`
+ );
+
+ const referenceBrowsingContext = lazy.TabManager.getBrowsingContextById(
+ referenceContextId
+ );
+ if (!referenceBrowsingContext) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${referenceContextId} not found`
+ );
+ }
+
+ if (referenceBrowsingContext.parent) {
+ throw new lazy.error.InvalidArgumentError(
+ `referenceContext with id ${referenceContextId} is not a top-level browsing context`
+ );
+ }
+
+ referenceTab = lazy.TabManager.getTabForBrowsingContext(
+ referenceBrowsingContext
+ );
+ }
+
+ const tab = await lazy.TabManager.addTab({
+ focus: false,
+ referenceTab,
+ });
+ browser = lazy.TabManager.getBrowserForTab(tab);
+ }
+
+ await lazy.waitForInitialNavigationCompleted(
+ browser.browsingContext.webProgress
+ );
+
+ return {
+ context: lazy.TabManager.getIdForBrowser(browser),
+ };
+ }
+
+ /**
+ * An object that holds the WebDriver Bidi browsing context information.
+ *
+ * @typedef BrowsingContextInfo
+ *
+ * @property {string} context
+ * The id of the browsing context.
+ * @property {string=} parent
+ * The parent of the browsing context if it's the root browsing context
+ * of the to be processed browsing context tree.
+ * @property {string} url
+ * The current documents location.
+ * @property {Array<BrowsingContextInfo>=} children
+ * List of child browsing contexts. Only set if maxDepth hasn't been
+ * reached yet.
+ */
+
+ /**
+ * An object that holds the WebDriver Bidi browsing context tree information.
+ *
+ * @typedef BrowsingContextGetTreeResult
+ *
+ * @property {Array<BrowsingContextInfo>} contexts
+ * List of child browsing contexts.
+ */
+
+ /**
+ * Returns a tree of all browsing contexts that are descendents of the
+ * given context, or all top-level contexts when no root is provided.
+ *
+ * @param {Object=} options
+ * @param {number=} maxDepth
+ * Depth of the browsing context tree to traverse. If not specified
+ * the whole tree is returned.
+ * @param {string=} root
+ * Id of the root browsing context.
+ *
+ * @returns {BrowsingContextGetTreeResult}
+ * Tree of browsing context information.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ getTree(options = {}) {
+ const { maxDepth = null, root: rootId = null } = options;
+
+ if (maxDepth !== null) {
+ lazy.assert.positiveInteger(
+ maxDepth,
+ `Expected "maxDepth" to be a positive integer, got ${maxDepth}`
+ );
+ }
+
+ let contexts;
+ if (rootId !== null) {
+ // With a root id specified return the context info for itself
+ // and the full tree.
+ lazy.assert.string(
+ rootId,
+ `Expected "root" to be a string, got ${rootId}`
+ );
+ contexts = [this.#getBrowsingContext(rootId)];
+ } else {
+ // Return all top-level browsing contexts.
+ contexts = lazy.TabManager.browsers.map(
+ browser => browser.browsingContext
+ );
+ }
+
+ const contextsInfo = contexts.map(context => {
+ return this.#getBrowsingContextInfo(context, { maxDepth });
+ });
+
+ return { contexts: contextsInfo };
+ }
+
+ /**
+ * An object that holds the WebDriver Bidi navigation information.
+ *
+ * @typedef BrowsingContextNavigateResult
+ *
+ * @property {String} navigation
+ * Unique id for this navigation.
+ * @property {String} url
+ * The requested or reached URL.
+ */
+
+ /**
+ * Navigate the given context to the provided url, with the provided wait condition.
+ *
+ * @param {Object=} options
+ * @param {string} context
+ * Id of the browsing context to navigate.
+ * @param {string} url
+ * Url for the navigation.
+ * @param {WaitCondition=} wait
+ * Wait condition for the navigation, one of "none", "interactive", "complete".
+ *
+ * @returns {BrowsingContextNavigateResult}
+ * Navigation result.
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchFrameError}
+ * If the browsing context for contextId cannot be found.
+ */
+ async navigate(options = {}) {
+ const { context: contextId, url, wait = WaitCondition.None } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ lazy.assert.string(url, `Expected "url" to be string, got ${url}`);
+
+ const waitConditions = Object.values(WaitCondition);
+ if (!waitConditions.includes(wait)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "wait" to be one of ${waitConditions}, got ${wait}`
+ );
+ }
+
+ const context = this.#getBrowsingContext(contextId);
+
+ // webProgress will be stable even if the context navigates, retrieve it
+ // immediately before doing any asynchronous call.
+ const webProgress = context.webProgress;
+
+ const base = await this.messageHandler.handleCommand({
+ moduleName: "browsingContext",
+ commandName: "_getBaseURL",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ retryOnAbort: true,
+ });
+
+ let targetURI;
+ try {
+ const baseURI = Services.io.newURI(base);
+ targetURI = Services.io.newURI(url, null, baseURI);
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "url" to be a valid URL (${e.message})`
+ );
+ }
+
+ return this.#awaitNavigation(webProgress, targetURI, {
+ wait,
+ });
+ }
+
+ /**
+ * Start and await a navigation on the provided BrowsingContext. Returns a
+ * promise which resolves when the navigation is done according to the provided
+ * navigation strategy.
+ *
+ * @param {WebProgress} webProgress
+ * The WebProgress instance to observe for this navigation.
+ * @param {nsIURI} targetURI
+ * The URI to navigate to.
+ * @param {Object} options
+ * @param {WaitCondition} options.wait
+ * The WaitCondition to use to wait for the navigation.
+ */
+ async #awaitNavigation(webProgress, targetURI, options) {
+ const { wait } = options;
+
+ const context = webProgress.browsingContext;
+ const browserId = context.browserId;
+
+ const resolveWhenStarted = wait === WaitCondition.None;
+ const listener = new lazy.ProgressListener(webProgress, {
+ expectNavigation: true,
+ resolveWhenStarted,
+ // In case the webprogress is already navigating, always wait for an
+ // explicit start flag.
+ waitForExplicitStart: true,
+ });
+
+ const onDocumentInteractive = (evtName, wrappedEvt) => {
+ if (webProgress.browsingContext.id !== wrappedEvt.contextId) {
+ // Ignore load events for unrelated browsing contexts.
+ return;
+ }
+
+ if (wrappedEvt.readyState === "interactive") {
+ listener.stopIfStarted();
+ }
+ };
+
+ const contextDescriptor = {
+ type: lazy.ContextDescriptorType.TopBrowsingContext,
+ id: browserId,
+ };
+
+ // For the Interactive wait condition, resolve as soon as
+ // the document becomes interactive.
+ if (wait === WaitCondition.Interactive) {
+ await this.messageHandler.eventsDispatcher.on(
+ "browsingContext._documentInteractive",
+ contextDescriptor,
+ onDocumentInteractive
+ );
+ }
+
+ const navigated = listener.start();
+ navigated.finally(async () => {
+ if (listener.isStarted) {
+ listener.stop();
+ }
+
+ if (wait === WaitCondition.Interactive) {
+ await this.messageHandler.eventsDispatcher.off(
+ "browsingContext._documentInteractive",
+ contextDescriptor,
+ onDocumentInteractive
+ );
+ }
+ });
+
+ context.loadURI(targetURI.spec, {
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ hasValidUserGestureActivation: true,
+ });
+ await navigated;
+
+ let url;
+ if (wait === WaitCondition.None) {
+ // If wait condition is None, the navigation resolved before the current
+ // context has navigated.
+ url = listener.targetURI.spec;
+ } else {
+ url = listener.currentURI.spec;
+ }
+
+ return {
+ // TODO: The navigation id should be a real id mapped to the navigation.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1763122
+ navigation: null,
+ url,
+ };
+ }
+
+ /**
+ * Retrieves a browsing context based on its id.
+ *
+ * @param {Number} contextId
+ * Id of the browsing context.
+ * @returns {BrowsingContext=}
+ * The browsing context or null if <var>contextId</var> is null.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ #getBrowsingContext(contextId) {
+ // The WebDriver BiDi specification expects null to be
+ // returned if no browsing context id has been specified.
+ if (contextId === null) {
+ return null;
+ }
+
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (context === null) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${contextId} not found`
+ );
+ }
+
+ return context;
+ }
+
+ /**
+ * Get the WebDriver BiDi browsing context information.
+ *
+ * @param {BrowsingContext} context
+ * The browsing context to get the information from.
+ * @param {Object=} options
+ * @param {boolean=} isRoot
+ * Flag that indicates if this browsing context is the root of all the
+ * browsing contexts to be returned. Defaults to true.
+ * @param {number=} maxDepth
+ * Depth of the browsing context tree to traverse. If not specified
+ * the whole tree is returned.
+ * @returns {BrowsingContextInfo}
+ * The information about the browsing context.
+ */
+ #getBrowsingContextInfo(context, options = {}) {
+ const { isRoot = true, maxDepth = null } = options;
+
+ let children = null;
+ if (maxDepth === null || maxDepth > 0) {
+ children = context.children.map(context =>
+ this.#getBrowsingContextInfo(context, {
+ maxDepth: maxDepth === null ? maxDepth : maxDepth - 1,
+ isRoot: false,
+ })
+ );
+ }
+
+ const contextInfo = {
+ context: lazy.TabManager.getIdForBrowsingContext(context),
+ url: context.currentURI.spec,
+ children,
+ };
+
+ if (isRoot) {
+ // Only emit the parent id for the top-most browsing context.
+ const parentId = lazy.TabManager.getIdForBrowsingContext(context.parent);
+ contextInfo.parent = parentId;
+ }
+
+ return contextInfo;
+ }
+
+ #onContextAttached = async (eventName, data = {}) => {
+ const { browsingContext, why } = data;
+
+ // Filter out top-level browsing contexts that are created because of a
+ // cross-group navigation.
+ if (why === "replace") {
+ return;
+ }
+
+ // Filter out notifications for chrome context until support gets
+ // added (bug 1722679).
+ if (!browsingContext.webProgress) {
+ return;
+ }
+
+ const browsingContextInfo = this.#getBrowsingContextInfo(browsingContext, {
+ maxDepth: 0,
+ });
+
+ // This event is emitted from the parent process but for a given browsing
+ // context. Set the event's contextInfo to the message handler corresponding
+ // to this browsing context.
+ const contextInfo = {
+ contextId: browsingContext.id,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+ this.emitEvent(
+ "browsingContext.contextCreated",
+ browsingContextInfo,
+ contextInfo
+ );
+ };
+
+ #subscribeEvent(event) {
+ if (event === "browsingContext.contextCreated") {
+ this.#contextListener.startListening();
+ this.#subscribedEvents.add(event);
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ if (event === "browsingContext.contextCreated") {
+ this.#contextListener.stopListening();
+ this.#subscribedEvents.delete(event);
+ }
+ }
+
+ /**
+ * Internal commands
+ */
+
+ _applySessionData(params) {
+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
+ // class.
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData.
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+
+ static get supportedEvents() {
+ return [
+ "browsingContext.contextCreated",
+ "browsingContext.domContentLoaded",
+ "browsingContext.load",
+ ];
+ }
+}
+
+export const browsingContext = BrowsingContextModule;
diff --git a/remote/webdriver-bidi/modules/root/log.sys.mjs b/remote/webdriver-bidi/modules/root/log.sys.mjs
new file mode 100644
index 0000000000..db2390d3ba
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/log.sys.mjs
@@ -0,0 +1,15 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class LogModule extends Module {
+ destroy() {}
+
+ static get supportedEvents() {
+ return ["log.entryAdded"];
+ }
+}
+
+export const log = LogModule;
diff --git a/remote/webdriver-bidi/modules/root/network.sys.mjs b/remote/webdriver-bidi/modules/root/network.sys.mjs
new file mode 100644
index 0000000000..e82bc3c072
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/network.sys.mjs
@@ -0,0 +1,342 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ NetworkListener:
+ "chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+});
+
+/**
+ * @typedef {Object} BaseParameters
+ * @property {string=} context
+ * @property {boolean} isRedirect
+ * @property {Navigation=} navigation
+ * @property {RequestData} request
+ * @property {number} timestamp
+ */
+
+/**
+ * @typedef {Object} Cookie
+ * @property {Array<number>=} binaryValue
+ * @property {string} domain
+ * @property {number=} expires
+ * @property {boolean} httpOnly
+ * @property {string} name
+ * @property {string} path
+ * @property {('lax' | 'none' | 'strict')} sameSite
+ * @property {boolean} secure
+ * @property {number} size
+ * @property {string=} value
+ */
+
+/**
+ * @typedef {Object} FetchTimingInfo
+ * @property {number} originTime
+ * @property {number} requestTime
+ * @property {number} redirectStart
+ * @property {number} redirectEnd
+ * @property {number} fetchStart
+ * @property {number} dnsStart
+ * @property {number} dnsEnd
+ * @property {number} connectStart
+ * @property {number} connectEnd
+ * @property {number} tlsStart
+ * @property {number} requestStart
+ * @property {number} responseStart
+ * @property {number} responseEnd
+ */
+
+/**
+ * @typedef {Object} Header
+ * @property {Array<number>=} binaryValue
+ * @property {string} name
+ * @property {string=} value
+ */
+
+/**
+ * @typedef {string} InitiatorType
+ **/
+
+/**
+ * Enum of possible initiator types.
+ *
+ * @readonly
+ * @enum {InitiatorType}
+ **/
+const InitiatorType = {
+ Other: "other",
+ Parser: "parser",
+ Preflight: "preflight",
+ Script: "script",
+};
+/**
+ * @typedef {Object} Initiator
+ * @property {InitiatorType} type
+ * @property {number=} columnNumber
+ * @property {number=} lineNumber
+ * @property {string=} request
+ * @property {StackTrace=} stackTrace
+ */
+
+/**
+ * @typedef {Object} RequestData
+ * @property {number|null} bodySize
+ * Defaults to null.
+ * @property {Array<Cookie>} cookies
+ * @property {Array<Header>} headers
+ * @property {number} headersSize
+ * @property {string} method
+ * @property {string} request
+ * @property {FetchTimingInfo} timings
+ * @property {string} url
+ */
+
+/**
+ * @typedef {Object} BeforeRequestSentParametersProperties
+ * @property {Initiator} initiator
+ */
+
+/**
+ * Parameters for the BeforeRequestSent event
+ *
+ * @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
+ */
+
+/**
+ * @typedef {Object} ResponseContent
+ * @property {number|null} size
+ * Defaults to null.
+ */
+
+/**
+ * @typedef {Object} ResponseData
+ * @property {string} url
+ * @property {string} protocol
+ * @property {number} status
+ * @property {string} statusText
+ * @property {boolean} fromCache
+ * @property {Array<Header>} headers
+ * @property {string} mimeType
+ * @property {number} bytesReceived
+ * @property {number|null} headersSize
+ * Defaults to null.
+ * @property {number|null} bodySize
+ * Defaults to null.
+ * @property {ResponseContent} content
+ */
+
+/**
+ * @typedef {Object} ResponseStartedParametersProperties
+ * @property {ResponseData} response
+ */
+
+/**
+ * Parameters for the ResponseStarted event
+ *
+ * @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters
+ */
+
+/**
+ * @typedef {Object} ResponseCompletedParametersProperties
+ * @property {ResponseData} response
+ */
+
+/**
+ * Parameters for the ResponseCompleted event
+ *
+ * @typedef {BaseParameters & ResponseCompletedParametersProperties} ResponseCompletedParameters
+ */
+
+class NetworkModule extends Module {
+ #beforeRequestSentMap;
+ #networkListener;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Map of request ids to redirect counts. A WebDriver BiDi request id is
+ // identical for redirects of a given request, this map allows to know if we
+ // already emitted a beforeRequestSent event for a given request with a
+ // specific redirectCount.
+ this.#beforeRequestSentMap = new Map();
+
+ // Set of event names which have active subscriptions
+ this.#subscribedEvents = new Set();
+
+ this.#networkListener = new lazy.NetworkListener();
+ this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
+ this.#networkListener.on("response-completed", this.#onResponseEvent);
+ this.#networkListener.on("response-started", this.#onResponseEvent);
+ }
+
+ destroy() {
+ this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent);
+ this.#networkListener.off("response-completed", this.#onResponseEvent);
+ this.#networkListener.off("response-started", this.#onResponseEvent);
+
+ this.#beforeRequestSentMap = null;
+ this.#subscribedEvents = null;
+ }
+
+ #getContextInfo(browsingContext) {
+ return {
+ contextId: browsingContext.id,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+ }
+
+ #onBeforeRequestSent = (name, data) => {
+ const { contextId, requestData, timestamp, redirectCount } = data;
+
+ const isRedirect = redirectCount > 0;
+ this.#beforeRequestSentMap.set(requestData.requestId, redirectCount);
+
+ // Bug 1805479: Handle the initiator, including stacktrace details.
+ const initiator = {
+ type: InitiatorType.Other,
+ };
+
+ const baseParameters = {
+ context: contextId,
+ isRedirect,
+ redirectCount,
+ // Bug 1805405: Handle the navigation id.
+ navigation: null,
+ request: requestData,
+ timestamp,
+ };
+
+ const beforeRequestSentEvent = {
+ ...baseParameters,
+ initiator,
+ };
+
+ const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
+ this.emitEvent(
+ "network.beforeRequestSent",
+ beforeRequestSentEvent,
+ this.#getContextInfo(browsingContext)
+ );
+ };
+
+ #onResponseEvent = (name, data) => {
+ const {
+ contextId,
+ requestData,
+ responseData,
+ timestamp,
+ redirectCount,
+ } = data;
+
+ const isRedirect = redirectCount > 0;
+
+ const requestId = requestData.requestId;
+ if (this.#beforeRequestSentMap.get(requestId) != redirectCount) {
+ throw new lazy.error.UnknownError(
+ `Redirect count of the request ${requestId} does not match the before request sent map`
+ );
+ }
+
+ const baseParameters = {
+ context: contextId,
+ isRedirect,
+ redirectCount,
+ // Bug 1805405: Handle the navigation id.
+ navigation: null,
+ request: requestData,
+ timestamp,
+ };
+
+ const responseEvent = {
+ ...baseParameters,
+ response: responseData,
+ };
+
+ const protocolEventName =
+ name === "response-started"
+ ? "network.responseStarted"
+ : "network.responseCompleted";
+
+ const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
+ this.emitEvent(
+ protocolEventName,
+ responseEvent,
+ this.#getContextInfo(browsingContext)
+ );
+ };
+
+ #startListening(event) {
+ if (this.#subscribedEvents.size == 0) {
+ this.#networkListener.startListening();
+ }
+ this.#subscribedEvents.add(event);
+ }
+
+ #stopListening(event) {
+ this.#subscribedEvents.delete(event);
+ if (this.#subscribedEvents.size == 0) {
+ this.#networkListener.stopListening();
+ }
+ }
+
+ #subscribeEvent(event) {
+ if (this.constructor.supportedEvents.includes(event)) {
+ this.#startListening(event);
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ if (this.constructor.supportedEvents.includes(event)) {
+ this.#stopListening(event);
+ }
+ }
+
+ /**
+ * Internal commands
+ */
+
+ _applySessionData(params) {
+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
+ // class.
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData.
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+
+ static get supportedEvents() {
+ return [
+ "network.beforeRequestSent",
+ "network.responseCompleted",
+ "network.responseStarted",
+ ];
+ }
+}
+
+export const network = NetworkModule;
diff --git a/remote/webdriver-bidi/modules/root/script.sys.mjs b/remote/webdriver-bidi/modules/root/script.sys.mjs
new file mode 100644
index 0000000000..308033c816
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/script.sys.mjs
@@ -0,0 +1,532 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ ContextDescriptorType:
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ RealmType: "chrome://remote/content/webdriver-bidi/Realm.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+});
+
+/**
+ * @typedef {string} ScriptEvaluateResultType
+ **/
+
+/**
+ * Enum of possible evaluation result types.
+ *
+ * @readonly
+ * @enum {ScriptEvaluateResultType}
+ **/
+const ScriptEvaluateResultType = {
+ Exception: "exception",
+ Success: "success",
+};
+
+class ScriptModule extends Module {
+ destroy() {}
+
+ /**
+ * Used to represent a frame of a JavaScript stack trace.
+ *
+ * @typedef StackFrame
+ *
+ * @property {number} columnNumber
+ * @property {string} functionName
+ * @property {number} lineNumber
+ * @property {string} url
+ */
+
+ /**
+ * Used to represent a JavaScript stack at a point in script execution.
+ *
+ * @typedef StackTrace
+ *
+ * @property {Array<StackFrame>} callFrames
+ */
+
+ /**
+ * Used to represent a JavaScript exception.
+ *
+ * @typedef ExceptionDetails
+ *
+ * @property {number} columnNumber
+ * @property {RemoteValue} exception
+ * @property {number} lineNumber
+ * @property {StackTrace} stackTrace
+ * @property {string} text
+ */
+
+ /**
+ * Used as return value for script.evaluate, as one of the available variants
+ * {ScriptEvaluateResultException} or {ScriptEvaluateResultSuccess}.
+ *
+ * @typedef ScriptEvaluateResult
+ */
+
+ /**
+ * Used as return value for script.evaluate when the script completes with a
+ * thrown exception.
+ *
+ * @typedef ScriptEvaluateResultException
+ *
+ * @property {ExceptionDetails} exceptionDetails
+ * @property {string} realm
+ * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Exception]
+ */
+
+ /**
+ * Used as return value for script.evaluate when the script completes
+ * normally.
+ *
+ * @typedef ScriptEvaluateResultSuccess
+ *
+ * @property {string} realm
+ * @property {RemoteValue} result
+ * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Success]
+ */
+
+ /**
+ * Calls a provided function with given arguments and scope in the provided
+ * target, which is either a realm or a browsing context.
+ *
+ * @param {Object=} options
+ * @param {Array<RemoteValue>=} arguments
+ * The arguments to pass to the function call.
+ * @param {boolean} awaitPromise
+ * Determines if the command should wait for the return value of the
+ * expression to resolve, if this return value is a Promise.
+ * @param {string} functionDeclaration
+ * The expression to evaluate.
+ * @param {OwnershipModel=} resultOwnership
+ * The ownership model to use for the results of this evaluation. Defaults
+ * to `OwnershipModel.None`.
+ * @param {Object} target
+ * The target for the evaluation, which either matches the definition for
+ * a RealmTarget or for ContextTarget.
+ * @param {RemoteValue=} this
+ * The value of the this keyword for the function call.
+ *
+ * @returns {ScriptEvaluateResult}
+ *
+ * @throws {InvalidArgumentError}
+ * If any of the arguments does not have the expected type.
+ * @throws {NoSuchFrameError}
+ * If the target cannot be found.
+ */
+ async callFunction(options = {}) {
+ const {
+ arguments: commandArguments = null,
+ awaitPromise,
+ functionDeclaration,
+ resultOwnership = lazy.OwnershipModel.None,
+ target = {},
+ this: thisParameter = null,
+ } = options;
+
+ lazy.assert.string(
+ functionDeclaration,
+ `Expected "functionDeclaration" to be a string, got ${functionDeclaration}`
+ );
+
+ lazy.assert.boolean(
+ awaitPromise,
+ `Expected "awaitPromise" to be a boolean, got ${awaitPromise}`
+ );
+
+ this.#assertResultOwnership(resultOwnership);
+
+ if (commandArguments != null) {
+ lazy.assert.array(
+ commandArguments,
+ `Expected "arguments" to be an array, got ${commandArguments}`
+ );
+ }
+
+ const { contextId, realmId, sandbox } = this.#assertTarget(target);
+ const context = await this.#getContextFromTarget({ contextId, realmId });
+ const evaluationResult = await this.messageHandler.forwardCommand({
+ moduleName: "script",
+ commandName: "callFunctionDeclaration",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ awaitPromise,
+ commandArguments,
+ functionDeclaration,
+ realmId,
+ resultOwnership,
+ sandbox,
+ thisParameter,
+ },
+ });
+
+ return this.#buildReturnValue(evaluationResult);
+ }
+
+ /**
+ * The script.disown command disowns the given handles. This does not
+ * guarantee the handled object will be garbage collected, as there can be
+ * other handles or strong ECMAScript references.
+ *
+ * @param {Object=} options
+ * @param {Array<string>} handles
+ * Array of handle ids to disown.
+ * @param {Object} target
+ * The target owning the handles, which either matches the definition for
+ * a RealmTarget or for ContextTarget.
+ */
+ async disown(options = {}) {
+ const { handles, target = {} } = options;
+
+ lazy.assert.array(
+ handles,
+ `Expected "handles" to be an array, got ${handles}`
+ );
+ handles.forEach(handle => {
+ lazy.assert.string(
+ handle,
+ `Expected "handles" to be an array of strings, got ${handle}`
+ );
+ });
+
+ const { contextId, realmId, sandbox } = this.#assertTarget(target);
+ const context = await this.#getContextFromTarget({ contextId, realmId });
+ await this.messageHandler.forwardCommand({
+ moduleName: "script",
+ commandName: "disownHandles",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ handles,
+ realmId,
+ sandbox,
+ },
+ });
+ }
+
+ /**
+ * Evaluate a provided expression in the provided target, which is either a
+ * realm or a browsing context.
+ *
+ * @param {Object=} options
+ * @param {boolean} awaitPromise
+ * Determines if the command should wait for the return value of the
+ * expression to resolve, if this return value is a Promise.
+ * @param {string} expression
+ * The expression to evaluate.
+ * @param {OwnershipModel=} resultOwnership
+ * The ownership model to use for the results of this evaluation. Defaults
+ * to `OwnershipModel.None`.
+ * @param {Object} target
+ * The target for the evaluation, which either matches the definition for
+ * a RealmTarget or for ContextTarget.
+ *
+ * @returns {ScriptEvaluateResult}
+ *
+ * @throws {InvalidArgumentError}
+ * If any of the arguments does not have the expected type.
+ * @throws {NoSuchFrameError}
+ * If the target cannot be found.
+ */
+ async evaluate(options = {}) {
+ const {
+ awaitPromise,
+ expression: source,
+ resultOwnership = lazy.OwnershipModel.None,
+ target = {},
+ } = options;
+
+ lazy.assert.string(
+ source,
+ `Expected "expression" to be a string, got ${source}`
+ );
+
+ lazy.assert.boolean(
+ awaitPromise,
+ `Expected "awaitPromise" to be a boolean, got ${awaitPromise}`
+ );
+
+ this.#assertResultOwnership(resultOwnership);
+
+ const { contextId, realmId, sandbox } = this.#assertTarget(target);
+ const context = await this.#getContextFromTarget({ contextId, realmId });
+ const evaluationResult = await this.messageHandler.forwardCommand({
+ moduleName: "script",
+ commandName: "evaluateExpression",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ awaitPromise,
+ expression: source,
+ realmId,
+ resultOwnership,
+ sandbox,
+ },
+ });
+
+ return this.#buildReturnValue(evaluationResult);
+ }
+
+ /**
+ * An object that holds basic information about a realm.
+ *
+ * @typedef BaseRealmInfo
+ *
+ * @property {string} id
+ * The realm unique identifier.
+ * @property {string} origin
+ * The serialization of an origin.
+ */
+
+ /**
+ *
+ * @typedef WindowRealmInfoProperties
+ *
+ * @property {string} context
+ * The browsing context id, associated with the realm.
+ * @property {string=} sandbox
+ * The name of the sandbox. If the value is null or empty
+ * string, the default realm will be returned.
+ * @property {RealmType.Window} type
+ * The window realm type.
+ */
+
+ /**
+ * An object that holds information about a window realm.
+ *
+ * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo
+ */
+
+ /**
+ * An object that holds information about a realm.
+ *
+ * @typedef {WindowRealmInfo} RealmInfo
+ */
+
+ /**
+ * An object that holds a list of realms.
+ *
+ * @typedef ScriptGetRealmsResult
+ *
+ * @property {Array<RealmInfo>} realms
+ * List of realms.
+ */
+
+ /**
+ * Returns a list of all realms, optionally filtered to realms
+ * of a specific type, or to the realms associated with
+ * a specified browsing context.
+ *
+ * @param {Object=} options
+ * @param {string=} context
+ * The id of the browsing context to filter
+ * only realms associated with it. If not provided, return realms
+ * associated with all browsing contexts.
+ * @param {RealmType=} type
+ * Type of realm to filter.
+ * If not provided, return realms of all types.
+ *
+ * @returns {ScriptGetRealmsResult}
+ *
+ * @throws {InvalidArgumentError}
+ * If any of the arguments does not have the expected type.
+ * @throws {NoSuchFrameError}
+ * If the context cannot be found.
+ */
+ async getRealms(options = {}) {
+ const { context: contextId = null, type = null } = options;
+ const destination = {};
+
+ if (contextId !== null) {
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+ destination.id = this.#getBrowsingContext(contextId).id;
+ } else {
+ destination.contextDescriptor = {
+ type: lazy.ContextDescriptorType.All,
+ };
+ }
+
+ if (type !== null) {
+ const supportedRealmTypes = Object.values(lazy.RealmType);
+ if (!supportedRealmTypes.includes(type)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "type" to be one of ${supportedRealmTypes}, got ${type}`
+ );
+ }
+
+ // Remove this check when other realm types are supported
+ if (type !== lazy.RealmType.Window) {
+ throw new lazy.error.UnsupportedOperationError(
+ `Unsupported "type": ${type}. Only "type" ${lazy.RealmType.Window} is currently supported.`
+ );
+ }
+ }
+
+ return { realms: await this.#getRealmInfos(destination) };
+ }
+
+ #assertResultOwnership(resultOwnership) {
+ if (
+ ![lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes(
+ resultOwnership
+ )
+ ) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "resultOwnership" to be one of ${Object.values(
+ lazy.OwnershipModel
+ )}, got ${resultOwnership}`
+ );
+ }
+ }
+
+ #assertTarget(target) {
+ lazy.assert.object(
+ target,
+ `Expected "target" to be an object, got ${target}`
+ );
+
+ const {
+ context: contextId = null,
+ realm: realmId = null,
+ sandbox = null,
+ } = target;
+
+ if (realmId != null && (contextId != null || sandbox != null)) {
+ throw new lazy.error.InvalidArgumentError(
+ `A context and a realm reference are mutually exclusive`
+ );
+ }
+
+ if (contextId != null) {
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ if (sandbox != null) {
+ lazy.assert.string(
+ sandbox,
+ `Expected "sandbox" to be a string, got ${sandbox}`
+ );
+ }
+ } else if (realmId != null) {
+ lazy.assert.string(
+ realmId,
+ `Expected "realm" to be a string, got ${realmId}`
+ );
+ } else {
+ throw new lazy.error.InvalidArgumentError(`No context or realm provided`);
+ }
+
+ return { contextId, realmId, sandbox };
+ }
+
+ #buildReturnValue(evaluationResult) {
+ const rv = { realm: evaluationResult.realmId };
+ switch (evaluationResult.evaluationStatus) {
+ // TODO: Compare with EvaluationStatus.Normal after Bug 1774444 is fixed.
+ case "normal":
+ rv.type = ScriptEvaluateResultType.Success;
+ rv.result = evaluationResult.result;
+ break;
+ // TODO: Compare with EvaluationStatus.Throw after Bug 1774444 is fixed.
+ case "throw":
+ rv.type = ScriptEvaluateResultType.Exception;
+ rv.exceptionDetails = evaluationResult.exceptionDetails;
+ break;
+ default:
+ throw new lazy.error.UnsupportedOperationError(
+ `Unsupported evaluation status ${evaluationResult.evaluationStatus}`
+ );
+ }
+ return rv;
+ }
+
+ #getBrowsingContext(contextId) {
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (context === null) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${contextId} not found`
+ );
+ }
+
+ if (!context.currentWindowGlobal) {
+ throw new lazy.error.NoSuchFrameError(
+ `No window found for BrowsingContext with id ${contextId}`
+ );
+ }
+
+ return context;
+ }
+
+ async #getContextFromTarget({ contextId, realmId }) {
+ if (contextId !== null) {
+ return this.#getBrowsingContext(contextId);
+ }
+
+ const destination = {
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.All,
+ },
+ };
+ const realms = await this.#getRealmInfos(destination);
+ const realm = realms.find(realm => realm.realm == realmId);
+
+ if (realm && realm.context !== null) {
+ return this.#getBrowsingContext(realm.context);
+ }
+
+ throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`);
+ }
+
+ async #getRealmInfos(destination) {
+ let realms = await this.messageHandler.forwardCommand({
+ moduleName: "script",
+ commandName: "getWindowRealms",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ ...destination,
+ },
+ });
+
+ const isBroadcast = !!destination.contextDescriptor;
+ if (!isBroadcast) {
+ realms = [realms];
+ }
+
+ return realms
+ .flat()
+ .map(realm => {
+ // Resolve browsing context to a TabManager id.
+ realm.context = lazy.TabManager.getIdForBrowsingContext(realm.context);
+ return realm;
+ })
+ .filter(realm => realm.context !== null);
+ }
+
+ static get supportedEvents() {
+ return [];
+ }
+}
+
+export const script = ScriptModule;
diff --git a/remote/webdriver-bidi/modules/root/session.sys.mjs b/remote/webdriver-bidi/modules/root/session.sys.mjs
new file mode 100644
index 0000000000..281b913f32
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/session.sys.mjs
@@ -0,0 +1,406 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ ContextDescriptorType:
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ RootMessageHandler:
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+class SessionModule extends Module {
+ #browsingContextIdEventMap;
+ #globalEventSet;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Map with top-level browsing context id keys and values
+ // that are a set of event names for events
+ // that are enabled in the given browsing context.
+ // TODO: Bug 1804417. Use navigable instead of browsing context id.
+ this.#browsingContextIdEventMap = new Map();
+
+ // Set of event names which are strings of the form [moduleName].[eventName]
+ // for events that are enabled for all browsing contexts.
+ // We should only add an actual event listener on the MessageHandler the
+ // first time an event is subscribed to.
+ this.#globalEventSet = new Set();
+ }
+
+ destroy() {
+ this.#browsingContextIdEventMap = null;
+ this.#globalEventSet = null;
+ }
+
+ /**
+ * Commands
+ */
+
+ /**
+ * Enable certain events either globally, or for a list of browsing contexts.
+ *
+ * @params {Object=} params
+ * @params {Array<string>} events
+ * List of events to subscribe to.
+ * @params {Array<string>=} contexts
+ * Optional list of top-level browsing context ids
+ * to subscribe the events for.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>events</var> or <var>contexts</var> are not valid types.
+ */
+ async subscribe(params = {}) {
+ const { events, contexts: contextIds = null } = params;
+
+ // Check input types until we run schema validation.
+ lazy.assert.array(events, "events: array value expected");
+ events.forEach(name => {
+ lazy.assert.string(name, `${name}: string value expected`);
+ });
+
+ if (contextIds !== null) {
+ lazy.assert.array(contextIds, "contexts: array value expected");
+ contextIds.forEach(contextId => {
+ lazy.assert.string(contextId, `${contextId}: string value expected`);
+ });
+ }
+
+ const listeners = this.#updateEventMap(events, contextIds, true);
+
+ // TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8).
+
+ // Subscribe to the relevant engine-internal events.
+ await this.messageHandler.eventsDispatcher.update(listeners);
+ }
+
+ /**
+ * Disable certain events either globally, or for a list of browsing contexts.
+ *
+ * @params {Object=} params
+ * @params {Array<string>} events
+ * List of events to unsubscribe from.
+ * @params {Array<string>=} contexts
+ * Optional list of top-level browsing context ids
+ * to unsubscribe the events from.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>events</var> or <var>contexts</var> are not valid types.
+ */
+ async unsubscribe(params = {}) {
+ const { events, contexts: contextIds = null } = params;
+
+ // Check input types until we run schema validation.
+ lazy.assert.array(events, "events: array value expected");
+ events.forEach(name => {
+ lazy.assert.string(name, `${name}: string value expected`);
+ });
+ if (contextIds !== null) {
+ lazy.assert.array(contextIds, "contexts: array value expected");
+ contextIds.forEach(contextId => {
+ lazy.assert.string(contextId, `${contextId}: string value expected`);
+ });
+ }
+
+ const listeners = this.#updateEventMap(events, contextIds, false);
+
+ // Unsubscribe from the relevant engine-internal events.
+ await this.messageHandler.eventsDispatcher.update(listeners);
+ }
+
+ #assertModuleSupportsEvent(moduleName, event) {
+ const rootModuleClass = this.#getRootModuleClass(moduleName);
+ if (!rootModuleClass?.supportsEvent(event)) {
+ throw new lazy.error.InvalidArgumentError(
+ `${event} is not a valid event name`
+ );
+ }
+ }
+
+ #getBrowserIdForContextId(contextId) {
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (!context) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing context with id ${contextId} not found`
+ );
+ }
+
+ return context.browserId;
+ }
+
+ #getRootModuleClass(moduleName) {
+ // Modules which support event subscriptions should have a root module
+ // defining supported events.
+ const rootDestination = { type: lazy.RootMessageHandler.type };
+ const moduleClasses = this.messageHandler.getAllModuleClasses(
+ moduleName,
+ rootDestination
+ );
+
+ if (!moduleClasses.length) {
+ throw new lazy.error.InvalidArgumentError(
+ `Module ${moduleName} does not exist`
+ );
+ }
+
+ return moduleClasses[0];
+ }
+
+ #getTopBrowsingContextId(contextId) {
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (!context) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing context with id ${contextId} not found`
+ );
+ }
+ const topContext = context.top;
+ return lazy.TabManager.getIdForBrowsingContext(topContext);
+ }
+
+ /**
+ * Obtain a set of events based on the given event name.
+ *
+ * Could contain a period for a specific event,
+ * or just the module name for all events.
+ *
+ * @param {string} event
+ * Name of the event to process.
+ *
+ * @returns {Set<string>}
+ * A Set with the expanded events in the form of `<module>.<event>`.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>event</var> does not reference a valid event.
+ */
+ #obtainEvents(event) {
+ const events = new Set();
+
+ // Check if a period is present that splits the event name into the module,
+ // and the actual event. Hereby only care about the first found instance.
+ const index = event.indexOf(".");
+ if (index >= 0) {
+ const [moduleName] = event.split(".");
+ this.#assertModuleSupportsEvent(moduleName, event);
+ events.add(event);
+ } else {
+ // Interpret the name as module, and register all its available events
+ const rootModuleClass = this.#getRootModuleClass(event);
+ const supportedEvents = rootModuleClass?.supportedEvents;
+
+ for (const eventName of supportedEvents) {
+ events.add(eventName);
+ }
+ }
+
+ return events;
+ }
+
+ /**
+ * Obtain a list of event enabled browsing context ids.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#event-enabled-browsing-contexts
+ *
+ * @param {string} eventName
+ * The name of the event.
+ *
+ * @return {Set<string>} The set of browsing context.
+ */
+ #obtainEventEnabledBrowsingContextIds(eventName) {
+ const contextIds = new Set();
+ for (const [
+ contextId,
+ events,
+ ] of this.#browsingContextIdEventMap.entries()) {
+ if (events.has(eventName)) {
+ // Check that a browsing context still exists for a given id
+ const context = lazy.TabManager.getBrowsingContextById(contextId);
+ if (context) {
+ contextIds.add(contextId);
+ }
+ }
+ }
+
+ return contextIds;
+ }
+
+ #onMessageHandlerEvent = (name, event) => {
+ this.messageHandler.emitProtocolEvent(name, event);
+ };
+
+ /**
+ * Update global event state for top-level browsing contexts.
+ *
+ * @see https://w3c.github.io/webdriver-bidi/#update-the-event-map
+ *
+ * @param {Array<string>} requestedEventNames
+ * The list of the event names to run the update for.
+ * @param {Array<string>|null} browsingContextIds
+ * The list of the browsing context ids to update or null.
+ * @param {boolean} enabled
+ * True, if events have to be enabled. Otherwise false.
+ *
+ * @return {Array<Subscription>} subscriptions
+ * The list of information to subscribe/unsubscribe to.
+ *
+ * @throws {InvalidArgumentError}
+ * If failed unsubscribe from event from <var>requestedEventNames</var> for
+ * browsing context id from <var>browsingContextIds</var>, if present.
+ */
+ #updateEventMap(requestedEventNames, browsingContextIds, enabled) {
+ const globalEventSet = new Set(this.#globalEventSet);
+ const eventMap = structuredClone(this.#browsingContextIdEventMap);
+
+ const eventNames = new Set();
+
+ requestedEventNames.forEach(name => {
+ this.#obtainEvents(name).forEach(event => eventNames.add(event));
+ });
+ const enabledEvents = new Map();
+ const subscriptions = [];
+
+ if (browsingContextIds === null) {
+ // Subscribe or unsubscribe events for all browsing contexts.
+ if (enabled) {
+ // Subscribe to each event.
+
+ // Get the list of all top level browsing context ids.
+ const allTopBrowsingContextIds = lazy.TabManager.allBrowserUniqueIds;
+
+ for (const eventName of eventNames) {
+ if (!globalEventSet.has(eventName)) {
+ const alreadyEnabledContextIds = this.#obtainEventEnabledBrowsingContextIds(
+ eventName
+ );
+ globalEventSet.add(eventName);
+ for (const contextId of alreadyEnabledContextIds) {
+ eventMap.get(contextId).delete(eventName);
+
+ // Since we're going to subscribe to all top-level
+ // browsing context ids to not have duplicate subscriptions,
+ // we have to unsubscribe from already subscribed.
+ subscriptions.push({
+ event: eventName,
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.TopBrowsingContext,
+ id: this.#getBrowserIdForContextId(contextId),
+ },
+ callback: this.#onMessageHandlerEvent,
+ enable: false,
+ });
+ }
+
+ // Get a list of all top-level browsing context ids
+ // that are not contained in alreadyEnabledContextIds.
+ const newlyEnabledContextIds = allTopBrowsingContextIds.filter(
+ contextId => !alreadyEnabledContextIds.has(contextId)
+ );
+
+ enabledEvents.set(eventName, newlyEnabledContextIds);
+
+ subscriptions.push({
+ event: eventName,
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.All,
+ },
+ callback: this.#onMessageHandlerEvent,
+ enable: true,
+ });
+ }
+ }
+ } else {
+ // Unsubscribe each event which has a global subscription.
+ for (const eventName of eventNames) {
+ if (globalEventSet.has(eventName)) {
+ globalEventSet.delete(eventName);
+
+ subscriptions.push({
+ event: eventName,
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.All,
+ },
+ callback: this.#onMessageHandlerEvent,
+ enable: false,
+ });
+ } else {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to unsubscribe from event ${eventName}`
+ );
+ }
+ }
+ }
+ } else {
+ // Subscribe or unsubscribe events for given list of browsing context ids.
+ const targets = new Map();
+ for (const contextId of browsingContextIds) {
+ const topLevelContextId = this.#getTopBrowsingContextId(contextId);
+ if (!eventMap.has(topLevelContextId)) {
+ eventMap.set(topLevelContextId, new Set());
+ }
+ targets.set(topLevelContextId, eventMap.get(topLevelContextId));
+ }
+
+ for (const eventName of eventNames) {
+ // Do nothing if we want to subscribe,
+ // but the event has already a global subscription.
+ if (enabled && this.#globalEventSet.has(eventName)) {
+ continue;
+ }
+ for (const [contextId, target] of targets.entries()) {
+ // Subscribe if an event doesn't have a subscription for a specific context id.
+ if (enabled && !target.has(eventName)) {
+ target.add(eventName);
+ if (!enabledEvents.has(eventName)) {
+ enabledEvents.set(eventName, new Set());
+ }
+ enabledEvents.get(eventName).add(contextId);
+
+ subscriptions.push({
+ event: eventName,
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.TopBrowsingContext,
+ id: this.#getBrowserIdForContextId(contextId),
+ },
+ callback: this.#onMessageHandlerEvent,
+ enable: true,
+ });
+ } else if (!enabled) {
+ // Unsubscribe from each event for a specific context id if the event has a subscription.
+ if (target.has(eventName)) {
+ target.delete(eventName);
+
+ subscriptions.push({
+ event: eventName,
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.TopBrowsingContext,
+ id: this.#getBrowserIdForContextId(contextId),
+ },
+ callback: this.#onMessageHandlerEvent,
+ enable: false,
+ });
+ } else {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to unsubscribe from event ${eventName} for context ${contextId}`
+ );
+ }
+ }
+ }
+ }
+ }
+
+ this.#globalEventSet = globalEventSet;
+ this.#browsingContextIdEventMap = eventMap;
+
+ return subscriptions;
+ }
+}
+
+// To export the class as lower-case
+export const session = SessionModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs
new file mode 100644
index 0000000000..68a220cee2
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs
@@ -0,0 +1,31 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+class BrowsingContextModule extends Module {
+ destroy() {}
+
+ interceptEvent(name, payload) {
+ if (
+ name == "browsingContext.domContentLoaded" ||
+ name == "browsingContext.load"
+ ) {
+ // Resolve browsing context to a TabManager id.
+ payload.context = lazy.TabManager.getIdForBrowsingContext(
+ payload.context
+ );
+ }
+
+ return payload;
+ }
+}
+
+export const browsingContext = BrowsingContextModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs b/remote/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs
new file mode 100644
index 0000000000..e6f62a29ad
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs
@@ -0,0 +1,28 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+class LogModule extends Module {
+ destroy() {}
+
+ interceptEvent(name, payload) {
+ if (name == "log.entryAdded") {
+ // Resolve browsing context to a TabManager id.
+ payload.source.context = lazy.TabManager.getIdForBrowsingContext(
+ payload.source.context
+ );
+ }
+
+ return payload;
+ }
+}
+
+export const log = LogModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
new file mode 100644
index 0000000000..65dd71cefb
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs
@@ -0,0 +1,159 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ LoadListener: "chrome://remote/content/shared/listeners/LoadListener.sys.mjs",
+});
+
+class BrowsingContextModule extends Module {
+ #loadListener;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Setup the LoadListener as early as possible.
+ this.#loadListener = new lazy.LoadListener(this.messageHandler.window);
+ this.#loadListener.on("DOMContentLoaded", this.#onDOMContentLoaded);
+ this.#loadListener.on("load", this.#onLoad);
+
+ // Set of event names which have active subscriptions.
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {
+ this.#loadListener.destroy();
+ this.#subscribedEvents = null;
+ }
+
+ #getNavigationInfo(data) {
+ return {
+ context: this.messageHandler.context,
+ // TODO: The navigation id should be a real id mapped to the navigation.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1763122
+ navigation: null,
+ timestamp: Date.now(),
+ url: data.target.baseURI,
+ };
+ }
+
+ #startListening() {
+ if (this.#subscribedEvents.size == 0) {
+ this.#loadListener.startListening();
+ }
+ }
+
+ #stopListening() {
+ if (this.#subscribedEvents.size == 0) {
+ this.#loadListener.stopListening();
+ }
+ }
+
+ #subscribeEvent(event) {
+ switch (event) {
+ case "browsingContext._documentInteractive":
+ this.#startListening();
+ this.#subscribedEvents.add("browsingContext._documentInteractive");
+ break;
+ case "browsingContext.domContentLoaded":
+ this.#startListening();
+ this.#subscribedEvents.add("browsingContext.domContentLoaded");
+ break;
+ case "browsingContext.load":
+ this.#startListening();
+ this.#subscribedEvents.add("browsingContext.load");
+ break;
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ switch (event) {
+ case "browsingContext._documentInteractive":
+ this.#subscribedEvents.delete("browsingContext._documentInteractive");
+ break;
+ case "browsingContext.domContentLoaded":
+ this.#subscribedEvents.delete("browsingContext.domContentLoaded");
+ break;
+ case "browsingContext.load":
+ this.#subscribedEvents.delete("browsingContext.load");
+ break;
+ }
+
+ this.#stopListening();
+ }
+
+ #onDOMContentLoaded = (eventName, data) => {
+ if (this.#subscribedEvents.has("browsingContext._documentInteractive")) {
+ this.messageHandler.emitEvent("browsingContext._documentInteractive", {
+ baseURL: data.target.baseURI,
+ contextId: this.messageHandler.contextId,
+ documentURL: data.target.URL,
+ innerWindowId: this.messageHandler.innerWindowId,
+ readyState: data.target.readyState,
+ });
+ }
+
+ if (this.#subscribedEvents.has("browsingContext.domContentLoaded")) {
+ this.emitEvent(
+ "browsingContext.domContentLoaded",
+ this.#getNavigationInfo(data)
+ );
+ }
+ };
+
+ #onLoad = (eventName, data) => {
+ if (this.#subscribedEvents.has("browsingContext.load")) {
+ this.emitEvent("browsingContext.load", this.#getNavigationInfo(data));
+ }
+ };
+
+ /**
+ * Internal commands
+ */
+
+ _applySessionData(params) {
+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
+ // class.
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData.
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+
+ _getBaseURL() {
+ return this.messageHandler.window.document.baseURI;
+ }
+
+ _getScreenshotRect() {
+ const win = this.messageHandler.window;
+ return new DOMRect(
+ win.pageXOffset,
+ win.pageYOffset,
+ win.innerWidth,
+ win.innerHeight
+ );
+ }
+}
+
+export const browsingContext = BrowsingContextModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal/log.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/log.sys.mjs
new file mode 100644
index 0000000000..46e2340936
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/log.sys.mjs
@@ -0,0 +1,242 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ConsoleAPIListener:
+ "chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs",
+ ConsoleListener:
+ "chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs",
+ isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+});
+
+class LogModule extends Module {
+ #consoleAPIListener;
+ #consoleMessageListener;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Create the console-api listener and listen on "message" events.
+ this.#consoleAPIListener = new lazy.ConsoleAPIListener(
+ this.messageHandler.innerWindowId
+ );
+ this.#consoleAPIListener.on("message", this.#onConsoleAPIMessage);
+
+ // Create the console listener and listen on error messages.
+ this.#consoleMessageListener = new lazy.ConsoleListener(
+ this.messageHandler.innerWindowId
+ );
+ this.#consoleMessageListener.on("error", this.#onJavaScriptError);
+
+ // Set of event names which have active subscriptions.
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {
+ this.#consoleAPIListener.off("message", this.#onConsoleAPIMessage);
+ this.#consoleAPIListener.destroy();
+ this.#consoleMessageListener.off("error", this.#onJavaScriptError);
+ this.#consoleMessageListener.destroy();
+
+ this.#subscribedEvents = null;
+ }
+
+ #buildSource() {
+ return {
+ realm: this.messageHandler.window.windowGlobalChild?.innerWindowId.toString(),
+ context: this.messageHandler.context,
+ };
+ }
+
+ /**
+ * Map the internal stacktrace representation to a WebDriver BiDi
+ * compatible one.
+ *
+ * Currently chrome frames will be filtered out until chrome scope
+ * is supported (bug 1722679).
+ *
+ * @param {Array<StackFrame>=} stackTrace
+ * Stack frames to process.
+ *
+ * @returns {Object=} Object, containing the list of frames as `callFrames`.
+ */
+ #buildStackTrace(stackTrace) {
+ if (stackTrace == undefined) {
+ return undefined;
+ }
+
+ const callFrames = stackTrace
+ .filter(frame => !lazy.isChromeFrame(frame))
+ .map(frame => {
+ return {
+ columnNumber: frame.columnNumber - 1,
+ functionName: frame.functionName,
+ lineNumber: frame.lineNumber - 1,
+ url: frame.filename,
+ };
+ });
+
+ return { callFrames };
+ }
+
+ #getLogEntryLevelFromConsoleMethod(method) {
+ switch (method) {
+ case "assert":
+ case "error":
+ return "error";
+ case "debug":
+ case "trace":
+ return "debug";
+ case "warn":
+ return "warn";
+ default:
+ return "info";
+ }
+ }
+
+ #onConsoleAPIMessage = (eventName, data = {}) => {
+ const {
+ // `arguments` cannot be used as variable name in functions
+ arguments: messageArguments,
+ // `level` corresponds to the console method used
+ level: method,
+ stacktrace,
+ timeStamp,
+ } = data;
+
+ // Step numbers below refer to the specifications at
+ // https://w3c.github.io/webdriver-bidi/#event-log-entryAdded
+
+ // Translate the console message method to a log.LogEntry level
+ const logEntrylevel = this.#getLogEntryLevelFromConsoleMethod(method);
+
+ // Use the message's timeStamp or fallback on the current time value.
+ const timestamp = timeStamp || Date.now();
+
+ // Start assembling the text representation of the message.
+ let text = "";
+
+ // Formatters have already been applied at this points.
+ // message.arguments corresponds to the "formatted args" from the
+ // specifications.
+
+ // Concatenate all formatted arguments in text
+ // TODO: For m1 we only support string arguments, so we rely on the builtin
+ // toString for each argument which will be available in message.arguments.
+ const args = messageArguments || [];
+ text += args.map(String).join(" ");
+
+ // Serialize each arg as remote value.
+ const serializedArgs = [];
+ for (const arg of args) {
+ // Note that we can pass a `null` realm for now since realms are only
+ // involved when creating object references, which will not happen with
+ // OwnershipModel.None. This will be revisited in Bug 1731589.
+ serializedArgs.push(
+ lazy.serialize(arg, 1, lazy.OwnershipModel.None, new Map(), null)
+ );
+ }
+
+ // Set source to an object which contains realm and browsing context.
+ const source = this.#buildSource();
+
+ // Set stack trace only for certain methods.
+ let stackTrace;
+ if (["assert", "error", "trace", "warn"].includes(method)) {
+ stackTrace = this.#buildStackTrace(stacktrace);
+ }
+
+ // Build the ConsoleLogEntry
+ const entry = {
+ type: "console",
+ method,
+ source,
+ args: serializedArgs,
+ level: logEntrylevel,
+ text,
+ timestamp,
+ stackTrace,
+ };
+
+ // TODO: Those steps relate to:
+ // - emitting associated BrowsingContext. See log.entryAdded full support
+ // in https://bugzilla.mozilla.org/show_bug.cgi?id=1724669#c0
+ // - handling cases where session doesn't exist or the event is not
+ // monitored. The implementation differs from the spec here because we
+ // only react to events if there is a session & if the session subscribed
+ // to those events.
+
+ this.emitEvent("log.entryAdded", entry);
+ };
+
+ #onJavaScriptError = (eventName, data = {}) => {
+ const { level, message, stacktrace, timeStamp } = data;
+
+ // Build the JavascriptLogEntry
+ const entry = {
+ type: "javascript",
+ level,
+ source: this.#buildSource(),
+ text: message,
+ timestamp: timeStamp || Date.now(),
+ stackTrace: this.#buildStackTrace(stacktrace),
+ };
+
+ this.emitEvent("log.entryAdded", entry);
+ };
+
+ #subscribeEvent(event) {
+ if (event === "log.entryAdded") {
+ this.#consoleAPIListener.startListening();
+ this.#consoleMessageListener.startListening();
+ this.#subscribedEvents.add(event);
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ if (event === "log.entryAdded") {
+ this.#consoleAPIListener.stopListening();
+ this.#consoleMessageListener.stopListening();
+ this.#subscribedEvents.delete(event);
+ }
+ }
+
+ /**
+ * Internal commands
+ */
+
+ _applySessionData(params) {
+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
+ // class.
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData.
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+}
+
+export const log = LogModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
new file mode 100644
index 0000000000..a2fa1fafc4
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
@@ -0,0 +1,344 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ deserialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs",
+ isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
+ serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ WindowRealm: "chrome://remote/content/webdriver-bidi/Realm.sys.mjs",
+});
+
+/**
+ * @typedef {string} EvaluationStatus
+ **/
+
+/**
+ * Enum of possible evaluation states.
+ *
+ * @readonly
+ * @enum {EvaluationStatus}
+ **/
+const EvaluationStatus = {
+ Normal: "normal",
+ Throw: "throw",
+};
+
+class ScriptModule extends Module {
+ #defaultRealm;
+ #realms;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ this.#defaultRealm = new lazy.WindowRealm(this.messageHandler.window);
+
+ // Maps sandbox names to instances of window realms.
+ this.#realms = new Map();
+ }
+
+ destroy() {
+ this.#defaultRealm.destroy();
+
+ for (const realm of this.#realms.values()) {
+ realm.destroy();
+ }
+ this.#realms = null;
+ }
+
+ #buildExceptionDetails(exception, stack, realm, resultOwnership) {
+ exception = this.#toRawObject(exception);
+ const frames = lazy.getFramesFromStack(stack) || [];
+
+ const callFrames = frames
+ // Remove chrome/internal frames
+ .filter(frame => !lazy.isChromeFrame(frame))
+ // Translate frames from getFramesFromStack to frames expected by
+ // WebDriver BiDi.
+ .map(frame => {
+ return {
+ columnNumber: frame.columnNumber - 1,
+ functionName: frame.functionName,
+ lineNumber: frame.lineNumber - 1,
+ url: frame.filename,
+ };
+ });
+
+ return {
+ columnNumber: stack.column - 1,
+ exception: lazy.serialize(
+ exception,
+ 1,
+ resultOwnership,
+ new Map(),
+ realm
+ ),
+ lineNumber: stack.line - 1,
+ stackTrace: { callFrames },
+ text: lazy.stringify(exception),
+ };
+ }
+
+ async #buildReturnValue(rv, realm, awaitPromise, resultOwnership) {
+ let evaluationStatus, exception, result, stack;
+
+ if ("return" in rv) {
+ evaluationStatus = EvaluationStatus.Normal;
+ if (
+ awaitPromise &&
+ // Only non-primitive return values are wrapped in Debugger.Object.
+ rv.return instanceof Debugger.Object &&
+ rv.return.isPromise
+ ) {
+ try {
+ // Force wrapping the promise resolution result in a Debugger.Object
+ // wrapper for consistency with the synchronous codepath.
+ const asyncResult = await rv.return.unsafeDereference();
+ result = realm.globalObjectReference.makeDebuggeeValue(asyncResult);
+ } catch (asyncException) {
+ evaluationStatus = EvaluationStatus.Throw;
+ exception = realm.globalObjectReference.makeDebuggeeValue(
+ asyncException
+ );
+ stack = rv.return.promiseResolutionSite;
+ }
+ } else {
+ // rv.return is a Debugger.Object or a primitive.
+ result = rv.return;
+ }
+ } else if ("throw" in rv) {
+ // rv.throw will be set if the evaluation synchronously failed, either if
+ // the script contains a syntax error or throws an exception.
+ evaluationStatus = EvaluationStatus.Throw;
+ exception = rv.throw;
+ stack = rv.stack;
+ }
+
+ switch (evaluationStatus) {
+ case EvaluationStatus.Normal:
+ return {
+ evaluationStatus,
+ result: lazy.serialize(
+ this.#toRawObject(result),
+ 1,
+ resultOwnership,
+ new Map(),
+ realm
+ ),
+ realmId: realm.id,
+ };
+ case EvaluationStatus.Throw:
+ return {
+ evaluationStatus,
+ exceptionDetails: this.#buildExceptionDetails(
+ exception,
+ stack,
+ realm,
+ resultOwnership
+ ),
+ realmId: realm.id,
+ };
+ default:
+ throw new lazy.error.UnsupportedOperationError(
+ `Unsupported completion value for expression evaluation`
+ );
+ }
+ }
+
+ #getRealm(realmId, sandboxName) {
+ if (realmId === null) {
+ return this.#getRealmFromSandboxName(sandboxName);
+ }
+
+ if (this.#defaultRealm.id == realmId) {
+ return this.#defaultRealm;
+ }
+
+ const sandboxRealm = Array.from(this.#realms.values()).find(
+ realm => realm.id === realmId
+ );
+
+ if (sandboxRealm) {
+ return sandboxRealm;
+ }
+
+ throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`);
+ }
+
+ #getRealmFromSandboxName(sandboxName) {
+ if (sandboxName === null || sandboxName === "") {
+ return this.#defaultRealm;
+ }
+
+ if (this.#realms.has(sandboxName)) {
+ return this.#realms.get(sandboxName);
+ }
+
+ const realm = new lazy.WindowRealm(this.messageHandler.window, {
+ sandboxName,
+ });
+
+ this.#realms.set(sandboxName, realm);
+
+ return realm;
+ }
+
+ #toRawObject(maybeDebuggerObject) {
+ if (maybeDebuggerObject instanceof Debugger.Object) {
+ // Retrieve the referent for the provided Debugger.object.
+ // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html
+ const rawObject = maybeDebuggerObject.unsafeDereference();
+
+ // TODO: Getters for Maps and Sets iterators return "Opaque" objects and
+ // are not iterable. RemoteValue.jsm' serializer should handle calling
+ // waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since
+ // we serialize with maxDepth=1, calling waiveXrays once on the root
+ // object allows to return correctly serialized values.
+ return Cu.waiveXrays(rawObject);
+ }
+
+ // If maybeDebuggerObject was not a Debugger.Object, it is a primitive value
+ // which can be used as is.
+ return maybeDebuggerObject;
+ }
+
+ /**
+ * Call a function in the current window global.
+ *
+ * @param {Object} options
+ * @param {boolean} awaitPromise
+ * Determines if the command should wait for the return value of the
+ * expression to resolve, if this return value is a Promise.
+ * @param {Array<RemoteValue>=} commandArguments
+ * The arguments to pass to the function call.
+ * @param {string} functionDeclaration
+ * The body of the function to call.
+ * @param {string=} realmId
+ * The id of the realm.
+ * @param {OwnershipModel} resultOwnership
+ * The ownership model to use for the results of this evaluation.
+ * @param {string=} sandbox
+ * The name of the sandbox.
+ * @param {RemoteValue=} thisParameter
+ * The value of the this keyword for the function call.
+ *
+ * @return {Object}
+ * - evaluationStatus {EvaluationStatus} One of "normal", "throw".
+ * - exceptionDetails {ExceptionDetails=} the details of the exception if
+ * the evaluation status was "throw".
+ * - result {RemoteValue=} the result of the evaluation serialized as a
+ * RemoteValue if the evaluation status was "normal".
+ */
+ async callFunctionDeclaration(options) {
+ const {
+ awaitPromise,
+ commandArguments = null,
+ functionDeclaration,
+ realmId = null,
+ resultOwnership,
+ sandbox: sandboxName = null,
+ thisParameter = null,
+ } = options;
+
+ const realm = this.#getRealm(realmId, sandboxName);
+
+ const deserializedArguments =
+ commandArguments !== null
+ ? commandArguments.map(arg => lazy.deserialize(realm, arg))
+ : [];
+
+ const deserializedThis =
+ thisParameter !== null ? lazy.deserialize(realm, thisParameter) : null;
+
+ const rv = realm.executeInGlobalWithBindings(
+ functionDeclaration,
+ deserializedArguments,
+ deserializedThis
+ );
+
+ return this.#buildReturnValue(rv, realm, awaitPromise, resultOwnership);
+ }
+
+ /**
+ * Delete the provided handles from the realm corresponding to the provided
+ * sandbox name.
+ *
+ * @param {Object=} options
+ * @param {Array<string>} handles
+ * Array of handle ids to disown.
+ * @param {string=} realmId
+ * The id of the realm.
+ * @param {string=} sandbox
+ * The name of the sandbox.
+ */
+ disownHandles(options) {
+ const { handles, realmId = null, sandbox: sandboxName = null } = options;
+ const realm = this.#getRealm(realmId, sandboxName);
+ for (const handle of handles) {
+ realm.removeObjectHandle(handle);
+ }
+ }
+
+ /**
+ * Evaluate a provided expression in the current window global.
+ *
+ * @param {Object} options
+ * @param {boolean} awaitPromise
+ * Determines if the command should wait for the return value of the
+ * expression to resolve, if this return value is a Promise.
+ * @param {string} expression
+ * The expression to evaluate.
+ * @param {string=} realmId
+ * The id of the realm.
+ * @param {OwnershipModel} resultOwnership
+ * The ownership model to use for the results of this evaluation.
+ * @param {string=} sandbox
+ * The name of the sandbox.
+ *
+ * @return {Object}
+ * - evaluationStatus {EvaluationStatus} One of "normal", "throw".
+ * - exceptionDetails {ExceptionDetails=} the details of the exception if
+ * the evaluation status was "throw".
+ * - result {RemoteValue=} the result of the evaluation serialized as a
+ * RemoteValue if the evaluation status was "normal".
+ */
+ async evaluateExpression(options) {
+ const {
+ awaitPromise,
+ expression,
+ realmId = null,
+ resultOwnership,
+ sandbox: sandboxName = null,
+ } = options;
+
+ const realm = this.#getRealm(realmId, sandboxName);
+ const rv = realm.executeInGlobal(expression);
+
+ return this.#buildReturnValue(rv, realm, awaitPromise, resultOwnership);
+ }
+
+ /**
+ * Get realms for the current window global.
+ *
+ * @return {Array<Object>}
+ * - context {BrowsingContext} The browsing context, associated with the realm.
+ * - id {string} The realm unique identifier.
+ * - origin {string} The serialization of an origin.
+ * - sandbox {string=} The name of the sandbox.
+ * - type {RealmType.Window} The window realm type.
+ */
+ getWindowRealms() {
+ return [this.#defaultRealm, ...this.#realms.values()].map(realm =>
+ realm.getInfo()
+ );
+ }
+}
+
+export const script = ScriptModule;
diff --git a/remote/webdriver-bidi/moz.build b/remote/webdriver-bidi/moz.build
new file mode 100644
index 0000000000..3c13a0ac11
--- /dev/null
+++ b/remote/webdriver-bidi/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Remote Protocol", "WebDriver BiDi")
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
diff --git a/remote/webdriver-bidi/test/xpcshell/test_Realm.js b/remote/webdriver-bidi/test/xpcshell/test_Realm.js
new file mode 100644
index 0000000000..ff39adcd82
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/test_Realm.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Realm } = ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/Realm.sys.mjs"
+);
+
+add_test(function test_id() {
+ const realm1 = new Realm();
+ const id1 = realm1.id;
+ Assert.equal(typeof id1, "string");
+
+ const realm2 = new Realm();
+ const id2 = realm2.id;
+ Assert.equal(typeof id2, "string");
+
+ Assert.notEqual(id1, id2, "Ids for different realms are different");
+
+ run_next_test();
+});
+
+add_test(function test_handleObjectMap() {
+ const realm = new Realm();
+
+ // Test an unknown handle.
+ Assert.equal(
+ realm.getObjectForHandle("unknown"),
+ undefined,
+ "Unknown handles return undefined"
+ );
+
+ // Test creating a simple handle.
+ const object = {};
+ const handle = realm.getHandleForObject(object);
+ Assert.equal(typeof handle, "string", "Created a valid handle");
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ object,
+ "Using the handle returned the original object"
+ );
+
+ // Test another handle for the same object.
+ const secondHandle = realm.getHandleForObject(object);
+ Assert.equal(typeof secondHandle, "string", "Created a valid handle");
+ Assert.notEqual(secondHandle, handle, "A different handle was generated");
+ Assert.equal(
+ realm.getObjectForHandle(secondHandle),
+ object,
+ "Using the second handle also returned the original object"
+ );
+
+ // Test using the handles in another realm.
+ const otherRealm = new Realm();
+ Assert.equal(
+ otherRealm.getObjectForHandle(handle),
+ undefined,
+ "A realm returns undefined for handles from another realm"
+ );
+
+ // Removing an unknown handle should not throw or have any side effect on
+ // existing handles.
+ realm.removeObjectHandle("unknown");
+ Assert.equal(realm.getObjectForHandle(handle), object);
+ Assert.equal(realm.getObjectForHandle(secondHandle), object);
+
+ // Remove the second handle
+ realm.removeObjectHandle(secondHandle);
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ object,
+ "The first handle is still resolving the object"
+ );
+ Assert.equal(
+ realm.getObjectForHandle(secondHandle),
+ undefined,
+ "The second handle returns undefined after calling removeObjectHandle"
+ );
+
+ // Remove the original handle
+ realm.removeObjectHandle(handle);
+ Assert.equal(
+ realm.getObjectForHandle(handle),
+ undefined,
+ "The first handle returns undefined as well"
+ );
+
+ run_next_test();
+});
diff --git a/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js b/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js
new file mode 100644
index 0000000000..8147711417
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js
@@ -0,0 +1,1016 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const browser = Services.appShell.createWindowlessBrowser(false);
+const domEl = browser.document.createElement("div");
+browser.document.body.appendChild(domEl);
+
+const PRIMITIVE_TYPES = [
+ { value: undefined, serialized: { type: "undefined" } },
+ { value: null, serialized: { type: "null" } },
+ { value: "foo", serialized: { type: "string", value: "foo" } },
+ { value: Number.NaN, serialized: { type: "number", value: "NaN" } },
+ { value: -0, serialized: { type: "number", value: "-0" } },
+ {
+ value: Number.POSITIVE_INFINITY,
+ serialized: { type: "number", value: "Infinity" },
+ },
+ {
+ value: Number.NEGATIVE_INFINITY,
+ serialized: { type: "number", value: "-Infinity" },
+ },
+ { value: 42, serialized: { type: "number", value: 42 } },
+ { value: false, serialized: { type: "boolean", value: false } },
+ { value: 42n, serialized: { type: "bigint", value: "42" } },
+];
+
+const REMOTE_SIMPLE_VALUES = [
+ {
+ value: new RegExp(/foo/),
+ serialized: {
+ type: "regexp",
+ value: {
+ pattern: "foo",
+ flags: "",
+ },
+ },
+ deserializable: true,
+ },
+ {
+ value: new RegExp(/foo/g),
+ serialized: {
+ type: "regexp",
+ value: {
+ pattern: "foo",
+ flags: "g",
+ },
+ },
+ deserializable: true,
+ },
+ {
+ value: new Date(1654004849000),
+ serialized: {
+ type: "date",
+ value: "2022-05-31T13:47:29.000Z",
+ },
+ deserializable: true,
+ },
+];
+
+const REMOTE_COMPLEX_VALUES = [
+ { value: Symbol("foo"), serialized: { type: "symbol" } },
+ {
+ value: [1],
+ serialized: {
+ type: "array",
+ },
+ },
+ {
+ value: [1],
+ maxDepth: 0,
+ serialized: {
+ type: "array",
+ },
+ },
+ {
+ value: [1, "2", true, new RegExp(/foo/g)],
+ maxDepth: 1,
+ serialized: {
+ type: "array",
+ value: [
+ { type: "number", value: 1 },
+ { type: "string", value: "2" },
+ { type: "boolean", value: true },
+ {
+ type: "regexp",
+ value: {
+ pattern: "foo",
+ flags: "g",
+ },
+ },
+ ],
+ },
+ deserializable: true,
+ },
+ {
+ value: [1, [3, "4"]],
+ maxDepth: 1,
+ serialized: {
+ type: "array",
+ value: [{ type: "number", value: 1 }, { type: "array" }],
+ },
+ },
+ {
+ value: [1, [3, "4"]],
+ maxDepth: 2,
+ serialized: {
+ type: "array",
+ value: [
+ { type: "number", value: 1 },
+ {
+ type: "array",
+ value: [
+ { type: "number", value: 3 },
+ { type: "string", value: "4" },
+ ],
+ },
+ ],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Map(),
+ maxDepth: 1,
+ serialized: {
+ type: "map",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Map([]),
+ maxDepth: 1,
+ serialized: {
+ type: "map",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ serialized: {
+ type: "map",
+ },
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ maxDepth: 0,
+ serialized: {
+ type: "map",
+ },
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ maxDepth: 1,
+ serialized: {
+ type: "map",
+ value: [
+ [
+ { type: "number", value: 1 },
+ { type: "number", value: 2 },
+ ],
+ ["2", { type: "string", value: "3" }],
+ [
+ { type: "boolean", value: true },
+ { type: "boolean", value: false },
+ ],
+ ],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Set(),
+ maxDepth: 1,
+ serialized: {
+ type: "set",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Set([]),
+ maxDepth: 1,
+ serialized: {
+ type: "set",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Set([1, "2", true]),
+ serialized: {
+ type: "set",
+ },
+ },
+ {
+ value: new Set([1, "2", true]),
+ maxDepth: 0,
+ serialized: {
+ type: "set",
+ },
+ },
+ {
+ value: new Set([1, "2", true]),
+ maxDepth: 1,
+ serialized: {
+ type: "set",
+ value: [
+ { type: "number", value: 1 },
+ { type: "string", value: "2" },
+ { type: "boolean", value: true },
+ ],
+ },
+ deserializable: true,
+ },
+ { value: new WeakMap([[{}, 1]]), serialized: { type: "weakmap" } },
+ { value: new WeakSet([{}]), serialized: { type: "weakset" } },
+ { value: new Error("error message"), serialized: { type: "error" } },
+ {
+ value: new SyntaxError("syntax error message"),
+ serialized: { type: "error" },
+ },
+ {
+ value: new TypeError("type error message"),
+ serialized: { type: "error" },
+ },
+ { value: new Promise(() => true), serialized: { type: "promise" } },
+ { value: new Int8Array(), serialized: { type: "typedarray" } },
+ { value: new ArrayBuffer(), serialized: { type: "arraybuffer" } },
+ {
+ value: browser.document.querySelectorAll("div"),
+ serialized: { type: "nodelist" },
+ },
+ {
+ value: browser.document.getElementsByTagName("div"),
+ serialized: { type: "htmlcollection" },
+ },
+ {
+ value: domEl,
+ serialized: {
+ type: "node",
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ children: [],
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ },
+ },
+ },
+ { value: browser.document.defaultView, serialized: { type: "window" } },
+ { value: new URL("https://example.com"), serialized: { type: "object" } },
+ { value: () => true, serialized: { type: "function" } },
+ { value() {}, serialized: { type: "function" } },
+ {
+ value: {},
+ maxDepth: 1,
+ serialized: {
+ type: "object",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: {
+ "1": 1,
+ "2": "2",
+ foo: true,
+ },
+ serialized: {
+ type: "object",
+ },
+ },
+ {
+ value: {
+ "1": 1,
+ "2": "2",
+ foo: true,
+ },
+ maxDepth: 0,
+ serialized: {
+ type: "object",
+ },
+ },
+ {
+ value: {
+ "1": 1,
+ "2": "2",
+ foo: true,
+ },
+ maxDepth: 1,
+ serialized: {
+ type: "object",
+ value: [
+ ["1", { type: "number", value: 1 }],
+ ["2", { type: "string", value: "2" }],
+ ["foo", { type: "boolean", value: true }],
+ ],
+ },
+ deserializable: true,
+ },
+ {
+ value: {
+ "1": 1,
+ "2": "2",
+ "3": {
+ bar: "foo",
+ },
+ foo: true,
+ },
+ maxDepth: 2,
+ serialized: {
+ type: "object",
+ value: [
+ ["1", { type: "number", value: 1 }],
+ ["2", { type: "string", value: "2" }],
+ [
+ "3",
+ {
+ type: "object",
+ value: [["bar", { type: "string", value: "foo" }]],
+ },
+ ],
+ ["foo", { type: "boolean", value: true }],
+ ],
+ },
+ deserializable: true,
+ },
+];
+
+const { Realm } = ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/Realm.sys.mjs"
+);
+
+const { deserialize, serialize, stringify } = ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs"
+);
+
+add_test(function test_deserializePrimitiveTypes() {
+ const realm = new Realm();
+
+ for (const type of PRIMITIVE_TYPES) {
+ const { value: expectedValue, serialized } = type;
+
+ info(`Checking '${serialized.type}'`);
+ const value = deserialize(realm, serialized);
+
+ if (serialized.value == "NaN") {
+ ok(Number.isNaN(value), `Got expected value for ${serialized}`);
+ } else {
+ Assert.strictEqual(
+ value,
+ expectedValue,
+ `Got expected value for ${serialized}`
+ );
+ }
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeDateLocalValue() {
+ const realm = new Realm();
+
+ const validaDateStrings = [
+ "2009",
+ "2009-05",
+ "2009-05-19",
+ "2009T15:00",
+ "2009-05T15:00",
+ "2009-05-19T15:00",
+ "2009-05-19T15:00:15",
+ "2009-05-19T15:00:15.452",
+ "2009-05-19T15:00:15.452Z",
+ "2009-05-19T15:00:15.452+02:00",
+ "2009-05-19T15:00:15.452-02:00",
+ "-271821-04-20T00:00:00Z",
+ "+000000-01-01T00:00:00Z",
+ ];
+ for (const dateString of validaDateStrings) {
+ info(`Checking '${dateString}'`);
+ const value = deserialize(realm, { type: "date", value: dateString });
+
+ Assert.equal(
+ value.getTime(),
+ new Date(dateString).getTime(),
+ `Got expected value for ${dateString}`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeLocalValues() {
+ const realm = new Realm();
+
+ for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) {
+ const { value: expectedValue, serialized, deserializable } = type;
+
+ // Skip non deserializable cases
+ if (!deserializable) {
+ continue;
+ }
+
+ info(`Checking '${serialized.type}'`);
+ const value = deserialize(realm, serialized);
+ assertLocalValue(serialized.type, value, expectedValue);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeLocalValuesByHandle() {
+ // Create two realms, realm1 will be used to serialize values, while realm2
+ // will be used as a reference empty realm without any object reference.
+ const realm1 = new Realm();
+ const realm2 = new Realm();
+
+ for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) {
+ const { value: expectedValue, serialized } = type;
+
+ // No need to skip non-deserializable cases here.
+
+ info(`Checking '${serialized.type}'`);
+ // Serialize the value once to get a handle.
+ const serializedValue = serialize(
+ expectedValue,
+ 0,
+ "root",
+ new Map(),
+ realm1
+ );
+
+ // Create a remote reference containing only the handle.
+ // `deserialize` should not need any other property.
+ const remoteReference = { handle: serializedValue.handle };
+
+ // Check that the remote reference can be deserialized in realm1.
+ const deserializedValue = deserialize(realm1, remoteReference);
+ assertLocalValue(serialized.type, deserializedValue, expectedValue);
+
+ Assert.throws(
+ () => deserialize(realm2, remoteReference),
+ /InvalidArgumentError:/,
+ `Got expected error when using the wrong realm for deserialize`
+ );
+
+ realm1.removeObjectHandle(serializedValue.handle);
+ Assert.throws(
+ () => deserialize(realm1, remoteReference),
+ /InvalidArgumentError:/,
+ `Got expected error when after deleting the object handle`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializePrimitiveTypesInvalidValues() {
+ const realm = new Realm();
+
+ const invalidValues = [
+ { type: "bigint", values: [undefined, null, false, "foo", [], {}] },
+ { type: "boolean", values: [undefined, null, 42, "foo", [], {}] },
+ {
+ type: "number",
+ values: [undefined, null, false, "43", [], {}],
+ },
+ { type: "string", values: [undefined, null, false, 42, [], {}] },
+ ];
+
+ for (const invalidValue of invalidValues) {
+ const { type, values } = invalidValue;
+
+ for (const value of values) {
+ info(`Checking '${type}' with value ${value}`);
+
+ Assert.throws(
+ () => deserialize(realm, { type, value }),
+ /InvalidArgument/,
+ `Got expected error for type ${type} and value ${value}`
+ );
+ }
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeDateLocalValueInvalidValues() {
+ const realm = new Realm();
+
+ const invalidaDateStrings = [
+ "10",
+ "20009",
+ "+20009",
+ "2009-",
+ "2009-0",
+ "2009-15",
+ "2009-02-1",
+ "2009-02-50",
+ "2022-02-29",
+ "15:00",
+ "T15:00",
+ "9-05-19T15:00",
+ "2009-5-19T15:00",
+ "2009-05-1T15:00",
+ "2009-02-10T15",
+ "2009-05-19T15:",
+ "2009-05-19T1:00",
+ "2009-05-19T10:1",
+ "2022-06-31T15:00",
+ "2009-05-19T60:00",
+ "2009-05-19T15:70",
+ "2009-05-19T15:00.25",
+ "2009-05-19+10:00",
+ "2009-05-19Z",
+ "2009-05-19 15:00",
+ "2009-05-19t15:00Z",
+ "2009-05-19T15:00z",
+ "2009-05-19T15:00+01",
+ "2009-05-19T10:10+1:00",
+ "2009-05-19T10:10+01:1",
+ "2009-05-19T15:00+75:00",
+ "2009-05-19T15:00+02:80",
+ "2009-05-19T15:00-00:00",
+ "02009-05-19T15:00",
+ ];
+ for (const dateString of invalidaDateStrings) {
+ info(`Checking '${dateString}'`);
+
+ Assert.throws(
+ () => deserialize(realm, { type: "date", value: dateString }),
+ /InvalidArgumentError:/,
+ `Got expected error for date string: ${dateString}`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeLocalValuesInvalidType() {
+ const realm = new Realm();
+
+ const invalidTypes = [undefined, null, false, 42, {}];
+
+ for (const invalidType of invalidTypes) {
+ info(`Checking type: '${invalidType}'`);
+
+ Assert.throws(
+ () => deserialize(realm, { type: invalidType }),
+ /InvalidArgumentError:/,
+ `Got expected error for type ${invalidType}`
+ );
+
+ Assert.throws(
+ () =>
+ deserialize(realm, {
+ type: "array",
+ value: [{ type: invalidType }],
+ }),
+ /InvalidArgumentError:/,
+ `Got expected error for nested type ${invalidType}`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_deserializeLocalValuesInvalidValues() {
+ const realm = new Realm();
+
+ const invalidValues = [
+ { type: "array", values: [undefined, null, false, 42, "foo", {}] },
+ {
+ type: "regexp",
+ values: [
+ undefined,
+ null,
+ false,
+ "foo",
+ 42,
+ [],
+ {},
+ { pattern: null },
+ { pattern: 1 },
+ { pattern: true },
+ { pattern: "foo", flags: null },
+ { pattern: "foo", flags: 1 },
+ { pattern: "foo", flags: false },
+ { pattern: "foo", flags: "foo" },
+ ],
+ },
+ {
+ type: "date",
+ values: [
+ undefined,
+ null,
+ false,
+ "foo",
+ "05 October 2011 14:48 UTC",
+ "Tue Jun 14 2022 10:46:50 GMT+0200!",
+ 42,
+ [],
+ {},
+ ],
+ },
+ {
+ type: "map",
+ values: [
+ undefined,
+ null,
+ false,
+ "foo",
+ 42,
+ ["1"],
+ [[]],
+ [["1"]],
+ [{ "1": "2" }],
+ {},
+ ],
+ },
+ {
+ type: "set",
+ values: [undefined, null, false, "foo", 42, {}],
+ },
+ {
+ type: "object",
+ values: [
+ undefined,
+ null,
+ false,
+ "foo",
+ 42,
+ {},
+ ["1"],
+ [[]],
+ [["1"]],
+ [{ "1": "2" }],
+ [
+ [
+ { type: "number", value: "1" },
+ { type: "number", value: "2" },
+ ],
+ ],
+ [
+ [
+ { type: "object", value: [] },
+ { type: "number", value: "1" },
+ ],
+ ],
+ [
+ [
+ {
+ type: "regexp",
+ value: {
+ pattern: "foo",
+ },
+ },
+ { type: "number", value: "1" },
+ ],
+ ],
+ ],
+ },
+ ];
+
+ for (const invalidValue of invalidValues) {
+ const { type, values } = invalidValue;
+
+ for (const value of values) {
+ info(`Checking '${type}' with value ${value}`);
+
+ Assert.throws(
+ () => deserialize(realm, { type, value }),
+ /InvalidArgumentError:/,
+ `Got expected error for type ${type} and value ${value}`
+ );
+ }
+ }
+
+ run_next_test();
+});
+
+add_test(function test_serializePrimitiveTypes() {
+ const realm = new Realm();
+
+ for (const type of PRIMITIVE_TYPES) {
+ const { value, serialized } = type;
+
+ const serializationInternalMap = new Map();
+ const serializedValue = serialize(
+ value,
+ 0,
+ "none",
+ serializationInternalMap,
+ realm
+ );
+ assertInternalIds(serializationInternalMap, 0);
+ Assert.deepEqual(serialized, serializedValue, "Got expected structure");
+
+ // For primitive values, the serialization with ownershipType=root should
+ // be exactly identical to the one with ownershipType=none.
+ const serializationInternalMapWithRoot = new Map();
+ const serializedWithRoot = serialize(
+ value,
+ 0,
+ "root",
+ serializationInternalMapWithRoot,
+ realm
+ );
+ assertInternalIds(serializationInternalMapWithRoot, 0);
+ Assert.deepEqual(serialized, serializedWithRoot, "Got expected structure");
+ }
+
+ run_next_test();
+});
+
+add_test(function test_serializeRemoteSimpleValues() {
+ const realm = new Realm();
+
+ for (const type of REMOTE_SIMPLE_VALUES) {
+ const { value, serialized } = type;
+
+ info(`Checking '${serialized.type}' with none ownershipType`);
+ const serializationInternalMapWithNone = new Map();
+ const serializedValue = serialize(
+ value,
+ 0,
+ "none",
+ serializationInternalMapWithNone,
+ realm
+ );
+
+ assertInternalIds(serializationInternalMapWithNone, 0);
+ Assert.deepEqual(serialized, serializedValue, "Got expected structure");
+
+ info(`Checking '${serialized.type}' with root ownershipType`);
+ const serializationInternalMapWithRoot = new Map();
+ const serializedWithRoot = serialize(
+ value,
+ 0,
+ "root",
+ serializationInternalMapWithRoot,
+ realm
+ );
+
+ assertInternalIds(serializationInternalMapWithRoot, 0);
+ Assert.equal(
+ typeof serializedWithRoot.handle,
+ "string",
+ "Got a handle property"
+ );
+ Assert.deepEqual(
+ Object.assign({}, serialized, { handle: serializedWithRoot.handle }),
+ serializedWithRoot,
+ "Got expected structure, plus a generated handle id"
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_serializeRemoteComplexValues() {
+ const realm = new Realm();
+
+ for (const type of REMOTE_COMPLEX_VALUES) {
+ const { value, serialized, maxDepth } = type;
+
+ info(`Checking '${serialized.type}' with none ownershipType`);
+ const serializationInternalMapWithNone = new Map();
+ const serializedValue = serialize(
+ value,
+ maxDepth,
+ "none",
+ serializationInternalMapWithNone,
+ realm
+ );
+
+ assertInternalIds(serializationInternalMapWithNone, 0);
+ Assert.deepEqual(serialized, serializedValue, "Got expected structure");
+
+ info(`Checking '${serialized.type}' with root ownershipType`);
+ const serializationInternalMapWithRoot = new Map();
+ const serializedWithRoot = serialize(
+ value,
+ maxDepth,
+ "root",
+ serializationInternalMapWithRoot,
+ realm
+ );
+
+ assertInternalIds(serializationInternalMapWithRoot, 0);
+ Assert.equal(
+ typeof serializedWithRoot.handle,
+ "string",
+ "Got a handle property"
+ );
+ Assert.deepEqual(
+ Object.assign({}, serialized, { handle: serializedWithRoot.handle }),
+ serializedWithRoot,
+ "Got expected structure, plus a generated handle id"
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_serializeWithSerializationInternalMap() {
+ const dataSet = [
+ {
+ data: [1],
+ serializedData: [{ type: "number", value: 1 }],
+ type: "array",
+ },
+ {
+ data: new Map([[true, false]]),
+ serializedData: [
+ [
+ { type: "boolean", value: true },
+ { type: "boolean", value: false },
+ ],
+ ],
+ type: "map",
+ },
+ {
+ data: new Set(["foo"]),
+ serializedData: [{ type: "string", value: "foo" }],
+ type: "set",
+ },
+ {
+ data: { foo: "bar" },
+ serializedData: [["foo", { type: "string", value: "bar" }]],
+ type: "object",
+ },
+ ];
+ const realm = new Realm();
+
+ for (const { type, data, serializedData } of dataSet) {
+ info(`Checking '${type}' with serializationInternalMap`);
+
+ const serializationInternalMap = new Map();
+ const value = [
+ data,
+ data,
+ [data],
+ new Set([data]),
+ new Map([["bar", data]]),
+ { bar: data },
+ ];
+
+ const serializedValue = serialize(
+ value,
+ 2,
+ "none",
+ serializationInternalMap,
+ realm
+ );
+
+ assertInternalIds(serializationInternalMap, 1);
+
+ const internalId = serializationInternalMap.get(data).internalId;
+
+ const serialized = {
+ type: "array",
+ value: [
+ {
+ type,
+ value: serializedData,
+ internalId,
+ },
+ {
+ type,
+ internalId,
+ },
+ {
+ type: "array",
+ value: [{ type, internalId }],
+ },
+ {
+ type: "set",
+ value: [{ type, internalId }],
+ },
+ {
+ type: "map",
+ value: [["bar", { type, internalId }]],
+ },
+ {
+ type: "object",
+ value: [["bar", { type, internalId }]],
+ },
+ ],
+ };
+
+ Assert.deepEqual(serialized, serializedValue, "Got expected structure");
+ }
+
+ run_next_test();
+});
+
+add_test(function test_serializeMultipleValuesWithSerializationInternalMap() {
+ const realm = new Realm();
+ const serializationInternalMap = new Map();
+ const obj1 = { foo: "bar" };
+ const obj2 = [1, 2];
+ const value = [obj1, obj2, obj1, obj2];
+
+ serialize(value, 2, "none", serializationInternalMap, realm);
+
+ assertInternalIds(serializationInternalMap, 2);
+
+ const internalId1 = serializationInternalMap.get(obj1).internalId;
+ const internalId2 = serializationInternalMap.get(obj2).internalId;
+
+ Assert.notEqual(
+ internalId1,
+ internalId2,
+ "Internal ids for different object are also different"
+ );
+
+ run_next_test();
+});
+
+add_test(function test_stringify() {
+ const STRINGIFY_TEST_CASES = [
+ [undefined, "undefined"],
+ [null, "null"],
+ ["foobar", "foobar"],
+ ["2", "2"],
+ [-0, "0"],
+ [Infinity, "Infinity"],
+ [-Infinity, "-Infinity"],
+ [3, "3"],
+ [1.4, "1.4"],
+ [true, "true"],
+ [42n, "42"],
+ [{ toString: () => "bar" }, "bar", "toString: () => 'bar'"],
+ [{ toString: () => 4 }, "[object Object]", "toString: () => 4"],
+ [{ toString: undefined }, "[object Object]", "toString: undefined"],
+ [{ toString: null }, "[object Object]", "toString: null"],
+ [
+ {
+ toString: () => {
+ throw new Error("toString error");
+ },
+ },
+ "[object Object]",
+ "toString: () => { throw new Error('toString error'); }",
+ ],
+ ];
+
+ for (const [value, expectedString, description] of STRINGIFY_TEST_CASES) {
+ info(`Checking '${description || value}'`);
+ const stringifiedValue = stringify(value);
+
+ Assert.strictEqual(expectedString, stringifiedValue, "Got expected string");
+ }
+
+ run_next_test();
+});
+
+function assertLocalValue(type, value, expectedValue) {
+ let formattedValue = value;
+ let formattedExpectedValue = expectedValue;
+
+ // Format certain types for easier assertion
+ if (type == "map") {
+ Assert.equal(
+ Object.prototype.toString.call(expectedValue),
+ "[object Map]",
+ "Got expected type Map"
+ );
+
+ formattedValue = Array.from(value.values());
+ formattedExpectedValue = Array.from(expectedValue.values());
+ } else if (type == "set") {
+ Assert.equal(
+ Object.prototype.toString.call(expectedValue),
+ "[object Set]",
+ "Got expected type Set"
+ );
+
+ formattedValue = Array.from(value);
+ formattedExpectedValue = Array.from(expectedValue);
+ }
+
+ Assert.deepEqual(
+ formattedValue,
+ formattedExpectedValue,
+ "Got expected structure"
+ );
+}
+
+function assertInternalIds(serializationInternalMap, amount) {
+ const remoteValuesWithInternalIds = Array.from(
+ serializationInternalMap.values()
+ ).filter(remoteValue => !!remoteValue.internalId);
+
+ Assert.equal(
+ remoteValuesWithInternalIds.length,
+ amount,
+ "Got expected amount of internalIds in serializationInternalMap"
+ );
+}
diff --git a/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js b/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js
new file mode 100644
index 0000000000..10b43e4ed6
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { splitMethod } = ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs"
+);
+
+add_test(function test_Connection_splitMethod() {
+ for (const t of [42, null, true, {}, [], undefined]) {
+ Assert.throws(() => splitMethod(t), /TypeError/, `${typeof t} throws`);
+ }
+ for (const s of ["", ".", "foo.", ".bar", "foo.bar.baz"]) {
+ Assert.throws(
+ () => splitMethod(s),
+ /Invalid method format: '.*'/,
+ `"${s}" throws`
+ );
+ }
+ deepEqual(splitMethod("foo.bar"), {
+ module: "foo",
+ command: "bar",
+ });
+
+ run_next_test();
+});
diff --git a/remote/webdriver-bidi/test/xpcshell/xpcshell.ini b/remote/webdriver-bidi/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..8873ae646d
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/xpcshell.ini
@@ -0,0 +1,7 @@
+# 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/.
+
+[test_Realm.js]
+[test_RemoteValue.js]
+[test_WebDriverBiDiConnection.js]