tor-browser

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

UrlbarTestUtils.sys.mjs (58531B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      5 
      6 import {
      7  UrlbarProvider,
      8  UrlbarUtils,
      9 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     10 
     11 const lazy = {};
     12 
     13 ChromeUtils.defineESModuleGetters(lazy, {
     14  BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs",
     15  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     16  DEFAULT_FORM_HISTORY_PARAM:
     17    "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     18  ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
     19  FormHistoryTestUtils:
     20    "resource://testing-common/FormHistoryTestUtils.sys.mjs",
     21  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     22  NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
     23  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     24  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
     25  UrlbarController:
     26    "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs",
     27  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     28  UrlbarSearchUtils:
     29    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     30  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     31 });
     32 
     33 /**
     34 * Utility class for testing <html:moz-urlbar> elements.
     35 */
     36 class UrlbarInputTestUtils {
     37  /**
     38   * @param {(window: ChromeWindow) => UrlbarInput} getUrlbarInputForWindow
     39   */
     40  constructor(getUrlbarInputForWindow) {
     41    this.#urlbar = getUrlbarInputForWindow;
     42  }
     43 
     44  #urlbar;
     45 
     46  /**
     47   * This maps the categories used by the FX_SEARCHBAR_SELECTED_RESULT_METHOD
     48   * histogram to its indexes in the `labels` array. This only needs to be
     49   * used by tests that need to map from category names to indexes in histogram
     50   * snapshots. Actual app code can use these category names directly when
     51   * they add to a histogram.
     52   */
     53  SELECTED_RESULT_METHODS = {
     54    enter: 0,
     55    enterSelection: 1,
     56    click: 2,
     57    arrowEnterSelection: 3,
     58    tabEnterSelection: 4,
     59    rightClickEnter: 5,
     60  };
     61 
     62  // Fallback to the console.
     63  info = console.log;
     64 
     65  /**
     66   * Running this init allows helpers to access test scope helpers, like Assert
     67   * and SimpleTest. Note this initialization is not enforced, thus helpers
     68   * should always check the properties set here and provide a fallback path.
     69   *
     70   * @param {object} scope The global scope where tests are being run.
     71   */
     72  init(scope) {
     73    if (!scope) {
     74      throw new Error("Must initialize UrlbarInputTestUtils with a test scope");
     75    }
     76    // If you add other properties to `this`, null them in uninit().
     77    this.Assert = scope.Assert;
     78    this.info = scope.info;
     79    this.registerCleanupFunction = scope.registerCleanupFunction;
     80 
     81    if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
     82      this.initXPCShellDependencies();
     83    } else {
     84      // xpcshell doesn't support EventUtils.
     85      this.EventUtils = scope.EventUtils;
     86      this.SimpleTest = scope.SimpleTest;
     87    }
     88 
     89    this.registerCleanupFunction(() => {
     90      this.Assert = null;
     91      this.info = console.log;
     92      this.registerCleanupFunction = null;
     93      this.EventUtils = null;
     94      this.SimpleTest = null;
     95    });
     96  }
     97 
     98  /**
     99   * Waits to a search to be complete.
    100   *
    101   * @param {ChromeWindow} win The window containing the urlbar
    102   */
    103  async promiseSearchComplete(win) {
    104    let waitForQuery = () => {
    105      return this.promisePopupOpen(win, () => {}).then(
    106        () => this.#urlbar(win).lastQueryContextPromise
    107      );
    108    };
    109    /** @type {UrlbarQueryContext} */
    110    let context = await waitForQuery();
    111    if (this.#urlbar(win).searchMode) {
    112      // Search mode may start a second query.
    113      context = await waitForQuery();
    114    }
    115    if (this.#urlbar(win).view.oneOffSearchButtons?._rebuilding) {
    116      await new Promise(resolve =>
    117        this.#urlbar(win).view.oneOffSearchButtons.addEventListener(
    118          "rebuild",
    119          resolve,
    120          {
    121            once: true,
    122          }
    123        )
    124      );
    125    }
    126    return context;
    127  }
    128 
    129  /**
    130   * Starts a search for a given string and waits for the search to be complete.
    131   *
    132   * @param {object} options The options object.
    133   * @param {ChromeWindow} options.window The window containing the urlbar
    134   * @param {string} options.value the search string
    135   * @param {Function} options.waitForFocus The SimpleTest function
    136   * @param {boolean} [options.fireInputEvent] whether an input event should be
    137   *        used when starting the query (simulates the user's typing, sets
    138   *        userTypedValued, triggers engagement event telemetry, etc.)
    139   * @param {number} [options.selectionStart] The input's selectionStart
    140   * @param {number} [options.selectionEnd] The input's selectionEnd
    141   * @param {boolean} [options.reopenOnBlur] Whether this method should repoen
    142   *        the view if the input is blurred before the query finishes. This is
    143   *        necessary to work around spurious blurs in CI, which close the view
    144   *        and cancel the query, defeating the typical use of this method where
    145   *        your test waits for the query to finish. However, this behavior
    146   *        isn't always desired, for example if your test intentionally blurs
    147   *        the input before the query finishes. In that case, pass false.
    148   * @returns {Promise}
    149   *   The promise for the last query context.
    150   */
    151  async promiseAutocompleteResultPopup({
    152    window,
    153    value,
    154    waitForFocus,
    155    fireInputEvent = true,
    156    selectionStart = -1,
    157    selectionEnd = -1,
    158    reopenOnBlur = true,
    159  }) {
    160    if (this.SimpleTest) {
    161      await this.SimpleTest.promiseFocus(window);
    162    } else {
    163      await new Promise(resolve => waitForFocus(resolve, window));
    164    }
    165 
    166    const setup = () => {
    167      this.#urlbar(window).focus();
    168      // Using the value setter in some cases may trim and fetch unexpected
    169      // results, then pick an alternate path.
    170      if (
    171        lazy.UrlbarPrefs.get("trimURLs") &&
    172        value != lazy.BrowserUIUtils.trimURL(value)
    173      ) {
    174        this.#urlbar(window)._setValue(value);
    175        fireInputEvent = true;
    176      } else {
    177        this.#urlbar(window).value = value;
    178      }
    179      if (selectionStart >= 0 && selectionEnd >= 0) {
    180        this.#urlbar(window).selectionEnd = selectionEnd;
    181        this.#urlbar(window).selectionStart = selectionStart;
    182      }
    183 
    184      // An input event will start a new search, so be careful not to start a
    185      // search if we fired an input event since that would start two searches.
    186      if (fireInputEvent) {
    187        // This is necessary to get the urlbar to set gBrowser.userTypedValue.
    188        this.fireInputEvent(window);
    189      } else {
    190        this.#urlbar(window).setPageProxyState("invalid");
    191        this.#urlbar(window).startQuery();
    192      }
    193    };
    194    setup();
    195 
    196    // In Linux TV test, as there is case that the input field lost the focus
    197    // until showing popup, timeout failure happens since the expected poup
    198    // never be shown. To avoid this, if losing the focus, retry setup to open
    199    // popup.
    200    if (reopenOnBlur) {
    201      this.#urlbar(window).inputField.addEventListener("blur", setup, {
    202        once: true,
    203      });
    204    }
    205    const result = await this.promiseSearchComplete(window);
    206    if (reopenOnBlur) {
    207      this.#urlbar(window).inputField.removeEventListener("blur", setup);
    208    }
    209    return result;
    210  }
    211 
    212  /**
    213   * Waits for a result to be added at a certain index. Since we implement lazy
    214   * results replacement, even if we have a result at an index, it may be
    215   * related to the previous query, this methods ensures the result is current.
    216   *
    217   * @param {ChromeWindow} win The window containing the urlbar
    218   * @param {number} index The index to look for
    219   * @throws {Error} When the index exceeds the number of available results
    220   */
    221  async waitForAutocompleteResultAt(win, index) {
    222    // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement.
    223    await this.promiseSearchComplete(win);
    224    let container = this.getResultsContainer(win);
    225    if (index >= container.children.length) {
    226      throw new Error("Not enough results");
    227    }
    228    return container.children[index];
    229  }
    230 
    231  /**
    232   * Returns the oneOffSearchButtons object for the urlbar.
    233   *
    234   * @param {ChromeWindow} win The window containing the urlbar
    235   * @returns {object} The oneOffSearchButtons
    236   */
    237  getOneOffSearchButtons(win) {
    238    return this.#urlbar(win).view.oneOffSearchButtons;
    239  }
    240 
    241  /**
    242   * Returns a specific button of a result.
    243   *
    244   * @param {ChromeWindow} win The window containing the urlbar
    245   * @param {string} buttonName The name of the button, e.g. "menu", "0", etc.
    246   * @param {number} resultIndex The index of the result
    247   * @returns {HTMLSpanElement} The button
    248   */
    249  getButtonForResultIndex(win, buttonName, resultIndex) {
    250    return this.getRowAt(win, resultIndex).querySelector(
    251      `.urlbarView-button-${buttonName}`
    252    );
    253  }
    254 
    255  /**
    256   * Show the result menu button regardless of the result being hovered or
    257   + selected.
    258   *
    259   * @param {ChromeWindow} win The window containing the urlbar
    260   */
    261  disableResultMenuAutohide(win) {
    262    let container = this.getResultsContainer(win);
    263    let attr = "disable-resultmenu-autohide";
    264    container.toggleAttribute(attr, true);
    265    this.registerCleanupFunction?.(() => {
    266      container.toggleAttribute(attr, false);
    267    });
    268  }
    269 
    270  /**
    271   * Opens the result menu of a specific result.
    272   *
    273   * @param {ChromeWindow} win The window containing the urlbar
    274   * @param {object} [options] The options object.
    275   * @param {number} [options.resultIndex] The index of the result. Defaults
    276   *        to the current selected index.
    277   * @param {boolean} [options.byMouse] Whether to open the menu by mouse or
    278   *        keyboard.
    279   * @param {string} [options.activationKey] Key to activate the button with,
    280   *        defaults to KEY_Enter.
    281   */
    282  async openResultMenu(
    283    win,
    284    {
    285      resultIndex = this.#urlbar(win).view.selectedRowIndex,
    286      byMouse = false,
    287      activationKey = "KEY_Enter",
    288    } = {}
    289  ) {
    290    this.Assert?.ok(this.#urlbar(win).view.isOpen, "view should be open");
    291    let menuButton = this.getButtonForResultIndex(
    292      win,
    293      "result-menu",
    294      resultIndex
    295    );
    296    this.Assert?.ok(
    297      menuButton,
    298      `found the menu button at result index ${resultIndex}`
    299    );
    300    let promiseMenuOpen = lazy.BrowserTestUtils.waitForEvent(
    301      this.#urlbar(win).view.resultMenu,
    302      "popupshown"
    303    );
    304    if (byMouse) {
    305      this.info(
    306        `synthesizing mousemove on row to make the menu button visible`
    307      );
    308      await this.EventUtils.promiseElementReadyForUserInput(
    309        menuButton.closest(".urlbarView-row"),
    310        win,
    311        this.info
    312      );
    313      this.info(`got mousemove, now clicking the menu button`);
    314      this.EventUtils.synthesizeMouseAtCenter(menuButton, {}, win);
    315      this.info(`waiting for the menu popup to open via mouse`);
    316    } else {
    317      this.info(`selecting the result at index ${resultIndex}`);
    318      while (this.#urlbar(win).view.selectedRowIndex != resultIndex) {
    319        this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
    320      }
    321      if (this.getSelectedElement(win) != menuButton) {
    322        this.EventUtils.synthesizeKey("KEY_Tab", {}, win);
    323      }
    324      this.Assert?.equal(
    325        this.getSelectedElement(win),
    326        menuButton,
    327        `selected the menu button at result index ${resultIndex}`
    328      );
    329      this.EventUtils.synthesizeKey(activationKey, {}, win);
    330      this.info(`waiting for ${activationKey} to open the menu popup`);
    331    }
    332    await promiseMenuOpen;
    333    this.Assert?.equal(
    334      this.#urlbar(win).view.resultMenu.state,
    335      "open",
    336      "Checking popup state"
    337    );
    338  }
    339 
    340  /**
    341   * Opens the result menu of a specific result and gets a menu item by either
    342   * accesskey or command name. Either `accesskey` or `command` must be given.
    343   *
    344   * @param {object} options
    345   *   The options object.
    346   * @param {ChromeWindow} options.window
    347   *   The window containing the urlbar.
    348   * @param {string} [options.accesskey]
    349   *   The access key of the menu item to return.
    350   * @param {string} [options.command]
    351   *   The command name of the menu item to return.
    352   * @param {number} [options.resultIndex]
    353   *   The index of the result. Defaults to the current selected index.
    354   * @param {boolean} [options.openByMouse]
    355   *   Whether to open the menu by mouse or keyboard.
    356   * @param {Array} [options.submenuSelectors]
    357   *   If the command is in the top-level result menu, leave this as an empty
    358   *   array. If it's in a submenu, set this to an array where each element i is
    359   *   a selector that can be used to get the i'th menu item that opens a
    360   *   submenu.
    361   * @returns {Promise<XULElement>}
    362   *   Returns the menu item element.
    363   */
    364  async openResultMenuAndGetItem({
    365    window,
    366    accesskey,
    367    command,
    368    resultIndex = this.#urlbar(window).view.selectedRowIndex,
    369    openByMouse = false,
    370    submenuSelectors = [],
    371  }) {
    372    await this.openResultMenu(window, { resultIndex, byMouse: openByMouse });
    373 
    374    // Open the sequence of submenus that contains the item.
    375    for (let selector of submenuSelectors) {
    376      let menuitem =
    377        this.#urlbar(window).view.resultMenu.querySelector(selector);
    378      if (!menuitem) {
    379        throw new Error("Submenu item not found for selector: " + selector);
    380      }
    381 
    382      let promisePopup = lazy.BrowserTestUtils.waitForEvent(
    383        this.#urlbar(window).view.resultMenu,
    384        "popupshown"
    385      );
    386 
    387      if (AppConstants.platform == "macosx") {
    388        // Synthesized clicks don't work in the native Mac menu.
    389        this.info(
    390          "Calling openMenu() on submenu item with selector: " + selector
    391        );
    392        menuitem.openMenu(true);
    393      } else {
    394        this.info("Clicking submenu item with selector: " + selector);
    395        this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, window);
    396      }
    397 
    398      this.info("Waiting for submenu popupshown event");
    399      await promisePopup;
    400      this.info("Got the submenu popupshown event");
    401    }
    402 
    403    // Now get the item.
    404    let menuitem;
    405    if (accesskey) {
    406      await lazy.BrowserTestUtils.waitForCondition(() => {
    407        menuitem = this.#urlbar(window).view.resultMenu.querySelector(
    408          `menuitem[accesskey=${accesskey}]`
    409        );
    410        return menuitem;
    411      }, "Waiting for strings to load");
    412    } else if (command) {
    413      menuitem = this.#urlbar(window).view.resultMenu.querySelector(
    414        `menuitem[data-command=${command}]`
    415      );
    416    } else {
    417      throw new Error("accesskey or command must be specified");
    418    }
    419 
    420    return menuitem;
    421  }
    422 
    423  /**
    424   * Opens the result menu of a specific result and presses an access key to
    425   * activate a menu item.
    426   *
    427   * @param {ChromeWindow} win The window containing the urlbar
    428   * @param {string} accesskey The access key to press once the menu is open
    429   * @param {object} [options] The options object.
    430   * @param {number} [options.resultIndex] The index of the result. Defaults
    431   *        to the current selected index.
    432   * @param {boolean} [options.openByMouse] Whether to open the menu by mouse
    433   *        or keyboard.
    434   */
    435  async openResultMenuAndPressAccesskey(
    436    win,
    437    accesskey,
    438    {
    439      resultIndex = this.#urlbar(win).view.selectedRowIndex,
    440      openByMouse = false,
    441    } = {}
    442  ) {
    443    let menuitem = await this.openResultMenuAndGetItem({
    444      accesskey,
    445      resultIndex,
    446      openByMouse,
    447      window: win,
    448    });
    449    if (!menuitem) {
    450      throw new Error("Menu item not found for accesskey: " + accesskey);
    451    }
    452 
    453    let promiseCommand = lazy.BrowserTestUtils.waitForEvent(
    454      this.#urlbar(win).view.resultMenu,
    455      "command"
    456    );
    457 
    458    if (AppConstants.platform == "macosx") {
    459      // The native Mac menu doesn't support access keys.
    460      this.info("calling doCommand() to activate menu item");
    461      menuitem.doCommand();
    462      this.#urlbar(win).view.resultMenu.hidePopup(true);
    463    } else {
    464      this.info(`pressing access key (${accesskey}) to activate menu item`);
    465      this.EventUtils.synthesizeKey(accesskey, {}, win);
    466    }
    467 
    468    this.info("waiting for command event");
    469    await promiseCommand;
    470    this.info("got the command event");
    471  }
    472 
    473  /**
    474   * Opens the result menu of a specific result and clicks a menu item with a
    475   * specified command name.
    476   *
    477   * @param {ChromeWindow} win
    478   *   The window containing the urlbar.
    479   * @param {string|Array} commandOrArray
    480   *   If the command is in the top-level result menu, set this to the command
    481   *   name. If it's in a submenu, set this to an array where each element i is
    482   *   a selector that can be used to click the i'th menu item that opens a
    483   *   submenu, and the last element is the command name.
    484   * @param {object} options
    485   *   The options object.
    486   * @param {number} [options.resultIndex]
    487   *   The index of the result. Defaults to the current selected index.
    488   * @param {boolean} [options.openByMouse]
    489   *   Whether to open the menu by mouse or keyboard.
    490   */
    491  async openResultMenuAndClickItem(
    492    win,
    493    commandOrArray,
    494    {
    495      resultIndex = this.#urlbar(win).view.selectedRowIndex,
    496      openByMouse = false,
    497    } = {}
    498  ) {
    499    let submenuSelectors = Array.isArray(commandOrArray)
    500      ? commandOrArray
    501      : [commandOrArray];
    502    let command = submenuSelectors.pop();
    503 
    504    let menuitem = await this.openResultMenuAndGetItem({
    505      resultIndex,
    506      openByMouse,
    507      command,
    508      submenuSelectors,
    509      window: win,
    510    });
    511    if (!menuitem) {
    512      throw new Error("Menu item not found for command: " + command);
    513    }
    514 
    515    let promiseCommand = lazy.BrowserTestUtils.waitForEvent(
    516      this.#urlbar(win).view.resultMenu,
    517      "command"
    518    );
    519 
    520    if (AppConstants.platform == "macosx") {
    521      // Synthesized clicks don't work in the native Mac menu.
    522      this.info("calling doCommand() to activate menu item");
    523      menuitem.doCommand();
    524      this.#urlbar(win).view.resultMenu.hidePopup(true);
    525    } else {
    526      this.info("Clicking menu item with command: " + command);
    527      this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
    528    }
    529 
    530    this.info("Waiting for command event");
    531    await promiseCommand;
    532    this.info("Got the command event");
    533  }
    534 
    535  /**
    536   * Returns true if the oneOffSearchButtons are visible.
    537   *
    538   * @param {ChromeWindow} win The window containing the urlbar
    539   * @returns {boolean} True if the buttons are visible.
    540   */
    541  getOneOffSearchButtonsVisible(win) {
    542    let buttons = this.getOneOffSearchButtons(win);
    543    return buttons.style.display != "none" && !buttons.container.hidden;
    544  }
    545 
    546  /**
    547   * Gets an abstracted representation of the result at an index.
    548   *
    549   * @param {ChromeWindow} win The window containing the urlbar
    550   * @param {number} index The index to look for
    551   * @returns {Promise<object>} An object with numerous properties describing the result.
    552   */
    553  async getDetailsOfResultAt(win, index) {
    554    let element = await this.waitForAutocompleteResultAt(win, index);
    555    let details = {};
    556    let result = element.result;
    557    details.result = result;
    558    let { url, postData } = UrlbarUtils.getUrlFromResult(result);
    559    details.url = url;
    560    details.postData = postData;
    561    details.type = result.type;
    562    details.source = result.source;
    563    details.heuristic = result.heuristic;
    564    details.autofill = !!result.autofill;
    565    details.image =
    566      element.getElementsByClassName("urlbarView-favicon")[0]?.src;
    567    details.title = result.getDisplayableValueAndHighlights("title").value;
    568    details.tags = "tags" in result.payload ? result.payload.tags : [];
    569    details.isSponsored = result.payload.isSponsored;
    570    details.userContextId = result.payload.userContextId;
    571    let actions = element.getElementsByClassName("urlbarView-action");
    572    let urls = element.getElementsByClassName("urlbarView-url");
    573    let typeIcon = element.querySelector(".urlbarView-type-icon");
    574    await win.document.l10n.translateFragment(element);
    575    details.displayed = {
    576      title: element.getElementsByClassName("urlbarView-title")[0]?.textContent,
    577      action: actions.length ? actions[0].textContent : null,
    578      url: urls.length ? urls[0].textContent : null,
    579      typeIcon: typeIcon
    580        ? win.getComputedStyle(typeIcon)["background-image"]
    581        : null,
    582    };
    583    details.element = {
    584      action: element.getElementsByClassName("urlbarView-action")[0],
    585      row: element,
    586      separator: element.getElementsByClassName(
    587        "urlbarView-title-separator"
    588      )[0],
    589      title: element.getElementsByClassName("urlbarView-title")[0],
    590      url: element.getElementsByClassName("urlbarView-url")[0],
    591    };
    592    if (details.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
    593      details.searchParams = {
    594        engine: result.payload.engine,
    595        keyword: result.payload.keyword,
    596        query: result.payload.query,
    597        suggestion: result.payload.suggestion,
    598        inPrivateWindow: result.payload.inPrivateWindow,
    599        isPrivateEngine: result.payload.isPrivateEngine,
    600      };
    601    } else if (details.type == UrlbarUtils.RESULT_TYPE.KEYWORD) {
    602      details.keyword = result.payload.keyword;
    603    } else if (details.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
    604      details.dynamicType = result.payload.dynamicType;
    605    }
    606    return details;
    607  }
    608 
    609  /**
    610   * Gets the currently selected element.
    611   *
    612   * @param {ChromeWindow} win The window containing the urlbar.
    613   * @returns {HtmlElement|XulElement} The selected element.
    614   */
    615  getSelectedElement(win) {
    616    return this.#urlbar(win).view.selectedElement || null;
    617  }
    618 
    619  /**
    620   * Gets the index of the currently selected element.
    621   *
    622   * @param {ChromeWindow} win The window containing the urlbar.
    623   * @returns {number} The selected index.
    624   */
    625  getSelectedElementIndex(win) {
    626    return this.#urlbar(win).view.selectedElementIndex;
    627  }
    628 
    629  /**
    630   * Gets the row at a specific index.
    631   *
    632   * @param {ChromeWindow} win The window containing the urlbar.
    633   * @param {number} index The index to look for.
    634   * @returns {HTMLElement|XulElement} The selected row.
    635   */
    636  getRowAt(win, index) {
    637    return this.getResultsContainer(win).children.item(index);
    638  }
    639 
    640  /**
    641   * Gets the currently selected row. If the selected element is a descendant of
    642   * a row, this will return the ancestor row.
    643   *
    644   * @param {ChromeWindow} win The window containing the urlbar.
    645   * @returns {HTMLElement|XulElement} The selected row.
    646   */
    647  getSelectedRow(win) {
    648    return this.getRowAt(win, this.getSelectedRowIndex(win));
    649  }
    650 
    651  /**
    652   * Gets the index of the currently selected element.
    653   *
    654   * @param {ChromeWindow} win The window containing the urlbar.
    655   * @returns {number} The selected row index.
    656   */
    657  getSelectedRowIndex(win) {
    658    return this.#urlbar(win).view.selectedRowIndex;
    659  }
    660 
    661  /**
    662   * Selects the element at the index specified.
    663   *
    664   * @param {ChromeWindow} win The window containing the urlbar.
    665   * @param {number} index The index to select.
    666   */
    667  setSelectedRowIndex(win, index) {
    668    this.#urlbar(win).view.selectedRowIndex = index;
    669  }
    670 
    671  /**
    672   * Gets the results container div for the address bar.
    673   *
    674   * @param {ChromeWindow} win
    675   * @returns {HTMLDivElement}
    676   */
    677  getResultsContainer(win) {
    678    return this.#urlbar(win).view.panel.querySelector(".urlbarView-results");
    679  }
    680 
    681  /**
    682   * Gets the number of results.
    683   * You must wait for the query to be complete before using this.
    684   *
    685   * @param {ChromeWindow} win The window containing the urlbar
    686   * @returns {number} the number of results.
    687   */
    688  getResultCount(win) {
    689    return this.getResultsContainer(win).children.length;
    690  }
    691 
    692  /**
    693   * Ensures at least one search suggestion is present.
    694   *
    695   * @param {ChromeWindow} win The window containing the urlbar
    696   * @returns {Promise<number>}
    697   *   The index of the first suggestion
    698   * @throws {Error} When the index exceeds the number of available results
    699   */
    700  promiseSuggestionsPresent(win) {
    701    // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. When
    702    // we do that, we'll have to be sure the suggestions we find are relevant
    703    // for the current query. For now let's just wait for the search to be
    704    // complete.
    705    return this.promiseSearchComplete(win).then(context => {
    706      // Look for search suggestions.
    707      let firstSearchSuggestionIndex = context.results.findIndex(
    708        r => r.type == UrlbarUtils.RESULT_TYPE.SEARCH && r.payload.suggestion
    709      );
    710      if (firstSearchSuggestionIndex == -1) {
    711        throw new Error("Cannot find a search suggestion");
    712      }
    713      return firstSearchSuggestionIndex;
    714    });
    715  }
    716 
    717  /**
    718   * Waits for the given number of connections to an http server.
    719   *
    720   * @param {object} httpserver an HTTP Server instance
    721   * @param {number} count Number of connections to wait for
    722   * @returns {Promise} resolved when all the expected connections were started.
    723   */
    724  promiseSpeculativeConnections(httpserver, count) {
    725    if (!httpserver) {
    726      throw new Error("Must provide an http server");
    727    }
    728    return lazy.BrowserTestUtils.waitForCondition(
    729      () => httpserver.connectionNumber == count,
    730      "Waiting for speculative connection setup"
    731    );
    732  }
    733 
    734  /**
    735   * Waits for the popup to be shown.
    736   *
    737   * @param {ChromeWindow} win The window containing the urlbar
    738   * @param {Function} openFn Function to be used to open the popup.
    739   * @returns {Promise} resolved once the popup is closed
    740   */
    741  async promisePopupOpen(win, openFn) {
    742    if (!openFn) {
    743      throw new Error("openFn should be supplied to promisePopupOpen");
    744    }
    745    await openFn();
    746    let urlbar = this.#urlbar(win);
    747    if (urlbar.view.isOpen) {
    748      return;
    749    }
    750    this.info("Waiting for the urlbar view to open");
    751    await new Promise(resolve => {
    752      urlbar.controller.addListener({
    753        onViewOpen() {
    754          urlbar.controller.removeListener(this);
    755          resolve();
    756        },
    757      });
    758    });
    759    this.info("Urlbar view opened");
    760  }
    761 
    762  /**
    763   * Waits for the popup to be hidden.
    764   *
    765   * @param {ChromeWindow} win The window containing the urlbar
    766   * @param {Function} [closeFn] Function to be used to close the popup, if not
    767   *        supplied it will default to a closing the popup directly.
    768   * @returns {Promise} resolved once the popup is closed
    769   */
    770  async promisePopupClose(win, closeFn = null) {
    771    let urlbar = this.#urlbar(win);
    772    let closePromise = new Promise(resolve => {
    773      if (!urlbar.view.isOpen) {
    774        resolve();
    775        return;
    776      }
    777      urlbar.controller.addListener({
    778        onViewClose() {
    779          urlbar.controller.removeListener(this);
    780          resolve();
    781        },
    782      });
    783    });
    784    if (closeFn) {
    785      this.info("Awaiting custom close function");
    786      await closeFn();
    787      this.info("Done awaiting custom close function");
    788    } else {
    789      this.info("Closing the view directly");
    790      urlbar.view.close();
    791    }
    792    this.info("Waiting for the view to close");
    793    await closePromise;
    794    this.info("Urlbar view closed");
    795  }
    796 
    797  /**
    798   * Open the input field context menu and run a task on it.
    799   *
    800   * @param {ChromeWindow} win the current window
    801   * @param {Function} task a task function to run, gets the contextmenu popup
    802   *        as argument.
    803   */
    804  async withContextMenu(win, task) {
    805    let textBox = this.#urlbar(win).querySelector("moz-input-box");
    806    let cxmenu = textBox.menupopup;
    807    let openPromise = lazy.BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
    808    this.EventUtils.synthesizeMouseAtCenter(
    809      this.#urlbar(win).inputField,
    810      {
    811        type: "contextmenu",
    812        button: 2,
    813      },
    814      win
    815    );
    816    await openPromise;
    817    // On Mac sometimes the menuitems are not ready.
    818    await new Promise(win.requestAnimationFrame);
    819    try {
    820      await task(cxmenu);
    821    } finally {
    822      // Close the context menu if the task didn't pick anything.
    823      if (cxmenu.state == "open" || cxmenu.state == "showing") {
    824        let closePromise = lazy.BrowserTestUtils.waitForEvent(
    825          cxmenu,
    826          "popuphidden"
    827        );
    828        cxmenu.hidePopup();
    829        await closePromise;
    830      }
    831    }
    832  }
    833 
    834  /**
    835   * @param {ChromeWindow} win The browser window
    836   * @returns {boolean} Whether the popup is open
    837   */
    838  isPopupOpen(win) {
    839    return this.#urlbar(win).view.isOpen;
    840  }
    841 
    842  /**
    843   * Asserts that the input is in a given search mode, or no search mode. Can
    844   * only be used if UrlbarTestUtils has been initialized with init().
    845   *
    846   * @param {ChromeWindow} window
    847   *   The browser window.
    848   * @param {object} expectedSearchMode
    849   *   The expected search mode object.
    850   */
    851  async assertSearchMode(window, expectedSearchMode) {
    852    this.Assert.equal(
    853      !!this.#urlbar(window).searchMode,
    854      this.#urlbar(window).hasAttribute("searchmode"),
    855      "Urlbar should never be in search mode without the corresponding attribute."
    856    );
    857 
    858    this.Assert.equal(
    859      !!this.#urlbar(window).searchMode,
    860      !!expectedSearchMode,
    861      "searchMode should exist on moz-urlbar"
    862    );
    863 
    864    let results = this.#urlbar(window).querySelector(".urlbarView-results");
    865    await lazy.BrowserTestUtils.waitForCondition(
    866      () =>
    867        results.hasAttribute("actionmode") ==
    868        (this.#urlbar(window).searchMode?.source ==
    869          UrlbarUtils.RESULT_SOURCE.ACTIONS)
    870    );
    871    this.Assert.ok(true, "Urlbar results have proper actionmode attribute");
    872 
    873    if (!expectedSearchMode) {
    874      // Check the input's placeholder.
    875      const prefName =
    876        "browser.urlbar.placeholderName" +
    877        (lazy.PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : "");
    878      let engineName = Services.prefs.getStringPref(prefName, "");
    879      let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
    880 
    881      let expectedPlaceholder;
    882      if (keywordEnabled && engineName) {
    883        expectedPlaceholder = {
    884          id: "urlbar-placeholder-with-name",
    885          args: { name: engineName },
    886        };
    887      } else if (keywordEnabled && !engineName) {
    888        expectedPlaceholder = { id: "urlbar-placeholder" };
    889      } else {
    890        expectedPlaceholder = { id: "urlbar-placeholder-keyword-disabled" };
    891      }
    892 
    893      await lazy.BrowserTestUtils.waitForCondition(() => {
    894        let l10nAttributes = window.document.l10n.getAttributes(
    895          this.#urlbar(window).inputField
    896        );
    897        return (
    898          l10nAttributes.id == expectedPlaceholder.id &&
    899          l10nAttributes.args?.name == expectedPlaceholder.args?.name
    900        );
    901      });
    902      this.Assert.ok(
    903        true,
    904        "Expected placeholder l10n when search mode is inactive"
    905      );
    906      return;
    907    }
    908 
    909    // Default to full search mode for less verbose tests.
    910    expectedSearchMode = { ...expectedSearchMode };
    911    if (!expectedSearchMode.hasOwnProperty("isPreview")) {
    912      expectedSearchMode.isPreview = false;
    913    }
    914 
    915    let isGeneralPurposeEngine = false;
    916    if (expectedSearchMode.engineName) {
    917      let engine = Services.search.getEngineByName(
    918        expectedSearchMode.engineName
    919      );
    920      isGeneralPurposeEngine = engine.isGeneralPurposeEngine;
    921      expectedSearchMode.isGeneralPurposeEngine = isGeneralPurposeEngine;
    922    }
    923 
    924    // expectedSearchMode may come from UrlbarUtils.LOCAL_SEARCH_MODES.  The
    925    // objects in that array include useful metadata like icon URIs and pref
    926    // names that are not usually included in actual search mode objects.  For
    927    // convenience, ignore those properties if they aren't also present in the
    928    // urlbar's actual search mode object.
    929    let ignoreProperties = [
    930      "icon",
    931      "pref",
    932      "restrict",
    933      "telemetryLabel",
    934      "uiLabel",
    935    ];
    936    for (let prop of ignoreProperties) {
    937      if (
    938        prop in expectedSearchMode &&
    939        !(prop in this.#urlbar(window).searchMode)
    940      ) {
    941        this.info(
    942          `Ignoring unimportant property '${prop}' in expected search mode`
    943        );
    944        delete expectedSearchMode[prop];
    945      }
    946    }
    947 
    948    this.Assert.deepEqual(
    949      this.#urlbar(window).searchMode,
    950      expectedSearchMode,
    951      "Expected searchMode"
    952    );
    953 
    954    // Only the addressbar still has the legacy search mode indicator.
    955    if (this.#urlbar(window).sapName == "urlbar") {
    956      // Check the textContent and l10n attributes of the indicator and label.
    957      let expectedTextContent = "";
    958      let expectedL10n = { id: null, args: null };
    959      if (expectedSearchMode.engineName) {
    960        expectedTextContent = expectedSearchMode.engineName;
    961      } else if (expectedSearchMode.source) {
    962        let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
    963        this.Assert.ok(name, "Expected result source should have a name");
    964        expectedL10n = { id: `urlbar-search-mode-${name}`, args: null };
    965      } else {
    966        this.Assert.ok(false, "Unexpected searchMode");
    967      }
    968 
    969      if (expectedTextContent) {
    970        this.Assert.equal(
    971          this.#urlbar(window)._searchModeIndicatorTitle.textContent,
    972          expectedTextContent,
    973          "Expected textContent"
    974        );
    975      }
    976      this.Assert.deepEqual(
    977        window.document.l10n.getAttributes(
    978          this.#urlbar(window)._searchModeIndicatorTitle
    979        ),
    980        expectedL10n,
    981        "Expected l10n"
    982      );
    983    }
    984 
    985    // Check the input's placeholder.
    986    let expectedPlaceholderL10n;
    987    if (this.#urlbar(window).sapName == "searchbar") {
    988      // Placeholder stays constant in searchbar.
    989      expectedPlaceholderL10n = {
    990        id: "searchbar-input",
    991        args: null,
    992      };
    993    } else if (expectedSearchMode.engineName) {
    994      expectedPlaceholderL10n = {
    995        id: isGeneralPurposeEngine
    996          ? "urlbar-placeholder-search-mode-web-2"
    997          : "urlbar-placeholder-search-mode-other-engine",
    998        args: { name: expectedSearchMode.engineName },
    999      };
   1000    } else if (expectedSearchMode.source) {
   1001      let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
   1002      expectedPlaceholderL10n = {
   1003        id: `urlbar-placeholder-search-mode-other-${name}`,
   1004        args: null,
   1005      };
   1006    }
   1007    this.Assert.deepEqual(
   1008      window.document.l10n.getAttributes(this.#urlbar(window).inputField),
   1009      expectedPlaceholderL10n,
   1010      "Expected placeholder l10n when search mode is active"
   1011    );
   1012 
   1013    // If this is an engine search mode, check that all results are either
   1014    // search results with the same engine or have the same host as the engine.
   1015    // Search mode preview can show other results since it is not supposed to
   1016    // start a query.
   1017    if (
   1018      expectedSearchMode.engineName &&
   1019      !expectedSearchMode.isPreview &&
   1020      this.isPopupOpen(window)
   1021    ) {
   1022      let resultCount = this.getResultCount(window);
   1023      for (let i = 0; i < resultCount; i++) {
   1024        let result = await this.getDetailsOfResultAt(window, i);
   1025        if (result.source == UrlbarUtils.RESULT_SOURCE.SEARCH) {
   1026          this.Assert.equal(
   1027            expectedSearchMode.engineName,
   1028            result.searchParams.engine,
   1029            "Search mode result matches engine name."
   1030          );
   1031        } else {
   1032          let engine = Services.search.getEngineByName(
   1033            expectedSearchMode.engineName
   1034          );
   1035          let engineRootDomain =
   1036            lazy.UrlbarSearchUtils.getRootDomainFromEngine(engine);
   1037          let resultUrl = new URL(result.url);
   1038          this.Assert.ok(
   1039            resultUrl.hostname.includes(engineRootDomain),
   1040            "Search mode result matches engine host."
   1041          );
   1042        }
   1043      }
   1044    }
   1045  }
   1046 
   1047  /**
   1048   * Enters search mode by clicking a one-off.  The view must already be open
   1049   * before you call this. Can only be used if UrlbarTestUtils has been
   1050   * initialized with init().
   1051   *
   1052   * @param {ChromeWindow} window
   1053   *   The window to operate on.
   1054   * @param {object} searchMode
   1055   *   If given, the one-off matching this search mode will be clicked; it
   1056   *   should be a full search mode object as described in
   1057   *   UrlbarInput.setSearchMode.  If not given, the first one-off is clicked.
   1058   */
   1059  async enterSearchMode(window, searchMode = null) {
   1060    this.info(`Enter Search Mode ${JSON.stringify(searchMode)}`);
   1061 
   1062    // Ensure any pending query is complete.
   1063    await this.promiseSearchComplete(window);
   1064 
   1065    // Ensure the the one-offs are finished rebuilding and visible.
   1066    let oneOffs = this.getOneOffSearchButtons(window);
   1067    await lazy.TestUtils.waitForCondition(
   1068      () => !oneOffs._rebuilding,
   1069      "Waiting for one-offs to finish rebuilding"
   1070    );
   1071    this.Assert.equal(
   1072      UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
   1073      true,
   1074      "One-offs are visible"
   1075    );
   1076 
   1077    let buttons = oneOffs.getSelectableButtons(true);
   1078    if (!searchMode) {
   1079      searchMode = { engineName: buttons[0].engine.name };
   1080      let engine = Services.search.getEngineByName(searchMode.engineName);
   1081      if (engine.isGeneralPurposeEngine) {
   1082        searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
   1083      }
   1084    }
   1085 
   1086    if (!searchMode.entry) {
   1087      searchMode.entry = "oneoff";
   1088    }
   1089 
   1090    let oneOff = buttons.find(o =>
   1091      searchMode.engineName
   1092        ? o.engine.name == searchMode.engineName
   1093        : o.source == searchMode.source
   1094    );
   1095    this.Assert.ok(oneOff, "Found one-off button for search mode");
   1096    this.EventUtils.synthesizeMouseAtCenter(oneOff, {}, window);
   1097    await this.promiseSearchComplete(window);
   1098    this.Assert.ok(this.isPopupOpen(window), "Urlbar view is still open.");
   1099    await this.assertSearchMode(window, searchMode);
   1100  }
   1101 
   1102  /**
   1103   * Removes the scheme from an url according to user prefs.
   1104   *
   1105   * @param {string} url
   1106   *  The url that is supposed to be trimmed.
   1107   * @param {object} [options]
   1108   *  Options for the trimming.
   1109   * @param {boolean} [options.removeSingleTrailingSlash]
   1110   *    Remove trailing slash, when trimming enabled.
   1111   * @returns {string}
   1112   *  The sanitized URL.
   1113   */
   1114  trimURL(url, { removeSingleTrailingSlash = true } = {}) {
   1115    if (!lazy.UrlbarPrefs.get("trimURLs")) {
   1116      return url;
   1117    }
   1118 
   1119    let sanitizedURL = url;
   1120    if (removeSingleTrailingSlash) {
   1121      sanitizedURL =
   1122        lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(sanitizedURL);
   1123    }
   1124 
   1125    // Also remove emphasis markers if present.
   1126    if (lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps")) {
   1127      sanitizedURL = sanitizedURL.replace(/^<?https:\/\/>?/, "");
   1128    } else {
   1129      sanitizedURL = sanitizedURL.replace(/^<?http:\/\/>?/, "");
   1130    }
   1131 
   1132    return sanitizedURL;
   1133  }
   1134 
   1135  /**
   1136   * Returns the trimmed protocol with slashes.
   1137   *
   1138   * @returns {string} The trimmed protocol including slashes. Returns an empty
   1139   *                   string, when the protocol trimming is disabled.
   1140   */
   1141  getTrimmedProtocolWithSlashes() {
   1142    if (Services.prefs.getBoolPref("browser.urlbar.trimURLs")) {
   1143      return lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps")
   1144        ? "https://"
   1145        : "http://"; // eslint-disable-this-line @microsoft/sdl/no-insecure-url
   1146    }
   1147    return "";
   1148  }
   1149 
   1150  /**
   1151   * Exits search mode. If neither `backspace` nor `clickClose` is given, we'll
   1152   * default to backspacing. Can only be used if UrlbarTestUtils has been
   1153   * initialized with init().
   1154   *
   1155   * @param {ChromeWindow} window
   1156   *   The window to operate on.
   1157   * @param {object} options
   1158   *   Options object
   1159   * @param {boolean} [options.backspace]
   1160   *   Exits search mode by backspacing at the beginning of the search string.
   1161   * @param {boolean} [options.clickClose]
   1162   *   Exits search mode by clicking the close button on the search mode
   1163   *   indicator.
   1164   * @param {boolean} [options.waitForSearch]
   1165   *   Whether the test should wait for a search after exiting search mode.
   1166   *   Defaults to true.
   1167   */
   1168  async exitSearchMode(
   1169    window,
   1170    { backspace, clickClose, waitForSearch = true } = {}
   1171  ) {
   1172    let urlbar = this.#urlbar(window);
   1173    // If the Urlbar is not extended, ignore the clickClose parameter. The close
   1174    // button is not clickable in this state. This state might be encountered on
   1175    // Linux, where prefers-reduced-motion is enabled in automation.
   1176    if (!urlbar.hasAttribute("breakout-extend") && clickClose) {
   1177      if (waitForSearch) {
   1178        let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
   1179        urlbar.searchMode = null;
   1180        await searchPromise;
   1181      } else {
   1182        urlbar.searchMode = null;
   1183      }
   1184      return;
   1185    }
   1186 
   1187    if (!backspace && !clickClose) {
   1188      backspace = true;
   1189    }
   1190 
   1191    if (backspace) {
   1192      let urlbarValue = urlbar.value;
   1193      urlbar.selectionStart = urlbar.selectionEnd = 0;
   1194      if (waitForSearch) {
   1195        let searchPromise = this.promiseSearchComplete(window);
   1196        this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
   1197        await searchPromise;
   1198      } else {
   1199        this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
   1200      }
   1201      this.Assert.equal(
   1202        urlbar.value,
   1203        urlbarValue,
   1204        "Urlbar value hasn't changed."
   1205      );
   1206      await this.assertSearchMode(window, null);
   1207    } else if (clickClose) {
   1208      // We need to hover the indicator to make the close button clickable in the
   1209      // test.
   1210      let indicator = urlbar.querySelector("#urlbar-search-mode-indicator");
   1211      this.EventUtils.synthesizeMouseAtCenter(
   1212        indicator,
   1213        { type: "mouseover" },
   1214        window
   1215      );
   1216      let closeButton = urlbar.querySelector(
   1217        "#urlbar-search-mode-indicator-close"
   1218      );
   1219      if (waitForSearch) {
   1220        let searchPromise = this.promiseSearchComplete(window);
   1221        this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
   1222        await searchPromise;
   1223      } else {
   1224        this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
   1225      }
   1226      await this.assertSearchMode(window, null);
   1227    }
   1228  }
   1229 
   1230  /**
   1231   * Returns the userContextId (container id) for the last search.
   1232   *
   1233   * @param {ChromeWindow} win The browser window
   1234   * @returns {Promise<number>}
   1235   *   resolved when fetching is complete. Its value is a userContextId
   1236   */
   1237  async promiseUserContextId(win) {
   1238    const defaultId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
   1239    let context = await this.#urlbar(win).lastQueryContextPromise;
   1240    return context.userContextId || defaultId;
   1241  }
   1242 
   1243  /**
   1244   * Dispatches an input event to the input field.
   1245   *
   1246   * @param {ChromeWindow} win The browser window
   1247   */
   1248  fireInputEvent(win) {
   1249    // Set event.data to the last character in the input, for a couple of
   1250    // reasons: It simulates the user typing, and it's necessary for autofill.
   1251    let event = new InputEvent("input", {
   1252      data: this.#urlbar(win).value[this.#urlbar(win).value.length - 1] || null,
   1253    });
   1254    this.#urlbar(win).inputField.dispatchEvent(event);
   1255  }
   1256 
   1257  /**
   1258   * Returns a new mock controller.  This is useful for xpcshell tests.
   1259   *
   1260   * @param {object} options Additional options to pass to the UrlbarController
   1261   *        constructor.
   1262   * @returns {UrlbarController} A new controller.
   1263   */
   1264  newMockController(options = {}) {
   1265    return new lazy.UrlbarController(
   1266      Object.assign(
   1267        {
   1268          input: {
   1269            isPrivate: false,
   1270            onFirstResult() {
   1271              return false;
   1272            },
   1273            getSearchSource() {
   1274              return "dummy-search-source";
   1275            },
   1276            window: {
   1277              location: {
   1278                href: AppConstants.BROWSER_CHROME_URL,
   1279              },
   1280            },
   1281          },
   1282        },
   1283        options
   1284      )
   1285    );
   1286  }
   1287 
   1288  /**
   1289   * Initializes some external components used by the urlbar.  This is necessary
   1290   * in xpcshell tests but not in browser tests.
   1291   */
   1292  async initXPCShellDependencies() {
   1293    // The FormHistoryStartup component must be initialized since urlbar uses
   1294    // form history.
   1295    Cc["@mozilla.org/satchel/form-history-startup;1"]
   1296      .getService(Ci.nsIObserver)
   1297      .observe(null, "profile-after-change", null);
   1298  }
   1299 
   1300  /**
   1301   * Enrolls in a mock Nimbus feature.
   1302   *
   1303   * @param {object} value
   1304   *   Define any desired Nimbus variables in this object.
   1305   * @param {string} [feature]
   1306   *   The feature to init.
   1307   * @param {string} [enrollmentType]
   1308   *   The enrollment type, either "rollout" (default) or "config".
   1309   * @returns {Promise<() => Promise<void>>}
   1310   *   A cleanup function that will unenroll the feature, returns a promise.
   1311   */
   1312  async initNimbusFeature(
   1313    value = {},
   1314    feature = "urlbar",
   1315    enrollmentType = "rollout"
   1316  ) {
   1317    this.info("initNimbusFeature awaiting ExperimentAPI.init");
   1318    const initializedExperimentAPI = await lazy.ExperimentAPI.init();
   1319 
   1320    this.info("initNimbusFeature awaiting ExperimentAPI.ready");
   1321    await lazy.ExperimentAPI.ready();
   1322 
   1323    this.info(
   1324      `initNimbusFeature awaiting NimbusTestUtils.enrollWithFeatureConfig`
   1325    );
   1326    const doExperimentCleanup =
   1327      await lazy.NimbusTestUtils.enrollWithFeatureConfig(
   1328        {
   1329          featureId: lazy.NimbusFeatures[feature].featureId,
   1330          value,
   1331        },
   1332        {
   1333          isRollout: enrollmentType === "rollout",
   1334        }
   1335      );
   1336 
   1337    this.info("initNimbusFeature done");
   1338 
   1339    const cleanup = async () => {
   1340      await doExperimentCleanup();
   1341      if (initializedExperimentAPI) {
   1342        // Only reset if we're in an xpcshell-test and actually initialized the
   1343        // ExperimentAPI.
   1344        lazy.ExperimentAPI._resetForTests();
   1345      }
   1346    };
   1347 
   1348    this.registerCleanupFunction?.(async () => {
   1349      // If `cleanup()` has already been called (i.e., by the caller), it will
   1350      // throw an error here.
   1351      try {
   1352        await cleanup();
   1353      } catch (error) {}
   1354    });
   1355 
   1356    return cleanup;
   1357  }
   1358 
   1359  /**
   1360   * Simulate that user clicks moz-urlbar and inputs text into it.
   1361   *
   1362   * @param {ChromeWindow} win
   1363   *   The browser window containing target moz-urlbar.
   1364   * @param {string} text
   1365   *   The text to be input.
   1366   */
   1367  async inputIntoURLBar(win, text) {
   1368    if (this.#urlbar(win).focused) {
   1369      this.#urlbar(win).select();
   1370    } else {
   1371      this.EventUtils.synthesizeMouseAtCenter(
   1372        this.#urlbar(win).inputField,
   1373        {},
   1374        win
   1375      );
   1376      await lazy.TestUtils.waitForCondition(() => this.#urlbar(win).focused);
   1377    }
   1378    if (text.length > 1) {
   1379      // Set most of the string directly instead of going through sendString,
   1380      // so that we don't make life unnecessarily hard for consumers by
   1381      // possibly starting multiple searches.
   1382      this.#urlbar(win)._setValue(text.substr(0, text.length - 1));
   1383    }
   1384    this.EventUtils.sendString(text.substr(-1, 1), win);
   1385  }
   1386 
   1387  /**
   1388   * Checks the urlbar value fomatting for a given URL.
   1389   *
   1390   * @param {ChromeWindow} win
   1391   *   The input in this window will be tested.
   1392   * @param {string} urlFormatString
   1393   *   The URL to test. The parts the are expected to be de-emphasized should be
   1394   *   wrapped in "<" and ">" chars.
   1395   * @param {object} [options]
   1396   *   Options object.
   1397   * @param {string} [options.clobberedURLString]
   1398   *      Normally the URL is de-emphasized in-place, thus it's enough to pass
   1399   *      urlString. In some cases however the formatter may decide to replace
   1400   *      the URL with a fixed one, because it can't properly guess a host. In
   1401   *      that case clobberedURLString is the expected de-emphasized value. The
   1402   *      parts the are expected to be de-emphasized should be wrapped in "<"
   1403   *      and ">" chars.
   1404   * @param {string} [options.additionalMsg]
   1405   *   Additional message to use for Assert.equal.
   1406   * @param {number} [options.selectionType]
   1407   *   The selectionType for which the input should be checked.
   1408   */
   1409  async checkFormatting(
   1410    win,
   1411    urlFormatString,
   1412    {
   1413      clobberedURLString = null,
   1414      additionalMsg = null,
   1415      selectionType = Ci.nsISelectionController.SELECTION_URLSECONDARY,
   1416    } = {}
   1417  ) {
   1418    await new Promise(resolve => win.requestAnimationFrame(resolve));
   1419    let selectionController = this.#urlbar(win).editor.selectionController;
   1420    let selection = selectionController.getSelection(selectionType);
   1421    let value = this.#urlbar(win).editor.rootElement.textContent;
   1422    let result = "";
   1423    for (let i = 0; i < selection.rangeCount; i++) {
   1424      let range = selection.getRangeAt(i).toString();
   1425      let pos = value.indexOf(range);
   1426      result += value.substring(0, pos) + "<" + range + ">";
   1427      value = value.substring(pos + range.length);
   1428    }
   1429    result += value;
   1430    this.Assert.equal(
   1431      result,
   1432      clobberedURLString || urlFormatString,
   1433      "Correct part of the URL is de-emphasized" +
   1434        (additionalMsg ? ` (${additionalMsg})` : "")
   1435    );
   1436  }
   1437 
   1438  searchModeSwitcherPopup(win) {
   1439    return this.#urlbar(win).querySelector(".searchmode-switcher-popup");
   1440  }
   1441 
   1442  async openSearchModeSwitcher(win) {
   1443    let popup = this.searchModeSwitcherPopup(win);
   1444    let button = this.#urlbar(win).querySelector(".searchmode-switcher");
   1445    this.Assert.ok(lazy.BrowserTestUtils.isVisible(button));
   1446    await this.EventUtils.promiseElementReadyForUserInput(button, win);
   1447 
   1448    let promiseMenuOpen = lazy.BrowserTestUtils.waitForPopupEvent(
   1449      popup,
   1450      "shown"
   1451    );
   1452    let rebuildPromise = lazy.BrowserTestUtils.waitForEvent(popup, "rebuild");
   1453    // Ensure the pop-up opens.
   1454    button.open = true;
   1455    await Promise.all([promiseMenuOpen, rebuildPromise]);
   1456 
   1457    return popup;
   1458  }
   1459 
   1460  searchModeSwitcherPopupClosed(win) {
   1461    return lazy.BrowserTestUtils.waitForPopupEvent(
   1462      this.searchModeSwitcherPopup(win),
   1463      "hidden"
   1464    );
   1465  }
   1466 
   1467  /**
   1468   * Gets the icon url of the search mode switcher icon.
   1469   *
   1470   * @param {ChromeWindow} win
   1471   * @returns {?string}
   1472   */
   1473  getSearchModeSwitcherIcon(win) {
   1474    let searchModeSwitcherButton = this.#urlbar(win).querySelector(
   1475      ".searchmode-switcher-icon"
   1476    );
   1477 
   1478    // match and capture the URL inside `url("...")`
   1479    let re = /url\("([^"]+)"\)/;
   1480    let { listStyleImage } = win.getComputedStyle(searchModeSwitcherButton);
   1481    return listStyleImage.match(re)?.[1] ?? null;
   1482  }
   1483 
   1484  async openTrustPanel(win) {
   1485    let btn = win.document.getElementById("trust-icon");
   1486    let popupShown = lazy.BrowserTestUtils.waitForEvent(
   1487      win.document,
   1488      "popupshown"
   1489    );
   1490    this.EventUtils.synthesizeMouseAtCenter(btn, {}, win);
   1491    await popupShown;
   1492  }
   1493 
   1494  async openTrustPanelSubview(win, viewId) {
   1495    let view = win.document.getElementById(viewId);
   1496    let shown = lazy.BrowserTestUtils.waitForEvent(view, "ViewShown");
   1497    this.EventUtils.synthesizeMouseAtCenter(
   1498      win.document.getElementById("trustpanel-popup-connection"),
   1499      {},
   1500      win
   1501    );
   1502    await shown;
   1503  }
   1504 
   1505  async closeTrustPanel(win) {
   1506    let popupHidden = lazy.BrowserTestUtils.waitForEvent(
   1507      win.document,
   1508      "popuphidden"
   1509    );
   1510    this.EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
   1511    await popupHidden;
   1512  }
   1513 
   1514  async selectMenuItem(menupopup, targetSelector) {
   1515    let target = menupopup.querySelector(targetSelector);
   1516    let selected;
   1517    for (let i = 0; i < menupopup.children.length; i++) {
   1518      this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, menupopup.ownerGlobal);
   1519      await lazy.BrowserTestUtils.waitForCondition(() => {
   1520        let current = menupopup.querySelector("[_moz-menuactive]");
   1521        if (selected != current) {
   1522          selected = current;
   1523          return true;
   1524        }
   1525        return false;
   1526      });
   1527      if (selected == target) {
   1528        break;
   1529      }
   1530    }
   1531  }
   1532 }
   1533 
   1534 UrlbarInputTestUtils.prototype.formHistory = {
   1535  /**
   1536   * Adds values to the urlbar's form history.
   1537   *
   1538   * @param {Array} values
   1539   *   The form history entries to remove.
   1540   * @returns {Promise} resolved once the operation is complete.
   1541   */
   1542  add(values = []) {
   1543    return lazy.FormHistoryTestUtils.add(
   1544      lazy.DEFAULT_FORM_HISTORY_PARAM,
   1545      values
   1546    );
   1547  },
   1548 
   1549  /**
   1550   * Removes values from the urlbar's form history.  If you want to remove all
   1551   * history, use clearFormHistory.
   1552   *
   1553   * @param {Array} values
   1554   *   The form history entries to remove.
   1555   * @returns {Promise} resolved once the operation is complete.
   1556   */
   1557  remove(values = []) {
   1558    return lazy.FormHistoryTestUtils.remove(
   1559      lazy.DEFAULT_FORM_HISTORY_PARAM,
   1560      values
   1561    );
   1562  },
   1563 
   1564  /**
   1565   * Removes all values from the urlbar's form history.  If you want to remove
   1566   * individual values, use removeFormHistory.
   1567   *
   1568   * @returns {Promise} resolved once the operation is complete.
   1569   */
   1570  clear() {
   1571    return lazy.FormHistoryTestUtils.clear(lazy.DEFAULT_FORM_HISTORY_PARAM);
   1572  },
   1573 
   1574  /**
   1575   * Searches the urlbar's form history.
   1576   *
   1577   * @param {object} criteria
   1578   *   Criteria to narrow the search.  See FormHistory.search.
   1579   * @returns {Promise}
   1580   *   A promise resolved with an array of found form history entries.
   1581   */
   1582  search(criteria = {}) {
   1583    return lazy.FormHistoryTestUtils.search(
   1584      lazy.DEFAULT_FORM_HISTORY_PARAM,
   1585      criteria
   1586    );
   1587  },
   1588 
   1589  /**
   1590   * Returns a promise that's resolved on the next form history change.
   1591   *
   1592   * @param {string} change
   1593   *   Null to listen for any change, or one of: add, remove, update
   1594   * @returns {Promise}
   1595   *   Resolved on the next specified form history change.
   1596   */
   1597  promiseChanged(change = null) {
   1598    return lazy.TestUtils.topicObserved(
   1599      "satchel-storage-changed",
   1600      (subject, data) => !change || data == "formhistory-" + change
   1601    );
   1602  },
   1603 };
   1604 
   1605 /**
   1606 * A test provider.  If you need a test provider whose behavior is different
   1607 * from this, then consider modifying the implementation below if you think the
   1608 * new behavior would be useful for other tests.  Otherwise, you can create a
   1609 * new TestProvider instance and then override its methods.
   1610 */
   1611 class TestProvider extends UrlbarProvider {
   1612  /**
   1613   * Constructor.
   1614   *
   1615   * @param {object} options
   1616   *   Constructor options
   1617   * @param {Array} [options.results]
   1618   *   An array of UrlbarResult objects that will be the provider's results.
   1619   * @param {string} [options.name]
   1620   *   The provider's name.  Provider names should be unique.
   1621   * @param {Values<typeof UrlbarUtils.PROVIDER_TYPE>} [options.type]
   1622   *   The provider's type.
   1623   * @param {number} [options.priority]
   1624   *   The provider's priority.  Built-in providers have a priority of zero.
   1625   * @param {number} [options.addTimeout]
   1626   *   If non-zero, each result will be added on this timeout.  If zero, all
   1627   *   results will be added immediately and synchronously.
   1628   *   If there's no results, the query will be completed after this timeout.
   1629   * @param {Function} [options.getViewTemplate]
   1630   *   If given, override the UrlbarProvider.getViewTemplate().
   1631   * @param {Function} [options.getViewUpdate]
   1632   *   If given, override the UrlbarProvider.getViewUpdate().
   1633   * @param {Function} [options.onCancel]
   1634   *   If given, a function that will be called when the provider's cancelQuery
   1635   *   method is called.
   1636   * @param {Function} [options.onSelection]
   1637   *   If given, a function that will be called when
   1638   *   {@link UrlbarView.#selectElement} method is called.
   1639   * @param {Function} [options.onEngagement]
   1640   *   If given, a function that will be called when engagement.
   1641   * @param {Function} [options.onAbandonment]
   1642   *   If given, a function that will be called when abandonment.
   1643   * @param {Function} [options.onImpression]
   1644   *   If given, a function that will be called when an engagement or
   1645   *   abandonment has occured.
   1646   * @param {Function} [options.onSearchSessionEnd]
   1647   *   If given, a function that will be called when a search session
   1648   *   concludes.
   1649   * @param {Function} [options.delayResultsPromise]
   1650   *   If given, we'll await on this before returning results.
   1651   */
   1652  constructor({
   1653    results = [],
   1654    name = "TestProvider" + Services.uuid.generateUUID(),
   1655    type = UrlbarUtils.PROVIDER_TYPE.PROFILE,
   1656    priority = 0,
   1657    addTimeout = 0,
   1658    getViewTemplate = null,
   1659    getViewUpdate = null,
   1660    onCancel = null,
   1661    onSelection = null,
   1662    onEngagement = null,
   1663    onAbandonment = null,
   1664    onImpression = null,
   1665    onSearchSessionEnd = null,
   1666    delayResultsPromise = null,
   1667  } = {}) {
   1668    if (delayResultsPromise && addTimeout) {
   1669      throw new Error(
   1670        "Can't provide both `addTimeout` and `delayResultsPromise`"
   1671      );
   1672    }
   1673    super();
   1674    this.results = results;
   1675    this.priority = priority;
   1676    this.addTimeout = addTimeout;
   1677    this.delayResultsPromise = delayResultsPromise;
   1678    this._name = name;
   1679    this._type = type;
   1680    this._onCancel = onCancel;
   1681    this._onSelection = onSelection;
   1682 
   1683    // As this has been a common source of mistakes, auto-upgrade the provider
   1684    // type to heuristic if any result is heuristic.
   1685    if (!type && this.results?.some(r => r.heuristic)) {
   1686      this._type = UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
   1687    }
   1688 
   1689    if (getViewTemplate) {
   1690      this.getViewTemplate = getViewTemplate.bind(this);
   1691    }
   1692 
   1693    if (getViewUpdate) {
   1694      this.getViewUpdate = getViewUpdate.bind(this);
   1695    }
   1696 
   1697    if (onEngagement) {
   1698      this.onEngagement = onEngagement.bind(this);
   1699    }
   1700 
   1701    if (onAbandonment) {
   1702      this.onAbandonment = onAbandonment.bind(this);
   1703    }
   1704 
   1705    if (onImpression) {
   1706      this.onImpression = onAbandonment.bind(this);
   1707    }
   1708 
   1709    if (onSearchSessionEnd) {
   1710      this.onSearchSessionEnd = onSearchSessionEnd.bind(this);
   1711    }
   1712  }
   1713 
   1714  get name() {
   1715    return this._name;
   1716  }
   1717 
   1718  get type() {
   1719    return this._type;
   1720  }
   1721 
   1722  getPriority(_context) {
   1723    return this.priority;
   1724  }
   1725 
   1726  async isActive(_context) {
   1727    return true;
   1728  }
   1729 
   1730  async startQuery(context, addCallback) {
   1731    if (!this.results.length && this.addTimeout) {
   1732      await new Promise(resolve => lazy.setTimeout(resolve, this.addTimeout));
   1733    }
   1734    if (this.delayResultsPromise) {
   1735      await this.delayResultsPromise;
   1736    }
   1737    for (let result of this.results) {
   1738      if (!this.addTimeout) {
   1739        addCallback(this, result);
   1740      } else {
   1741        await new Promise(resolve => {
   1742          lazy.setTimeout(() => {
   1743            addCallback(this, result);
   1744            resolve();
   1745          }, this.addTimeout);
   1746        });
   1747      }
   1748    }
   1749  }
   1750 
   1751  cancelQuery(_context) {
   1752    this._onCancel?.();
   1753  }
   1754 
   1755  onSelection(result, element) {
   1756    this._onSelection?.(result, element);
   1757  }
   1758 }
   1759 
   1760 UrlbarInputTestUtils.prototype.TestProvider = TestProvider;
   1761 
   1762 export var UrlbarTestUtils = new UrlbarInputTestUtils(window => window.gURLBar);
   1763 export var SearchbarTestUtils = new UrlbarInputTestUtils(window =>
   1764  window.document.getElementById("searchbar-new")
   1765 );