summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/webdriver-bidi/modules/root/browsingContext.sys.mjs')
-rw-r--r--remote/webdriver-bidi/modules/root/browsingContext.sys.mjs1964
1 files changed, 1964 insertions, 0 deletions
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..bc600e89cd
--- /dev/null
+++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs
@@ -0,0 +1,1964 @@
+/* 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, {
+ 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",
+ EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
+ getTimeoutMultiplier: "chrome://remote/content/shared/AppInfo.sys.mjs",
+ modal: "chrome://remote/content/shared/Prompt.sys.mjs",
+ registerNavigationId:
+ "chrome://remote/content/shared/NavigationManager.sys.mjs",
+ NavigationListener:
+ "chrome://remote/content/shared/listeners/NavigationListener.sys.mjs",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ PollPromise: "chrome://remote/content/shared/Sync.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",
+ PromptListener:
+ "chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
+ setDefaultAndAssertSerializationOptions:
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ UserContextManager:
+ "chrome://remote/content/shared/UserContextManager.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",
+});
+
+// Maximal window dimension allowed when emulating a viewport.
+const MAX_WINDOW_SIZE = 10000000;
+
+/**
+ * @typedef {string} ClipRectangleType
+ */
+
+/**
+ * Enum of possible clip rectangle types supported by the
+ * browsingContext.captureScreenshot command.
+ *
+ * @readonly
+ * @enum {ClipRectangleType}
+ */
+export const ClipRectangleType = {
+ Box: "box",
+ Element: "element",
+};
+
+/**
+ * @typedef {object} CreateType
+ */
+
+/**
+ * Enum of types supported by the browsingContext.create command.
+ *
+ * @readonly
+ * @enum {CreateType}
+ */
+const CreateType = {
+ tab: "tab",
+ window: "window",
+};
+
+/**
+ * @typedef {string} LocatorType
+ */
+
+/**
+ * Enum of types supported by the browsingContext.locateNodes command.
+ *
+ * @readonly
+ * @enum {LocatorType}
+ */
+export const LocatorType = {
+ css: "css",
+ innerText: "innerText",
+ xpath: "xpath",
+};
+
+/**
+ * @typedef {string} OriginType
+ */
+
+/**
+ * Enum of origin type supported by the
+ * browsingContext.captureScreenshot command.
+ *
+ * @readonly
+ * @enum {OriginType}
+ */
+export const OriginType = {
+ document: "document",
+ viewport: "viewport",
+};
+
+const TIMEOUT_SET_HISTORY_INDEX = 1000;
+
+/**
+ * Enum of user prompt types supported by the browsingContext.handleUserPrompt
+ * command, these types can be retrieved from `dialog.args.promptType`.
+ *
+ * @readonly
+ * @enum {UserPromptType}
+ */
+const UserPromptType = {
+ alert: "alert",
+ confirm: "confirm",
+ prompt: "prompt",
+ beforeunload: "beforeunload",
+};
+
+/**
+ * An object that contains details of a viewport.
+ *
+ * @typedef {object} Viewport
+ *
+ * @property {number} height
+ * The height of the viewport.
+ * @property {number} width
+ * The width of the viewport.
+ */
+
+/**
+ * @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;
+ #navigationListener;
+ #promptListener;
+ #subscribedEvents;
+
+ /**
+ * Create a new module instance.
+ *
+ * @param {MessageHandler} messageHandler
+ * The MessageHandler instance which owns this Module instance.
+ */
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ this.#contextListener = new lazy.BrowsingContextListener();
+ this.#contextListener.on("attached", this.#onContextAttached);
+ this.#contextListener.on("discarded", this.#onContextDiscarded);
+
+ // Create the navigation listener and listen to "navigation-started" and
+ // "location-changed" events.
+ this.#navigationListener = new lazy.NavigationListener(
+ this.messageHandler.navigationManager
+ );
+ this.#navigationListener.on("location-changed", this.#onLocationChanged);
+ this.#navigationListener.on(
+ "navigation-started",
+ this.#onNavigationStarted
+ );
+
+ // Create the prompt listener and listen to "closed" and "opened" events.
+ this.#promptListener = new lazy.PromptListener();
+ this.#promptListener.on("closed", this.#onPromptClosed);
+ this.#promptListener.on("opened", this.#onPromptOpened);
+
+ // Set of event names which have active subscriptions.
+ this.#subscribedEvents = new Set();
+
+ // Treat the event of moving a page to BFCache as context discarded event for iframes.
+ this.messageHandler.on("windowglobal-pagehide", this.#onPageHideEvent);
+ }
+
+ destroy() {
+ this.#contextListener.off("attached", this.#onContextAttached);
+ this.#contextListener.off("discarded", this.#onContextDiscarded);
+ this.#contextListener.destroy();
+
+ this.#promptListener.off("closed", this.#onPromptClosed);
+ this.#promptListener.off("opened", this.#onPromptOpened);
+ this.#promptListener.destroy();
+
+ this.#subscribedEvents = null;
+
+ this.messageHandler.off("windowglobal-pagehide", this.#onPageHideEvent);
+ }
+
+ /**
+ * Activates and focuses the given top-level browsing context.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async activate(options = {}) {
+ const { context: contextId } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+ const context = this.#getBrowsingContext(contextId);
+
+ if (context.parent) {
+ throw new lazy.error.InvalidArgumentError(
+ `Browsing Context with id ${contextId} is not top-level`
+ );
+ }
+
+ const tab = lazy.TabManager.getTabForBrowsingContext(context);
+ const window = lazy.TabManager.getWindowForTab(tab);
+
+ await lazy.windowManager.focusWindow(window);
+ await lazy.TabManager.selectTab(tab);
+ }
+
+ /**
+ * Used as an argument for browsingContext.captureScreenshot command, as one of the available variants
+ * {BoxClipRectangle} or {ElementClipRectangle}, to represent a target of the command.
+ *
+ * @typedef ClipRectangle
+ */
+
+ /**
+ * Used as an argument for browsingContext.captureScreenshot command
+ * to represent a box which is going to be a target of the command.
+ *
+ * @typedef BoxClipRectangle
+ *
+ * @property {ClipRectangleType} [type=ClipRectangleType.Box]
+ * @property {number} x
+ * @property {number} y
+ * @property {number} width
+ * @property {number} height
+ */
+
+ /**
+ * Used as an argument for browsingContext.captureScreenshot command
+ * to represent an element which is going to be a target of the command.
+ *
+ * @typedef ElementClipRectangle
+ *
+ * @property {ClipRectangleType} [type=ClipRectangleType.Element]
+ * @property {SharedReference} element
+ */
+
+ /**
+ * Capture a base64-encoded screenshot of the provided browsing context.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context to screenshot.
+ * @param {ClipRectangle=} options.clip
+ * A box or an element of which a screenshot should be taken.
+ * If not present, take a screenshot of the whole viewport.
+ * @param {OriginType=} options.origin
+ *
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ */
+ async captureScreenshot(options = {}) {
+ const {
+ clip = null,
+ context: contextId,
+ origin = OriginType.viewport,
+ } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+ const context = this.#getBrowsingContext(contextId);
+
+ const originTypeValues = Object.values(OriginType);
+ lazy.assert.that(
+ value => originTypeValues.includes(value),
+ `Expected "origin" to be one of ${originTypeValues}, got ${origin}`
+ )(origin);
+
+ if (clip !== null) {
+ lazy.assert.object(clip, `Expected "clip" to be a object, got ${clip}`);
+
+ const { type } = clip;
+ switch (type) {
+ case ClipRectangleType.Box: {
+ const { x, y, width, height } = clip;
+
+ lazy.assert.number(x, `Expected "x" to be a number, got ${x}`);
+ lazy.assert.number(y, `Expected "y" to be a number, got ${y}`);
+ lazy.assert.number(
+ width,
+ `Expected "width" to be a number, got ${width}`
+ );
+ lazy.assert.number(
+ height,
+ `Expected "height" to be a number, got ${height}`
+ );
+
+ break;
+ }
+
+ case ClipRectangleType.Element: {
+ const { element } = clip;
+
+ lazy.assert.object(
+ element,
+ `Expected "element" to be an object, got ${element}`
+ );
+
+ break;
+ }
+
+ default:
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "type" to be one of ${Object.values(
+ ClipRectangleType
+ )}, got ${type}`
+ );
+ }
+ }
+
+ const rect = await this.messageHandler.handleCommand({
+ moduleName: "browsingContext",
+ commandName: "_getScreenshotRect",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ clip,
+ origin,
+ },
+ retryOnAbort: true,
+ });
+
+ if (rect.width === 0 || rect.height === 0) {
+ throw new lazy.error.UnableToCaptureScreen(
+ `The dimensions of requested screenshot are incorrect, got width: ${rect.width} and height: ${rect.height}.`
+ );
+ }
+
+ 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 very last tab is currently unspecified.
+ // As such behave like Marionette and don't allow closing it.
+ // See: https://github.com/w3c/webdriver-bidi/issues/187
+ return;
+ }
+
+ 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 {boolean=} options.background
+ * Whether the tab/window should be open in the background. Defaults to false,
+ * which means that the tab/window will be open in the foreground.
+ * @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.
+ * @param {string=} options.userContext
+ * The id of the user context which should own the browsing context.
+ * Defaults to the default user context.
+ *
+ * @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 {
+ background = false,
+ referenceContext: referenceContextId = null,
+ type: typeHint,
+ userContext: userContextId = null,
+ } = options;
+
+ if (![CreateType.tab, CreateType.window].includes(typeHint)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "type" to be one of ${Object.values(
+ CreateType
+ )}, got ${typeHint}`
+ );
+ }
+
+ lazy.assert.boolean(
+ background,
+ lazy.pprint`Expected "background" to be a boolean, got ${background}`
+ );
+
+ let referenceContext = null;
+ if (referenceContextId !== null) {
+ lazy.assert.string(
+ referenceContextId,
+ lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}`
+ );
+
+ referenceContext =
+ lazy.TabManager.getBrowsingContextById(referenceContextId);
+ if (!referenceContext) {
+ throw new lazy.error.NoSuchFrameError(
+ `Browsing Context with id ${referenceContextId} not found`
+ );
+ }
+
+ if (referenceContext.parent) {
+ throw new lazy.error.InvalidArgumentError(
+ `referenceContext with id ${referenceContextId} is not a top-level browsing context`
+ );
+ }
+ }
+
+ let userContext = lazy.UserContextManager.defaultUserContextId;
+ if (referenceContext !== null) {
+ userContext =
+ lazy.UserContextManager.getIdByBrowsingContext(referenceContext);
+ }
+
+ if (userContextId !== null) {
+ lazy.assert.string(
+ userContextId,
+ lazy.pprint`Expected "userContext" to be a string, got ${userContextId}`
+ );
+
+ if (!lazy.UserContextManager.hasUserContextId(userContextId)) {
+ throw new lazy.error.NoSuchUserContextError(
+ `User Context with id ${userContextId} was not found`
+ );
+ }
+
+ userContext = userContextId;
+
+ if (
+ lazy.AppInfo.isAndroid &&
+ userContext != lazy.UserContextManager.defaultUserContextId
+ ) {
+ throw new lazy.error.UnsupportedOperationError(
+ `browsingContext.create with non-default "userContext" not supported for ${lazy.AppInfo.name}`
+ );
+ }
+ }
+
+ let browser;
+
+ // Since each tab in GeckoView has its own Gecko instance running,
+ // which means also its own window object, for Android we will need to focus
+ // a previously focused window in case of opening the tab in the background.
+ const previousWindow = Services.wm.getMostRecentBrowserWindow();
+ const previousTab =
+ lazy.TabManager.getTabBrowser(previousWindow).selectedTab;
+
+ // On Android there is only a single window allowed. As such fallback to
+ // open a new tab instead.
+ const type = lazy.AppInfo.isAndroid ? "tab" : typeHint;
+
+ switch (type) {
+ case "window":
+ const newWindow = await lazy.windowManager.openBrowserWindow({
+ focus: !background,
+ userContextId: userContext,
+ });
+ 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 (referenceContext !== null) {
+ referenceTab =
+ lazy.TabManager.getTabForBrowsingContext(referenceContext);
+ }
+
+ const tab = await lazy.TabManager.addTab({
+ focus: !background,
+ referenceTab,
+ userContextId: userContext,
+ });
+ browser = lazy.TabManager.getBrowserForTab(tab);
+ }
+
+ await lazy.waitForInitialNavigationCompleted(
+ browser.browsingContext.webProgress,
+ {
+ unloadTimeout: 5000,
+ }
+ );
+
+ // The tab on Android is always opened in the foreground,
+ // so we need to select the previous tab,
+ // and we have to wait until is fully loaded.
+ // TODO: Bug 1845559. This workaround can be removed,
+ // when the API to create a tab for Android supports the background option.
+ if (lazy.AppInfo.isAndroid && background) {
+ await lazy.windowManager.focusWindow(previousWindow);
+ await lazy.TabManager.selectTab(previousTab);
+ }
+
+ // Force a reflow by accessing `clientHeight` (see Bug 1847044).
+ browser.parentElement.clientHeight;
+
+ 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 {string} userContext
+ * The id of the user context owning this browsing context.
+ * @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 };
+ }
+
+ /**
+ * Closes an open prompt.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ * @param {boolean=} options.accept
+ * Whether user prompt should be accepted or dismissed.
+ * Defaults to true.
+ * @param {string=} options.userText
+ * Input to the user prompt's value field.
+ * Defaults to an empty string.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchAlertError}
+ * If there is no current user prompt.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ * @throws {UnsupportedOperationError}
+ * Raised when the command is called for "beforeunload" prompt.
+ */
+ async handleUserPrompt(options = {}) {
+ const { accept = true, context: contextId, userText = "" } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = this.#getBrowsingContext(contextId);
+
+ lazy.assert.boolean(
+ accept,
+ `Expected "accept" to be a boolean, got ${accept}`
+ );
+
+ lazy.assert.string(
+ userText,
+ `Expected "userText" to be a string, got ${userText}`
+ );
+
+ const tab = lazy.TabManager.getTabForBrowsingContext(context);
+ const browser = lazy.TabManager.getBrowserForTab(tab);
+ const window = lazy.TabManager.getWindowForTab(tab);
+ const dialog = lazy.modal.findPrompt({
+ window,
+ contentBrowser: browser,
+ });
+
+ const closePrompt = async callback => {
+ const dialogClosed = new lazy.EventPromise(
+ window,
+ "DOMModalDialogClosed"
+ );
+ callback();
+ await dialogClosed;
+ };
+
+ if (dialog && dialog.isOpen) {
+ switch (dialog.promptType) {
+ case UserPromptType.alert: {
+ await closePrompt(() => dialog.accept());
+ return;
+ }
+ case UserPromptType.confirm: {
+ await closePrompt(() => {
+ if (accept) {
+ dialog.accept();
+ } else {
+ dialog.dismiss();
+ }
+ });
+
+ return;
+ }
+ case UserPromptType.prompt: {
+ await closePrompt(() => {
+ if (accept) {
+ dialog.text = userText;
+ dialog.accept();
+ } else {
+ dialog.dismiss();
+ }
+ });
+
+ return;
+ }
+ case UserPromptType.beforeunload: {
+ // TODO: Bug 1824220. Implement support for "beforeunload" prompts.
+ throw new lazy.error.UnsupportedOperationError(
+ '"beforeunload" prompts are not supported yet.'
+ );
+ }
+ }
+ }
+
+ throw new lazy.error.NoSuchAlertError();
+ }
+
+ /**
+ * Used as an argument for browsingContext.locateNodes command, as one of the available variants
+ * {CssLocator}, {InnerTextLocator} or {XPathLocator}, to represent a way of how lookup of nodes
+ * is going to be performed.
+ *
+ * @typedef Locator
+ */
+
+ /**
+ * Used as an argument for browsingContext.locateNodes command
+ * to represent a lookup by css selector.
+ *
+ * @typedef CssLocator
+ *
+ * @property {LocatorType} [type=LocatorType.css]
+ * @property {string} value
+ */
+
+ /**
+ * Used as an argument for browsingContext.locateNodes command
+ * to represent a lookup by inner text.
+ *
+ * @typedef InnerTextLocator
+ *
+ * @property {LocatorType} [type=LocatorType.innerText]
+ * @property {string} value
+ * @property {boolean=} ignoreCase
+ * @property {("full"|"partial")=} matchType
+ * @property {number=} maxDepth
+ */
+
+ /**
+ * Used as an argument for browsingContext.locateNodes command
+ * to represent a lookup by xpath.
+ *
+ * @typedef XPathLocator
+ *
+ * @property {LocatorType} [type=LocatorType.xpath]
+ * @property {string} value
+ */
+
+ /**
+ * Returns a list of all nodes matching
+ * the specified locator.
+ *
+ * @param {object} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ * @param {Locator} options.locator
+ * The type of lookup which is going to be used.
+ * @param {number=} options.maxNodeCount
+ * The maximum amount of nodes which is going to be returned.
+ * Defaults to return all the found nodes.
+ * @param {OwnershipModel=} options.ownership
+ * The ownership model to use for the serialization
+ * of the DOM nodes. Defaults to `OwnershipModel.None`.
+ * @property {string=} sandbox
+ * The name of the sandbox. If the value is null or empty
+ * string, the default realm will be used.
+ * @property {SerializationOptions=} serializationOptions
+ * An object which holds the information of how the DOM nodes
+ * should be serialized.
+ * @property {Array<SharedReference>=} startNodes
+ * A list of references to nodes, which are used as
+ * starting points for lookup.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {InvalidSelectorError}
+ * Raised if a locator value is invalid.
+ * @throws {NoSuchFrameError}
+ * If the browsing context cannot be found.
+ * @throws {UnsupportedOperationError}
+ * Raised when unsupported lookup types are used.
+ */
+ async locateNodes(options = {}) {
+ const {
+ context: contextId,
+ locator,
+ maxNodeCount = null,
+ ownership = lazy.OwnershipModel.None,
+ sandbox = null,
+ serializationOptions,
+ startNodes = null,
+ } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = this.#getBrowsingContext(contextId);
+
+ lazy.assert.object(
+ locator,
+ `Expected "locator" to be an object, got ${locator}`
+ );
+
+ const locatorTypes = Object.values(LocatorType);
+
+ lazy.assert.that(
+ locatorType => locatorTypes.includes(locatorType),
+ `Expected "locator.type" to be one of ${locatorTypes}, got ${locator.type}`
+ )(locator.type);
+
+ if (![LocatorType.css, LocatorType.xpath].includes(locator.type)) {
+ throw new lazy.error.UnsupportedOperationError(
+ `"locator.type" argument with value: ${locator.type} is not supported yet.`
+ );
+ }
+
+ if (maxNodeCount != null) {
+ const maxNodeCountErrorMsg = `Expected "maxNodeCount" to be an integer and greater than 0, got ${maxNodeCount}`;
+ lazy.assert.that(maxNodeCount => {
+ lazy.assert.integer(maxNodeCount, maxNodeCountErrorMsg);
+ return maxNodeCount > 0;
+ }, maxNodeCountErrorMsg)(maxNodeCount);
+ }
+
+ const ownershipTypes = Object.values(lazy.OwnershipModel);
+ lazy.assert.that(
+ ownership => ownershipTypes.includes(ownership),
+ `Expected "ownership" to be one of ${ownershipTypes}, got ${ownership}`
+ )(ownership);
+
+ if (sandbox != null) {
+ lazy.assert.string(
+ sandbox,
+ `Expected "sandbox" to be a string, got ${sandbox}`
+ );
+ }
+
+ const serializationOptionsWithDefaults =
+ lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
+
+ if (startNodes != null) {
+ lazy.assert.that(startNodes => {
+ lazy.assert.array(
+ startNodes,
+ `Expected "startNodes" to be an array, got ${startNodes}`
+ );
+ return !!startNodes.length;
+ }, `Expected "startNodes" to have at least one element, got ${startNodes}`)(
+ startNodes
+ );
+ }
+
+ const result = await this.messageHandler.forwardCommand({
+ moduleName: "browsingContext",
+ commandName: "_locateNodes",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ locator,
+ maxNodeCount,
+ resultOwnership: ownership,
+ sandbox,
+ serializationOptions: serializationOptionsWithDefaults,
+ startNodes,
+ },
+ });
+
+ return {
+ nodes: result.serializedNodes,
+ };
+ }
+
+ /**
+ * 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 context 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,
+ () => {
+ context.loadURI(targetURI, {
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ hasValidUserGestureActivation: true,
+ });
+ },
+ {
+ 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),
+ };
+ }
+
+ /**
+ * Reload the given context's document, with the provided wait condition.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context to navigate.
+ * @param {bool=} options.ignoreCache
+ * If true ignore the browser cache. [Not yet supported]
+ * @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 context cannot be found.
+ */
+ async reload(options = {}) {
+ const {
+ context: contextId,
+ ignoreCache,
+ wait = WaitCondition.None,
+ } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ if (typeof ignoreCache != "undefined") {
+ throw new lazy.error.UnsupportedOperationError(
+ `Argument "ignoreCache" is not supported yet.`
+ );
+ }
+
+ 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;
+
+ return this.#awaitNavigation(
+ webProgress,
+ () => {
+ context.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+ },
+ { wait }
+ );
+ }
+
+ /**
+ * Set the top-level browsing context's viewport to a given dimension.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ * @param {Viewport|null} options.viewport
+ * Dimensions to set the viewport to, or `null` to reset it
+ * to the original dimensions.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {UnsupportedOperationError}
+ * Raised when the command is called on Android.
+ */
+ async setViewport(options = {}) {
+ const { context: contextId, viewport } = options;
+
+ if (lazy.AppInfo.isAndroid) {
+ // Bug 1840084: Add Android support for modifying the viewport.
+ throw new lazy.error.UnsupportedOperationError(
+ `Command not yet supported for ${lazy.AppInfo.name}`
+ );
+ }
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = this.#getBrowsingContext(contextId);
+ if (context.parent) {
+ throw new lazy.error.InvalidArgumentError(
+ `Browsing Context with id ${contextId} is not top-level`
+ );
+ }
+
+ const browser = context.embedderElement;
+ const currentHeight = browser.clientHeight;
+ const currentWidth = browser.clientWidth;
+
+ let targetHeight, targetWidth;
+ if (viewport === undefined) {
+ // Don't modify the viewport's size.
+ targetHeight = currentHeight;
+ targetWidth = currentWidth;
+ } else if (viewport === null) {
+ // Reset viewport to the original dimensions.
+ targetHeight = browser.parentElement.clientHeight;
+ targetWidth = browser.parentElement.clientWidth;
+
+ browser.style.removeProperty("height");
+ browser.style.removeProperty("width");
+ } else {
+ lazy.assert.object(
+ viewport,
+ `Expected "viewport" to be an object, got ${viewport}`
+ );
+
+ const { height, width } = viewport;
+ targetHeight = lazy.assert.positiveInteger(
+ height,
+ `Expected viewport's "height" to be a positive integer, got ${height}`
+ );
+ targetWidth = lazy.assert.positiveInteger(
+ width,
+ `Expected viewport's "width" to be a positive integer, got ${width}`
+ );
+
+ if (targetHeight > MAX_WINDOW_SIZE || targetWidth > MAX_WINDOW_SIZE) {
+ throw new lazy.error.UnsupportedOperationError(
+ `"width" or "height" cannot be larger than ${MAX_WINDOW_SIZE} px`
+ );
+ }
+
+ browser.style.setProperty("height", targetHeight + "px");
+ browser.style.setProperty("width", targetWidth + "px");
+ }
+
+ if (targetHeight !== currentHeight || targetWidth !== currentWidth) {
+ // Wait until the viewport has been resized
+ await this.messageHandler.forwardCommand({
+ moduleName: "browsingContext",
+ commandName: "_awaitViewportDimensions",
+ destination: {
+ type: lazy.WindowGlobalMessageHandler.type,
+ id: context.id,
+ },
+ params: {
+ height: targetHeight,
+ width: targetWidth,
+ },
+ });
+ }
+ }
+
+ /**
+ * Traverses the history of a given context by a given delta.
+ *
+ * @param {object=} options
+ * @param {string} options.context
+ * Id of the browsing context.
+ * @param {number} options.delta
+ * The number of steps we have to traverse.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {NoSuchFrameException}
+ * When a context is not available.
+ * @throws {NoSuchHistoryEntryError}
+ * When a requested history entry does not exist.
+ */
+ async traverseHistory(options = {}) {
+ const { context: contextId, delta } = options;
+
+ lazy.assert.string(
+ contextId,
+ `Expected "context" to be a string, got ${contextId}`
+ );
+
+ const context = this.#getBrowsingContext(contextId);
+
+ lazy.assert.integer(
+ delta,
+ `Expected "delta" to be an integer, got ${delta}`
+ );
+
+ const sessionHistory = context.sessionHistory;
+ const allSteps = sessionHistory.count;
+ const currentIndex = sessionHistory.index;
+ const targetIndex = currentIndex + delta;
+ const validEntry = targetIndex >= 0 && targetIndex < allSteps;
+
+ if (!validEntry) {
+ throw new lazy.error.NoSuchHistoryEntryError(
+ `History entry with delta ${delta} not found`
+ );
+ }
+
+ context.goToIndex(targetIndex);
+
+ // On some platforms the requested index isn't set immediately.
+ await lazy.PollPromise(
+ (resolve, reject) => {
+ if (sessionHistory.index == targetIndex) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ {
+ errorMessage: `History was not updated for index "${targetIndex}"`,
+ timeout: TIMEOUT_SET_HISTORY_INDEX * lazy.getTimeoutMultiplier(),
+ }
+ );
+ }
+
+ /**
+ * 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 {Function} startNavigationFn
+ * A callback that starts a navigation.
+ * @param {object} options
+ * @param {WaitCondition} options.wait
+ * The WaitCondition to use to wait for the navigation.
+ *
+ * @returns {Promise<BrowsingContextNavigateResult>}
+ * A Promise that resolves to navigate results when the navigation is done.
+ */
+ async #awaitNavigation(webProgress, startNavigationFn, 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
+ );
+ }
+
+ // If WaitCondition is Complete, we should try to wait for the corresponding
+ // responseCompleted event to be received.
+ let onNavigationRequestCompleted;
+
+ // However, a navigation will not necessarily have network events.
+ // For instance: same document navigation, or when using file or data
+ // protocols (for which we don't have network events yet).
+ // Therefore we will not unconditionally wait for a navigation request and
+ // this flag should only be set when a responseCompleted event should be
+ // expected.
+ let shouldWaitForNavigationRequest = false;
+
+ // Cleaning up the listeners will be done at the end of this method.
+ let unsubscribeNavigationListeners;
+
+ if (wait === WaitCondition.Complete) {
+ let resolveOnNetworkEvent;
+ onNavigationRequestCompleted = new Promise(
+ r => (resolveOnNetworkEvent = r)
+ );
+ const onBeforeRequestSent = (name, data) => {
+ if (data.navigation) {
+ shouldWaitForNavigationRequest = true;
+ }
+ };
+ const onNetworkRequestCompleted = (name, data) => {
+ if (data.navigation) {
+ resolveOnNetworkEvent();
+ }
+ };
+
+ // The network request can either end with _responseCompleted or _fetchError
+ await this.messageHandler.eventsDispatcher.on(
+ "network._beforeRequestSent",
+ contextDescriptor,
+ onBeforeRequestSent
+ );
+ await this.messageHandler.eventsDispatcher.on(
+ "network._responseCompleted",
+ contextDescriptor,
+ onNetworkRequestCompleted
+ );
+ await this.messageHandler.eventsDispatcher.on(
+ "network._fetchError",
+ contextDescriptor,
+ onNetworkRequestCompleted
+ );
+
+ unsubscribeNavigationListeners = async () => {
+ await this.messageHandler.eventsDispatcher.off(
+ "network._beforeRequestSent",
+ contextDescriptor,
+ onBeforeRequestSent
+ );
+ await this.messageHandler.eventsDispatcher.off(
+ "network._responseCompleted",
+ contextDescriptor,
+ onNetworkRequestCompleted
+ );
+ await this.messageHandler.eventsDispatcher.off(
+ "network._fetchError",
+ contextDescriptor,
+ onNetworkRequestCompleted
+ );
+ };
+ }
+
+ const navigated = listener.start();
+
+ try {
+ const navigationId = lazy.registerNavigationId({
+ contextDetails: { context: webProgress.browsingContext },
+ });
+
+ await startNavigationFn();
+ await navigated;
+
+ if (shouldWaitForNavigationRequest) {
+ await onNavigationRequestCompleted;
+ }
+
+ 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 {
+ navigation: navigationId,
+ url,
+ };
+ } finally {
+ if (listener.isStarted) {
+ listener.stop();
+ }
+
+ if (wait === WaitCondition.Interactive) {
+ await this.messageHandler.eventsDispatcher.off(
+ "browsingContext._documentInteractive",
+ contextDescriptor,
+ onDocumentInteractive
+ );
+ } else if (
+ wait === WaitCondition.Complete &&
+ shouldWaitForNavigationRequest
+ ) {
+ await unsubscribeNavigationListeners();
+ }
+ }
+ }
+
+ /**
+ * 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 userContext = lazy.UserContextManager.getIdByBrowsingContext(context);
+ const contextInfo = {
+ children,
+ context: lazy.TabManager.getIdForBrowsingContext(context),
+ url: context.currentURI.spec,
+ userContext,
+ };
+
+ 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 = {}) => {
+ if (this.#subscribedEvents.has("browsingContext.contextCreated")) {
+ const { browsingContext, why } = data;
+
+ // Filter out top-level browsing contexts that are created because of a
+ // cross-group navigation.
+ if (why === "replace") {
+ return;
+ }
+
+ // TODO: Bug 1852941. We should also filter out events which are emitted
+ // for DevTools frames.
+
+ // 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
+ );
+ }
+ };
+
+ #onContextDiscarded = async (eventName, data = {}) => {
+ if (this.#subscribedEvents.has("browsingContext.contextDestroyed")) {
+ const { browsingContext, why } = data;
+
+ // Filter out top-level browsing contexts that are destroyed because of a
+ // cross-group navigation.
+ if (why === "replace") {
+ return;
+ }
+
+ // TODO: Bug 1852941. We should also filter out events which are emitted
+ // for DevTools frames.
+
+ // Filter out notifications for chrome context until support gets
+ // added (bug 1722679).
+ if (!browsingContext.webProgress) {
+ return;
+ }
+
+ // If this event is for a child context whose top or parent context is also destroyed,
+ // we don't need to send it, in this case the event for the top/parent context is enough.
+ if (
+ browsingContext.parent &&
+ (browsingContext.top.isDiscarded || browsingContext.parent.isDiscarded)
+ ) {
+ 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.contextDestroyed",
+ browsingContextInfo,
+ contextInfo
+ );
+ }
+ };
+
+ #onLocationChanged = async (eventName, data) => {
+ const { navigationId, navigableId, url } = data;
+ const context = this.#getBrowsingContext(navigableId);
+
+ if (this.#subscribedEvents.has("browsingContext.fragmentNavigated")) {
+ const contextInfo = {
+ contextId: context.id,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+ this.emitEvent(
+ "browsingContext.fragmentNavigated",
+ {
+ context: navigableId,
+ navigation: navigationId,
+ timestamp: Date.now(),
+ url,
+ },
+ contextInfo
+ );
+ }
+ };
+
+ #onPromptClosed = async (eventName, data) => {
+ if (this.#subscribedEvents.has("browsingContext.userPromptClosed")) {
+ const { contentBrowser, detail } = data;
+ const contextId = lazy.TabManager.getIdForBrowser(contentBrowser);
+
+ if (contextId === null) {
+ return;
+ }
+
+ // 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,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+
+ const params = {
+ context: contextId,
+ ...detail,
+ };
+
+ this.emitEvent("browsingContext.userPromptClosed", params, contextInfo);
+ }
+ };
+
+ #onPromptOpened = async (eventName, data) => {
+ if (this.#subscribedEvents.has("browsingContext.userPromptOpened")) {
+ const { contentBrowser, prompt } = data;
+
+ // Do not send opened event for unsupported prompt types.
+ if (!(prompt.promptType in UserPromptType)) {
+ return;
+ }
+
+ const contextId = lazy.TabManager.getIdForBrowser(contentBrowser);
+ // 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,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+
+ const eventPayload = {
+ context: contextId,
+ type: prompt.promptType,
+ message: await prompt.getText(),
+ };
+
+ // Bug 1859814: Since the platform doesn't provide the access to the `defaultValue` of the prompt,
+ // we use prompt the `value` instead. The `value` is set to `defaultValue` when `defaultValue` is provided.
+ // This approach doesn't allow us to distinguish between the `defaultValue` being set to an empty string and
+ // `defaultValue` not set, because `value` is always defaulted to an empty string.
+ // We should switch to using the actual `defaultValue` when it's available and check for the `null` here.
+ const defaultValue = await prompt.getInputText();
+ if (defaultValue) {
+ eventPayload.defaultValue = defaultValue;
+ }
+
+ this.emitEvent(
+ "browsingContext.userPromptOpened",
+ eventPayload,
+ contextInfo
+ );
+ }
+ };
+
+ #onNavigationStarted = async (eventName, data) => {
+ const { navigableId, navigationId, url } = data;
+ const context = this.#getBrowsingContext(navigableId);
+
+ if (this.#subscribedEvents.has("browsingContext.navigationStarted")) {
+ const contextInfo = {
+ contextId: context.id,
+ type: lazy.WindowGlobalMessageHandler.type,
+ };
+
+ this.emitEvent(
+ "browsingContext.navigationStarted",
+ {
+ context: navigableId,
+ navigation: navigationId,
+ timestamp: Date.now(),
+ url,
+ },
+ contextInfo
+ );
+ }
+ };
+
+ #onPageHideEvent = (name, eventPayload) => {
+ const { context } = eventPayload;
+ if (context.parent) {
+ this.#onContextDiscarded("windowglobal-pagehide", {
+ browsingContext: context,
+ });
+ }
+ };
+
+ #stopListeningToContextEvent(event) {
+ this.#subscribedEvents.delete(event);
+
+ const hasContextEvent =
+ this.#subscribedEvents.has("browsingContext.contextCreated") ||
+ this.#subscribedEvents.has("browsingContext.contextDestroyed");
+
+ if (!hasContextEvent) {
+ this.#contextListener.stopListening();
+ }
+ }
+
+ #stopListeningToNavigationEvent(event) {
+ this.#subscribedEvents.delete(event);
+
+ const hasNavigationEvent =
+ this.#subscribedEvents.has("browsingContext.fragmentNavigated") ||
+ this.#subscribedEvents.has("browsingContext.navigationStarted");
+
+ if (!hasNavigationEvent) {
+ this.#navigationListener.stopListening();
+ }
+ }
+
+ #stopListeningToPromptEvent(event) {
+ this.#subscribedEvents.delete(event);
+
+ const hasPromptEvent =
+ this.#subscribedEvents.has("browsingContext.userPromptClosed") ||
+ this.#subscribedEvents.has("browsingContext.userPromptOpened");
+
+ if (!hasPromptEvent) {
+ this.#promptListener.stopListening();
+ }
+ }
+
+ #subscribeEvent(event) {
+ switch (event) {
+ case "browsingContext.contextCreated":
+ case "browsingContext.contextDestroyed": {
+ this.#contextListener.startListening();
+ this.#subscribedEvents.add(event);
+ break;
+ }
+ case "browsingContext.fragmentNavigated":
+ case "browsingContext.navigationStarted": {
+ this.#navigationListener.startListening();
+ this.#subscribedEvents.add(event);
+ break;
+ }
+ case "browsingContext.userPromptClosed":
+ case "browsingContext.userPromptOpened": {
+ this.#promptListener.startListening();
+ this.#subscribedEvents.add(event);
+ break;
+ }
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ switch (event) {
+ case "browsingContext.contextCreated":
+ case "browsingContext.contextDestroyed": {
+ this.#stopListeningToContextEvent(event);
+ break;
+ }
+ case "browsingContext.fragmentNavigated":
+ case "browsingContext.navigationStarted": {
+ this.#stopListeningToNavigationEvent(event);
+ break;
+ }
+ case "browsingContext.userPromptClosed":
+ case "browsingContext.userPromptOpened": {
+ this.#stopListeningToPromptEvent(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * 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.contextDestroyed",
+ "browsingContext.domContentLoaded",
+ "browsingContext.fragmentNavigated",
+ "browsingContext.load",
+ "browsingContext.navigationStarted",
+ "browsingContext.userPromptClosed",
+ "browsingContext.userPromptOpened",
+ ];
+ }
+}
+
+export const browsingContext = BrowsingContextModule;