tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browsingContext.sys.mjs (82883B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
     11  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     12  BrowsingContextListener:
     13    "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
     14  capture: "chrome://remote/content/shared/Capture.sys.mjs",
     15  ContextDescriptorType:
     16    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
     17  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     18  EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
     19  getTimeoutMultiplier: "chrome://remote/content/shared/AppInfo.sys.mjs",
     20  getWebDriverSessionById:
     21    "chrome://remote/content/shared/webdriver/Session.sys.mjs",
     22  Log: "chrome://remote/content/shared/Log.sys.mjs",
     23  modal: "chrome://remote/content/shared/Prompt.sys.mjs",
     24  registerNavigationId:
     25    "chrome://remote/content/shared/NavigationManager.sys.mjs",
     26  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     27  NavigationListener:
     28    "chrome://remote/content/shared/listeners/NavigationListener.sys.mjs",
     29  PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
     30  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     31  print: "chrome://remote/content/shared/PDF.sys.mjs",
     32  ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
     33  PromptListener:
     34    "chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
     35  RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
     36  SessionDataMethod:
     37    "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
     38  setDefaultAndAssertSerializationOptions:
     39    "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     40  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
     41  UserContextManager:
     42    "chrome://remote/content/shared/UserContextManager.sys.mjs",
     43  waitForInitialNavigationCompleted:
     44    "chrome://remote/content/shared/Navigate.sys.mjs",
     45  WindowGlobalMessageHandler:
     46    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
     47  windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
     48 });
     49 
     50 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     51  lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
     52 );
     53 
     54 // Maximal window dimension allowed when emulating a viewport.
     55 const MAX_WINDOW_SIZE = 10000000;
     56 
     57 /**
     58 * @typedef {string} ClipRectangleType
     59 */
     60 
     61 /**
     62 * Enum of possible clip rectangle types supported by the
     63 * browsingContext.captureScreenshot command.
     64 *
     65 * @readonly
     66 * @enum {ClipRectangleType}
     67 */
     68 export const ClipRectangleType = {
     69  Box: "box",
     70  Element: "element",
     71 };
     72 
     73 /**
     74 * @typedef {object} CreateType
     75 */
     76 
     77 /**
     78 * Enum of types supported by the browsingContext.create command.
     79 *
     80 * @readonly
     81 * @enum {CreateType}
     82 */
     83 const CreateType = {
     84  tab: "tab",
     85  window: "window",
     86 };
     87 
     88 /**
     89 * @typedef {object} DownloadEndStatus
     90 */
     91 
     92 /**
     93 * Enum of values for the status of the browsingContext.downloadEnd event.
     94 *
     95 * @readonly
     96 * @enum {DownloadStatus}
     97 */
     98 const DownloadEndStatus = {
     99  canceled: "canceled",
    100  complete: "complete",
    101 };
    102 
    103 /**
    104 * @typedef {string} LocatorType
    105 */
    106 
    107 /**
    108 * Enum of types supported by the browsingContext.locateNodes command.
    109 *
    110 * @readonly
    111 * @enum {LocatorType}
    112 */
    113 export const LocatorType = {
    114  accessibility: "accessibility",
    115  context: "context",
    116  css: "css",
    117  innerText: "innerText",
    118  xpath: "xpath",
    119 };
    120 
    121 /**
    122 * @typedef {string} OriginType
    123 */
    124 
    125 /**
    126 * Enum of origin type supported by the
    127 * browsingContext.captureScreenshot command.
    128 *
    129 * @readonly
    130 * @enum {OriginType}
    131 */
    132 export const OriginType = {
    133  document: "document",
    134  viewport: "viewport",
    135 };
    136 
    137 const TIMEOUT_SET_HISTORY_INDEX = 1000;
    138 const TIMEOUT_WAIT_FOR_VISIBILITY = 250;
    139 
    140 /**
    141 * Enum of user prompt types supported by the browsingContext.handleUserPrompt
    142 * command, these types can be retrieved from `dialog.args.promptType`.
    143 *
    144 * @readonly
    145 * @enum {UserPromptType}
    146 */
    147 const UserPromptType = {
    148  alert: "alert",
    149  confirm: "confirm",
    150  prompt: "prompt",
    151  beforeunload: "beforeunload",
    152 };
    153 
    154 /**
    155 * An object that contains details of a viewport.
    156 *
    157 * @typedef {object} Viewport
    158 *
    159 * @property {number} height
    160 *     The height of the viewport.
    161 * @property {number} width
    162 *     The width of the viewport.
    163 */
    164 
    165 /**
    166 * @typedef {string} WaitCondition
    167 */
    168 
    169 /**
    170 * Wait conditions supported by WebDriver BiDi for navigation.
    171 *
    172 * @enum {WaitCondition}
    173 */
    174 const WaitCondition = {
    175  None: "none",
    176  Interactive: "interactive",
    177  Complete: "complete",
    178 };
    179 
    180 /**
    181 * An enum that specifies the scope of a browsing context.
    182 *
    183 * @readonly
    184 * @enum {string}
    185 */
    186 export const MozContextScope = {
    187  CHROME: "chrome",
    188  CONTENT: "content",
    189 };
    190 
    191 /**
    192 * Used as an argument for browsingContext._updateNavigableViewport command
    193 * to represent an object which holds viewport settings which should be applied.
    194 *
    195 * @typedef ViewportOverride
    196 *
    197 * @property {number|null} devicePixelRatio
    198 *     A value to override device pixel ratio, or `null` to reset it to
    199 *     the original value.
    200 * @property {Viewport|null} viewport
    201 *     Dimensions to set the viewport to, or `null` to reset it
    202 *     to the original dimensions.
    203 */
    204 
    205 class BrowsingContextModule extends RootBiDiModule {
    206  #blockedCreateCommands;
    207  #contextListener;
    208  #navigationListener;
    209  #promptListener;
    210  #subscribedEvents;
    211 
    212  /**
    213   * Create a new module instance.
    214   *
    215   * @param {MessageHandler} messageHandler
    216   *     The MessageHandler instance which owns this Module instance.
    217   */
    218  constructor(messageHandler) {
    219    super(messageHandler);
    220 
    221    this.#contextListener = new lazy.BrowsingContextListener();
    222    this.#contextListener.on("attached", this.#onContextAttached);
    223    this.#contextListener.on("discarded", this.#onContextDiscarded);
    224 
    225    this.#navigationListener = new lazy.NavigationListener(
    226      this.messageHandler.navigationManager
    227    );
    228    this.#navigationListener.on("download-end", this.#onDownloadEnd);
    229    this.#navigationListener.on("download-started", this.#onDownloadStarted);
    230    this.#navigationListener.on(
    231      "fragment-navigated",
    232      this.#onFragmentNavigated
    233    );
    234    this.#navigationListener.on("history-updated", this.#onHistoryUpdated);
    235    this.#navigationListener.on(
    236      "navigation-committed",
    237      this.#onNavigationCommitted
    238    );
    239    this.#navigationListener.on("navigation-failed", this.#onNavigationFailed);
    240    this.#navigationListener.on(
    241      "navigation-started",
    242      this.#onNavigationStarted
    243    );
    244 
    245    // Create the prompt listener and listen to "closed" and "opened" events.
    246    this.#promptListener = new lazy.PromptListener();
    247    this.#promptListener.on("closed", this.#onPromptClosed);
    248    this.#promptListener.on("opened", this.#onPromptOpened);
    249 
    250    // Set of event names which have active subscriptions.
    251    this.#subscribedEvents = new Set();
    252 
    253    // Treat the event of moving a page to BFCache as context discarded event for iframes.
    254    this.messageHandler.on("windowglobal-pagehide", this.#onPageHideEvent);
    255 
    256    // Maps browsers to a promise and resolver that is used to block the create method.
    257    this.#blockedCreateCommands = new WeakMap();
    258  }
    259 
    260  destroy() {
    261    this.#blockedCreateCommands = new WeakMap();
    262 
    263    this.#contextListener.off("attached", this.#onContextAttached);
    264    this.#contextListener.off("discarded", this.#onContextDiscarded);
    265    this.#contextListener.destroy();
    266 
    267    this.#navigationListener.off(
    268      "fragment-navigated",
    269      this.#onFragmentNavigated
    270    );
    271    this.#navigationListener.off("history-updated", this.#onHistoryUpdated);
    272    this.#navigationListener.off(
    273      "navigation-committed",
    274      this.#onNavigationCommitted
    275    );
    276    this.#navigationListener.off("navigation-failed", this.#onNavigationFailed);
    277    this.#navigationListener.off(
    278      "navigation-started",
    279      this.#onNavigationStarted
    280    );
    281    this.#navigationListener.destroy();
    282 
    283    this.#promptListener.off("closed", this.#onPromptClosed);
    284    this.#promptListener.off("opened", this.#onPromptOpened);
    285    this.#promptListener.destroy();
    286 
    287    this.#subscribedEvents = null;
    288 
    289    this.messageHandler.off("windowglobal-pagehide", this.#onPageHideEvent);
    290  }
    291 
    292  /**
    293   * Activates and focuses the given top-level browsing context.
    294   *
    295   * @param {object=} options
    296   * @param {string} options.context
    297   *     Id of the browsing context.
    298   *
    299   * @throws {InvalidArgumentError}
    300   *     Raised if an argument is of an invalid type or value.
    301   * @throws {NoSuchFrameError}
    302   *     If the browsing context cannot be found.
    303   */
    304  async activate(options = {}) {
    305    const { context: contextId } = options;
    306 
    307    lazy.assert.string(
    308      contextId,
    309      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    310    );
    311    const context = this._getNavigable(contextId);
    312 
    313    lazy.assert.topLevel(
    314      context,
    315      lazy.pprint`Browsing context with id ${contextId} is not top-level`
    316    );
    317 
    318    const targetTab = lazy.TabManager.getTabForBrowsingContext(context);
    319    const targetWindow = lazy.TabManager.getWindowForTab(targetTab);
    320    const selectedTab = lazy.TabManager.getTabBrowser(targetWindow).selectedTab;
    321 
    322    const activated = [
    323      lazy.windowManager.focusWindow(targetWindow),
    324      lazy.TabManager.selectTab(targetTab),
    325    ];
    326 
    327    if (targetTab !== selectedTab && !lazy.AppInfo.isAndroid) {
    328      // We need to wait until the "document.visibilityState" of the currently
    329      // selected tab in the target window is marked as "hidden".
    330      //
    331      // Bug 1884142: It's not supported on Android for the TestRunner package.
    332      const selectedBrowser = lazy.TabManager.getBrowserForTab(selectedTab);
    333      activated.push(
    334        this.#waitForVisibilityState(selectedBrowser.browsingContext, "hidden")
    335      );
    336    }
    337 
    338    await Promise.all(activated);
    339  }
    340 
    341  /**
    342   * Used as an argument for browsingContext.captureScreenshot command, as one of the available variants
    343   * {BoxClipRectangle} or {ElementClipRectangle}, to represent a target of the command.
    344   *
    345   * @typedef ClipRectangle
    346   */
    347 
    348  /**
    349   * Used as an argument for browsingContext.captureScreenshot command
    350   * to represent a box which is going to be a target of the command.
    351   *
    352   * @typedef BoxClipRectangle
    353   *
    354   * @property {ClipRectangleType} [type=ClipRectangleType.Box]
    355   * @property {number} x
    356   * @property {number} y
    357   * @property {number} width
    358   * @property {number} height
    359   */
    360 
    361  /**
    362   * Used as an argument for the browsingContext.captureScreenshot command to
    363   * represent the output image format.
    364   *
    365   * @typedef ImageFormat
    366   *
    367   * @property {string} type
    368   *     The output screenshot format such as `image/png`.
    369   * @property {number=} quality
    370   *     A number between 0 and 1 representing the screenshot quality.
    371   */
    372 
    373  /**
    374   * Used as an argument for browsingContext.captureScreenshot command
    375   * to represent an element which is going to be a target of the command.
    376   *
    377   * @typedef ElementClipRectangle
    378   *
    379   * @property {ClipRectangleType} [type=ClipRectangleType.Element]
    380   * @property {SharedReference} element
    381   */
    382 
    383  /**
    384   * Capture a base64-encoded screenshot of the provided browsing context.
    385   *
    386   * @param {object=} options
    387   * @param {string} options.context
    388   *     Id of the browsing context to screenshot.
    389   * @param {ClipRectangle=} options.clip
    390   *     A box or an element of which a screenshot should be taken.
    391   *     If not present, take a screenshot of the whole viewport.
    392   * @param {OriginType=} options.origin
    393   * @param {ImageFormat=} options.format
    394   *    Configuration options for the output image.
    395   *
    396   * @throws {NoSuchFrameError}
    397   *     If the browsing context cannot be found.
    398   */
    399  async captureScreenshot(options = {}) {
    400    const {
    401      clip = null,
    402      context: contextId,
    403      origin = OriginType.viewport,
    404      format = { type: "image/png", quality: undefined },
    405    } = options;
    406 
    407    lazy.assert.string(
    408      contextId,
    409      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    410    );
    411    const context = this._getNavigable(contextId);
    412 
    413    const originTypeValues = Object.values(OriginType);
    414    lazy.assert.that(
    415      value => originTypeValues.includes(value),
    416      `Expected "origin" to be one of ${originTypeValues}, ` +
    417        lazy.pprint`got ${origin}`
    418    )(origin);
    419 
    420    lazy.assert.object(
    421      format,
    422      lazy.pprint`Expected "format" to be an object, got ${format}`
    423    );
    424 
    425    const { quality, type: formatType } = format;
    426 
    427    lazy.assert.string(
    428      formatType,
    429      lazy.pprint`Expected "type" to be a string, got ${formatType}`
    430    );
    431 
    432    if (quality !== undefined) {
    433      lazy.assert.number(
    434        quality,
    435        lazy.pprint`Expected "quality" to be a number, got ${quality}`
    436      );
    437 
    438      lazy.assert.that(
    439        imageQuality => imageQuality >= 0 && imageQuality <= 1,
    440        lazy.pprint`Expected "quality" to be in the range of 0 to 1, got ${quality}`
    441      )(quality);
    442    }
    443 
    444    if (clip !== null) {
    445      lazy.assert.object(
    446        clip,
    447        lazy.pprint`Expected "clip" to be an object, got ${clip}`
    448      );
    449 
    450      const { type: clipType } = clip;
    451      switch (clipType) {
    452        case ClipRectangleType.Box: {
    453          const { x, y, width, height } = clip;
    454 
    455          lazy.assert.number(
    456            x,
    457            lazy.pprint`Expected "x" to be a number, got ${x}`
    458          );
    459          lazy.assert.number(
    460            y,
    461            lazy.pprint`Expected "y" to be a number, got ${y}`
    462          );
    463          lazy.assert.number(
    464            width,
    465            lazy.pprint`Expected "width" to be a number, got ${width}`
    466          );
    467          lazy.assert.number(
    468            height,
    469            lazy.pprint`Expected "height" to be a number, got ${height}`
    470          );
    471 
    472          break;
    473        }
    474 
    475        case ClipRectangleType.Element: {
    476          const { element } = clip;
    477 
    478          lazy.assert.object(
    479            element,
    480            lazy.pprint`Expected "element" to be an object, got ${element}`
    481          );
    482 
    483          break;
    484        }
    485 
    486        default:
    487          throw new lazy.error.InvalidArgumentError(
    488            `Expected "type" to be one of ${Object.values(
    489              ClipRectangleType
    490            )}, ` + lazy.pprint`got ${clipType}`
    491          );
    492      }
    493    }
    494 
    495    const rect = await this.messageHandler.handleCommand({
    496      moduleName: "browsingContext",
    497      commandName: "_getScreenshotRect",
    498      destination: {
    499        type: lazy.WindowGlobalMessageHandler.type,
    500        id: context.id,
    501      },
    502      params: {
    503        clip,
    504        origin,
    505      },
    506      retryOnAbort: true,
    507    });
    508 
    509    if (rect.width === 0 || rect.height === 0) {
    510      throw new lazy.error.UnableToCaptureScreen(
    511        `The dimensions of requested screenshot are incorrect, got width: ${rect.width} and height: ${rect.height}.`
    512      );
    513    }
    514 
    515    const canvas = await lazy.capture.canvas(
    516      context.topChromeWindow,
    517      context,
    518      rect.x,
    519      rect.y,
    520      rect.width,
    521      rect.height
    522    );
    523 
    524    return {
    525      data: lazy.capture.toBase64(canvas, formatType, quality),
    526    };
    527  }
    528 
    529  /**
    530   * Close the provided browsing context.
    531   *
    532   * @param {object=} options
    533   * @param {string} options.context
    534   *     Id of the browsing context to close.
    535   * @param {boolean=} options.promptUnload
    536   *     Flag to indicate if a potential beforeunload prompt should be shown
    537   *     when closing the browsing context. Defaults to false.
    538   *
    539   * @throws {NoSuchFrameError}
    540   *     If the browsing context cannot be found.
    541   * @throws {InvalidArgumentError}
    542   *     If the browsing context is not a top-level one.
    543   */
    544  async close(options = {}) {
    545    const { context: contextId, promptUnload = false } = options;
    546 
    547    lazy.assert.string(
    548      contextId,
    549      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    550    );
    551 
    552    lazy.assert.boolean(
    553      promptUnload,
    554      lazy.pprint`Expected "promptUnload" to be a boolean, got ${promptUnload}`
    555    );
    556 
    557    const context = this._getNavigable(contextId);
    558    lazy.assert.topLevel(
    559      context,
    560      lazy.pprint`Browsing context with id ${contextId} is not top-level`
    561    );
    562 
    563    if (lazy.TabManager.getTabCount() === 1) {
    564      // The behavior when closing the very last tab is currently unspecified.
    565      // As such behave like Marionette and don't allow closing it.
    566      // See: https://github.com/w3c/webdriver-bidi/issues/187
    567      return;
    568    }
    569 
    570    const tab = lazy.TabManager.getTabForBrowsingContext(context);
    571    await lazy.TabManager.removeTab(tab, { skipPermitUnload: !promptUnload });
    572  }
    573 
    574  /**
    575   * Create a new browsing context using the provided type "tab" or "window".
    576   *
    577   * @param {object=} options
    578   * @param {boolean=} options.background
    579   *     Whether the tab/window should be open in the background. Defaults to false,
    580   *     which means that the tab/window will be open in the foreground.
    581   * @param {string=} options.referenceContext
    582   *     Id of the top-level browsing context to use as reference.
    583   *     If options.type is "tab", the new tab will open in the same window as
    584   *     the reference context, and will be added next to the reference context.
    585   *     If options.type is "window", the reference context is ignored.
    586   * @param {CreateType} options.type
    587   *     Type of browsing context to create.
    588   * @param {string=} options.userContext
    589   *     The id of the user context which should own the browsing context.
    590   *     Defaults to the default user context.
    591   *
    592   * @throws {InvalidArgumentError}
    593   *     If the browsing context is not a top-level one.
    594   * @throws {NoSuchFrameError}
    595   *     If the browsing context cannot be found.
    596   */
    597  async create(options = {}) {
    598    const {
    599      background = false,
    600      referenceContext: referenceContextId = null,
    601      type: typeHint,
    602      userContext: userContextId = null,
    603    } = options;
    604 
    605    if (![CreateType.tab, CreateType.window].includes(typeHint)) {
    606      throw new lazy.error.InvalidArgumentError(
    607        `Expected "type" to be one of ${Object.values(CreateType)}, ` +
    608          lazy.pprint`got ${typeHint}`
    609      );
    610    }
    611 
    612    lazy.assert.boolean(
    613      background,
    614      lazy.pprint`Expected "background" to be a boolean, got ${background}`
    615    );
    616 
    617    let referenceContext = null;
    618    if (referenceContextId !== null) {
    619      lazy.assert.string(
    620        referenceContextId,
    621        lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}`
    622      );
    623 
    624      referenceContext =
    625        lazy.NavigableManager.getBrowsingContextById(referenceContextId);
    626      if (!referenceContext) {
    627        throw new lazy.error.NoSuchFrameError(
    628          `Browsing Context with id ${referenceContextId} not found`
    629        );
    630      }
    631 
    632      if (referenceContext.parent) {
    633        throw new lazy.error.InvalidArgumentError(
    634          `referenceContext with id ${referenceContextId} is not a top-level browsing context`
    635        );
    636      }
    637    }
    638 
    639    let userContext = lazy.UserContextManager.defaultUserContextId;
    640    if (referenceContext !== null) {
    641      userContext =
    642        lazy.UserContextManager.getIdByBrowsingContext(referenceContext);
    643    }
    644 
    645    if (userContextId !== null) {
    646      lazy.assert.string(
    647        userContextId,
    648        lazy.pprint`Expected "userContext" to be a string, got ${userContextId}`
    649      );
    650 
    651      if (!lazy.UserContextManager.hasUserContextId(userContextId)) {
    652        throw new lazy.error.NoSuchUserContextError(
    653          `User Context with id ${userContextId} was not found`
    654        );
    655      }
    656 
    657      userContext = userContextId;
    658 
    659      if (
    660        lazy.AppInfo.isAndroid &&
    661        userContext != lazy.UserContextManager.defaultUserContextId
    662      ) {
    663        throw new lazy.error.UnsupportedOperationError(
    664          `browsingContext.create with non-default "userContext" not supported for ${lazy.AppInfo.name}`
    665        );
    666      }
    667    }
    668 
    669    let browser;
    670 
    671    // Since each tab in GeckoView has its own Gecko instance running,
    672    // which means also its own window object, for Android we will need to focus
    673    // a previously focused window in case of opening the tab in the background.
    674    const previousWindow = Services.wm.getMostRecentBrowserWindow();
    675    const previousTab =
    676      lazy.TabManager.getTabBrowser(previousWindow).selectedTab;
    677 
    678    // The type supported varies by platform, as Android can only support one window.
    679    // As such, type compatibility will need to be checked and will fallback if necessary.
    680    let type;
    681    if (
    682      (typeHint == "tab" && lazy.TabManager.supportsTabs()) ||
    683      (typeHint == "window" && lazy.windowManager.supportsWindows())
    684    ) {
    685      type = typeHint;
    686    } else if (lazy.TabManager.supportsTabs()) {
    687      type = "tab";
    688    } else if (lazy.windowManager.supportsWindows()) {
    689      type = "window";
    690    } else {
    691      throw new lazy.error.UnsupportedOperationError(
    692        `Not supported in ${lazy.AppInfo.name}`
    693      );
    694    }
    695 
    696    let waitForVisibilityStatePromise;
    697    switch (type) {
    698      case "window": {
    699        const newWindow = await lazy.windowManager.openBrowserWindow({
    700          focus: !background,
    701          userContextId: userContext,
    702        });
    703        browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser;
    704        break;
    705      }
    706      case "tab": {
    707        // The window to open the new tab in.
    708        let window = Services.wm.getMostRecentBrowserWindow();
    709 
    710        let referenceTab;
    711        if (referenceContext !== null) {
    712          referenceTab =
    713            lazy.TabManager.getTabForBrowsingContext(referenceContext);
    714          window = lazy.TabManager.getWindowForTab(referenceTab);
    715        }
    716 
    717        if (!background && !lazy.AppInfo.isAndroid) {
    718          // When opening a new foreground tab we need to wait until the
    719          // "document.visibilityState" of the currently selected tab in this
    720          // window is marked as "hidden".
    721          //
    722          // Bug 1884142: It's not supported on Android for the TestRunner package.
    723          const selectedTab = lazy.TabManager.getTabBrowser(window).selectedTab;
    724 
    725          // Create the promise immediately, but await it later in parallel with
    726          // waitForInitialNavigationCompleted.
    727          waitForVisibilityStatePromise = this.#waitForVisibilityState(
    728            lazy.TabManager.getBrowserForTab(selectedTab).browsingContext,
    729            "hidden"
    730          );
    731        }
    732 
    733        const tab = await lazy.TabManager.addTab({
    734          focus: !background,
    735          referenceTab,
    736          userContextId: userContext,
    737        });
    738        browser = lazy.TabManager.getBrowserForTab(tab);
    739      }
    740    }
    741 
    742    // ConfigurationModule cannot block parsing for initial about:blank load, so we block
    743    // browsing_context.create till configuration is applied.
    744    let blocker = this.#blockedCreateCommands.get(browser);
    745    // If the configuration is done before we have a browser, a resolved blocker already exists.
    746    if (!blocker) {
    747      blocker = Promise.withResolvers();
    748      if (!this.#hasConfigurationForContext(userContext)) {
    749        blocker.resolve();
    750      }
    751      this.#blockedCreateCommands.set(browser, blocker);
    752    }
    753 
    754    await Promise.all([
    755      lazy.waitForInitialNavigationCompleted(
    756        browser.browsingContext.webProgress,
    757        {
    758          unloadTimeout: 5000,
    759        }
    760      ),
    761      waitForVisibilityStatePromise,
    762      blocker.promise,
    763    ]);
    764 
    765    this.#blockedCreateCommands.delete(browser);
    766 
    767    // The tab on Android is always opened in the foreground,
    768    // so we need to select the previous tab,
    769    // and we have to wait until is fully loaded.
    770    // TODO: Bug 1845559. This workaround can be removed,
    771    // when the API to create a tab for Android supports the background option.
    772    if (lazy.AppInfo.isAndroid && background) {
    773      await lazy.windowManager.focusWindow(previousWindow);
    774      await lazy.TabManager.selectTab(previousTab);
    775    }
    776 
    777    // Force a reflow by accessing `clientHeight` (see Bug 1847044).
    778    browser.parentElement.clientHeight;
    779 
    780    if (!background && !lazy.AppInfo.isAndroid) {
    781      // See Bug 2002097, on slow platforms, the newly created tab might not be
    782      // visible immediately.
    783      await this.#waitForVisibilityState(
    784        browser.browsingContext,
    785        "visible",
    786        // Waiting for visibility can potentially be racy. If several contexts
    787        // are created in parallel, we might not be able to catch the document
    788        // in the expected state.
    789        { timeout: TIMEOUT_WAIT_FOR_VISIBILITY * lazy.getTimeoutMultiplier() }
    790      );
    791    }
    792 
    793    return {
    794      context: lazy.NavigableManager.getIdForBrowser(browser),
    795    };
    796  }
    797 
    798  /* eslint-disable jsdoc/valid-types */
    799  /**
    800   * An object that holds the WebDriver Bidi browsing context information.
    801   *
    802   * @typedef BrowsingContextInfo
    803   *
    804   * @property {string} context
    805   *     The id of the browsing context.
    806   * @property {string=} parent
    807   *     The parent of the browsing context if it's the root browsing context
    808   *     of the to be processed browsing context tree.
    809   * @property {string} url
    810   *     The current documents location.
    811   * @property {string} userContext
    812   *     The id of the user context owning this browsing context.
    813   * @property {Array<BrowsingContextInfo>=} children
    814   *     List of child browsing contexts. Only set if maxDepth hasn't been
    815   *     reached yet.
    816   * @property {string} clientWindow
    817   *     The id of the window the browsing context belongs to.
    818   * @property {string=} "moz:name"
    819   *     Name of the browsing context.
    820   * @property {MozContextScope=} "moz:scope"
    821   *     The scope of the browsing context.
    822   */
    823  /* eslint-enable jsdoc/valid-types */
    824 
    825  /**
    826   * An object that holds the WebDriver Bidi browsing context tree information.
    827   *
    828   * @typedef BrowsingContextGetTreeResult
    829   *
    830   * @property {Array<BrowsingContextInfo>} contexts
    831   *     List of child browsing contexts.
    832   */
    833 
    834  /**
    835   * Returns a tree of all browsing contexts that are descendants of the
    836   * given context, or all top-level contexts when no root is provided.
    837   *
    838   * @param {object=} options
    839   * @param {number=} options.maxDepth
    840   *     Depth of the browsing context tree to traverse. If not specified
    841   *     the whole tree is returned.
    842   * @param {string=} options.root
    843   *     Id of the root browsing context.
    844   * @param {MozContextScope=} options."moz:scope"
    845   *     The scope from which browsing contexts are retrieved. This
    846   *     parameter cannot be used when a root browsing context is specified.
    847   *
    848   * @returns {BrowsingContextGetTreeResult}
    849   *     Tree of browsing context information.
    850   * @throws {NoSuchFrameError}
    851   *     If the browsing context cannot be found.
    852   */
    853  getTree(options = {}) {
    854    const {
    855      maxDepth = null,
    856      root: rootId = null,
    857      "moz:scope": scope = null,
    858    } = options;
    859 
    860    if (maxDepth !== null) {
    861      lazy.assert.positiveInteger(
    862        maxDepth,
    863        lazy.pprint`Expected "maxDepth" to be a positive integer, got ${maxDepth}`
    864      );
    865    }
    866 
    867    if (scope !== null) {
    868      const contextScopes = Object.values(MozContextScope);
    869      lazy.assert.that(
    870        _scope => contextScopes.includes(_scope),
    871        `Expected "moz:scope" to be one of ${contextScopes}, ` +
    872          lazy.pprint`got ${scope}`
    873      )(scope);
    874 
    875      if (scope != MozContextScope.CONTENT) {
    876        // By default only content browsing contexts are allowed.
    877        lazy.assert.hasSystemAccess();
    878      }
    879    }
    880 
    881    let contexts;
    882    if (rootId !== null) {
    883      // With a root id specified return the context info for itself
    884      // and the full tree.
    885      lazy.assert.string(
    886        rootId,
    887        lazy.pprint`Expected "root" to be a string, got ${rootId}`
    888      );
    889 
    890      if (scope) {
    891        // At the moment we only allow to set a specific scope
    892        // when querying at the top-level.
    893        throw new lazy.error.InvalidArgumentError(
    894          `"root" and "moz:scope" are mutual exclusive`
    895        );
    896      }
    897 
    898      contexts = [this._getNavigable(rootId, { supportsChromeScope: true })];
    899    } else {
    900      switch (scope) {
    901        case MozContextScope.CHROME: {
    902          // Return all browsing contexts related to chrome windows.
    903          contexts = lazy.windowManager.windows.map(win => win.browsingContext);
    904          break;
    905        }
    906        default: {
    907          // Return all top-level browsing contexts.
    908          contexts = lazy.TabManager.getBrowsers().map(
    909            browser => browser.browsingContext
    910          );
    911        }
    912      }
    913    }
    914 
    915    const contextsInfo = contexts.map(context => {
    916      return getBrowsingContextInfo(context, { maxDepth });
    917    });
    918 
    919    return { contexts: contextsInfo };
    920  }
    921 
    922  /**
    923   * Closes an open prompt.
    924   *
    925   * @param {object=} options
    926   * @param {string} options.context
    927   *     Id of the browsing context.
    928   * @param {boolean=} options.accept
    929   *     Whether user prompt should be accepted or dismissed.
    930   *     Defaults to true.
    931   * @param {string=} options.userText
    932   *     Input to the user prompt's value field.
    933   *     Defaults to an empty string.
    934   *
    935   * @throws {InvalidArgumentError}
    936   *     Raised if an argument is of an invalid type or value.
    937   * @throws {NoSuchAlertError}
    938   *     If there is no current user prompt.
    939   * @throws {NoSuchFrameError}
    940   *     If the browsing context cannot be found.
    941   * @throws {UnsupportedOperationError}
    942   *     Raised when the command is called for "beforeunload" prompt.
    943   */
    944  async handleUserPrompt(options = {}) {
    945    const { accept = true, context: contextId, userText = "" } = options;
    946 
    947    lazy.assert.string(
    948      contextId,
    949      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    950    );
    951 
    952    const context = this._getNavigable(contextId);
    953 
    954    lazy.assert.boolean(
    955      accept,
    956      lazy.pprint`Expected "accept" to be a boolean, got ${accept}`
    957    );
    958 
    959    lazy.assert.string(
    960      userText,
    961      lazy.pprint`Expected "userText" to be a string, got ${userText}`
    962    );
    963 
    964    const tab = lazy.TabManager.getTabForBrowsingContext(context);
    965    const browser = lazy.TabManager.getBrowserForTab(tab);
    966    const window = lazy.TabManager.getWindowForTab(tab);
    967    const dialog = lazy.modal.findPrompt({
    968      window,
    969      contentBrowser: browser,
    970    });
    971 
    972    const closePrompt = async callback => {
    973      const dialogClosed = new lazy.EventPromise(
    974        window,
    975        "DOMModalDialogClosed"
    976      );
    977      callback();
    978      await dialogClosed;
    979    };
    980 
    981    if (dialog && dialog.isOpen) {
    982      switch (dialog.promptType) {
    983        case UserPromptType.alert:
    984          await closePrompt(() => dialog.accept());
    985          return;
    986 
    987        case UserPromptType.beforeunload:
    988        case UserPromptType.confirm:
    989          await closePrompt(() => {
    990            if (accept) {
    991              dialog.accept();
    992            } else {
    993              dialog.dismiss();
    994            }
    995          });
    996 
    997          return;
    998 
    999        case UserPromptType.prompt:
   1000          await closePrompt(() => {
   1001            if (accept) {
   1002              dialog.text = userText;
   1003              dialog.accept();
   1004            } else {
   1005              dialog.dismiss();
   1006            }
   1007          });
   1008 
   1009          return;
   1010 
   1011        default:
   1012          throw new lazy.error.UnsupportedOperationError(
   1013            `Prompts of type "${dialog.promptType}" are not supported`
   1014          );
   1015      }
   1016    }
   1017 
   1018    throw new lazy.error.NoSuchAlertError();
   1019  }
   1020 
   1021  /**
   1022   * Used as an argument for browsingContext.locateNodes command, as one of the available variants
   1023   * {AccessibilityLocator}, {ContextLocator}, {CssLocator}, {InnerTextLocator} or {XPathLocator},
   1024   * to represent a way of how lookup of nodes is going to be performed.
   1025   *
   1026   * @typedef Locator
   1027   */
   1028 
   1029  /**
   1030   * Used as a value argument for browsingContext.locateNodes command
   1031   * in case of a lookup by accessibility attributes.
   1032   *
   1033   * @typedef AccessibilityLocatorValue
   1034   *
   1035   * @property {string=} name
   1036   * @property {string=} role
   1037   */
   1038 
   1039  /**
   1040   * Used as an argument for browsingContext.locateNodes command
   1041   * to represent a lookup by accessibility attributes.
   1042   *
   1043   * @typedef AccessibilityLocator
   1044   *
   1045   * @property {LocatorType} [type=LocatorType.accessibility]
   1046   * @property {AccessibilityLocatorValue} value
   1047   */
   1048 
   1049  /**
   1050   * Used as a value argument for browsingContext.locateNodes command
   1051   * in case of a lookup for a context container.
   1052   *
   1053   * @typedef ContextLocatorValue
   1054   *
   1055   * @property {string} context
   1056   */
   1057 
   1058  /**
   1059   * Used as an argument for browsingContext.locateNodes command
   1060   * to represent a lookup for a context container.
   1061   *
   1062   * @typedef ContextLocator
   1063   *
   1064   * @property {LocatorType} [type=LocatorType.context]
   1065   * @property {ContextLocatorValue} value
   1066   */
   1067 
   1068  /**
   1069   * Used as an argument for browsingContext.locateNodes command
   1070   * to represent a lookup by css selector.
   1071   *
   1072   * @typedef CssLocator
   1073   *
   1074   * @property {LocatorType} [type=LocatorType.css]
   1075   * @property {string} value
   1076   */
   1077 
   1078  /**
   1079   * Used as an argument for browsingContext.locateNodes command
   1080   * to represent a lookup by inner text.
   1081   *
   1082   * @typedef InnerTextLocator
   1083   *
   1084   * @property {LocatorType} [type=LocatorType.innerText]
   1085   * @property {string} value
   1086   * @property {boolean=} ignoreCase
   1087   * @property {("full"|"partial")=} matchType
   1088   * @property {number=} maxDepth
   1089   */
   1090 
   1091  /**
   1092   * Used as an argument for browsingContext.locateNodes command
   1093   * to represent a lookup by xpath.
   1094   *
   1095   * @typedef XPathLocator
   1096   *
   1097   * @property {LocatorType} [type=LocatorType.xpath]
   1098   * @property {string} value
   1099   */
   1100 
   1101  /**
   1102   * Returns a list of all nodes matching
   1103   * the specified locator.
   1104   *
   1105   * @param {object} options
   1106   * @param {string} options.context
   1107   *     Id of the browsing context.
   1108   * @param {Locator} options.locator
   1109   *     The type of lookup which is going to be used.
   1110   * @param {number=} options.maxNodeCount
   1111   *     The maximum amount of nodes which is going to be returned.
   1112   *     Defaults to return all the found nodes.
   1113   * @property {SerializationOptions=} serializationOptions
   1114   *     An object which holds the information of how the DOM nodes
   1115   *     should be serialized.
   1116   * @property {Array<SharedReference>=} startNodes
   1117   *     A list of references to nodes, which are used as
   1118   *     starting points for lookup.
   1119   *
   1120   * @throws {InvalidArgumentError}
   1121   *     Raised if an argument is of an invalid type or value.
   1122   * @throws {InvalidSelectorError}
   1123   *     Raised if a locator value is invalid.
   1124   * @throws {NoSuchFrameError}
   1125   *     If the browsing context cannot be found.
   1126   * @throws {UnsupportedOperationError}
   1127   *     Raised when unsupported lookup types are used.
   1128   */
   1129  async locateNodes(options = {}) {
   1130    const {
   1131      context: navigableId,
   1132      locator,
   1133      maxNodeCount = null,
   1134      serializationOptions,
   1135      startNodes = null,
   1136    } = options;
   1137 
   1138    lazy.assert.string(
   1139      navigableId,
   1140      lazy.pprint`Expected "context" to be a string, got ${navigableId}`
   1141    );
   1142 
   1143    const context = this._getNavigable(navigableId);
   1144 
   1145    lazy.assert.object(
   1146      locator,
   1147      lazy.pprint`Expected "locator" to be an object, got ${locator}`
   1148    );
   1149 
   1150    const locatorTypes = Object.values(LocatorType);
   1151 
   1152    lazy.assert.that(
   1153      locatorType => locatorTypes.includes(locatorType),
   1154      `Expected "locator.type" to be one of ${locatorTypes}, ` +
   1155        lazy.pprint`got ${locator.type}`
   1156    )(locator.type);
   1157 
   1158    if (
   1159      [LocatorType.css, LocatorType.innerText, LocatorType.xpath].includes(
   1160        locator.type
   1161      )
   1162    ) {
   1163      lazy.assert.string(
   1164        locator.value,
   1165        `Expected "locator.value" of "locator.type" "${locator.type}" to be a string, ` +
   1166          lazy.pprint`got ${locator.value}`
   1167      );
   1168    }
   1169    if (locator.type == LocatorType.accessibility) {
   1170      lazy.assert.object(
   1171        locator.value,
   1172        `Expected "locator.value" of "locator.type" "${locator.type}" to be an object, ` +
   1173          lazy.pprint`got ${locator.value}`
   1174      );
   1175 
   1176      const { name = null, role = null } = locator.value;
   1177      if (name !== null) {
   1178        lazy.assert.string(
   1179          locator.value.name,
   1180          `Expected "locator.value.name" of "locator.type" "${locator.type}" to be a string, ` +
   1181            lazy.pprint`got ${name}`
   1182        );
   1183      }
   1184      if (role !== null) {
   1185        lazy.assert.string(
   1186          locator.value.role,
   1187          `Expected "locator.value.role" of "locator.type" "${locator.type}" to be a string, ` +
   1188            lazy.pprint`got ${role}`
   1189        );
   1190      }
   1191    }
   1192 
   1193    if (locator.type == LocatorType.context) {
   1194      if (startNodes !== null) {
   1195        throw new lazy.error.InvalidArgumentError(
   1196          `Expected "startNodes" to be null when using "locator.type" "${locator.type}", ` +
   1197            lazy.pprint`got ${startNodes}`
   1198        );
   1199      }
   1200 
   1201      lazy.assert.object(
   1202        locator.value,
   1203        `Expected "locator.value" of "locator.type" "${locator.type}" to be an object, ` +
   1204          lazy.pprint`got ${locator.value}`
   1205      );
   1206      const selector = locator.value;
   1207      const contextId = selector.context;
   1208      lazy.assert.string(
   1209        contextId,
   1210        `Expected "locator.value.context" of "locator.type" "${locator.type}" to be a string, ` +
   1211          lazy.pprint`got ${contextId}`
   1212      );
   1213 
   1214      const childContext = this._getNavigable(contextId);
   1215      if (childContext.parent !== context) {
   1216        throw new lazy.error.InvalidArgumentError(
   1217          `Expected "locator.context" (${contextId}) to be a direct child context of "context" (${navigableId})`
   1218        );
   1219      }
   1220 
   1221      // Replace the locator selector context value by the internal browsing
   1222      // context id.
   1223      locator.value.context = childContext.id;
   1224    }
   1225 
   1226    if (
   1227      ![
   1228        LocatorType.accessibility,
   1229        LocatorType.context,
   1230        LocatorType.css,
   1231        LocatorType.xpath,
   1232      ].includes(locator.type)
   1233    ) {
   1234      throw new lazy.error.UnsupportedOperationError(
   1235        `"locator.type" argument with value: ${locator.type} is not supported yet.`
   1236      );
   1237    }
   1238 
   1239    if (maxNodeCount != null) {
   1240      const maxNodeCountErrorMsg = lazy.pprint`Expected "maxNodeCount" to be an integer and greater than 0, got ${maxNodeCount}`;
   1241      lazy.assert.that(maxNodeCount => {
   1242        lazy.assert.integer(maxNodeCount, maxNodeCountErrorMsg);
   1243        return maxNodeCount > 0;
   1244      }, maxNodeCountErrorMsg)(maxNodeCount);
   1245    }
   1246 
   1247    const serializationOptionsWithDefaults =
   1248      lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
   1249 
   1250    if (startNodes != null) {
   1251      lazy.assert.isNonEmptyArray(
   1252        startNodes,
   1253        lazy.pprint`Expected "startNodes" to be a non-empty array, got ${startNodes}`
   1254      );
   1255    }
   1256 
   1257    const result = await this._forwardToWindowGlobal(
   1258      "_locateNodes",
   1259      context.id,
   1260      {
   1261        locator,
   1262        maxNodeCount,
   1263        serializationOptions: serializationOptionsWithDefaults,
   1264        startNodes,
   1265      },
   1266      { retryOnAbort: true }
   1267    );
   1268 
   1269    return {
   1270      nodes: result.serializedNodes,
   1271    };
   1272  }
   1273 
   1274  /**
   1275   * An object that holds the WebDriver Bidi navigation information.
   1276   *
   1277   * @typedef BrowsingContextNavigateResult
   1278   *
   1279   * @property {string} navigation
   1280   *     Unique id for this navigation.
   1281   * @property {string} url
   1282   *     The requested or reached URL.
   1283   */
   1284 
   1285  /**
   1286   * Navigate the given context to the provided url, with the provided wait condition.
   1287   *
   1288   * @param {object=} options
   1289   * @param {string} options.context
   1290   *     Id of the browsing context to navigate.
   1291   * @param {string} options.url
   1292   *     Url for the navigation.
   1293   * @param {WaitCondition=} options.wait
   1294   *     Wait condition for the navigation, one of "none", "interactive", "complete".
   1295   *     Defaults to "none".
   1296   *
   1297   * @returns {BrowsingContextNavigateResult}
   1298   *     Navigation result.
   1299   *
   1300   * @throws {InvalidArgumentError}
   1301   *     Raised if an argument is of an invalid type or value.
   1302   * @throws {NoSuchFrameError}
   1303   *     If the browsing context for context cannot be found.
   1304   */
   1305  async navigate(options = {}) {
   1306    const { context: contextId, url, wait = WaitCondition.None } = options;
   1307 
   1308    lazy.assert.string(
   1309      contextId,
   1310      lazy.pprint`Expected "context" to be a string, got ${contextId}`
   1311    );
   1312 
   1313    lazy.assert.string(
   1314      url,
   1315      lazy.pprint`Expected "url" to be string, got ${url}`
   1316    );
   1317 
   1318    const waitConditions = Object.values(WaitCondition);
   1319    if (!waitConditions.includes(wait)) {
   1320      throw new lazy.error.InvalidArgumentError(
   1321        `Expected "wait" to be one of ${waitConditions}, ` +
   1322          lazy.pprint`got ${wait}`
   1323      );
   1324    }
   1325 
   1326    const context = this._getNavigable(contextId);
   1327 
   1328    // webProgress will be stable even if the context navigates, retrieve it
   1329    // immediately before doing any asynchronous call.
   1330    const webProgress = context.webProgress;
   1331 
   1332    const base = await this.messageHandler.handleCommand({
   1333      moduleName: "browsingContext",
   1334      commandName: "_getBaseURL",
   1335      destination: {
   1336        type: lazy.WindowGlobalMessageHandler.type,
   1337        id: context.id,
   1338      },
   1339      retryOnAbort: true,
   1340    });
   1341 
   1342    let targetURI;
   1343    try {
   1344      const baseURI = Services.io.newURI(base);
   1345      targetURI = Services.io.newURI(url, null, baseURI);
   1346    } catch (e) {
   1347      throw new lazy.error.InvalidArgumentError(
   1348        `Expected "url" to be a valid URL (${e.message})`
   1349      );
   1350    }
   1351 
   1352    return this.#awaitNavigation(
   1353      webProgress,
   1354      () => {
   1355        context.loadURI(targetURI, {
   1356          loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
   1357          // Fake user activation.
   1358          hasValidUserGestureActivation: true,
   1359          // Prevent HTTPS-First upgrades.
   1360          schemelessInput: Ci.nsILoadInfo.SchemelessInputTypeSchemeful,
   1361          triggeringPrincipal:
   1362            Services.scriptSecurityManager.getSystemPrincipal(),
   1363        });
   1364      },
   1365      {
   1366        targetURI,
   1367        wait,
   1368      }
   1369    );
   1370  }
   1371 
   1372  /**
   1373   * An object that holds the information about margins
   1374   * for Webdriver BiDi browsingContext.print command.
   1375   *
   1376   * @typedef BrowsingContextPrintMarginParameters
   1377   *
   1378   * @property {number=} bottom
   1379   *     Bottom margin in cm. Defaults to 1cm (~0.4 inches).
   1380   * @property {number=} left
   1381   *     Left margin in cm. Defaults to 1cm (~0.4 inches).
   1382   * @property {number=} right
   1383   *     Right margin in cm. Defaults to 1cm (~0.4 inches).
   1384   * @property {number=} top
   1385   *     Top margin in cm. Defaults to 1cm (~0.4 inches).
   1386   */
   1387 
   1388  /**
   1389   * An object that holds the information about paper size
   1390   * for Webdriver BiDi browsingContext.print command.
   1391   *
   1392   * @typedef BrowsingContextPrintPageParameters
   1393   *
   1394   * @property {number=} height
   1395   *     Paper height in cm. Defaults to US letter height (27.94cm / 11 inches).
   1396   * @property {number=} width
   1397   *     Paper width in cm. Defaults to US letter width (21.59cm / 8.5 inches).
   1398   */
   1399 
   1400  /**
   1401   * Used as return value for Webdriver BiDi browsingContext.print command.
   1402   *
   1403   * @typedef BrowsingContextPrintResult
   1404   *
   1405   * @property {string} data
   1406   *     Base64 encoded PDF representing printed document.
   1407   */
   1408 
   1409  /**
   1410   * Creates a paginated PDF representation of a document
   1411   * of the provided browsing context, and returns it
   1412   * as a Base64-encoded string.
   1413   *
   1414   * @param {object=} options
   1415   * @param {string} options.context
   1416   *     Id of the browsing context.
   1417   * @param {boolean=} options.background
   1418   *     Whether or not to print background colors and images.
   1419   *     Defaults to false, which prints without background graphics.
   1420   * @param {BrowsingContextPrintMarginParameters=} options.margin
   1421   *     Paper margins.
   1422   * @param {('landscape'|'portrait')=} options.orientation
   1423   *     Paper orientation. Defaults to 'portrait'.
   1424   * @param {BrowsingContextPrintPageParameters=} options.page
   1425   *     Paper size.
   1426   * @param {Array<number|string>=} options.pageRanges
   1427   *     Paper ranges to print, e.g., ['1-5', 8, '11-13'].
   1428   *     Defaults to the empty array, which means print all pages.
   1429   * @param {number=} options.scale
   1430   *     Scale of the webpage rendering. Defaults to 1.0.
   1431   * @param {boolean=} options.shrinkToFit
   1432   *     Whether or not to override page size as defined by CSS.
   1433   *     Defaults to true, in which case the content will be scaled
   1434   *     to fit the paper size.
   1435   *
   1436   * @returns {BrowsingContextPrintResult}
   1437   *
   1438   * @throws {InvalidArgumentError}
   1439   *     Raised if an argument is of an invalid type or value.
   1440   * @throws {NoSuchFrameError}
   1441   *     If the browsing context cannot be found.
   1442   */
   1443  async print(options = {}) {
   1444    const {
   1445      context: contextId,
   1446      background,
   1447      margin,
   1448      orientation,
   1449      page,
   1450      pageRanges,
   1451      scale,
   1452      shrinkToFit,
   1453    } = options;
   1454 
   1455    lazy.assert.string(
   1456      contextId,
   1457      lazy.pprint`Expected "context" to be a string, got ${contextId}`
   1458    );
   1459    const context = this._getNavigable(contextId);
   1460 
   1461    const settings = lazy.print.addDefaultSettings({
   1462      background,
   1463      margin,
   1464      orientation,
   1465      page,
   1466      pageRanges,
   1467      scale,
   1468      shrinkToFit,
   1469    });
   1470 
   1471    for (const prop of ["top", "bottom", "left", "right"]) {
   1472      lazy.assert.positiveNumber(
   1473        settings.margin[prop],
   1474        `Expected "margin.${prop}" to be a positive number, ` +
   1475          lazy.pprint`got ${settings.margin[prop]}`
   1476      );
   1477    }
   1478    for (const prop of ["width", "height"]) {
   1479      lazy.assert.positiveNumber(
   1480        settings.page[prop],
   1481        `Expected "page.${prop}" to be a positive number, ` +
   1482          lazy.pprint`got ${settings.page[prop]}`
   1483      );
   1484    }
   1485    lazy.assert.positiveNumber(
   1486      settings.scale,
   1487      `Expected "scale" to be a positive number, ` +
   1488        lazy.pprint`got ${settings.scale}`
   1489    );
   1490    lazy.assert.that(
   1491      scale =>
   1492        scale >= lazy.print.minScaleValue && scale <= lazy.print.maxScaleValue,
   1493      `scale ${settings.scale} is outside the range ${lazy.print.minScaleValue}-${lazy.print.maxScaleValue}`
   1494    )(settings.scale);
   1495    lazy.assert.boolean(
   1496      settings.shrinkToFit,
   1497      lazy.pprint`Expected "shrinkToFit" to be a boolean, got ${settings.shrinkToFit}`
   1498    );
   1499    lazy.assert.that(
   1500      orientation => lazy.print.defaults.orientationValue.includes(orientation),
   1501      `Expected "orientation" to be one of ${lazy.print.defaults.orientationValue}", ` +
   1502        lazy.pprint`got {settings.orientation}`
   1503    )(settings.orientation);
   1504    lazy.assert.boolean(
   1505      settings.background,
   1506      lazy.pprint`Expected "background" to be a boolean, got ${settings.background}`
   1507    );
   1508    lazy.assert.array(
   1509      settings.pageRanges,
   1510      lazy.pprint`Expected "pageRanges" to be an array, got ${settings.pageRanges}`
   1511    );
   1512 
   1513    const printSettings = await lazy.print.getPrintSettings(settings);
   1514    const binaryString = await lazy.print.printToBinaryString(
   1515      context,
   1516      printSettings
   1517    );
   1518 
   1519    return {
   1520      data: btoa(binaryString),
   1521    };
   1522  }
   1523 
   1524  /**
   1525   * Reload the given context's document, with the provided wait condition.
   1526   *
   1527   * @param {object=} options
   1528   * @param {string} options.context
   1529   *     Id of the browsing context to navigate.
   1530   * @param {bool=} options.ignoreCache
   1531   *     If true ignore the browser cache. [Not yet supported]
   1532   * @param {WaitCondition=} options.wait
   1533   *     Wait condition for the navigation, one of "none", "interactive", "complete".
   1534   *     Defaults to "none".
   1535   *
   1536   * @returns {BrowsingContextNavigateResult}
   1537   *     Navigation result.
   1538   *
   1539   * @throws {InvalidArgumentError}
   1540   *     Raised if an argument is of an invalid type or value.
   1541   * @throws {NoSuchFrameError}
   1542   *     If the browsing context for context cannot be found.
   1543   */
   1544  async reload(options = {}) {
   1545    const {
   1546      context: contextId,
   1547      ignoreCache,
   1548      wait = WaitCondition.None,
   1549    } = options;
   1550 
   1551    lazy.assert.string(
   1552      contextId,
   1553      lazy.pprint`Expected "context" to be a string, got ${contextId}`
   1554    );
   1555 
   1556    if (typeof ignoreCache != "undefined") {
   1557      throw new lazy.error.UnsupportedOperationError(
   1558        `Argument "ignoreCache" is not supported yet.`
   1559      );
   1560    }
   1561 
   1562    const waitConditions = Object.values(WaitCondition);
   1563    if (!waitConditions.includes(wait)) {
   1564      throw new lazy.error.InvalidArgumentError(
   1565        `Expected "wait" to be one of ${waitConditions}, ` +
   1566          lazy.pprint`got ${wait}`
   1567      );
   1568    }
   1569 
   1570    const context = this._getNavigable(contextId);
   1571 
   1572    // webProgress will be stable even if the context navigates, retrieve it
   1573    // immediately before doing any asynchronous call.
   1574    const webProgress = context.webProgress;
   1575 
   1576    return this.#awaitNavigation(
   1577      webProgress,
   1578      () => {
   1579        context.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
   1580      },
   1581      { wait }
   1582    );
   1583  }
   1584 
   1585  /**
   1586   * Set the top-level browsing context's viewport to a given dimension.
   1587   *
   1588   * @param {object=} options
   1589   * @param {string=} options.context
   1590   *     Id of the browsing context.
   1591   * @param {(number|null)=} options.devicePixelRatio
   1592   *     A value to override device pixel ratio, or `null` to reset it to
   1593   *     the original value. Different values will not cause the rendering to change,
   1594   *     only image srcsets and media queries will be applied as if DPR is redefined.
   1595   * @param {(Viewport|null)=} options.viewport
   1596   *     Dimensions to set the viewport to, or `null` to reset it
   1597   *     to the original dimensions.
   1598   * @param {Array<string>=} options.userContexts
   1599   *     Optional list of user context ids.
   1600   *
   1601   * @throws {InvalidArgumentError}
   1602   *     Raised if an argument is of an invalid type or value.
   1603   * @throws {UnsupportedOperationError}
   1604   *     Raised when the command is called on Android.
   1605   */
   1606  async setViewport(options = {}) {
   1607    const {
   1608      context: contextId = null,
   1609      devicePixelRatio,
   1610      viewport,
   1611      userContexts: userContextIds = null,
   1612    } = options;
   1613 
   1614    const userContexts = new Set();
   1615 
   1616    if (contextId !== null) {
   1617      lazy.assert.string(
   1618        contextId,
   1619        lazy.pprint`Expected "context" to be a string, got ${contextId}`
   1620      );
   1621    } else if (userContextIds !== null) {
   1622      lazy.assert.isNonEmptyArray(
   1623        userContextIds,
   1624        lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}`
   1625      );
   1626 
   1627      for (const userContextId of userContextIds) {
   1628        lazy.assert.string(
   1629          userContextId,
   1630          lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}`
   1631        );
   1632 
   1633        const internalId =
   1634          lazy.UserContextManager.getInternalIdById(userContextId);
   1635 
   1636        if (internalId === null) {
   1637          throw new lazy.error.NoSuchUserContextError(
   1638            `User context with id: ${userContextId} doesn't exist`
   1639          );
   1640        }
   1641 
   1642        userContexts.add(internalId);
   1643      }
   1644    } else {
   1645      throw new lazy.error.InvalidArgumentError(
   1646        `At least one of "context" or "userContexts" arguments should be provided`
   1647      );
   1648    }
   1649 
   1650    if (contextId !== null && userContextIds !== null) {
   1651      throw new lazy.error.InvalidArgumentError(
   1652        `Providing both "context" and "userContexts" arguments is not supported`
   1653      );
   1654    }
   1655 
   1656    if (viewport !== undefined && viewport !== null) {
   1657      lazy.assert.object(
   1658        viewport,
   1659        lazy.pprint`Expected "viewport" to be an object, got ${viewport}`
   1660      );
   1661 
   1662      const { height, width } = viewport;
   1663      lazy.assert.positiveInteger(
   1664        height,
   1665        lazy.pprint`Expected viewport's "height" to be a positive integer, got ${height}`
   1666      );
   1667      lazy.assert.positiveInteger(
   1668        width,
   1669        lazy.pprint`Expected viewport's "width" to be a positive integer, got ${width}`
   1670      );
   1671 
   1672      if (height > MAX_WINDOW_SIZE || width > MAX_WINDOW_SIZE) {
   1673        throw new lazy.error.UnsupportedOperationError(
   1674          `"width" or "height" cannot be larger than ${MAX_WINDOW_SIZE} px`
   1675        );
   1676      }
   1677    }
   1678 
   1679    if (devicePixelRatio !== undefined && devicePixelRatio !== null) {
   1680      lazy.assert.number(
   1681        devicePixelRatio,
   1682        lazy.pprint`Expected "devicePixelRatio" to be a number or null, got ${devicePixelRatio}`
   1683      );
   1684      lazy.assert.that(
   1685        value => value > 0,
   1686        lazy.pprint`Expected "devicePixelRatio" to be greater than 0, got ${devicePixelRatio}`
   1687      )(devicePixelRatio);
   1688    }
   1689 
   1690    const navigables = new Set();
   1691    if (contextId !== null) {
   1692      const navigable = this._getNavigable(contextId);
   1693      lazy.assert.topLevel(
   1694        navigable,
   1695        `Browsing context with id ${contextId} is not top-level`
   1696      );
   1697 
   1698      navigables.add(navigable);
   1699    }
   1700 
   1701    if (lazy.AppInfo.isAndroid) {
   1702      // Bug 1840084: Add Android support for modifying the viewport.
   1703      throw new lazy.error.UnsupportedOperationError(
   1704        `Command not yet supported for ${lazy.AppInfo.name}`
   1705      );
   1706    }
   1707 
   1708    const viewportOverride = {
   1709      devicePixelRatio,
   1710      viewport,
   1711    };
   1712 
   1713    const sessionDataItems = [];
   1714    if (userContextIds !== null) {
   1715      for (const userContext of userContexts) {
   1716        // Prepare the list of navigables to update.
   1717        lazy.UserContextManager.getTabsForUserContext(userContext).forEach(
   1718          tab => {
   1719            const contentBrowser = lazy.TabManager.getBrowserForTab(tab);
   1720            navigables.add(contentBrowser.browsingContext);
   1721          }
   1722        );
   1723        sessionDataItems.push({
   1724          category: "viewport-overrides",
   1725          moduleName: "_configuration",
   1726          values: [viewportOverride],
   1727          contextDescriptor: {
   1728            type: lazy.ContextDescriptorType.UserContext,
   1729            id: userContext,
   1730          },
   1731          method: lazy.SessionDataMethod.Add,
   1732        });
   1733      }
   1734    } else {
   1735      for (const navigable of navigables) {
   1736        sessionDataItems.push({
   1737          category: "viewport-overrides",
   1738          moduleName: "_configuration",
   1739          values: [viewportOverride],
   1740          contextDescriptor: {
   1741            type: lazy.ContextDescriptorType.TopBrowsingContext,
   1742            id: navigable.browserId,
   1743          },
   1744          method: lazy.SessionDataMethod.Add,
   1745        });
   1746      }
   1747    }
   1748 
   1749    if (sessionDataItems.length) {
   1750      // TODO: Bug 1953079. Saving the viewport overrides in the session data works fine
   1751      // with one session, but when we start supporting multiple BiDi session, we will
   1752      // have to rethink this approach.
   1753      await this.messageHandler.updateSessionData(sessionDataItems);
   1754    }
   1755 
   1756    const commands = [];
   1757 
   1758    for (const navigable of navigables) {
   1759      commands.push(
   1760        this._updateNavigableViewport({
   1761          navigable,
   1762          viewportOverride,
   1763        })
   1764      );
   1765    }
   1766 
   1767    await Promise.all(commands);
   1768  }
   1769 
   1770  /**
   1771   * Traverses the history of a given context by a given delta.
   1772   *
   1773   * @param {object=} options
   1774   * @param {string} options.context
   1775   *     Id of the browsing context.
   1776   * @param {number} options.delta
   1777   *     The number of steps we have to traverse.
   1778   *
   1779   * @throws {InvalidArgumentError}
   1780   *     Raised if an argument is of an invalid type or value.
   1781   * @throws {NoSuchFrameException}
   1782   *     When a context is not available.
   1783   * @throws {NoSuchHistoryEntryError}
   1784   *     When a requested history entry does not exist.
   1785   */
   1786  async traverseHistory(options = {}) {
   1787    const { context: contextId, delta } = options;
   1788 
   1789    lazy.assert.string(
   1790      contextId,
   1791      lazy.pprint`Expected "context" to be a string, got ${contextId}`
   1792    );
   1793 
   1794    const context = this._getNavigable(contextId);
   1795 
   1796    lazy.assert.topLevel(
   1797      context,
   1798      lazy.pprint`Browsing context with id ${contextId} is not top-level`
   1799    );
   1800 
   1801    lazy.assert.integer(
   1802      delta,
   1803      lazy.pprint`Expected "delta" to be an integer, got ${delta}`
   1804    );
   1805 
   1806    const sessionHistory = context.sessionHistory;
   1807    const allSteps = sessionHistory.count;
   1808    const currentIndex = sessionHistory.index;
   1809    const targetIndex = currentIndex + delta;
   1810    const validEntry = targetIndex >= 0 && targetIndex < allSteps;
   1811 
   1812    if (!validEntry) {
   1813      throw new lazy.error.NoSuchHistoryEntryError(
   1814        `History entry with delta ${delta} not found`
   1815      );
   1816    }
   1817 
   1818    context.goToIndex(targetIndex);
   1819 
   1820    // On some platforms the requested index isn't set immediately.
   1821    await lazy.PollPromise(
   1822      (resolve, reject) => {
   1823        if (sessionHistory.index == targetIndex) {
   1824          resolve();
   1825        } else {
   1826          reject();
   1827        }
   1828      },
   1829      {
   1830        errorMessage: `History was not updated for index "${targetIndex}"`,
   1831        timeout: TIMEOUT_SET_HISTORY_INDEX * lazy.getTimeoutMultiplier(),
   1832      }
   1833    );
   1834  }
   1835 
   1836  /**
   1837   * Start and await a navigation on the provided BrowsingContext. Returns a
   1838   * promise which resolves when the navigation is done according to the provided
   1839   * navigation strategy.
   1840   *
   1841   * @param {WebProgress} webProgress
   1842   *     The WebProgress instance to observe for this navigation.
   1843   * @param {Function} startNavigationFn
   1844   *     A callback that starts a navigation.
   1845   * @param {object} options
   1846   * @param {string=} options.targetURI
   1847   *     The target URI for the navigation.
   1848   * @param {WaitCondition} options.wait
   1849   *     The WaitCondition to use to wait for the navigation.
   1850   *
   1851   * @returns {Promise<BrowsingContextNavigateResult>}
   1852   *     A Promise that resolves to navigate results when the navigation is done.
   1853   */
   1854  async #awaitNavigation(webProgress, startNavigationFn, options) {
   1855    const { targetURI, wait } = options;
   1856 
   1857    const context = webProgress.browsingContext;
   1858    const browserId = context.browserId;
   1859 
   1860    const resolveWhenCommitted = wait === WaitCondition.None;
   1861    const listener = new lazy.ProgressListener(webProgress, {
   1862      expectNavigation: true,
   1863      navigationManager: this.messageHandler.navigationManager,
   1864      resolveWhenCommitted,
   1865      targetURI,
   1866      // In case the webprogress is already navigating, always wait for an
   1867      // explicit start flag.
   1868      waitForExplicitStart: true,
   1869    });
   1870 
   1871    const onDocumentInteractive = (evtName, wrappedEvt) => {
   1872      if (webProgress.browsingContext.id !== wrappedEvt.contextId) {
   1873        // Ignore load events for unrelated browsing contexts.
   1874        return;
   1875      }
   1876 
   1877      if (wrappedEvt.readyState === "interactive") {
   1878        listener.stopIfStarted();
   1879      }
   1880    };
   1881 
   1882    const contextDescriptor = {
   1883      type: lazy.ContextDescriptorType.TopBrowsingContext,
   1884      id: browserId,
   1885    };
   1886 
   1887    // For the Interactive wait condition, resolve as soon as
   1888    // the document becomes interactive.
   1889    if (wait === WaitCondition.Interactive) {
   1890      await this.messageHandler.eventsDispatcher.on(
   1891        "browsingContext._documentInteractive",
   1892        contextDescriptor,
   1893        onDocumentInteractive
   1894      );
   1895    }
   1896 
   1897    const navigationId = lazy.registerNavigationId({
   1898      contextDetails: { context: webProgress.browsingContext },
   1899    });
   1900    const navigated = listener.start(navigationId);
   1901 
   1902    try {
   1903      await startNavigationFn();
   1904      await navigated;
   1905 
   1906      let url;
   1907      if (wait === WaitCondition.None) {
   1908        // If wait condition is None, the navigation resolved before the current
   1909        // context has navigated.
   1910        url = listener.targetURI.spec;
   1911      } else {
   1912        url = listener.currentURI.spec;
   1913      }
   1914 
   1915      return {
   1916        navigation: navigationId,
   1917        url,
   1918      };
   1919    } catch (e) {
   1920      // Get the current navigation object for the browsing context.
   1921      const navigation =
   1922        this.messageHandler.navigationManager.getNavigationForBrowsingContext(
   1923          webProgress.browsingContext
   1924        );
   1925 
   1926      // NavigationError with isBindingAborted represent navigations aborted by
   1927      // another navigation. If the navigation was committed and matches the
   1928      // navigationId, consider the navigation as successful.
   1929      if (
   1930        e?.isNavigationError &&
   1931        e.isBindingAborted &&
   1932        navigation &&
   1933        navigation.committed &&
   1934        navigation.navigationId == navigationId
   1935      ) {
   1936        return {
   1937          navigation: navigationId,
   1938          url: navigation.url,
   1939        };
   1940      }
   1941 
   1942      // Otherwise, bubble the error from the Navigation helper.
   1943      throw e;
   1944    } finally {
   1945      if (listener.isStarted) {
   1946        listener.stop();
   1947      }
   1948      listener.destroy();
   1949 
   1950      if (wait === WaitCondition.Interactive) {
   1951        await this.messageHandler.eventsDispatcher.off(
   1952          "browsingContext._documentInteractive",
   1953          contextDescriptor,
   1954          onDocumentInteractive
   1955        );
   1956      }
   1957    }
   1958  }
   1959 
   1960  /**
   1961   * Wrapper around RootBiDiModule._emitEventForBrowsingContext to additionally
   1962   * check that the payload of the event contains a valid `context` id.
   1963   *
   1964   * All browsingContext module events should have such a property set, and a
   1965   * missing id usually indicates that the browsing context which triggered the
   1966   * event is out of scope for the current WebDriver BiDi session (eg. chrome or
   1967   * webextension).
   1968   *
   1969   * @param {string} browsingContextId
   1970   *     The ID of the browsing context to which the event should be emitted.
   1971   * @param {string} eventName
   1972   *     The name of the event to be emitted.
   1973   * @param {object} eventPayload
   1974   *     The payload to be sent with the event.
   1975   * @param {number|string} eventPayload.context
   1976   *     A unique context id computed by the TabManager.
   1977   */
   1978  #emitContextEventForBrowsingContext(
   1979    browsingContextId,
   1980    eventName,
   1981    eventPayload
   1982  ) {
   1983    // All browsingContext events should include a context id in the payload.
   1984    const { context = null } = eventPayload;
   1985    if (context === null) {
   1986      // If the context could not be found by the TabManager, the event is most
   1987      // likely related to an unsupported context: eg chrome (bug 1722679) or
   1988      // webextension (bug 1755014).
   1989      lazy.logger.trace(
   1990        `[${browsingContextId}] Skipping event ${eventName} because of a missing unique context id`
   1991      );
   1992      return;
   1993    }
   1994 
   1995    this._emitEventForBrowsingContext(
   1996      browsingContextId,
   1997      eventName,
   1998      eventPayload
   1999    );
   2000  }
   2001 
   2002  #hasConfigurationForContext(userContext) {
   2003    const internalId = lazy.UserContextManager.getInternalIdById(userContext);
   2004    const contextDescriptor = {
   2005      type: lazy.ContextDescriptorType.UserContext,
   2006      id: internalId,
   2007    };
   2008    return this.messageHandler.sessionData.hasSessionData(
   2009      "_configuration",
   2010      undefined,
   2011      contextDescriptor
   2012    );
   2013  }
   2014 
   2015  #onContextAttached = async (eventName, data = {}) => {
   2016    if (this.#subscribedEvents.has("browsingContext.contextCreated")) {
   2017      const { browsingContext, why } = data;
   2018 
   2019      // Filter out top-level browsing contexts that are created because of a
   2020      // cross-group navigation.
   2021      if (why === "replace") {
   2022        return;
   2023      }
   2024 
   2025      // TODO: Bug 1852941. We should also filter out events which are emitted
   2026      // for DevTools frames.
   2027 
   2028      // Filter out notifications for chrome context until support gets
   2029      // added (bug 1722679).
   2030      if (!browsingContext.webProgress) {
   2031        return;
   2032      }
   2033 
   2034      // Filter out notifications for webextension contexts until support gets
   2035      // added (bug 1755014).
   2036      if (browsingContext.currentRemoteType === "extension") {
   2037        return;
   2038      }
   2039 
   2040      const browsingContextInfo = getBrowsingContextInfo(browsingContext, {
   2041        maxDepth: 0,
   2042      });
   2043 
   2044      this.#emitContextEventForBrowsingContext(
   2045        browsingContext.id,
   2046        "browsingContext.contextCreated",
   2047        browsingContextInfo
   2048      );
   2049 
   2050      // This is an internal event is used by the script module
   2051      // to ensure that "script.realmCreated" event is emitted
   2052      // after "browsingContext.contextCreated".
   2053      this.messageHandler.emitEvent(
   2054        "browsingContext._contextCreatedEmitted",
   2055        { browsingContext },
   2056        browsingContextInfo
   2057      );
   2058    }
   2059  };
   2060 
   2061  #onContextDiscarded = async (eventName, data = {}) => {
   2062    if (this.#subscribedEvents.has("browsingContext.contextDestroyed")) {
   2063      const { browsingContext, why } = data;
   2064 
   2065      // Filter out top-level browsing contexts that are destroyed because of a
   2066      // cross-group navigation.
   2067      if (why === "replace") {
   2068        return;
   2069      }
   2070 
   2071      // TODO: Bug 1852941. We should also filter out events which are emitted
   2072      // for DevTools frames.
   2073 
   2074      // Filter out notifications for chrome context until support gets
   2075      // added (bug 1722679).
   2076      if (!browsingContext.webProgress) {
   2077        return;
   2078      }
   2079 
   2080      // Filter out notifications for webextension contexts until support gets
   2081      // added (bug 1755014).
   2082      if (browsingContext.currentRemoteType === "extension") {
   2083        return;
   2084      }
   2085 
   2086      // If this event is for a child context whose top or parent context is also destroyed,
   2087      // we don't need to send it, in this case the event for the top/parent context is enough.
   2088      if (
   2089        browsingContext.parent &&
   2090        (browsingContext.top.isDiscarded || browsingContext.parent.isDiscarded)
   2091      ) {
   2092        return;
   2093      }
   2094 
   2095      const browsingContextInfo = getBrowsingContextInfo(browsingContext);
   2096 
   2097      this.#emitContextEventForBrowsingContext(
   2098        browsingContext.id,
   2099        "browsingContext.contextDestroyed",
   2100        browsingContextInfo
   2101      );
   2102    }
   2103  };
   2104 
   2105  #onDownloadEnd = async (eventName, data) => {
   2106    if (this.#subscribedEvents.has("browsingContext.downloadEnd")) {
   2107      const {
   2108        canceled,
   2109        contextId,
   2110        filepath,
   2111        navigableId,
   2112        navigationId,
   2113        timestamp,
   2114        url,
   2115      } = data;
   2116 
   2117      const browsingContextInfo = {
   2118        context: navigableId,
   2119        navigation: navigationId,
   2120        status: canceled
   2121          ? DownloadEndStatus.canceled
   2122          : DownloadEndStatus.complete,
   2123        timestamp,
   2124        url,
   2125      };
   2126 
   2127      if (!canceled) {
   2128        // Note: filepath should not be set for canceled downloads.
   2129        // https://www.w3.org/TR/webdriver-bidi/#cddl-type-browsingcontextdownloadcanceledparams
   2130        browsingContextInfo.filepath = filepath;
   2131      }
   2132 
   2133      this.#emitContextEventForBrowsingContext(
   2134        contextId,
   2135        "browsingContext.downloadEnd",
   2136        browsingContextInfo
   2137      );
   2138    }
   2139  };
   2140 
   2141  #onDownloadStarted = async (eventName, data) => {
   2142    if (this.#subscribedEvents.has("browsingContext.downloadWillBegin")) {
   2143      const {
   2144        contextId,
   2145        navigationId,
   2146        navigableId,
   2147        suggestedFilename,
   2148        timestamp,
   2149        url,
   2150      } = data;
   2151 
   2152      const browsingContextInfo = {
   2153        context: navigableId,
   2154        navigation: navigationId,
   2155        suggestedFilename,
   2156        timestamp,
   2157        url,
   2158      };
   2159 
   2160      this.#emitContextEventForBrowsingContext(
   2161        contextId,
   2162        "browsingContext.downloadWillBegin",
   2163        browsingContextInfo
   2164      );
   2165    }
   2166  };
   2167 
   2168  #onFragmentNavigated = async (eventName, data) => {
   2169    if (this.#subscribedEvents.has("browsingContext.fragmentNavigated")) {
   2170      const { contextId, navigationId, navigableId, url } = data;
   2171 
   2172      const browsingContextInfo = {
   2173        context: navigableId,
   2174        navigation: navigationId,
   2175        timestamp: Date.now(),
   2176        url,
   2177      };
   2178 
   2179      this.#emitContextEventForBrowsingContext(
   2180        contextId,
   2181        "browsingContext.fragmentNavigated",
   2182        browsingContextInfo
   2183      );
   2184    }
   2185  };
   2186 
   2187  #onHistoryUpdated = async (eventName, data) => {
   2188    if (this.#subscribedEvents.has("browsingContext.historyUpdated")) {
   2189      const { contextId, navigableId, url } = data;
   2190 
   2191      const browsingContextInfo = {
   2192        context: navigableId,
   2193        timestamp: Date.now(),
   2194        url,
   2195      };
   2196 
   2197      this.#emitContextEventForBrowsingContext(
   2198        contextId,
   2199        "browsingContext.historyUpdated",
   2200        browsingContextInfo
   2201      );
   2202    }
   2203  };
   2204 
   2205  #onPromptClosed = (eventName, data) => {
   2206    if (this.#subscribedEvents.has("browsingContext.userPromptClosed")) {
   2207      const { contentBrowser, detail } = data;
   2208      // TODO: Bug 2007385. Use only browsingContext from event details when the support for Android is added.
   2209      const browsingContext = lazy.AppInfo.isAndroid
   2210        ? contentBrowser.browsingContext
   2211        : detail.browsingContext;
   2212 
   2213      const navigableId =
   2214        lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
   2215 
   2216      if (navigableId === null) {
   2217        return;
   2218      }
   2219 
   2220      lazy.logger.trace(
   2221        `[${browsingContext.id}] Prompt closed (type: "${
   2222          detail.promptType
   2223        }", accepted: "${detail.accepted}")`
   2224      );
   2225 
   2226      const params = {
   2227        context: navigableId,
   2228        accepted: detail.accepted,
   2229        type: detail.promptType,
   2230        userText: detail.userText,
   2231      };
   2232 
   2233      this.#emitContextEventForBrowsingContext(
   2234        browsingContext.id,
   2235        "browsingContext.userPromptClosed",
   2236        params
   2237      );
   2238    }
   2239  };
   2240 
   2241  #onPromptOpened = async (eventName, data) => {
   2242    if (this.#subscribedEvents.has("browsingContext.userPromptOpened")) {
   2243      const { contentBrowser, prompt } = data;
   2244      const type = prompt.promptType;
   2245 
   2246      // TODO: Bug 2007385. We can remove this fallback
   2247      // when we have support for browsing context property on Android.
   2248      const browsingContext = lazy.AppInfo.isAndroid
   2249        ? contentBrowser.browsingContext
   2250        : data.browsingContext;
   2251 
   2252      prompt.getText().then(text => {
   2253        // We need the text to identify a user prompt when it gets
   2254        // randomly opened. Because on Android the text is asynchronously
   2255        // retrieved lets delay the logging without making the handler async.
   2256        lazy.logger.trace(
   2257          `[${browsingContext.id}] Prompt opened (type: "${
   2258            prompt.promptType
   2259          }", text: "${text}")`
   2260        );
   2261      });
   2262 
   2263      // Do not send opened event for unsupported prompt types.
   2264      if (!(type in UserPromptType)) {
   2265        lazy.logger.trace(`Prompt type "${type}" not supported`);
   2266        return;
   2267      }
   2268 
   2269      const navigableId =
   2270        lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
   2271 
   2272      const session = lazy.getWebDriverSessionById(
   2273        this.messageHandler.sessionId
   2274      );
   2275      const handlerConfig = session.userPromptHandler.getPromptHandler(type);
   2276 
   2277      const eventPayload = {
   2278        context: navigableId,
   2279        handler: handlerConfig.handler,
   2280        message: await prompt.getText(),
   2281        type,
   2282      };
   2283 
   2284      if (type === "prompt") {
   2285        eventPayload.defaultValue = await prompt.getInputText();
   2286      }
   2287 
   2288      this.#emitContextEventForBrowsingContext(
   2289        browsingContext.id,
   2290        "browsingContext.userPromptOpened",
   2291        eventPayload
   2292      );
   2293    }
   2294  };
   2295 
   2296  #onNavigationCommitted = async (eventName, data) => {
   2297    if (this.#subscribedEvents.has("browsingContext.navigationCommitted")) {
   2298      const { contextId, navigableId, navigationId, url } = data;
   2299 
   2300      const eventPayload = {
   2301        context: navigableId,
   2302        navigation: navigationId,
   2303        timestamp: Date.now(),
   2304        url,
   2305      };
   2306 
   2307      this.#emitContextEventForBrowsingContext(
   2308        contextId,
   2309        "browsingContext.navigationCommitted",
   2310        eventPayload
   2311      );
   2312    }
   2313  };
   2314 
   2315  #onNavigationFailed = async (eventName, data) => {
   2316    if (this.#subscribedEvents.has("browsingContext.navigationFailed")) {
   2317      const { contextId, navigableId, navigationId, url } = data;
   2318 
   2319      const eventPayload = {
   2320        context: navigableId,
   2321        navigation: navigationId,
   2322        timestamp: Date.now(),
   2323        url,
   2324      };
   2325 
   2326      this.#emitContextEventForBrowsingContext(
   2327        contextId,
   2328        "browsingContext.navigationFailed",
   2329        eventPayload
   2330      );
   2331    }
   2332  };
   2333 
   2334  #onNavigationStarted = async (eventName, data) => {
   2335    if (this.#subscribedEvents.has("browsingContext.navigationStarted")) {
   2336      const { contextId, navigableId, navigationId, url } = data;
   2337 
   2338      const eventPayload = {
   2339        context: navigableId,
   2340        navigation: navigationId,
   2341        timestamp: Date.now(),
   2342        url,
   2343      };
   2344 
   2345      this.#emitContextEventForBrowsingContext(
   2346        contextId,
   2347        "browsingContext.navigationStarted",
   2348        eventPayload
   2349      );
   2350    }
   2351  };
   2352 
   2353  #onPageHideEvent = (name, eventPayload) => {
   2354    const { context } = eventPayload;
   2355    if (context.parent) {
   2356      this.#onContextDiscarded("windowglobal-pagehide", {
   2357        browsingContext: context,
   2358      });
   2359    }
   2360  };
   2361 
   2362  #stopListeningToContextEvent(event) {
   2363    this.#subscribedEvents.delete(event);
   2364 
   2365    const hasContextEvent =
   2366      this.#subscribedEvents.has("browsingContext.contextCreated") ||
   2367      this.#subscribedEvents.has("browsingContext.contextDestroyed");
   2368 
   2369    if (!hasContextEvent) {
   2370      this.#contextListener.stopListening();
   2371    }
   2372  }
   2373 
   2374  #stopListeningToNavigationEvent(event) {
   2375    this.#subscribedEvents.delete(event);
   2376 
   2377    const hasNavigationEvent =
   2378      this.#subscribedEvents.has("browsingContext.downloadEnd") ||
   2379      this.#subscribedEvents.has("browsingContext.downloadWillBegin") ||
   2380      this.#subscribedEvents.has("browsingContext.fragmentNavigated") ||
   2381      this.#subscribedEvents.has("browsingContext.historyUpdated") ||
   2382      this.#subscribedEvents.has("browsingContext.navigationFailed") ||
   2383      this.#subscribedEvents.has("browsingContext.navigationStarted");
   2384 
   2385    if (!hasNavigationEvent) {
   2386      this.#navigationListener.stopListening();
   2387    }
   2388  }
   2389 
   2390  #stopListeningToPromptEvent(event) {
   2391    this.#subscribedEvents.delete(event);
   2392 
   2393    const hasPromptEvent =
   2394      this.#subscribedEvents.has("browsingContext.userPromptClosed") ||
   2395      this.#subscribedEvents.has("browsingContext.userPromptOpened");
   2396 
   2397    if (!hasPromptEvent) {
   2398      this.#promptListener.stopListening();
   2399    }
   2400  }
   2401 
   2402  #subscribeEvent(event) {
   2403    switch (event) {
   2404      case "browsingContext.contextCreated":
   2405      case "browsingContext.contextDestroyed": {
   2406        this.#contextListener.startListening();
   2407        this.#subscribedEvents.add(event);
   2408        break;
   2409      }
   2410      case "browsingContext.downloadEnd":
   2411      case "browsingContext.downloadWillBegin":
   2412      case "browsingContext.fragmentNavigated":
   2413      case "browsingContext.historyUpdated":
   2414      case "browsingContext.navigationCommitted":
   2415      case "browsingContext.navigationFailed":
   2416      case "browsingContext.navigationStarted": {
   2417        this.#navigationListener.startListening();
   2418        this.#subscribedEvents.add(event);
   2419        break;
   2420      }
   2421      case "browsingContext.userPromptClosed":
   2422      case "browsingContext.userPromptOpened": {
   2423        this.#promptListener.startListening();
   2424        this.#subscribedEvents.add(event);
   2425        break;
   2426      }
   2427    }
   2428  }
   2429 
   2430  #unsubscribeEvent(event) {
   2431    switch (event) {
   2432      case "browsingContext.contextCreated":
   2433      case "browsingContext.contextDestroyed": {
   2434        this.#stopListeningToContextEvent(event);
   2435        break;
   2436      }
   2437      case "browsingContext.downloadEnd":
   2438      case "browsingContext.downloadWillBegin":
   2439      case "browsingContext.fragmentNavigated":
   2440      case "browsingContext.historyUpdated":
   2441      case "browsingContext.navigationCommitted":
   2442      case "browsingContext.navigationFailed":
   2443      case "browsingContext.navigationStarted": {
   2444        this.#stopListeningToNavigationEvent(event);
   2445        break;
   2446      }
   2447      case "browsingContext.userPromptClosed":
   2448      case "browsingContext.userPromptOpened": {
   2449        this.#stopListeningToPromptEvent(event);
   2450        break;
   2451      }
   2452    }
   2453  }
   2454 
   2455  #waitForVisibilityState(browsingContext, expectedState, options = {}) {
   2456    const { timeout } = options;
   2457    return this._forwardToWindowGlobal(
   2458      "_awaitVisibilityState",
   2459      browsingContext.id,
   2460      { value: expectedState, timeout },
   2461      { retryOnAbort: true }
   2462    );
   2463  }
   2464 
   2465  /**
   2466   * Internal commands
   2467   */
   2468 
   2469  _applySessionData(params) {
   2470    // TODO: Bug 1775231. Move this logic to a shared module or an abstract
   2471    // class.
   2472    const { category } = params;
   2473    if (category === "event") {
   2474      const filteredSessionData = params.sessionData.filter(item =>
   2475        this.messageHandler.matchesContext(item.contextDescriptor)
   2476      );
   2477      for (const event of this.#subscribedEvents.values()) {
   2478        const hasSessionItem = filteredSessionData.some(
   2479          item => item.value === event
   2480        );
   2481        // If there are no session items for this context, we should unsubscribe from the event.
   2482        if (!hasSessionItem) {
   2483          this.#unsubscribeEvent(event);
   2484        }
   2485      }
   2486 
   2487      // Subscribe to all events, which have an item in SessionData.
   2488      for (const { value } of filteredSessionData) {
   2489        this.#subscribeEvent(value);
   2490      }
   2491    }
   2492  }
   2493 
   2494  /**
   2495   * Communicate to this module that the _ConfigurationModule is done.
   2496   *
   2497   * @param {BrowsingContext} navigable
   2498   *     Browsing context for which the configuration completed.
   2499   */
   2500  _onConfigurationComplete({ navigable }) {
   2501    const browser = navigable.embedderElement;
   2502 
   2503    if (!this.#blockedCreateCommands.has(browser)) {
   2504      this.#blockedCreateCommands.set(browser, Promise.withResolvers());
   2505    }
   2506 
   2507    const blocker = this.#blockedCreateCommands.get(browser);
   2508    blocker.resolve();
   2509  }
   2510 
   2511  /**
   2512   * Update the viewport of the navigable.
   2513   *
   2514   * @param {object} options
   2515   * @param {BrowsingContext} options.navigable
   2516   *     Navigable whose viewport should be updated.
   2517   * @param {ViewportOverride} options.viewportOverride
   2518   *     Object which holds viewport settings
   2519   *     which should be applied.
   2520   */
   2521  async _updateNavigableViewport(options) {
   2522    const { navigable, viewportOverride } = options;
   2523    const { devicePixelRatio, viewport } = viewportOverride;
   2524 
   2525    const browser = navigable.embedderElement;
   2526    const currentHeight = browser.clientHeight;
   2527    const currentWidth = browser.clientWidth;
   2528 
   2529    let targetHeight, targetWidth;
   2530    if (viewport === undefined) {
   2531      // Don't modify the viewport's size.
   2532      targetHeight = currentHeight;
   2533      targetWidth = currentWidth;
   2534    } else if (viewport === null) {
   2535      // Reset viewport to the original dimensions.
   2536      targetHeight = browser.parentElement.clientHeight;
   2537      targetWidth = browser.parentElement.clientWidth;
   2538 
   2539      browser.style.removeProperty("height");
   2540      browser.style.removeProperty("width");
   2541    } else {
   2542      const { height, width } = viewport;
   2543 
   2544      targetHeight = height;
   2545      targetWidth = width;
   2546 
   2547      browser.style.setProperty("height", targetHeight + "px");
   2548      browser.style.setProperty("width", targetWidth + "px");
   2549    }
   2550 
   2551    if (devicePixelRatio !== undefined) {
   2552      if (devicePixelRatio !== null) {
   2553        navigable.overrideDPPX = devicePixelRatio;
   2554      } else {
   2555        // Will reset to use the global default scaling factor.
   2556        navigable.overrideDPPX = 0;
   2557      }
   2558    }
   2559 
   2560    if (targetHeight !== currentHeight || targetWidth !== currentWidth) {
   2561      if (!navigable.isActive) {
   2562        // Force a synchronous update of the remote browser dimensions so that
   2563        // background tabs get resized.
   2564        browser.ownerDocument.synchronouslyUpdateRemoteBrowserDimensions(
   2565          /* aIncludeInactive = */ true
   2566        );
   2567      }
   2568      // Wait until the viewport has been resized
   2569      await this._forwardToWindowGlobal(
   2570        "_awaitViewportDimensions",
   2571        navigable.id,
   2572        {
   2573          height: targetHeight,
   2574          width: targetWidth,
   2575        },
   2576        { retryOnAbort: true }
   2577      );
   2578    }
   2579  }
   2580 
   2581  static get supportedEvents() {
   2582    return [
   2583      "browsingContext.contextCreated",
   2584      "browsingContext.contextDestroyed",
   2585      "browsingContext.domContentLoaded",
   2586      "browsingContext.downloadEnd",
   2587      "browsingContext.downloadWillBegin",
   2588      "browsingContext.fragmentNavigated",
   2589      "browsingContext.historyUpdated",
   2590      "browsingContext.load",
   2591      "browsingContext.navigationCommitted",
   2592      "browsingContext.navigationFailed",
   2593      "browsingContext.navigationStarted",
   2594      "browsingContext.userPromptClosed",
   2595      "browsingContext.userPromptOpened",
   2596    ];
   2597  }
   2598 }
   2599 
   2600 /**
   2601 * Get the WebDriver BiDi browsing context information.
   2602 *
   2603 * @param {BrowsingContext} context
   2604 *     The browsing context to get the information from.
   2605 * @param {object=} options
   2606 * @param {boolean=} options.includeParentId
   2607 *     Flag that indicates if the parent ID should be included.
   2608 *     Defaults to true.
   2609 * @param {number=} options.maxDepth
   2610 *     Depth of the browsing context tree to traverse. If not specified
   2611 *     the whole tree is returned.
   2612 *
   2613 * @returns {BrowsingContextInfo}
   2614 *     The information about the browsing context.
   2615 */
   2616 export const getBrowsingContextInfo = (context, options = {}) => {
   2617  const { includeParentId = true, maxDepth = null } = options;
   2618 
   2619  let children = null;
   2620  if (maxDepth === null || maxDepth > 0) {
   2621    // Bug 1996311: When executed for chrome browsing contexts as
   2622    // well include embedded browsers and their browsing context tree.
   2623    children = context.children.map(childContext =>
   2624      getBrowsingContextInfo(childContext, {
   2625        maxDepth: maxDepth === null ? maxDepth : maxDepth - 1,
   2626        includeParentId: false,
   2627      })
   2628    );
   2629  }
   2630 
   2631  const chromeWindow =
   2632    lazy.windowManager.getChromeWindowForBrowsingContext(context);
   2633  const originalOpener =
   2634    context.crossGroupOpener !== null
   2635      ? lazy.NavigableManager.getIdForBrowsingContext(context.crossGroupOpener)
   2636      : null;
   2637  const userContext = lazy.UserContextManager.getIdByBrowsingContext(context);
   2638 
   2639  const contextInfo = {
   2640    children,
   2641    context: lazy.NavigableManager.getIdForBrowsingContext(context),
   2642    // TODO: Bug 1904641. If a browsing context was not tracked in TabManager,
   2643    // because it was created and discarded before the WebDriver BiDi session was
   2644    // started, we get undefined as id for this browsing context.
   2645    // We should remove this condition, when we can provide a correct id here.
   2646    originalOpener: originalOpener === undefined ? null : originalOpener,
   2647    url: context.currentURI.spec,
   2648    userContext,
   2649    clientWindow: lazy.windowManager.getIdForWindow(chromeWindow),
   2650  };
   2651 
   2652  if (includeParentId) {
   2653    // Only emit the parent id for the top-most browsing context.
   2654    const parentId = lazy.NavigableManager.getIdForBrowsingContext(
   2655      context.parent
   2656    );
   2657    contextInfo.parent = parentId;
   2658  }
   2659 
   2660  if (lazy.RemoteAgent.allowSystemAccess) {
   2661    contextInfo["moz:scope"] = context.isContent
   2662      ? MozContextScope.CONTENT
   2663      : MozContextScope.CHROME;
   2664 
   2665    if ("name" in context) {
   2666      contextInfo["moz:name"] = context.name;
   2667    }
   2668  }
   2669 
   2670  return contextInfo;
   2671 };
   2672 
   2673 export const browsingContext = BrowsingContextModule;