summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi/modules
diff options
context:
space:
mode:
Diffstat (limited to 'remote/webdriver-bidi/modules')
-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
15 files changed, 3548 insertions, 0 deletions
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;