tor-browser

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

head.js (30518B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /* eslint no-unused-vars: [2, {"vars": "local"}] */
      7 
      8 Services.scriptloader.loadSubScript(
      9  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     10  this
     11 );
     12 
     13 // Import helpers for the inspector that are also shared with others
     14 Services.scriptloader.loadSubScript(
     15  "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
     16  this
     17 );
     18 
     19 // Load APZ test utils so we properly wait after resize
     20 Services.scriptloader.loadSubScript(
     21  "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
     22  this
     23 );
     24 Services.scriptloader.loadSubScript(
     25  "chrome://mochikit/content/tests/SimpleTest/paint_listener.js",
     26  this
     27 );
     28 
     29 const {
     30  _loadPreferredDevices,
     31 } = require("resource://devtools/client/responsive/actions/devices.js");
     32 const {
     33  getStr,
     34 } = require("resource://devtools/client/responsive/utils/l10n.js");
     35 const {
     36  getTopLevelWindow,
     37 } = require("resource://devtools/client/responsive/utils/window.js");
     38 const {
     39  addDevice,
     40  removeDevice,
     41  removeLocalDevices,
     42 } = require("resource://devtools/client/shared/devices.js");
     43 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
     44 const asyncStorage = require("resource://devtools/shared/async-storage.js");
     45 const localTypes = require("resource://devtools/client/responsive/types.js");
     46 
     47 loader.lazyRequireGetter(
     48  this,
     49  "ResponsiveUIManager",
     50  "resource://devtools/client/responsive/manager.js"
     51 );
     52 loader.lazyRequireGetter(
     53  this,
     54  "message",
     55  "resource://devtools/client/responsive/utils/message.js"
     56 );
     57 
     58 const E10S_MULTI_ENABLED =
     59  Services.prefs.getIntPref("dom.ipc.processCount") > 1;
     60 const TEST_URI_ROOT =
     61  "http://example.com/browser/devtools/client/responsive/test/browser/";
     62 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
     63 const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
     64  Ci.nsIHttpProtocolHandler
     65 ).userAgent;
     66 
     67 SimpleTest.requestCompleteLog();
     68 SimpleTest.waitForExplicitFinish();
     69 
     70 // Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
     71 // on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
     72 // should be enough.
     73 requestLongerTimeout(2);
     74 
     75 registerCleanupFunction(async () => {
     76  await asyncStorage.removeItem("devtools.responsive.deviceState");
     77  await removeLocalDevices();
     78 
     79  delete window.waitForAllPaintsFlushed;
     80  delete window.waitForAllPaints;
     81  delete window.promiseAllPaintsDone;
     82 });
     83 
     84 /**
     85 * Adds a new test task that adds a tab with the given URL, awaits the
     86 * preTask (if provided), opens responsive design mode, awaits the task,
     87 * closes responsive design mode, awaits the postTask (if provided), and
     88 * removes the tab. The final argument is an options object, with these
     89 * optional properties:
     90 *
     91 * onlyPrefAndTask: if truthy, only the pref will be set and the task
     92 *   will be called, with none of the tab creation/teardown or open/close
     93 *   of RDM (default false).
     94 * waitForDeviceList: if truthy, the function will wait until the device
     95 *   list is loaded before calling the task (default false).
     96 *
     97 * Example usage:
     98 *
     99 *   addRDMTaskWithPreAndPost(
    100 *     TEST_URL,
    101 *     async function preTask({ message, browser }) {
    102 *       // Your pre-task goes here...
    103 *     },
    104 *     async function task({ ui, manager, message, browser, preTaskValue, tab }) {
    105 *       // Your task goes here...
    106 *     },
    107 *     async function postTask({ message, browser, preTaskValue, taskValue }) {
    108 *       // Your post-task goes here...
    109 *     },
    110 *     { waitForDeviceList: true }
    111 *   );
    112 */
    113 function addRDMTaskWithPreAndPost(url, preTask, task, postTask, options) {
    114  let onlyPrefAndTask = false;
    115  let waitForDeviceList = false;
    116  if (typeof options == "object") {
    117    onlyPrefAndTask = !!options.onlyPrefAndTask;
    118    waitForDeviceList = !!options.waitForDeviceList;
    119  }
    120 
    121  add_task(async function () {
    122    let tab;
    123    let browser;
    124    let preTaskValue = null;
    125    let taskValue = null;
    126    let ui;
    127    let manager;
    128 
    129    if (!onlyPrefAndTask) {
    130      tab = await addTab(url);
    131      browser = tab.linkedBrowser;
    132 
    133      if (preTask) {
    134        preTaskValue = await preTask({ message, browser });
    135      }
    136 
    137      const rdmValues = await openRDM(tab, { waitForDeviceList });
    138      ui = rdmValues.ui;
    139      manager = rdmValues.manager;
    140    }
    141 
    142    try {
    143      taskValue = await task({
    144        ui,
    145        manager,
    146        message,
    147        browser,
    148        preTaskValue,
    149        tab,
    150      });
    151    } catch (err) {
    152      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
    153    }
    154 
    155    if (!onlyPrefAndTask) {
    156      // Close the toolbox first, as closing RDM might trigger a reload if
    157      // touch simulation was enabled, which will trigger RDP requests.
    158      await closeToolboxIfOpen();
    159 
    160      await closeRDM(tab);
    161 
    162      if (postTask) {
    163        await postTask({
    164          message,
    165          browser,
    166          preTaskValue,
    167          taskValue,
    168        });
    169      }
    170      await removeTab(tab);
    171    }
    172 
    173    // Flush prefs to not only undo our earlier change, but also undo
    174    // any changes made by the tasks.
    175    await SpecialPowers.flushPrefEnv();
    176  });
    177 }
    178 
    179 /**
    180 * This is a simplified version of addRDMTaskWithPreAndPost. Adds a new test
    181 * task that adds a tab with the given URL, opens responsive design mode,
    182 * closes responsive design mode, and removes the tab.
    183 *
    184 * Example usage:
    185 *
    186 *   addRDMTask(
    187 *     TEST_URL,
    188 *     async function task({ ui, manager, message, browser }) {
    189 *       // Your task goes here...
    190 *     },
    191 *     { waitForDeviceList: true }
    192 *   );
    193 */
    194 function addRDMTask(rdmURL, rdmTask, options) {
    195  addRDMTaskWithPreAndPost(rdmURL, undefined, rdmTask, undefined, options);
    196 }
    197 
    198 async function spawnViewportTask(ui, args, task) {
    199  // Await a reflow after the task.
    200  const result = await SpecialPowers.spawn(
    201    ui.getViewportBrowser(),
    202    [args],
    203    task
    204  );
    205  await promiseContentReflow(ui);
    206  return result;
    207 }
    208 
    209 function waitForFrameLoad(ui, targetURL) {
    210  return spawnViewportTask(ui, { targetURL }, async function (args) {
    211    if (
    212      (content.document.readyState == "complete" ||
    213        content.document.readyState == "interactive") &&
    214      content.location.href == args.targetURL
    215    ) {
    216      return;
    217    }
    218    await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
    219  });
    220 }
    221 
    222 function waitForViewportResizeTo(ui, width, height) {
    223  return new Promise(function (resolve) {
    224    const isSizeMatching = data => data.width == width && data.height == height;
    225 
    226    // If the viewport has already the expected size, we resolve the promise immediately.
    227    const size = ui.getViewportSize();
    228    if (isSizeMatching(size)) {
    229      info(`Viewport already resized to ${width} x ${height}`);
    230      resolve();
    231      return;
    232    }
    233 
    234    // Otherwise, we'll listen to the viewport's resize event, and the
    235    // browser's load end; since a racing condition can happen, where the
    236    // viewport's listener is added after the resize, because the viewport's
    237    // document was reloaded; therefore the test would hang forever.
    238    // See bug 1302879.
    239    const browser = ui.getViewportBrowser();
    240 
    241    const onContentResize = data => {
    242      if (!isSizeMatching(data)) {
    243        return;
    244      }
    245      ui.off("content-resize", onContentResize);
    246      browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
    247      info(`Got content-resize to ${width} x ${height}`);
    248      resolve();
    249    };
    250 
    251    const onBrowserLoadEnd = async function () {
    252      const data = ui.getViewportSize(ui);
    253      onContentResize(data);
    254    };
    255 
    256    info(`Waiting for viewport-resize to ${width} x ${height}`);
    257    // We're changing the viewport size, which may also change the content
    258    // size. We wait on the viewport resize event, and check for the
    259    // desired size.
    260    ui.on("content-resize", onContentResize);
    261    browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, {
    262      once: true,
    263    });
    264  });
    265 }
    266 
    267 var setViewportSize = async function (ui, manager, width, height) {
    268  const size = ui.getViewportSize();
    269  info(
    270    `Current size: ${size.width} x ${size.height}, ` +
    271      `set to: ${width} x ${height}`
    272  );
    273  if (size.width != width || size.height != height) {
    274    const resized = waitForViewportResizeTo(ui, width, height);
    275    ui.setViewportSize({ width, height });
    276    await resized;
    277  }
    278 };
    279 
    280 // This performs the same function as setViewportSize, but additionally
    281 // ensures that reflow of the viewport has completed.
    282 var setViewportSizeAndAwaitReflow = async function (
    283  ui,
    284  manager,
    285  width,
    286  height
    287 ) {
    288  await setViewportSize(ui, manager, width, height);
    289  await promiseContentReflow(ui);
    290  await promiseApzFlushedRepaints();
    291 };
    292 
    293 function getViewportDevicePixelRatio(ui) {
    294  return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
    295    // Note that devicePixelRatio doesn't return the override to privileged
    296    // code, see bug 1759962.
    297    return content.browsingContext.overrideDPPX || content.devicePixelRatio;
    298  });
    299 }
    300 
    301 function getElRect(selector, win) {
    302  const el = win.document.querySelector(selector);
    303  return el.getBoundingClientRect();
    304 }
    305 
    306 /**
    307 * Drag an element identified by 'selector' by [x,y] amount. Returns
    308 * the rect of the dragged element as it was before drag.
    309 */
    310 function dragElementBy(selector, x, y, ui) {
    311  const browserWindow = ui.getBrowserWindow();
    312  const rect = getElRect(selector, browserWindow);
    313  const startPoint = {
    314    clientX: Math.floor(rect.left + rect.width / 2),
    315    clientY: Math.floor(rect.top + rect.height / 2),
    316  };
    317  const endPoint = [startPoint.clientX + x, startPoint.clientY + y];
    318 
    319  EventUtils.synthesizeMouseAtPoint(
    320    startPoint.clientX,
    321    startPoint.clientY,
    322    { type: "mousedown" },
    323    browserWindow
    324  );
    325 
    326  // mousemove and mouseup are regular DOM listeners
    327  EventUtils.synthesizeMouseAtPoint(
    328    ...endPoint,
    329    { type: "mousemove" },
    330    browserWindow
    331  );
    332  EventUtils.synthesizeMouseAtPoint(
    333    ...endPoint,
    334    { type: "mouseup" },
    335    browserWindow
    336  );
    337 
    338  return rect;
    339 }
    340 
    341 /**
    342 * Resize the viewport and check that the resize happened as expected.
    343 *
    344 * @param {ResponsiveUI} ui
    345 *        The ResponsiveUI instance.
    346 * @param {string} selector
    347 *        The css selector of the resize handler, eg .viewport-horizontal-resize-handle.
    348 * @param {Array<number>} moveBy
    349 *        Array of 2 integers representing the x,y distance of the resize action.
    350 * @param {Array<number>} moveBy
    351 *        Array of 2 integers representing the actual resize performed.
    352 * @param {object} options
    353 * @param {boolean} options.hasDevice
    354 *        Whether a device is currently set and will be overridden by the resize
    355 */
    356 async function testViewportResize(
    357  ui,
    358  selector,
    359  moveBy,
    360  expectedHandleMove,
    361  { hasDevice } = {}
    362 ) {
    363  // If a device was defined, wait for the device-associaton-removed event.
    364  const deviceRemoved = hasDevice
    365    ? once(ui, "device-association-removed")
    366    : null;
    367 
    368  const resized = ui.once("viewport-resize-dragend");
    369  const startRect = dragElementBy(selector, ...moveBy, ui);
    370  await resized;
    371 
    372  const endRect = getElRect(selector, ui.getBrowserWindow());
    373  is(
    374    endRect.left - startRect.left,
    375    expectedHandleMove[0],
    376    `The x move of ${selector} is as expected`
    377  );
    378  is(
    379    endRect.top - startRect.top,
    380    expectedHandleMove[1],
    381    `The y move of ${selector} is as expected`
    382  );
    383 
    384  await deviceRemoved;
    385 }
    386 
    387 async function openDeviceModal(ui) {
    388  const { document, store } = ui.toolWindow;
    389 
    390  info("Opening device modal through device selector.");
    391  const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen);
    392  await selectMenuItem(
    393    ui,
    394    "#device-selector",
    395    getStr("responsive.editDeviceList2")
    396  );
    397  await onModalOpen;
    398 
    399  const modal = document.getElementById("device-modal-wrapper");
    400  ok(
    401    modal.classList.contains("opened") && !modal.classList.contains("closed"),
    402    "The device modal is displayed."
    403  );
    404 }
    405 
    406 async function selectMenuItem({ toolWindow }, selector, value) {
    407  const { document } = toolWindow;
    408 
    409  const button = document.querySelector(selector);
    410  isnot(
    411    button,
    412    null,
    413    `Selector "${selector}" should match an existing element.`
    414  );
    415 
    416  info(`Selecting ${value} in ${selector}.`);
    417 
    418  await testMenuItems(toolWindow, button, items => {
    419    const menuItem = findMenuItem(items, value);
    420    isnot(
    421      menuItem,
    422      undefined,
    423      `Value "${value}" should match an existing menu item.`
    424    );
    425    menuItem.click();
    426  });
    427 }
    428 
    429 /**
    430 * Runs the menu items from the button's context menu against a test function.
    431 *
    432 * @param  {Window} toolWindow
    433 *         A window reference.
    434 * @param  {Element} button
    435 *         The button that will show a context menu when clicked.
    436 * @param  {Function} testFn
    437 *         A test function that will be ran with the found menu item in the context menu
    438 *         as an argument.
    439 */
    440 async function testMenuItems(toolWindow, button, testFn) {
    441  // The context menu appears only in the top level window, which is different from
    442  // the inner toolWindow.
    443  const win = getTopLevelWindow(toolWindow);
    444 
    445  await new Promise(resolve => {
    446    win.document.addEventListener(
    447      "popupshown",
    448      async () => {
    449        // Handle MenuButton popups (device selector and network throttling).
    450        if (
    451          button.id === "device-selector" ||
    452          button.id == "user-agent-selector" ||
    453          button.id === "network-throttling"
    454        ) {
    455          let popupId;
    456          if (button.id === "device-selector") {
    457            popupId = "#device-selector-menu";
    458          } else if (button.id === "user-agent-selector") {
    459            popupId = "#user-agent-selector-menu";
    460          } else {
    461            popupId = "#network-throttling-menu";
    462          }
    463 
    464          const popup = toolWindow.document.querySelector(popupId);
    465          const menuItems = [...popup.querySelectorAll(".menuitem > .command")];
    466 
    467          testFn(menuItems);
    468 
    469          if (popup.classList.contains("tooltip-visible")) {
    470            // Close the tooltip explicitly.
    471            button.click();
    472            await waitUntil(() => !popup.classList.contains("tooltip-visible"));
    473          }
    474        } else {
    475          const popup = win.document.querySelector(
    476            'menupopup[menu-api="true"]'
    477          );
    478          const menuItems = [...popup.children];
    479 
    480          testFn(menuItems);
    481 
    482          popup.hidePopup();
    483        }
    484 
    485        resolve();
    486      },
    487      { once: true }
    488    );
    489 
    490    button.click();
    491  });
    492 }
    493 
    494 const selectDevice = async (ui, value) => {
    495  const browser = ui.getViewportBrowser();
    496  const waitForDevToolsReload = await watchForDevToolsReload(browser);
    497 
    498  const onDeviceChanged = once(ui, "device-changed");
    499  await selectMenuItem(ui, "#device-selector", value);
    500  const { reloadTriggered } = await onDeviceChanged;
    501  if (reloadTriggered) {
    502    await waitForDevToolsReload();
    503  }
    504 };
    505 
    506 const selectDevicePixelRatio = (ui, value) =>
    507  selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`);
    508 
    509 const selectNetworkThrottling = (ui, value) =>
    510  Promise.all([
    511    once(ui, "network-throttling-changed"),
    512    selectMenuItem(ui, "#network-throttling", value),
    513  ]);
    514 
    515 function getSessionHistory(browser) {
    516  if (Services.appinfo.sessionHistoryInParent) {
    517    const browsingContext = browser.browsingContext;
    518    const uri = browsingContext.currentWindowGlobal.documentURI.displaySpec;
    519    const history = browsingContext.sessionHistory;
    520    const body = ContentTask.spawn(
    521      browser,
    522      browsingContext,
    523      function (
    524        // eslint-disable-next-line no-shadow
    525        browsingContext
    526      ) {
    527        const docShell = browsingContext.docShell.QueryInterface(
    528          Ci.nsIWebNavigation
    529        );
    530        return docShell.document.body;
    531      }
    532    );
    533    const { SessionHistory } = ChromeUtils.importESModule(
    534      "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
    535    );
    536    return SessionHistory.collectFromParent(uri, body, history);
    537  }
    538  return ContentTask.spawn(browser, null, function () {
    539    const { SessionHistory } = ChromeUtils.importESModule(
    540      "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
    541    );
    542    return SessionHistory.collect(docShell);
    543  });
    544 }
    545 
    546 function getContentSize(ui) {
    547  return spawnViewportTask(ui, {}, () => ({
    548    width: content.screen.width,
    549    height: content.screen.height,
    550  }));
    551 }
    552 
    553 function getViewportScroll(ui) {
    554  return spawnViewportTask(ui, {}, () => ({
    555    x: content.scrollX,
    556    y: content.scrollY,
    557  }));
    558 }
    559 
    560 async function waitForPageShow(browser) {
    561  const tab = gBrowser.getTabForBrowser(browser);
    562  const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
    563  if (ui) {
    564    browser = ui.getViewportBrowser();
    565  }
    566  info(
    567    "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser"
    568  );
    569  // Need to wait an extra tick after pageshow to ensure everyone is up-to-date,
    570  // hence the waitForTick.
    571  await BrowserTestUtils.waitForContentEvent(browser, "pageshow");
    572  return waitForTick();
    573 }
    574 
    575 function waitForViewportScroll(ui) {
    576  return BrowserTestUtils.waitForContentEvent(
    577    ui.getViewportBrowser(),
    578    "scroll",
    579    true
    580  );
    581 }
    582 
    583 async function back(browser) {
    584  const waitForDevToolsReload = await watchForDevToolsReload(browser);
    585  const onPageShow = waitForPageShow(browser);
    586 
    587  browser.goBack();
    588 
    589  await onPageShow;
    590  await waitForDevToolsReload();
    591 }
    592 
    593 async function forward(browser) {
    594  const waitForDevToolsReload = await watchForDevToolsReload(browser);
    595  const onPageShow = waitForPageShow(browser);
    596 
    597  browser.goForward();
    598 
    599  await onPageShow;
    600  await waitForDevToolsReload();
    601 }
    602 
    603 function addDeviceForTest(device) {
    604  info(`Adding Test Device "${device.name}" to the list.`);
    605  addDevice(device);
    606 
    607  registerCleanupFunction(() => {
    608    // Note that assertions in cleanup functions are not displayed unless they failed.
    609    ok(
    610      removeDevice(device),
    611      `Removed Test Device "${device.name}" from the list.`
    612    );
    613  });
    614 }
    615 
    616 async function waitForClientClose(ui) {
    617  info("Waiting for RDM devtools client to close");
    618  await ui.commands.client.once("closed");
    619  info("RDM's devtools client is now closed");
    620 }
    621 
    622 async function testDevicePixelRatio(ui, expected) {
    623  const dppx = await getViewportDevicePixelRatio(ui);
    624  is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
    625 }
    626 
    627 function testTouchEventsOverride(ui, expected) {
    628  const { document } = ui.toolWindow;
    629  const touchButton = document.getElementById("touch-simulation-button");
    630 
    631  const flag = gBrowser.selectedBrowser.browsingContext.touchEventsOverride;
    632 
    633  is(
    634    flag === "enabled",
    635    expected,
    636    `Touch events override should be ${expected ? "enabled" : "disabled"}`
    637  );
    638  is(
    639    touchButton.classList.contains("checked"),
    640    expected,
    641    `Touch simulation button should be ${expected ? "" : "in"}active.`
    642  );
    643 }
    644 
    645 function testViewportDeviceMenuLabel(ui, expectedDeviceName) {
    646  info("Test viewport's device select label");
    647 
    648  const button = ui.toolWindow.document.querySelector("#device-selector");
    649  ok(
    650    button.textContent.includes(expectedDeviceName),
    651    `Device Select value ${button.textContent} should be: ${expectedDeviceName}`
    652  );
    653 }
    654 
    655 async function toggleTouchSimulation(ui) {
    656  const { document } = ui.toolWindow;
    657  const browser = ui.getViewportBrowser();
    658 
    659  const touchButton = document.getElementById("touch-simulation-button");
    660  const wasChecked = touchButton.classList.contains("checked");
    661  const onTouchSimulationChanged = once(ui, "touch-simulation-changed");
    662  const waitForDevToolsReload = await watchForDevToolsReload(browser);
    663  const onTouchButtonStateChanged = waitFor(
    664    () => touchButton.classList.contains("checked") !== wasChecked
    665  );
    666 
    667  touchButton.click();
    668  await Promise.all([
    669    onTouchSimulationChanged,
    670    onTouchButtonStateChanged,
    671    waitForDevToolsReload(),
    672  ]);
    673 }
    674 
    675 async function testUserAgent(ui, expected) {
    676  const { document } = ui.toolWindow;
    677  const userAgentInput = document.getElementById("user-agent-input");
    678 
    679  if (expected === DEFAULT_UA) {
    680    is(userAgentInput.value, "", "UA input should be empty");
    681  } else {
    682    is(userAgentInput.value, expected, `UA input should be set to ${expected}`);
    683  }
    684 
    685  await testUserAgentFromBrowser(ui.getViewportBrowser(), expected);
    686 }
    687 
    688 async function testUserAgentFromBrowser(browser, expected) {
    689  const ua = await SpecialPowers.spawn(browser, [], async function () {
    690    return content.navigator.userAgent;
    691  });
    692  is(ua, expected, `UA should be set to ${expected}`);
    693 }
    694 
    695 function testViewportDimensions(ui, w, h) {
    696  const viewport = ui.viewportElement;
    697 
    698  is(
    699    ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
    700    `${w}px`,
    701    `Viewport should have width of ${w}px`
    702  );
    703  is(
    704    ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
    705    `${h}px`,
    706    `Viewport should have height of ${h}px`
    707  );
    708 }
    709 
    710 async function changeUserAgentInput(
    711  ui,
    712  value,
    713  keyPressedAfterChange = "VK_RETURN"
    714 ) {
    715  const { Simulate } = ui.toolWindow.require(
    716    "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
    717  );
    718  const { document, store } = ui.toolWindow;
    719  const browser = ui.getViewportBrowser();
    720 
    721  const userAgentInput = document.getElementById("user-agent-input");
    722  userAgentInput.value = value;
    723  userAgentInput.focus();
    724  Simulate.change(userAgentInput);
    725 
    726  function pressKey() {
    727    EventUtils.synthesizeKey(keyPressedAfterChange, {}, ui.toolWindow);
    728  }
    729 
    730  if (keyPressedAfterChange === "VK_ESCAPE") {
    731    pressKey();
    732  } else {
    733    const userAgentChanged = waitUntilState(
    734      store,
    735      state => state.ui.userAgent === value
    736    );
    737    const changed = once(ui, "user-agent-changed");
    738    const waitForDevToolsReload = await watchForDevToolsReload(browser);
    739 
    740    pressKey();
    741 
    742    await Promise.all([changed, waitForDevToolsReload(), userAgentChanged]);
    743  }
    744 }
    745 
    746 /**
    747 * Assuming the device modal is open and the device adder form is shown, this helper
    748 * function adds `device` via the form, saves it, and waits for it to appear in the store.
    749 */
    750 function addDeviceInModal(ui, device) {
    751  const { Simulate } = ui.toolWindow.require(
    752    "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
    753  );
    754  const { document, store } = ui.toolWindow;
    755 
    756  const nameInput = document.querySelector("#device-form-name input");
    757  const [widthInput, heightInput] = document.querySelectorAll(
    758    "#device-form-size input"
    759  );
    760  const pixelRatioInput = document.querySelector(
    761    "#device-form-pixel-ratio input"
    762  );
    763  const userAgentInput = document.querySelector(
    764    "#device-form-user-agent input"
    765  );
    766  const touchInput = document.querySelector("#device-form-touch input");
    767 
    768  nameInput.value = device.name;
    769  Simulate.change(nameInput);
    770  widthInput.value = device.width;
    771  Simulate.change(widthInput);
    772  Simulate.blur(widthInput);
    773  heightInput.value = device.height;
    774  Simulate.change(heightInput);
    775  Simulate.blur(heightInput);
    776  pixelRatioInput.value = device.pixelRatio;
    777  Simulate.change(pixelRatioInput);
    778  userAgentInput.value = device.userAgent;
    779  Simulate.change(userAgentInput);
    780  touchInput.checked = device.touch;
    781  Simulate.change(touchInput);
    782 
    783  const existingCustomDevices = store.getState().devices.custom.length;
    784  const adderSave = document.querySelector("#device-form-save");
    785  const saved = waitUntilState(
    786    store,
    787    state => state.devices.custom.length == existingCustomDevices + 1
    788  );
    789  Simulate.click(adderSave);
    790  return saved;
    791 }
    792 
    793 async function editDeviceInModal(ui, device, newDevice) {
    794  const { Simulate } = ui.toolWindow.require(
    795    "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
    796  );
    797  const { document, store } = ui.toolWindow;
    798 
    799  const nameInput = document.querySelector("#device-form-name input");
    800  const [widthInput, heightInput] = document.querySelectorAll(
    801    "#device-form-size input"
    802  );
    803  const pixelRatioInput = document.querySelector(
    804    "#device-form-pixel-ratio input"
    805  );
    806  const userAgentInput = document.querySelector(
    807    "#device-form-user-agent input"
    808  );
    809  const touchInput = document.querySelector("#device-form-touch input");
    810 
    811  nameInput.value = newDevice.name;
    812  Simulate.change(nameInput);
    813  widthInput.value = newDevice.width;
    814  Simulate.change(widthInput);
    815  Simulate.blur(widthInput);
    816  heightInput.value = newDevice.height;
    817  Simulate.change(heightInput);
    818  Simulate.blur(heightInput);
    819  pixelRatioInput.value = newDevice.pixelRatio;
    820  Simulate.change(pixelRatioInput);
    821  userAgentInput.value = newDevice.userAgent;
    822  Simulate.change(userAgentInput);
    823  touchInput.checked = newDevice.touch;
    824  Simulate.change(touchInput);
    825 
    826  const existingCustomDevices = store.getState().devices.custom.length;
    827  const formSave = document.querySelector("#device-form-save");
    828 
    829  const saved = waitUntilState(
    830    store,
    831    state =>
    832      state.devices.custom.length == existingCustomDevices &&
    833      state.devices.custom.find(({ name }) => name == newDevice.name) &&
    834      !state.devices.custom.find(({ name }) => name == device.name)
    835  );
    836 
    837  // Editing a custom device triggers a "device-change" message.
    838  // Wait for the `device-changed` event to avoid unfinished requests during the
    839  // tests.
    840  const onDeviceChanged = ui.once("device-changed");
    841 
    842  Simulate.click(formSave);
    843 
    844  await onDeviceChanged;
    845  return saved;
    846 }
    847 
    848 function findMenuItem(menuItems, name) {
    849  return menuItems.find(menuItem => menuItem.textContent.includes(name));
    850 }
    851 
    852 function reloadOnUAChange(enabled) {
    853  const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent";
    854  Services.prefs.setBoolPref(pref, enabled);
    855 }
    856 
    857 function reloadOnTouchChange(enabled) {
    858  const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation";
    859  Services.prefs.setBoolPref(pref, enabled);
    860 }
    861 
    862 function rotateViewport(ui) {
    863  const { document } = ui.toolWindow;
    864  const rotateButton = document.getElementById("rotate-button");
    865  rotateButton.click();
    866 }
    867 
    868 // Call this to switch between on/off support for meta viewports.
    869 async function setTouchAndMetaViewportSupport(ui, value) {
    870  await ui.updateTouchSimulation(value);
    871  info("Reload so the new configuration applies cleanly to the page");
    872  await reloadBrowser();
    873 
    874  await promiseContentReflow(ui);
    875 }
    876 
    877 // This function checks that zoom, the initial containing block width and height
    878 // are all as expected.
    879 async function testViewportZoomWidthAndHeight(msg, ui, zoom, width, height) {
    880  if (typeof zoom !== "undefined") {
    881    const resolution = await spawnViewportTask(ui, {}, function () {
    882      return content.windowUtils.getResolution();
    883    });
    884    is(resolution, zoom, msg + " should have expected zoom.");
    885  }
    886 
    887  if (typeof width !== "undefined" || typeof height !== "undefined") {
    888    const innerSize = await spawnViewportTask(ui, {}, function () {
    889      return {
    890        width: content.document.documentElement.clientWidth,
    891        height: content.document.documentElement.clientHeight,
    892      };
    893    });
    894    if (typeof width !== "undefined") {
    895      is(innerSize.width, width, msg + " should have expected inner width.");
    896    }
    897    if (typeof height !== "undefined") {
    898      is(innerSize.height, height, msg + " should have expected inner height.");
    899    }
    900  }
    901 }
    902 
    903 function promiseContentReflow(ui) {
    904  return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
    905    return new Promise(resolve => {
    906      content.window.requestAnimationFrame(() => {
    907        content.window.requestAnimationFrame(resolve);
    908      });
    909    });
    910  });
    911 }
    912 
    913 // This function returns a promise that will be resolved when the
    914 // RDM zoom has been set and the content has finished rescaling
    915 // to the new size.
    916 async function promiseRDMZoom(ui, browser, zoom) {
    917  const currentZoom = ZoomManager.getZoomForBrowser(browser);
    918  if (currentZoom.toFixed(2) == zoom.toFixed(2)) {
    919    return;
    920  }
    921 
    922  const width = browser.getBoundingClientRect().width;
    923 
    924  ZoomManager.setZoomForBrowser(browser, zoom);
    925 
    926  // RDM resizes the browser as a result of a zoom change, so we wait for that.
    927  //
    928  // This also has the side effect of updating layout which ensures that any
    929  // remote frame dimension update message gets there in time.
    930  await BrowserTestUtils.waitForCondition(function () {
    931    return browser.getBoundingClientRect().width != width;
    932  });
    933 }
    934 
    935 async function waitForDeviceAndViewportState(ui) {
    936  const { store } = ui.toolWindow;
    937 
    938  // Wait until the viewport has been added and the device list has been loaded
    939  await waitUntilState(
    940    store,
    941    state =>
    942      state.viewports.length == 1 &&
    943      state.devices.listState == localTypes.loadableState.LOADED
    944  );
    945 }
    946 
    947 /**
    948 * Wait for the content page to be rendered with the expected pixel ratio.
    949 *
    950 * @param {ResponsiveUI} ui
    951 *        The ResponsiveUI instance.
    952 * @param {Integer} expected
    953 *        The expected dpr for the content page.
    954 * @param {object} options
    955 * @param {boolean} options.waitForTargetConfiguration
    956 *        If set to true, the function will wait for the targetConfigurationCommand configuration
    957 *        to reflect the ratio that was set. This can be used to prevent pending requests
    958 *        to the actor.
    959 */
    960 async function waitForDevicePixelRatio(
    961  ui,
    962  expected,
    963  { waitForTargetConfiguration } = {}
    964 ) {
    965  const dpx = await SpecialPowers.spawn(
    966    ui.getViewportBrowser(),
    967    [{ expected }],
    968    function (args) {
    969      const getDpr = function () {
    970        return content.browsingContext.overrideDPPX || content.devicePixelRatio;
    971      };
    972      const initial = getDpr();
    973      info(
    974        `Listening for pixel ratio change ` +
    975          `(current: ${initial}, expected: ${args.expected})`
    976      );
    977      return new Promise(resolve => {
    978        const mql = content.matchMedia(`(resolution: ${args.expected}dppx)`);
    979        if (mql.matches) {
    980          info(`Ratio already changed to ${args.expected}dppx`);
    981          resolve(getDpr());
    982          return;
    983        }
    984        mql.addListener(function listener() {
    985          info(`Ratio changed to ${args.expected}dppx`);
    986          mql.removeListener(listener);
    987          resolve(getDpr());
    988        });
    989      });
    990    }
    991  );
    992 
    993  if (waitForTargetConfiguration) {
    994    // Ensure the configuration was updated so we limit the risk of the client closing before
    995    // the server sent back the result of the updateConfiguration call.
    996    await waitFor(() => {
    997      return (
    998        ui.commands.targetConfigurationCommand.configuration.overrideDPPX ===
    999        expected
   1000      );
   1001    });
   1002  }
   1003 
   1004  return dpx;
   1005 }