summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi
diff options
context:
space:
mode:
Diffstat (limited to 'remote/webdriver-bidi')
-rw-r--r--remote/webdriver-bidi/NewSessionHandler.sys.mjs57
-rw-r--r--remote/webdriver-bidi/RemoteValue.sys.mjs1066
-rw-r--r--remote/webdriver-bidi/WebDriverBiDi.sys.mjs242
-rw-r--r--remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs214
-rw-r--r--remote/webdriver-bidi/jar.mn34
-rw-r--r--remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs42
-rw-r--r--remote/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs18
-rw-r--r--remote/webdriver-bidi/modules/root/browsingContext.sys.mjs812
-rw-r--r--remote/webdriver-bidi/modules/root/input.sys.mjs99
-rw-r--r--remote/webdriver-bidi/modules/root/log.sys.mjs15
-rw-r--r--remote/webdriver-bidi/modules/root/network.sys.mjs320
-rw-r--r--remote/webdriver-bidi/modules/root/script.sys.mjs747
-rw-r--r--remote/webdriver-bidi/modules/root/session.sys.mjs405
-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-in-root/script.sys.mjs28
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs159
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/input.sys.mjs103
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/log.sys.mjs258
-rw-r--r--remote/webdriver-bidi/modules/windowglobal/script.sys.mjs483
-rw-r--r--remote/webdriver-bidi/moz.build14
-rw-r--r--remote/webdriver-bidi/test/browser/browser.ini5
-rw-r--r--remote/webdriver-bidi/test/browser/browser_RemoteValue.js1633
-rw-r--r--remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js25
-rw-r--r--remote/webdriver-bidi/test/xpcshell/xpcshell.ini5
25 files changed, 6843 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/RemoteValue.sys.mjs b/remote/webdriver-bidi/RemoteValue.sys.mjs
new file mode 100644
index 0000000000..933b457ca1
--- /dev/null
+++ b/remote/webdriver-bidi/RemoteValue.sys.mjs
@@ -0,0 +1,1066 @@
+/* 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",
+ generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
+);
+
+/**
+ * @typedef {object} IncludeShadowTreeMode
+ */
+
+/**
+ * Enum of include shadow tree modes supported by the serialization.
+ *
+ * @readonly
+ * @enum {IncludeShadowTreeMode}
+ */
+export const IncludeShadowTreeMode = {
+ All: "all",
+ None: "none",
+ Open: "open",
+};
+
+/**
+ * @typedef {object} OwnershipModel
+ */
+
+/**
+ * Enum of ownership models supported by the serialization.
+ *
+ * @readonly
+ * @enum {OwnershipModel}
+ */
+export const OwnershipModel = {
+ None: "none",
+ Root: "root",
+};
+
+/**
+ * An object which holds the information of how
+ * ECMAScript objects should be serialized.
+ *
+ * @typedef {object} SerializationOptions
+ *
+ * @property {number} [maxDomDepth=0]
+ * Depth of a serialization of DOM Nodes. Defaults to 0.
+ * @property {number} [maxObjectDepth=null]
+ * Depth of a serialization of objects. Defaults to null.
+ * @property {IncludeShadowTreeMode} [includeShadowTree=IncludeShadowTreeMode.None]
+ * Mode of a serialization of shadow dom. Defaults to "none".
+ */
+
+const TYPED_ARRAY_CLASSES = [
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Uint16Array",
+ "Uint32Array",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Float32Array",
+ "Float64Array",
+ "BigInt64Array",
+ "BigUint64Array",
+];
+
+/**
+ * Build the serialized RemoteValue.
+ *
+ * @returns {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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {Array} List of deserialized values.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>serializedValueList</var> is not an array.
+ */
+function deserializeValueList(realm, serializedValueList, options = {}) {
+ lazy.assert.array(
+ serializedValueList,
+ `Expected "serializedValueList" to be an array, got ${serializedValueList}`
+ );
+
+ const deserializedValues = [];
+
+ for (const item of serializedValueList) {
+ deserializedValues.push(deserialize(realm, item, options));
+ }
+
+ 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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {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, options = {}) {
+ 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, options);
+ const deserializedValue = deserialize(realm, serializedValue, options);
+
+ deserializedKeyValueList.push([deserializedKey, deserializedValue]);
+ }
+
+ return deserializedKeyValueList;
+}
+
+/**
+ * Deserialize a Node as referenced by the shared unique reference.
+ *
+ * This unique reference can be shared by WebDriver clients with the WebDriver
+ * classic implementation (Marionette) if the reference is for an Element or
+ * ShadowRoot.
+ *
+ * @param {string} sharedRef
+ * Shared unique reference of the Node.
+ * @param {Realm} realm
+ * The Realm in which the value is deserialized.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {Node} The deserialized DOM node.
+ */
+function deserializeSharedReference(sharedRef, realm, options = {}) {
+ const { nodeCache } = options;
+
+ const browsingContext = realm.browsingContext;
+ if (!browsingContext) {
+ throw new lazy.error.NoSuchNodeError("Realm isn't a Window global");
+ }
+
+ const node = nodeCache.getNode(browsingContext, sharedRef);
+
+ if (node === null) {
+ throw new lazy.error.NoSuchNodeError(
+ `The node with the reference ${sharedRef} is not known`
+ );
+ }
+
+ // Bug 1819902: Instead of a browsing context check compare the origin
+ const isSameBrowsingContext = sharedRef => {
+ const nodeDetails = nodeCache.getReferenceDetails(sharedRef);
+
+ if (nodeDetails.isTopBrowsingContext && browsingContext.parent === null) {
+ // As long as Navigables are not available any cross-group navigation will
+ // cause a swap of the current top-level browsing context. The only unique
+ // identifier in such a case is the browser id the top-level browsing
+ // context actually lives in.
+ return nodeDetails.browserId === browsingContext.browserId;
+ }
+
+ return nodeDetails.browsingContextId === browsingContext.id;
+ };
+
+ if (!isSameBrowsingContext(sharedRef)) {
+ return null;
+ }
+
+ return node;
+}
+
+/**
+ * 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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ * @param {Function} options.emitScriptMessage
+ * The function to emit "script.message" event.
+ *
+ * @returns {object} Deserialized representation of the value.
+ */
+export function deserialize(realm, serializedValue, options = {}) {
+ const { handle, sharedId, type, value } = serializedValue;
+
+ // With a shared id present deserialize as node reference.
+ if (sharedId !== undefined) {
+ lazy.assert.string(
+ sharedId,
+ `Expected "sharedId" to be a string, got ${sharedId}`
+ );
+
+ return deserializeSharedReference(sharedId, realm, options);
+ }
+
+ // 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.NoSuchHandleError(
+ `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}`
+ );
+ }
+
+ // Script channel
+ case "channel": {
+ const channel = message =>
+ options.emitScriptMessage(realm, value, message);
+ return realm.cloneIntoRealm(channel);
+ }
+
+ // Non-primitive protocol values
+ case "array":
+ const array = realm.cloneIntoRealm([]);
+ deserializeValueList(realm, value, options).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, options).forEach(([k, v]) =>
+ map.set(k, v)
+ );
+
+ return map;
+ case "object":
+ const object = realm.cloneIntoRealm({});
+ deserializeKeyValueList(realm, value, options).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, options).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.
+ *
+ * @returns {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);
+}
+
+/**
+ * Gets or creates a new shared unique reference for the DOM node.
+ *
+ * This unique reference can be shared by WebDriver clients with the WebDriver
+ * classic implementation (Marionette) if the reference is for an Element or
+ * ShadowRoot.
+ *
+ * @param {Node} node
+ * Node to create the unique reference for.
+ * @param {Realm} realm
+ * The Realm in which the value is serialized.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {string}
+ * Shared unique reference for the Node.
+ */
+function getSharedIdForNode(node, realm, options = {}) {
+ const { nodeCache } = options;
+
+ if (!Node.isInstance(node)) {
+ return null;
+ }
+
+ const browsingContext = realm.browsingContext;
+ if (!browsingContext) {
+ return null;
+ }
+
+ const unwrapped = Cu.unwaiveXrays(node);
+ return nodeCache.getOrCreateNodeReference(unwrapped);
+}
+
+/**
+ * Determines if <var>node</var> is shadow root.
+ *
+ * @param {Node} node
+ * Node to check.
+ *
+ * @returns {boolean}
+ * True if <var>node</var> is shadow root, false otherwise.
+ */
+function isShadowRoot(node) {
+ const DOCUMENT_FRAGMENT_NODE = 11;
+ return (
+ node &&
+ node.nodeType === DOCUMENT_FRAGMENT_NODE &&
+ node.containingShadowRoot == node
+ );
+}
+
+/**
+ * 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 {SerializationOptions} serializationOptions
+ * Options which define how ECMAScript objects should be serialized.
+ * @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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {object} Object for serialized values.
+ */
+function serializeArrayLike(
+ production,
+ handleId,
+ knownObject,
+ value,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+) {
+ const serialized = buildSerialized(production, handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && serializationOptions.maxObjectDepth !== 0) {
+ serialized.value = serializeList(
+ value,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ }
+
+ 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 {SerializationOptions} serializationOptions
+ * Options which define how ECMAScript objects should be serialized.
+ * @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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {Array} List of serialized values.
+ */
+function serializeList(
+ iterable,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+) {
+ const { maxObjectDepth } = serializationOptions;
+ const serialized = [];
+ const childSerializationOptions = {
+ ...serializationOptions,
+ };
+ if (maxObjectDepth !== null) {
+ childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
+ }
+
+ for (const item of iterable) {
+ serialized.push(
+ serialize(
+ item,
+ childSerializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ )
+ );
+ }
+
+ 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 {SerializationOptions} serializationOptions
+ * Options which define how ECMAScript objects should be serialized.
+ * @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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {Array} List of serialized values.
+ */
+function serializeMapping(
+ iterable,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+) {
+ const { maxObjectDepth } = serializationOptions;
+ const serialized = [];
+ const childSerializationOptions = {
+ ...serializationOptions,
+ };
+ if (maxObjectDepth !== null) {
+ childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
+ }
+
+ for (const [key, item] of iterable) {
+ const serializedKey =
+ typeof key == "string"
+ ? key
+ : serialize(
+ key,
+ childSerializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ const serializedValue = serialize(
+ item,
+ childSerializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+
+ serialized.push([serializedKey, serializedValue]);
+ }
+
+ return serialized;
+}
+
+/**
+ * Helper to serialize as a Node.
+ *
+ * @param {Node} node
+ * Node to be serialized.
+ * @param {SerializationOptions} serializationOptions
+ * Options which define how ECMAScript objects should be serialized.
+ * @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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {object} Serialized value.
+ */
+function serializeNode(
+ node,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+) {
+ const { includeShadowTree, maxDomDepth } = serializationOptions;
+ 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;
+ if (
+ maxDomDepth !== 0 &&
+ (!isShadowRoot(node) ||
+ (includeShadowTree === IncludeShadowTreeMode.Open &&
+ node.mode === "open") ||
+ includeShadowTree === IncludeShadowTreeMode.All)
+ ) {
+ const children = [];
+ const childSerializationOptions = {
+ ...serializationOptions,
+ };
+ if (maxDomDepth !== null) {
+ childSerializationOptions.maxDomDepth = maxDomDepth - 1;
+ }
+ for (const child of node.childNodes) {
+ children.push(
+ serialize(
+ child,
+ childSerializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ )
+ );
+ }
+
+ serialized.children = children;
+ }
+
+ if (isElement) {
+ serialized.attributes = [...node.attributes].reduce((map, attr) => {
+ map[attr.name] = attr.value;
+ return map;
+ }, {});
+
+ const shadowRoot = Cu.unwaiveXrays(node).openOrClosedShadowRoot;
+ serialized.shadowRoot = null;
+ if (shadowRoot !== null) {
+ serialized.shadowRoot = serialize(
+ shadowRoot,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ }
+ }
+
+ if (isShadowRoot(node)) {
+ serialized.mode = node.mode;
+ }
+
+ 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 {SerializationOptions} serializationOptions
+ * Options which define how ECMAScript objects should be serialized.
+ * @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.
+ * @param {object} options
+ * @param {NodeCache} options.nodeCache
+ * The cache containing DOM node references.
+ *
+ * @returns {object} Serialized representation of the value.
+ */
+export function serialize(
+ value,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+) {
+ const { maxObjectDepth } = serializationOptions;
+ 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,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ } 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 && maxObjectDepth !== 0) {
+ serialized.value = serializeMapping(
+ value.entries(),
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm
+ );
+ }
+ return serialized;
+ } else if (className == "Set") {
+ const serialized = buildSerialized("set", handleId);
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject && maxObjectDepth !== 0) {
+ serialized.value = serializeList(
+ value.values(),
+ serializationOptions,
+ 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);
+
+ // Get or create the shared id for WebDriver classic compat from the node.
+ const sharedId = getSharedIdForNode(value, realm, options);
+ if (sharedId !== null) {
+ serialized.sharedId = sharedId;
+ }
+
+ setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
+
+ if (!knownObject) {
+ serialized.value = serializeNode(
+ value,
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ }
+
+ 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 && maxObjectDepth !== 0) {
+ serialized.value = serializeMapping(
+ Object.entries(value),
+ serializationOptions,
+ ownershipType,
+ serializationInternalMap,
+ realm,
+ options
+ );
+ }
+ return serialized;
+}
+
+/**
+ * Set default serialization options.
+ *
+ * @param {SerializationOptions} options
+ * Options which define how ECMAScript objects should be serialized.
+ * @returns {SerializationOptions}
+ * Serialiation options with default value added.
+ */
+export function setDefaultSerializationOptions(options = {}) {
+ const serializationOptions = { ...options };
+ if (!("maxDomDepth" in serializationOptions)) {
+ serializationOptions.maxDomDepth = 0;
+ }
+ if (!("maxObjectDepth" in serializationOptions)) {
+ serializationOptions.maxObjectDepth = null;
+ }
+ if (!("includeShadowTree" in serializationOptions)) {
+ serializationOptions.includeShadowTree = IncludeShadowTreeMode.None;
+ }
+
+ return serializationOptions;
+}
+
+/**
+ * Set default values and assert if serialization options have
+ * expected types.
+ *
+ * @param {SerializationOptions} options
+ * Options which define how ECMAScript objects should be serialized.
+ * @returns {SerializationOptions}
+ * Serialiation options with default value added.
+ */
+export function setDefaultAndAssertSerializationOptions(options = {}) {
+ lazy.assert.object(options);
+
+ const serializationOptions = setDefaultSerializationOptions(options);
+
+ const { includeShadowTree, maxDomDepth, maxObjectDepth } =
+ serializationOptions;
+
+ if (maxDomDepth !== null) {
+ lazy.assert.positiveNumber(maxDomDepth);
+ }
+ if (maxObjectDepth !== null) {
+ lazy.assert.positiveNumber(maxObjectDepth);
+ }
+ const includeShadowTreeModesValues = Object.values(IncludeShadowTreeMode);
+ lazy.assert.that(
+ includeShadowTree =>
+ includeShadowTreeModesValues.includes(includeShadowTree),
+ `includeShadowTree ${includeShadowTree} doesn't match allowed values "${includeShadowTreeModesValues.join(
+ "/"
+ )}"`
+ )(includeShadowTree);
+
+ return serializationOptions;
+}
+
+/**
+ * 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 = lazy.generateUUID();
+ }
+
+ // Copy the internalId of the original remote value to the new remote value.
+ remoteValue.internalId = previousRemoteValue.internalId;
+ }
+}
+
+/**
+ * Safely stringify a value.
+ *
+ * @param {object} obj
+ * Value of any type to be stringified.
+ *
+ * @returns {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..8d35ce8ebc
--- /dev/null
+++ b/remote/webdriver-bidi/WebDriverBiDi.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 { 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.
+ *
+ * @returns {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
+ *
+ * @returns {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 {
+ await IOUtils.remove(this._bidiServerPath);
+ } catch (e) {
+ lazy.logger.warn(
+ `Failed to remove ${this._bidiServerPath} (${e.message})`
+ );
+ }
+
+ try {
+ // Close open session
+ this.deleteSession();
+ this.agent.server.registerPathHandler("/session", null);
+
+ // Close all open session-less connections
+ this._sessionlessConnections.forEach(connection => connection.close());
+ this._sessionlessConnections.clear();
+ } catch (e) {
+ lazy.logger.error("Failed to stop protocol", e);
+ } 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..96482222b3
--- /dev/null
+++ b/remote/webdriver-bidi/WebDriverBiDiConnection.sys.mjs
@@ -0,0 +1,214 @@
+/* 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.
+ */
+ 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 });
+
+ if (Services.profiler?.IsActive()) {
+ ChromeUtils.addProfilerMarker(
+ "BiDi: Event",
+ { category: "Remote-Protocol" },
+ method
+ );
+ }
+ }
+
+ /**
+ * 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;
+ const startTime = Cu.now();
+
+ 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(id, e);
+ }
+
+ if (Services.profiler?.IsActive()) {
+ ChromeUtils.addProfilerMarker(
+ "BiDi: Command",
+ { startTime, category: "Remote-Protocol" },
+ `${method} (${id})`
+ );
+ }
+ }
+}
+
+/**
+ * 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..34b1d67ecb
--- /dev/null
+++ b/remote/webdriver-bidi/jar.mn
@@ -0,0 +1,34 @@
+# 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/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)
+ content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs (modules/WindowGlobalBiDiModule.sys.mjs)
+
+ # WebDriver BiDi root modules
+ content/webdriver-bidi/modules/root/browsingContext.sys.mjs (modules/root/browsingContext.sys.mjs)
+ content/webdriver-bidi/modules/root/input.sys.mjs (modules/root/input.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/input.sys.mjs (modules/windowglobal/input.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)
+ content/webdriver-bidi/modules/windowglobal-in-root/script.sys.mjs (modules/windowglobal-in-root/script.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..1eada826fc
--- /dev/null
+++ b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs
@@ -0,0 +1,42 @@
+/* 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/. */
+
+export 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",
+ input: "chrome://remote/content/webdriver-bidi/modules/root/input.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",
+ script:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/script.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",
+ input:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal/input.sys.mjs",
+ log: "chrome://remote/content/webdriver-bidi/modules/windowglobal/log.sys.mjs",
+ script:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal/script.sys.mjs",
+});
diff --git a/remote/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs b/remote/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs
new file mode 100644
index 0000000000..5e5e04692e
--- /dev/null
+++ b/remote/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs
@@ -0,0 +1,18 @@
+/* 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";
+
+/**
+ * Base class for all WindowGlobal BiDi MessageHandler modules.
+ */
+export class WindowGlobalBiDiModule extends Module {
+ get nodeCache() {
+ return this.processActor.getNodeCache();
+ }
+
+ get processActor() {
+ return ChromeUtils.domProcessChild.getActor("WebDriverProcessData");
+ }
+}
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..34c71f9722
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
@@ -0,0 +1,812 @@
+/* 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",
+ print: "chrome://remote/content/shared/PDF.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} options.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} options.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,
+ {
+ unloadTimeout: 5000,
+ }
+ );
+
+ 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=} options.maxDepth
+ * Depth of the browsing context tree to traverse. If not specified
+ * the whole tree is returned.
+ * @param {string=} options.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} options.context
+ * Id of the browsing context to navigate.
+ * @param {string} options.url
+ * Url for the navigation.
+ * @param {WaitCondition=} options.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,
+ });
+ }
+
+ /**
+ * An object that holds the information about margins
+ * for Webdriver BiDi browsingContext.print command.
+ *
+ * @typedef BrowsingContextPrintMarginParameters
+ *
+ * @property {number=} bottom
+ * Bottom margin in cm. Defaults to 1cm (~0.4 inches).
+ * @property {number=} left
+ * Left margin in cm. Defaults to 1cm (~0.4 inches).
+ * @property {number=} right
+ * Right margin in cm. Defaults to 1cm (~0.4 inches).
+ * @property {number=} top
+ * Top margin in cm. Defaults to 1cm (~0.4 inches).
+ */
+
+ /**
+ * An object that holds the information about paper size
+ * for Webdriver BiDi browsingContext.print command.
+ *
+ * @typedef BrowsingContextPrintPageParameters
+ *
+ * @property {number=} height
+ * Paper height in cm. Defaults to US letter height (27.94cm / 11 inches).
+ * @property {number=} width
+ * Paper width in cm. Defaults to US letter width (21.59cm / 8.5 inches).
+ */
+
+ /**
+ * Used as return value for Webdriver BiDi browsingContext.print command.
+ *
+ * @typedef BrowsingContextPrintResult
+ *
+ * @property {string} data
+ * Base64 encoded PDF representing printed document.
+ */
+
+ /**
+ * Creates a paginated PDF representation of a document
+ * of the provided browsing context, and returns it
+ * as a Base64-encoded string.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ * @param {boolean=} options.background
+ * Whether or not to print background colors and images.
+ * Defaults to false, which prints without background graphics.
+ * @param {BrowsingContextPrintMarginParameters=} options.margin
+ * Paper margins.
+ * @param {('landscape'|'portrait')=} options.orientation
+ * Paper orientation. Defaults to 'portrait'.
+ * @param {BrowsingContextPrintPageParameters=} options.page
+ * Paper size.
+ * @param {Array<number|string>=} options.pageRanges
+ * Paper ranges to print, e.g., ['1-5', 8, '11-13'].
+ * Defaults to the empty array, which means print all pages.
+ * @param {number=} options.scale
+ * Scale of the webpage rendering. Defaults to 1.0.
+ * @param {boolean=} options.shrinkToFit
+ * Whether or not to override page size as defined by CSS.
+ * Defaults to true, in which case the content will be scaled
+ * to fit the paper size.
+ *
+ * @returns {BrowsingContextPrintResult}
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async print(options = {}) {
+ const {
+ context: contextId,
+ background,
+ margin,
+ orientation,
+ page,
+ pageRanges,
+ scale,
+ shrinkToFit,
+ } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+ const context = this.#getBrowsingContext(contextId);
+
+ const settings = lazy.print.addDefaultSettings({
+ background,
+ margin,
+ orientation,
+ page,
+ pageRanges,
+ scale,
+ shrinkToFit,
+ });
+
+ for (const prop of ["top", "bottom", "left", "right"]) {
+ lazy.assert.positiveNumber(
+ settings.margin[prop],
+ lazy.pprint`margin.${prop} is not a positive number`
+ );
+ }
+ for (const prop of ["width", "height"]) {
+ lazy.assert.positiveNumber(
+ settings.page[prop],
+ lazy.pprint`page.${prop} is not a positive number`
+ );
+ }
+ lazy.assert.positiveNumber(
+ settings.scale,
+ `scale ${settings.scale} is not a positive number`
+ );
+ lazy.assert.that(
+ scale =>
+ scale >= lazy.print.minScaleValue && scale <= lazy.print.maxScaleValue,
+ `scale ${settings.scale} is outside the range ${lazy.print.minScaleValue}-${lazy.print.maxScaleValue}`
+ )(settings.scale);
+ lazy.assert.boolean(settings.shrinkToFit);
+ lazy.assert.that(
+ orientation => lazy.print.defaults.orientationValue.includes(orientation),
+ `orientation ${
+ settings.orientation
+ } doesn't match allowed values "${lazy.print.defaults.orientationValue.join(
+ "/"
+ )}"`
+ )(settings.orientation);
+ lazy.assert.boolean(
+ settings.background,
+ `background ${settings.background} is not boolean`
+ );
+ lazy.assert.array(settings.pageRanges);
+
+ const printSettings = await lazy.print.getPrintSettings(settings);
+ const binaryString = await lazy.print.printToBinaryString(
+ context,
+ printSettings
+ );
+
+ return {
+ data: btoa(binaryString),
+ };
+ }
+
+ /**
+ * 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, {
+ 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=} options.isRoot
+ * Flag that indicates if this browsing context is the root of all the
+ * browsing contexts to be returned. Defaults to true.
+ * @param {number=} options.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/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs
new file mode 100644
index 0000000000..8edd8299b7
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/input.sys.mjs
@@ -0,0 +1,99 @@
+/* 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",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+});
+
+class InputModule extends Module {
+ destroy() {}
+
+ async performActions(options = {}) {
+ const { actions, 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`
+ );
+ }
+
+ // Bug 1821460: Fetch top-level browsing context.
+
+ await this.messageHandler.forwardCommand({
+ moduleName: "input",
+ commandName: "performActions",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ actions,
+ },
+ });
+
+ return {};
+ }
+
+ /**
+ * Reset the input state in the provided browsing context.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context to reset the input state.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>context</var> is not valid type.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async releaseActions(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`
+ );
+ }
+
+ // Bug 1821460: Fetch top-level browsing context.
+
+ await this.messageHandler.forwardCommand({
+ moduleName: "input",
+ commandName: "releaseActions",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {},
+ });
+
+ return {};
+ }
+
+ static get supportedEvents() {
+ return [];
+ }
+}
+
+export const input = InputModule;
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..b44f20e58f
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/network.sys.mjs
@@ -0,0 +1,320 @@
+/* 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, {
+ 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 {Navigation=} navigation
+ * @property {number} redirectCount
+ * @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
+ */
+
+/* eslint-disable jsdoc/valid-types */
+/**
+ * Parameters for the BeforeRequestSent event
+ *
+ * @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
+ */
+/* eslint-enable jsdoc/valid-types */
+
+/**
+ * @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
+ */
+
+/* eslint-disable jsdoc/valid-types */
+/**
+ * Parameters for the ResponseStarted event
+ *
+ * @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters
+ */
+/* eslint-enable jsdoc/valid-types */
+
+/**
+ * @typedef {object} ResponseCompletedParametersProperties
+ * @property {ResponseData} response
+ */
+
+/* eslint-disable jsdoc/valid-types */
+/**
+ * Parameters for the ResponseCompleted event
+ *
+ * @typedef {BaseParameters & ResponseCompletedParametersProperties} ResponseCompletedParameters
+ */
+/* eslint-enable jsdoc/valid-types */
+
+class NetworkModule extends Module {
+ #networkListener;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // 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.#subscribedEvents = null;
+ }
+
+ #getContextInfo(browsingContext) {
+ return {
+ contextId: browsingContext.id,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+ }
+
+ #onBeforeRequestSent = (name, data) => {
+ const { contextId, requestData, timestamp, redirectCount } = data;
+
+ // Bug 1805479: Handle the initiator, including stacktrace details.
+ const initiator = {
+ type: InitiatorType.Other,
+ };
+
+ const baseParameters = {
+ context: contextId,
+ // Bug 1805405: Handle the navigation id.
+ navigation: null,
+ redirectCount,
+ 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 baseParameters = {
+ context: contextId,
+ // Bug 1805405: Handle the navigation id.
+ navigation: null,
+ redirectCount,
+ 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..b5ab7fa30d
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/script.sys.mjs
@@ -0,0 +1,747 @@
+/* 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",
+ generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ RealmType: "chrome://remote/content/shared/Realm.sys.mjs",
+ setDefaultAndAssertSerializationOptions:
+ "chrome://remote/content/webdriver-bidi/RemoteValue.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 {
+ #preloadScriptMap;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Map in which the keys are UUIDs, and the values are structs
+ // with an item named expression, which is a string,
+ // and an item named sandbox which is a string or null.
+ this.#preloadScriptMap = new Map();
+ }
+
+ destroy() {
+ this.#preloadScriptMap = null;
+ }
+
+ /**
+ * Used as return value for script.addPreloadScript command.
+ *
+ * @typedef AddPreloadScriptResult
+ *
+ * @property {string} script
+ * The unique id associated with added preload script.
+ */
+
+ /**
+ * @typedef ChannelProperties
+ *
+ * @property {string} channel
+ * The channel id.
+ * @property {SerializationOptions=} serializationOptions
+ * An object which holds the information of how the result of evaluation
+ * in case of ECMAScript objects should be serialized.
+ * @property {OwnershipModel=} ownership
+ * The ownership model to use for the results of this evaluation. Defaults
+ * to `OwnershipModel.None`.
+ */
+
+ /**
+ * Represents a channel used to send custom messages from preload script
+ * to clients.
+ *
+ * @typedef ChannelValue
+ *
+ * @property {'channel'} type
+ * @property {ChannelProperties} value
+ */
+
+ /**
+ * Adds a preload script, which runs on creation of a new Window,
+ * before any author-defined script have run.
+ *
+ * @param {object=} options
+ * @param {Array<ChannelValue>=} options.arguments
+ * The arguments to pass to the function call.
+ * @param {string} options.functionDeclaration
+ * The expression to evaluate.
+ * @param {string=} options.sandbox
+ * The name of the sandbox. If the value is null or empty
+ * string, the default realm will be used.
+ *
+ * @returns {AddPreloadScriptResult}
+ *
+ * @throws {InvalidArgumentError}
+ * If any of the arguments does not have the expected type.
+ */
+ async addPreloadScript(options = {}) {
+ const {
+ arguments: commandArguments = [],
+ functionDeclaration,
+ sandbox = null,
+ } = options;
+
+ lazy.assert.string(
+ functionDeclaration,
+ `Expected "functionDeclaration" to be a string, got ${functionDeclaration}`
+ );
+
+ if (sandbox != null) {
+ lazy.assert.string(
+ sandbox,
+ `Expected "sandbox" to be a string, got ${sandbox}`
+ );
+ }
+
+ lazy.assert.array(
+ commandArguments,
+ `Expected "arguments" to be an array, got ${commandArguments}`
+ );
+ lazy.assert.that(
+ commandArguments =>
+ commandArguments.every(({ type, value }) => {
+ if (type === "channel") {
+ this.#assertChannelArgument(value);
+ return true;
+ }
+ return false;
+ }),
+ `One of the arguments has an unsupported type, only type "channel" is supported`
+ )(commandArguments);
+
+ const script = lazy.generateUUID();
+ const preloadScript = {
+ arguments: commandArguments,
+ functionDeclaration,
+ sandbox,
+ };
+
+ this.#preloadScriptMap.set(script, preloadScript);
+
+ await this.messageHandler.addSessionDataItem({
+ category: "preload-script",
+ moduleName: "script",
+ values: [
+ {
+ ...preloadScript,
+ script,
+ },
+ ],
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.All,
+ },
+ });
+
+ return { script };
+ }
+
+ /**
+ * 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>=} options.arguments
+ * The arguments to pass to the function call.
+ * @param {boolean} options.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} options.functionDeclaration
+ * The expression to evaluate.
+ * @param {OwnershipModel=} options.resultOwnership
+ * The ownership model to use for the results of this evaluation. Defaults
+ * to `OwnershipModel.None`.
+ * @param {SerializationOptions=} options.serializationOptions
+ * An object which holds the information of how the result of evaluation
+ * in case of ECMAScript objects should be serialized.
+ * @param {object} options.target
+ * The target for the evaluation, which either matches the definition for
+ * a RealmTarget or for ContextTarget.
+ * @param {RemoteValue=} options.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,
+ serializationOptions,
+ 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}`
+ );
+ commandArguments.forEach(({ type, value }) => {
+ if (type === "channel") {
+ this.#assertChannelArgument(value);
+ }
+ });
+ }
+
+ const { contextId, realmId, sandbox } = this.#assertTarget(target);
+ const context = await this.#getContextFromTarget({ contextId, realmId });
+ const serializationOptionsWithDefaults =
+ lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
+ 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,
+ serializationOptions: serializationOptionsWithDefaults,
+ 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>} options.handles
+ * Array of handle ids to disown.
+ * @param {object} options.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} options.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} options.expression
+ * The expression to evaluate.
+ * @param {OwnershipModel=} options.resultOwnership
+ * The ownership model to use for the results of this evaluation. Defaults
+ * to `OwnershipModel.None`.
+ * @param {SerializationOptions=} options.serializationOptions
+ * An object which holds the information of how the result of evaluation
+ * in case of ECMAScript objects should be serialized.
+ * @param {object} options.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,
+ serializationOptions,
+ 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 serializationOptionsWithDefaults =
+ lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
+ 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,
+ serializationOptions: serializationOptionsWithDefaults,
+ },
+ });
+
+ 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.
+ */
+
+ /* eslint-disable jsdoc/valid-types */
+ /**
+ * An object that holds information about a window realm.
+ *
+ * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo
+ */
+ /* eslint-enable jsdoc/valid-types */
+
+ /**
+ * 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=} options.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=} options.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) };
+ }
+
+ /**
+ * Removes a preload script.
+ *
+ * @param {object=} options
+ * @param {string} options.script
+ * The unique id associated with a preload script.
+ *
+ * @throws {InvalidArgumentError}
+ * If any of the arguments does not have the expected type.
+ * @throws {NoSuchScriptError}
+ * If the script cannot be found.
+ */
+ async removePreloadScript(options = {}) {
+ const { script } = options;
+
+ lazy.assert.string(
+ script,
+ `Expected "script" to be a string, got ${script}`
+ );
+
+ if (!this.#preloadScriptMap.has(script)) {
+ throw new lazy.error.NoSuchScriptError(
+ `Preload script with id ${script} not found`
+ );
+ }
+
+ const preloadScript = this.#preloadScriptMap.get(script);
+
+ await this.messageHandler.removeSessionDataItem({
+ category: "preload-script",
+ moduleName: "script",
+ values: [
+ {
+ ...preloadScript,
+ script,
+ },
+ ],
+ contextDescriptor: {
+ type: lazy.ContextDescriptorType.All,
+ },
+ });
+
+ this.#preloadScriptMap.delete(script);
+ }
+
+ #assertChannelArgument(value) {
+ lazy.assert.object(value);
+ const {
+ channel,
+ ownership = lazy.OwnershipModel.None,
+ serializationOptions,
+ } = value;
+ lazy.assert.string(channel);
+ lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
+ lazy.assert.that(
+ ownership =>
+ [lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes(
+ ownership
+ ),
+ `Expected "ownership" to be one of ${Object.values(
+ lazy.OwnershipModel
+ )}, got ${ownership}`
+ )(ownership);
+
+ return true;
+ }
+
+ #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 ["script.message"];
+ }
+}
+
+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..3814ce9437
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/session.sys.mjs
@@ -0,0 +1,405 @@
+/* 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.
+ *
+ * @param {object=} params
+ * @param {Array<string>} params.events
+ * List of events to subscribe to.
+ * @param {Array<string>=} params.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.
+ *
+ * @param {object=} params
+ * @param {Array<string>} params.events
+ * List of events to unsubscribe from.
+ * @param {Array<string>=} params.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.
+ *
+ * @returns {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.
+ *
+ * @returns {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-in-root/script.sys.mjs b/remote/webdriver-bidi/modules/windowglobal-in-root/script.sys.mjs
new file mode 100644
index 0000000000..c894541743
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal-in-root/script.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 ScriptModule extends Module {
+ destroy() {}
+
+ interceptEvent(name, payload) {
+ if (name == "script.message") {
+ // Resolve browsing context to a TabManager id.
+ payload.source.context = lazy.TabManager.getIdForBrowsingContext(
+ payload.source.context
+ );
+ }
+
+ return payload;
+ }
+}
+
+export const script = ScriptModule;
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..675626353c
--- /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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ LoadListener: "chrome://remote/content/shared/listeners/LoadListener.sys.mjs",
+});
+
+class BrowsingContextModule extends WindowGlobalBiDiModule {
+ #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.URL,
+ };
+ }
+
+ #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/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
new file mode 100644
index 0000000000..24259151ee
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
@@ -0,0 +1,103 @@
+/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ action: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
+ deserialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ element: "chrome://remote/content/marionette/element.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+});
+
+class InputModule extends WindowGlobalBiDiModule {
+ #actionState;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ this.#actionState = null;
+ }
+
+ destroy() {}
+
+ async performActions(options) {
+ const { actions } = options;
+ if (this.#actionState === null) {
+ this.#actionState = new lazy.action.State({
+ specCompatPointerOrigin: true,
+ });
+ }
+
+ await this.#deserializeActionOrigins(actions);
+ const actionChain = lazy.action.Chain.fromJSON(this.#actionState, actions);
+ await actionChain.dispatch(this.#actionState, this.messageHandler.window);
+ }
+
+ async releaseActions() {
+ if (this.#actionState === null) {
+ return;
+ }
+ await this.#actionState.release(this.messageHandler.window);
+ this.#actionState = null;
+ }
+
+ /**
+ * In the provided array of input.SourceActions, replace all origins matching
+ * the input.ElementOrigin production with the Element corresponding to this
+ * origin.
+ *
+ * Note that this method replaces the content of the `actions` in place, and
+ * does not return a new array.
+ *
+ * @param {Array<input.SourceActions>} actions
+ * The array of SourceActions to deserialize.
+ * @returns {Promise}
+ * A promise which resolves when all ElementOrigin origins have been
+ * deserialized.
+ */
+ async #deserializeActionOrigins(actions) {
+ const promises = [];
+ for (const actionsByTick of actions) {
+ for (const action of actionsByTick.actions) {
+ if (action.origin?.type === "element") {
+ promises.push(
+ (async () => {
+ action.origin = await this.#getElementFromElementOrigin(
+ action.origin
+ );
+ })()
+ );
+ }
+ }
+ }
+ return Promise.all(promises);
+ }
+
+ async #getElementFromElementOrigin(origin) {
+ const sharedReference = origin.element;
+ if (typeof sharedReference?.sharedId !== "string") {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "origin.element" to be a SharedReference, got: ${sharedReference}`
+ );
+ }
+
+ const realm = this.messageHandler.getRealm();
+
+ const element = lazy.deserialize(realm, sharedReference, {
+ nodeCache: this.nodeCache,
+ });
+ if (!lazy.element.isElement(element)) {
+ throw new lazy.error.NoSuchElementError(
+ `No element found for shared id: ${sharedReference.sharedId}`
+ );
+ }
+
+ return element;
+ }
+}
+
+export const input = InputModule;
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..ad83253804
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/log.sys.mjs
@@ -0,0 +1,258 @@
+/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.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",
+ setDefaultSerializationOptions:
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+});
+
+class LogModule extends WindowGlobalBiDiModule {
+ #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(realm) {
+ return {
+ realm: realm.id,
+ 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 defaultRealm = this.messageHandler.getRealm();
+ const nodeCache = this.nodeCache;
+ const serializedArgs = [];
+ for (const arg of args) {
+ // Note that we can pass a default 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 1742589.
+ serializedArgs.push(
+ lazy.serialize(
+ Cu.waiveXrays(arg),
+ lazy.setDefaultSerializationOptions(),
+ lazy.OwnershipModel.None,
+ new Map(),
+ defaultRealm,
+ {
+ nodeCache,
+ }
+ )
+ );
+ }
+
+ // Set source to an object which contains realm and browsing context.
+ // TODO: Bug 1742589. Use an actual realm from which the event came from.
+ const source = this.#buildSource(defaultRealm);
+
+ // 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;
+ const defaultRealm = this.messageHandler.getRealm();
+
+ // Build the JavascriptLogEntry
+ const entry = {
+ type: "javascript",
+ level,
+ // TODO: Bug 1742589. Use an actual realm from which the event came from.
+ source: this.#buildSource(defaultRealm),
+ 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..7b88f5c96e
--- /dev/null
+++ b/remote/webdriver-bidi/modules/windowglobal/script.sys.mjs
@@ -0,0 +1,483 @@
+/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.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",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ setDefaultSerializationOptions:
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+});
+
+/**
+ * @typedef {string} EvaluationStatus
+ */
+
+/**
+ * Enum of possible evaluation states.
+ *
+ * @readonly
+ * @enum {EvaluationStatus}
+ */
+const EvaluationStatus = {
+ Normal: "normal",
+ Throw: "throw",
+};
+
+class ScriptModule extends WindowGlobalBiDiModule {
+ #observerListening;
+ #preloadScripts;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ // Set of structs with an item named expression, which is a string,
+ // and an item named sandbox which is a string or null.
+ this.#preloadScripts = new Set();
+ }
+
+ destroy() {
+ this.#preloadScripts = null;
+
+ this.#stopObserving();
+ }
+
+ observe(subject, topic) {
+ if (topic !== "document-element-inserted") {
+ return;
+ }
+
+ const window = subject?.defaultView;
+
+ // Ignore events without a window and those from other tabs.
+ if (window === this.messageHandler.window) {
+ this.#evaluatePreloadScripts();
+ }
+ }
+
+ #buildExceptionDetails(exception, stack, realm, resultOwnership, options) {
+ exception = this.#toRawObject(exception);
+
+ // A stacktrace is mandatory to build exception details and a missing stack
+ // means we encountered an unexpected issue. Throw with an explicit error.
+ if (!stack) {
+ throw new Error(
+ `Missing stack, unable to build exceptionDetails for exception: ${lazy.stringify(
+ 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,
+ lazy.setDefaultSerializationOptions(),
+ resultOwnership,
+ new Map(),
+ realm,
+ options
+ ),
+ lineNumber: stack.line - 1,
+ stackTrace: { callFrames },
+ text: lazy.stringify(exception),
+ };
+ }
+
+ async #buildReturnValue(
+ rv,
+ realm,
+ awaitPromise,
+ resultOwnership,
+ serializationOptions,
+ options
+ ) {
+ 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);
+
+ // If the returned promise was rejected by calling its reject callback
+ // the stack will be available on promiseResolutionSite.
+ // Otherwise, (eg. rejected Promise chained with a then() call) we
+ // fallback on the promiseAllocationSite.
+ stack =
+ rv.return.promiseResolutionSite || rv.return.promiseAllocationSite;
+ }
+ } 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),
+ serializationOptions,
+ resultOwnership,
+ new Map(),
+ realm,
+ options
+ ),
+ realmId: realm.id,
+ };
+ case EvaluationStatus.Throw:
+ return {
+ evaluationStatus,
+ exceptionDetails: this.#buildExceptionDetails(
+ exception,
+ stack,
+ realm,
+ resultOwnership,
+ options
+ ),
+ realmId: realm.id,
+ };
+ default:
+ throw new lazy.error.UnsupportedOperationError(
+ `Unsupported completion value for expression evaluation`
+ );
+ }
+ }
+
+ /**
+ * Emit "script.message" event with provided data.
+ *
+ * @param {Realm} realm
+ * @param {ChannelProperties} channelProperties
+ * @param {RemoteValue} message
+ */
+ #emitScriptMessage = (realm, channelProperties, message) => {
+ const {
+ channel,
+ ownership: ownershipType = lazy.OwnershipModel.None,
+ serializationOptions,
+ } = channelProperties;
+
+ const data = lazy.serialize(
+ this.#toRawObject(message),
+ lazy.setDefaultSerializationOptions(serializationOptions),
+ ownershipType,
+ new Map(),
+ realm
+ );
+
+ this.emitEvent("script.message", {
+ channel,
+ data,
+ source: this.#getSource(realm),
+ });
+ };
+
+ #evaluatePreloadScripts() {
+ let resolveBlockerPromise;
+ const blockerPromise = new Promise(resolve => {
+ resolveBlockerPromise = resolve;
+ });
+
+ // Block script parsing.
+ this.messageHandler.window.document.blockParsing(blockerPromise);
+ for (const script of this.#preloadScripts.values()) {
+ const {
+ arguments: commandArguments,
+ functionDeclaration,
+ sandbox,
+ } = script;
+ const realm = this.messageHandler.getRealm({ sandboxName: sandbox });
+ const deserializedArguments = commandArguments.map(arg =>
+ lazy.deserialize(realm, arg, {
+ emitScriptMessage: this.#emitScriptMessage,
+ })
+ );
+ const rv = realm.executeInGlobalWithBindings(
+ functionDeclaration,
+ deserializedArguments
+ );
+
+ if ("throw" in rv) {
+ const exception = this.#toRawObject(rv.throw);
+ realm.reportError(lazy.stringify(exception), rv.stack);
+ }
+ }
+
+ // Continue script parsing.
+ resolveBlockerPromise();
+ }
+
+ #getSource(realm) {
+ return {
+ realm: realm.id,
+ context: this.messageHandler.context,
+ };
+ }
+
+ #startObserving() {
+ if (!this.#observerListening) {
+ Services.obs.addObserver(this, "document-element-inserted");
+ this.#observerListening = true;
+ }
+ }
+
+ #stopObserving() {
+ if (this.#observerListening) {
+ Services.obs.removeObserver(this, "document-element-inserted");
+ this.#observerListening = false;
+ }
+ }
+
+ #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} options.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>=} options.commandArguments
+ * The arguments to pass to the function call.
+ * @param {string} options.functionDeclaration
+ * The body of the function to call.
+ * @param {string=} options.realmId
+ * The id of the realm.
+ * @param {OwnershipModel} options.resultOwnership
+ * The ownership model to use for the results of this evaluation.
+ * @param {string=} options.sandbox
+ * The name of the sandbox.
+ * @param {SerializationOptions=} options.serializationOptions
+ * An object which holds the information of how the result of evaluation
+ * in case of ECMAScript objects should be serialized.
+ * @param {RemoteValue=} options.thisParameter
+ * The value of the this keyword for the function call.
+ *
+ * @returns {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,
+ serializationOptions,
+ thisParameter = null,
+ } = options;
+
+ const realm = this.messageHandler.getRealm({ realmId, sandboxName });
+ const nodeCache = this.nodeCache;
+
+ const deserializedArguments =
+ commandArguments !== null
+ ? commandArguments.map(arg =>
+ lazy.deserialize(realm, arg, {
+ emitScriptMessage: this.#emitScriptMessage,
+ nodeCache,
+ })
+ )
+ : [];
+
+ const deserializedThis =
+ thisParameter !== null
+ ? lazy.deserialize(realm, thisParameter, {
+ emitScriptMessage: this.#emitScriptMessage,
+ nodeCache,
+ })
+ : null;
+
+ const rv = realm.executeInGlobalWithBindings(
+ functionDeclaration,
+ deserializedArguments,
+ deserializedThis
+ );
+
+ return this.#buildReturnValue(
+ rv,
+ realm,
+ awaitPromise,
+ resultOwnership,
+ serializationOptions,
+ {
+ nodeCache,
+ }
+ );
+ }
+
+ /**
+ * Delete the provided handles from the realm corresponding to the provided
+ * sandbox name.
+ *
+ * @param {object=} options
+ * @param {Array<string>} options.handles
+ * Array of handle ids to disown.
+ * @param {string=} options.realmId
+ * The id of the realm.
+ * @param {string=} options.sandbox
+ * The name of the sandbox.
+ */
+ disownHandles(options) {
+ const { handles, realmId = null, sandbox: sandboxName = null } = options;
+ const realm = this.messageHandler.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} options.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} options.expression
+ * The expression to evaluate.
+ * @param {string=} options.realmId
+ * The id of the realm.
+ * @param {OwnershipModel} options.resultOwnership
+ * The ownership model to use for the results of this evaluation.
+ * @param {string=} options.sandbox
+ * The name of the sandbox.
+ *
+ * @returns {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,
+ serializationOptions,
+ } = options;
+
+ const realm = this.messageHandler.getRealm({ realmId, sandboxName });
+
+ const rv = realm.executeInGlobal(expression);
+
+ return this.#buildReturnValue(
+ rv,
+ realm,
+ awaitPromise,
+ resultOwnership,
+ serializationOptions,
+ {
+ nodeCache: this.nodeCache,
+ }
+ );
+ }
+
+ /**
+ * Get realms for the current window global.
+ *
+ * @returns {Array<object>}
+ * - context {BrowsingContext} The browsing context, associated with the realm.
+ * - origin {string} The serialization of an origin.
+ * - realm {string} The realm unique identifier.
+ * - sandbox {string=} The name of the sandbox.
+ * - type {RealmType.Window} The window realm type.
+ */
+ getWindowRealms() {
+ return Array.from(this.messageHandler.realms.values()).map(realm => {
+ const { context, origin, realm: id, sandbox, type } = realm.getInfo();
+ return { context, origin, realm: id, sandbox, type };
+ });
+ }
+
+ /**
+ * Internal commands
+ */
+
+ _applySessionData(params) {
+ // We only care about updates coming on context creation.
+ if (params.category === "preload-script" && params.initial) {
+ this.#preloadScripts = new Set();
+ for (const item of params.sessionData) {
+ if (this.messageHandler.matchesContext(item.contextDescriptor)) {
+ this.#preloadScripts.add(item.value);
+ }
+ }
+
+ if (this.#preloadScripts.size) {
+ this.#startObserving();
+ }
+ }
+ }
+}
+
+export const script = ScriptModule;
diff --git a/remote/webdriver-bidi/moz.build b/remote/webdriver-bidi/moz.build
new file mode 100644
index 0000000000..d55d16226a
--- /dev/null
+++ b/remote/webdriver-bidi/moz.build
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Remote Protocol", "WebDriver BiDi")
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
diff --git a/remote/webdriver-bidi/test/browser/browser.ini b/remote/webdriver-bidi/test/browser/browser.ini
new file mode 100644
index 0000000000..6151892b49
--- /dev/null
+++ b/remote/webdriver-bidi/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+tags = wd
+subsuite = remote
+
+[browser_RemoteValue.js]
diff --git a/remote/webdriver-bidi/test/browser/browser_RemoteValue.js b/remote/webdriver-bidi/test/browser/browser_RemoteValue.js
new file mode 100644
index 0000000000..fbff51d000
--- /dev/null
+++ b/remote/webdriver-bidi/test/browser/browser_RemoteValue.js
@@ -0,0 +1,1633 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { NodeCache } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
+);
+const { Realm, WindowRealm } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Realm.sys.mjs"
+);
+const { deserialize, serialize, setDefaultSerializationOptions, stringify } =
+ ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs"
+ );
+
+const browser = Services.appShell.createWindowlessBrowser(false);
+const bodyEl = browser.document.body;
+const domEl = browser.document.createElement("div");
+bodyEl.appendChild(domEl);
+const iframeEl = browser.document.createElement("iframe");
+bodyEl.appendChild(iframeEl);
+
+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: [{ type: "number", value: 1 }],
+ },
+ },
+ {
+ value: [1],
+ serializationOptions: {
+ maxObjectDepth: 0,
+ },
+ serialized: {
+ type: "array",
+ },
+ },
+ {
+ value: [1, "2", true, new RegExp(/foo/g)],
+ serializationOptions: {
+ maxObjectDepth: 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"]],
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "array",
+ value: [{ type: "number", value: 1 }, { type: "array" }],
+ },
+ },
+ {
+ value: [1, [3, "4"]],
+ serializationOptions: {
+ maxObjectDepth: 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(),
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "map",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Map([]),
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "map",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ 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 },
+ ],
+ ],
+ },
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ serializationOptions: {
+ maxObjectDepth: 0,
+ },
+ serialized: {
+ type: "map",
+ },
+ },
+ {
+ value: new Map([
+ [1, 2],
+ ["2", "3"],
+ [true, false],
+ ]),
+ serializationOptions: {
+ maxObjectDepth: 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(),
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "set",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Set([]),
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "set",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: new Set([1, "2", true]),
+ serialized: {
+ type: "set",
+ value: [
+ { type: "number", value: 1 },
+ { type: "string", value: "2" },
+ { type: "boolean", value: true },
+ ],
+ },
+ },
+ {
+ value: new Set([1, "2", true]),
+ serializationOptions: {
+ maxObjectDepth: 0,
+ },
+ serialized: {
+ type: "set",
+ },
+ },
+ {
+ value: new Set([1, "2", true]),
+ serializationOptions: {
+ maxObjectDepth: 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: [
+ {
+ type: "node",
+ value: {
+ nodeType: 1,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ ],
+ },
+ },
+ {
+ value: browser.document.getElementsByTagName("div"),
+ serialized: {
+ type: "htmlcollection",
+ value: [
+ {
+ type: "node",
+ value: {
+ nodeType: 1,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ ],
+ },
+ },
+ {
+ value: domEl,
+ serialized: {
+ type: "node",
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ },
+ { 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: {},
+ serializationOptions: {
+ maxObjectDepth: 1,
+ },
+ serialized: {
+ type: "object",
+ value: [],
+ },
+ deserializable: true,
+ },
+ {
+ value: {
+ 1: 1,
+ 2: "2",
+ foo: true,
+ },
+ serialized: {
+ type: "object",
+ value: [
+ ["1", { type: "number", value: 1 }],
+ ["2", { type: "string", value: "2" }],
+ ["foo", { type: "boolean", value: true }],
+ ],
+ },
+ },
+ {
+ value: {
+ 1: 1,
+ 2: "2",
+ foo: true,
+ },
+ serializationOptions: {
+ maxObjectDepth: 0,
+ },
+ serialized: {
+ type: "object",
+ },
+ },
+ {
+ value: {
+ 1: 1,
+ 2: "2",
+ foo: true,
+ },
+ serializationOptions: {
+ maxObjectDepth: 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,
+ },
+ serializationOptions: {
+ maxObjectDepth: 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,
+ },
+];
+
+add_task(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}`
+ );
+ }
+ }
+});
+
+add_task(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}`
+ );
+ }
+});
+
+add_task(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);
+ }
+});
+
+add_task(async function test_deserializeLocalValuesInWindowRealm() {
+ for (const type of REMOTE_SIMPLE_VALUES.concat(REMOTE_COMPLEX_VALUES)) {
+ const { value: expectedValue, serialized, deserializable } = type;
+
+ // Skip non deserializable cases
+ if (!deserializable) {
+ continue;
+ }
+
+ const value = await deserializeInWindowRealm(serialized);
+ assertLocalValue(serialized.type, value, expectedValue);
+ }
+});
+
+add_task(async function test_deserializeChannel() {
+ const realm = new Realm();
+ const channel = {
+ type: "channel",
+ value: { channel: "channel_name" },
+ };
+ const deserializationOptions = {
+ emitScriptMessage: (realm, channelProperties, message) => message,
+ };
+
+ info(`Checking 'channel'`);
+ const deserializedValue = deserialize(realm, channel, deserializationOptions);
+ Assert.equal(
+ Object.prototype.toString.call(deserializedValue),
+ "[object Function]",
+ "Got expected type Function"
+ );
+ Assert.equal(deserializedValue("foo"), "foo", "Got expected result");
+});
+
+add_task(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,
+ { maxObjectDepth: 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),
+ /NoSuchHandleError:/,
+ `Got expected error when using the wrong realm for deserialize`
+ );
+
+ realm1.removeObjectHandle(serializedValue.handle);
+ Assert.throws(
+ () => deserialize(realm1, remoteReference),
+ /NoSuchHandleError:/,
+ `Got expected error when after deleting the object handle`
+ );
+ }
+});
+
+add_task(function test_deserializeHandleInvalidTypes() {
+ const realm = new Realm();
+
+ for (const invalidType of [false, 42, {}, []]) {
+ info(`Checking type: '${invalidType}'`);
+
+ Assert.throws(
+ () => deserialize(realm, { type: "object", handle: invalidType }),
+ /InvalidArgumentError:/,
+ `Got expected error for type ${invalidType}`
+ );
+ }
+});
+
+add_task(function test_deserializeSharedIdInvalidTypes() {
+ const nodeCache = new NodeCache();
+
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ for (const invalidType of [false, 42, {}, []]) {
+ info(`Checking type: '${invalidType}'`);
+
+ const serializedValue = {
+ sharedId: invalidType,
+ };
+
+ Assert.throws(
+ () => deserialize(realm, serializedValue, { nodeCache }),
+ /InvalidArgumentError:/,
+ `Got expected error for type ${invalidType}`
+ );
+ }
+});
+
+add_task(function test_deserializeSharedIdInvalidValue() {
+ const nodeCache = new NodeCache();
+
+ const serializedValue = {
+ sharedId: "foo",
+ };
+
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ Assert.throws(
+ () => deserialize(realm, serializedValue, { nodeCache }),
+ /NoSuchNodeError:/,
+ "Got expected error for unknown 'sharedId'"
+ );
+});
+
+add_task(function test_deserializeSharedId() {
+ const nodeCache = new NodeCache();
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+
+ const serializedValue = {
+ sharedId: domElRef,
+ };
+
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ const node = deserialize(realm, serializedValue, { nodeCache });
+
+ Assert.equal(node, domEl);
+});
+
+add_task(function test_deserializeSharedIdPrecedenceOverHandle() {
+ const nodeCache = new NodeCache();
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+
+ const serializedValue = {
+ handle: "foo",
+ sharedId: domElRef,
+ };
+
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ const node = deserialize(realm, serializedValue, { nodeCache });
+
+ Assert.equal(node, domEl);
+});
+
+add_task(function test_deserializeSharedIdNoWindowRealm() {
+ const nodeCache = new NodeCache();
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+
+ const serializedValue = {
+ sharedId: domElRef,
+ };
+
+ const realm = new Realm();
+
+ Assert.throws(
+ () => deserialize(realm, serializedValue, { nodeCache }),
+ /NoSuchNodeError/,
+ `Got expected error for a non-window realm`
+ );
+});
+
+// Bug 1819902: Instead of a browsing context check compare the origin
+add_task(function test_deserializeSharedIdOtherBrowsingContext() {
+ const nodeCache = new NodeCache();
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+
+ const serializedValue = {
+ sharedId: domElRef,
+ };
+
+ const realm = new WindowRealm(iframeEl.contentWindow);
+
+ const node = deserialize(realm, serializedValue, { nodeCache });
+
+ Assert.equal(node, null);
+});
+
+add_task(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}`
+ );
+ }
+ }
+});
+
+add_task(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}`
+ );
+ }
+});
+
+add_task(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}`
+ );
+ }
+});
+
+add_task(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}`
+ );
+ }
+ }
+});
+
+add_task(function test_serializePrimitiveTypes() {
+ const realm = new Realm();
+
+ for (const type of PRIMITIVE_TYPES) {
+ const { value, serialized } = type;
+ const defaultSerializationOptions = setDefaultSerializationOptions();
+
+ const serializationInternalMap = new Map();
+ const serializedValue = serialize(
+ value,
+ defaultSerializationOptions,
+ "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,
+ defaultSerializationOptions,
+ "root",
+ serializationInternalMapWithRoot,
+ realm
+ );
+ assertInternalIds(serializationInternalMapWithRoot, 0);
+ Assert.deepEqual(serialized, serializedWithRoot, "Got expected structure");
+ }
+});
+
+add_task(function test_serializeRemoteSimpleValues() {
+ const realm = new Realm();
+
+ for (const type of REMOTE_SIMPLE_VALUES) {
+ const { value, serialized } = type;
+ const defaultSerializationOptions = setDefaultSerializationOptions();
+
+ info(`Checking '${serialized.type}' with none ownershipType`);
+ const serializationInternalMapWithNone = new Map();
+ const serializedValue = serialize(
+ value,
+ defaultSerializationOptions,
+ "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,
+ defaultSerializationOptions,
+ "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"
+ );
+ }
+});
+
+add_task(function test_serializeRemoteComplexValues() {
+ const realm = new Realm();
+
+ for (const type of REMOTE_COMPLEX_VALUES) {
+ const { value, serialized, serializationOptions } = type;
+ const serializationOptionsWithDefaults =
+ setDefaultSerializationOptions(serializationOptions);
+
+ info(`Checking '${serialized.type}' with none ownershipType`);
+ const serializationInternalMapWithNone = new Map();
+ const serializedValue = serialize(
+ value,
+ serializationOptionsWithDefaults,
+ "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,
+ serializationOptionsWithDefaults,
+ "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"
+ );
+ }
+});
+
+add_task(function test_serializeNodeChildren() {
+ const nodeCache = new NodeCache();
+ // Add the used elements to the cache so that we know the unique reference.
+ const bodyElRef = nodeCache.getOrCreateNodeReference(bodyEl);
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+ const iframeElRef = nodeCache.getOrCreateNodeReference(iframeEl);
+
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ const dataSet = [
+ {
+ node: bodyEl,
+ serializationOptions: {
+ maxDomDepth: null,
+ },
+ serialized: {
+ type: "node",
+ sharedId: bodyElRef,
+ value: {
+ nodeType: 1,
+ localName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 2,
+ children: [
+ {
+ type: "node",
+ sharedId: domElRef,
+ value: {
+ nodeType: 1,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ children: [],
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ {
+ type: "node",
+ sharedId: iframeElRef,
+ value: {
+ nodeType: 1,
+ localName: "iframe",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ children: [],
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ ],
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ },
+ {
+ node: bodyEl,
+ serializationOptions: {
+ maxDomDepth: 0,
+ },
+ serialized: {
+ type: "node",
+ sharedId: bodyElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 2,
+ localName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ },
+ {
+ node: bodyEl,
+ serializationOptions: {
+ maxDomDepth: 1,
+ },
+ serialized: {
+ type: "node",
+ sharedId: bodyElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 2,
+ children: [
+ {
+ type: "node",
+ sharedId: domElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ {
+ type: "node",
+ sharedId: iframeElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ localName: "iframe",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ ],
+ localName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ },
+ {
+ node: domEl,
+ serializationOptions: {
+ maxDomDepth: 0,
+ },
+ serialized: {
+ type: "node",
+ sharedId: domElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ },
+ {
+ node: domEl,
+ serializationOptions: {
+ maxDomDepth: 1,
+ },
+ serialized: {
+ type: "node",
+ sharedId: domElRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ children: [],
+ localName: "div",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: null,
+ },
+ },
+ },
+ ];
+
+ for (const { node, serializationOptions, serialized } of dataSet) {
+ const { maxDomDepth } = serializationOptions;
+ info(`Checking '${node.localName}' with maxDomDepth ${maxDomDepth}`);
+
+ const serializationInternalMap = new Map();
+
+ const serializedValue = serialize(
+ node,
+ serializationOptions,
+ "none",
+ serializationInternalMap,
+ realm,
+ { nodeCache }
+ );
+
+ Assert.deepEqual(serializedValue, serialized, "Got expected structure");
+ }
+});
+
+add_task(function test_serializeShadowRoot() {
+ const nodeCache = new NodeCache();
+ const realm = new WindowRealm(browser.document.defaultView);
+
+ for (const mode of ["open", "closed"]) {
+ info(`Checking shadow root with mode '${mode}'`);
+ const customElement = browser.document.createElement(
+ `${mode}-custom-element`
+ );
+ const insideShadowRootElement = browser.document.createElement("input");
+ bodyEl.appendChild(customElement);
+ const shadowRoot = customElement.attachShadow({ mode });
+ shadowRoot.appendChild(insideShadowRootElement);
+
+ // Add the used elements to the cache so that we know the unique reference.
+ const customElementRef = nodeCache.getOrCreateNodeReference(customElement);
+ const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
+ const insideShadowRootElementRef = nodeCache.getOrCreateNodeReference(
+ insideShadowRootElement
+ );
+
+ const dataSet = [
+ {
+ node: customElement,
+ serializationOptions: {
+ maxDomDepth: 1,
+ },
+ serialized: {
+ type: "node",
+ sharedId: customElementRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ children: [],
+ localName: `${mode}-custom-element`,
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: {
+ sharedId: shadowRootRef,
+ type: "node",
+ value: {
+ childNodeCount: 1,
+ mode,
+ nodeType: 11,
+ },
+ },
+ },
+ },
+ },
+ {
+ node: customElement,
+ serializationOptions: {
+ includeShadowTree: "open",
+ maxDomDepth: 1,
+ },
+ serialized: {
+ type: "node",
+ sharedId: customElementRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ children: [],
+ localName: `${mode}-custom-element`,
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: {
+ sharedId: shadowRootRef,
+ type: "node",
+ value: {
+ childNodeCount: 1,
+ mode,
+ nodeType: 11,
+ ...(mode === "open"
+ ? {
+ children: [
+ {
+ type: "node",
+ sharedId: insideShadowRootElementRef,
+ value: {
+ nodeType: 1,
+ localName: "input",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ ],
+ }
+ : {}),
+ },
+ },
+ },
+ },
+ },
+ {
+ node: customElement,
+ serializationOptions: {
+ includeShadowTree: "all",
+ maxDomDepth: 1,
+ },
+ serialized: {
+ type: "node",
+ sharedId: customElementRef,
+ value: {
+ attributes: {},
+ childNodeCount: 0,
+ children: [],
+ localName: `${mode}-custom-element`,
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ nodeType: 1,
+ shadowRoot: {
+ sharedId: shadowRootRef,
+ type: "node",
+ value: {
+ childNodeCount: 1,
+ mode,
+ nodeType: 11,
+ children: [
+ {
+ type: "node",
+ sharedId: insideShadowRootElementRef,
+ value: {
+ nodeType: 1,
+ localName: "input",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ childNodeCount: 0,
+ attributes: {},
+ shadowRoot: null,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ for (const { node, serializationOptions, serialized } of dataSet) {
+ const { maxDomDepth, includeShadowTree } = serializationOptions;
+ info(
+ `Checking shadow root with maxDomDepth ${maxDomDepth} and includeShadowTree ${includeShadowTree}`
+ );
+
+ const serializationInternalMap = new Map();
+
+ const serializedValue = serialize(
+ node,
+ serializationOptions,
+ "none",
+ serializationInternalMap,
+ realm,
+ { nodeCache }
+ );
+
+ Assert.deepEqual(serializedValue, serialized, "Got expected structure");
+ }
+ }
+});
+
+add_task(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,
+ { maxObjectDepth: 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");
+ }
+});
+
+add_task(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,
+ { maxObjectDepth: 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"
+ );
+});
+
+add_task(function test_serializeNodeSharedId() {
+ const nodeCache = new NodeCache();
+ // Already add the domEl to the cache so that we know the unique reference.
+ const domElRef = nodeCache.getOrCreateNodeReference(domEl);
+
+ const realm = new WindowRealm(browser.document.defaultView);
+ const serializationInternalMap = new Map();
+
+ const serializedValue = serialize(
+ domEl,
+ { maxDomDepth: 0 },
+ "root",
+ serializationInternalMap,
+ realm,
+ { nodeCache }
+ );
+
+ Assert.equal(nodeCache.size, 1, "No additional reference added");
+ Assert.equal(serializedValue.sharedId, domElRef);
+ Assert.notEqual(serializedValue.handle, domElRef);
+});
+
+add_task(function test_serializeNodeSharedId_noWindowRealm() {
+ const nodeCache = new NodeCache();
+ nodeCache.getOrCreateNodeReference(domEl);
+
+ const realm = new Realm();
+ const serializationInternalMap = new Map();
+
+ const serializedValue = serialize(
+ domEl,
+ { maxDomDepth: 0 },
+ "none",
+ serializationInternalMap,
+ realm,
+ { nodeCache }
+ );
+
+ Assert.equal(nodeCache.size, 1, "No additional reference added");
+ Assert.equal(serializedValue.sharedId, undefined);
+});
+
+add_task(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");
+ }
+});
+
+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"
+ );
+}
+
+function deserializeInWindowRealm(serialized) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [serialized],
+ async _serialized => {
+ const { WindowRealm } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Realm.sys.mjs"
+ );
+ const { deserialize } = ChromeUtils.importESModule(
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs"
+ );
+ const realm = new WindowRealm(content);
+ info(`Checking '${_serialized.type}'`);
+ return deserialize(realm, _serialized);
+ }
+ );
+}
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..e10c77caf3
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/test_WebDriverBiDiConnection.js
@@ -0,0 +1,25 @@
+/* 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_task(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",
+ });
+});
diff --git a/remote/webdriver-bidi/test/xpcshell/xpcshell.ini b/remote/webdriver-bidi/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..5675e62af4
--- /dev/null
+++ b/remote/webdriver-bidi/test/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+# 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_WebDriverBiDiConnection.js]