tor-browser

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

helpers.js (26998B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 /**
      7 * Wait for a single requestAnimationFrame tick.
      8 */
      9 function tick() {
     10  return new Promise(resolve => requestAnimationFrame(resolve));
     11 }
     12 
     13 /**
     14 * It can be confusing when waiting for something asynchronously. This function
     15 * logs out a message periodically (every 1 second) in order to create helpful
     16 * log messages.
     17 *
     18 * @param {string} message
     19 * @returns {Function}
     20 */
     21 function createPeriodicLogger() {
     22  let startTime = Date.now();
     23  let lastCount = 0;
     24  let lastMessage = null;
     25 
     26  return message => {
     27    if (lastMessage === message) {
     28      // The messages are the same, check if we should log them.
     29      const now = Date.now();
     30      const count = Math.floor((now - startTime) / 1000);
     31      if (count !== lastCount) {
     32        info(
     33          `${message} (After ${count} ${count === 1 ? "second" : "seconds"})`
     34        );
     35        lastCount = count;
     36      }
     37    } else {
     38      // The messages are different, log them now, and reset the waiting time.
     39      info(message);
     40      startTime = Date.now();
     41      lastCount = 0;
     42      lastMessage = message;
     43    }
     44  };
     45 }
     46 
     47 /**
     48 * Wait until a condition is fullfilled.
     49 *
     50 * @param {Function} condition
     51 * @param {string?} logMessage
     52 * @return The truthy result of the condition.
     53 */
     54 async function waitUntil(condition, message) {
     55  return TestUtils.waitForCondition(condition, message);
     56 }
     57 
     58 /**
     59 * This function looks inside of a container for some element that has a label.
     60 * It runs in a loop every requestAnimationFrame until it finds the element. If
     61 * it doesn't find the element it throws an error.
     62 *
     63 * @param {Element} container
     64 * @param {string} label
     65 * @returns {Promise<HTMLElement>}
     66 */
     67 function getElementByLabel(container, label) {
     68  return waitUntil(
     69    () => container.querySelector(`[label="${label}"]`),
     70    `Trying to find the button with the label "${label}".`
     71  );
     72 }
     73 /* exported getElementByLabel */
     74 
     75 /**
     76 * This function looks inside of a container for some element that has a tooltip.
     77 * It runs in a loop every requestAnimationFrame until it finds the element. If
     78 * it doesn't find the element it throws an error.
     79 *
     80 * @param {Element} container
     81 * @param {string} tooltip
     82 * @returns {Promise<HTMLElement>}
     83 */
     84 function getElementByTooltip(container, tooltip) {
     85  return waitUntil(
     86    () => container.querySelector(`[tooltiptext="${tooltip}"]`),
     87    `Trying to find the button with the tooltip "${tooltip}".`
     88  );
     89 }
     90 /* exported getElementByTooltip */
     91 
     92 /**
     93 * This function will select a node from the XPath.
     94 *
     95 * @returns {HTMLElement?}
     96 */
     97 function getElementByXPath(document, path) {
     98  return document.evaluate(
     99    path,
    100    document,
    101    null,
    102    XPathResult.FIRST_ORDERED_NODE_TYPE,
    103    null
    104  ).singleNodeValue;
    105 }
    106 /* exported getElementByXPath */
    107 
    108 /**
    109 * This function looks inside of a document for some element that contains
    110 * the given text. It runs in a loop every requestAnimationFrame until it
    111 * finds the element. If it doesn't find the element it throws an error.
    112 *
    113 * @param {HTMLDocument} document
    114 * @param {string} text
    115 * @returns {Promise<HTMLElement>}
    116 */
    117 async function getElementFromDocumentByText(document, text) {
    118  // Fallback on aria-label if there are no results for the text xpath.
    119  const xpath = `//*[contains(text(), '${text}')] | //*[contains(@aria-label, '${text}')]`;
    120  return waitUntil(() => {
    121    const element = getElementByXPath(document, xpath);
    122    if (element && BrowserTestUtils.isVisible(element)) {
    123      return element;
    124    }
    125    return null;
    126  }, `Trying to find a visible element with the text "${text}".`);
    127 }
    128 /* exported getElementFromDocumentByText */
    129 
    130 /**
    131 * This function is similar to getElementFromDocumentByText, but it immediately
    132 * returns and does not wait for an element to exist.
    133 *
    134 * @param {HTMLDocument} document
    135 * @param {string} text
    136 * @returns {HTMLElement?}
    137 */
    138 function maybeGetElementFromDocumentByText(document, text) {
    139  info(`Immediately trying to find the element with the text "${text}".`);
    140  const xpath = `//*[contains(text(), '${text}')]`;
    141  return getElementByXPath(document, xpath);
    142 }
    143 /* exported maybeGetElementFromDocumentByText */
    144 
    145 /**
    146 * Make sure the profiler popup is enabled.
    147 */
    148 async function makeSureProfilerPopupIsEnabled() {
    149  info("Make sure the profiler popup is enabled.");
    150 
    151  info("> Load the profiler menu button.");
    152  const { ProfilerMenuButton } = ChromeUtils.importESModule(
    153    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
    154  );
    155 
    156  if (!ProfilerMenuButton.isInNavbar()) {
    157    // Make sure the feature flag is enabled.
    158    SpecialPowers.pushPrefEnv({
    159      set: [["devtools.performance.popup.feature-flag", true]],
    160    });
    161 
    162    info("> The menu button is not in the nav bar, add it.");
    163    ProfilerMenuButton.addToNavbar();
    164 
    165    await waitUntil(
    166      () => gBrowser.ownerDocument.getElementById("profiler-button"),
    167      "> Waiting until the profiler button is added to the browser."
    168    );
    169 
    170    await SimpleTest.promiseFocus(gBrowser.ownerGlobal);
    171 
    172    registerCleanupFunction(() => {
    173      info(
    174        "Clean up after the test by disabling the profiler popup menu button."
    175      );
    176      if (!ProfilerMenuButton.isInNavbar()) {
    177        throw new Error(
    178          "Expected the profiler popup to still be in the navbar during the test cleanup."
    179        );
    180      }
    181      ProfilerMenuButton.remove();
    182    });
    183  } else {
    184    info("> The menu button was already enabled.");
    185  }
    186 }
    187 /* exported makeSureProfilerPopupIsEnabled */
    188 
    189 /**
    190 * XUL popups will fire the popupshown and popuphidden events. These will fire for
    191 * any type of popup in the browser. This function waits for one of those events, and
    192 * checks that the viewId of the popup is PanelUI-profiler
    193 *
    194 * @param {Window} window
    195 * @param {"popupshown" | "popuphidden"} eventName
    196 * @returns {Promise<void>}
    197 */
    198 function waitForProfilerPopupEvent(window, eventName) {
    199  return new Promise(resolve => {
    200    function handleEvent(event) {
    201      if (event.target.getAttribute("viewId") === "PanelUI-profiler") {
    202        window.removeEventListener(eventName, handleEvent);
    203        resolve();
    204      }
    205    }
    206    window.addEventListener(eventName, handleEvent);
    207  });
    208 }
    209 /* exported waitForProfilerPopupEvent */
    210 
    211 /**
    212 * Do not use this directly in a test. Prefer withPopupOpen and openPopupAndEnsureCloses.
    213 *
    214 * This function toggles the profiler menu button, and then uses user gestures
    215 * to click it open. It waits a tick to make sure it has a chance to initialize.
    216 *
    217 * @param {Window} window
    218 * @return {Promise<void>}
    219 */
    220 async function _toggleOpenProfilerPopup(window) {
    221  info("Toggle open the profiler popup.");
    222 
    223  info("> Find the profiler menu button.");
    224  const profilerDropmarker = window.document.getElementById(
    225    "profiler-button-dropmarker"
    226  );
    227  if (!profilerDropmarker) {
    228    throw new Error(
    229      "Could not find the profiler button dropmarker in the toolbar."
    230    );
    231  }
    232 
    233  const popupShown = waitForProfilerPopupEvent(window, "popupshown");
    234 
    235  info("> Trigger a click on the profiler button dropmarker.");
    236  await EventUtils.synthesizeMouseAtCenter(profilerDropmarker, {}, window);
    237 
    238  if (profilerDropmarker.getAttribute("open") !== "true") {
    239    throw new Error(
    240      "This test assumes that the button will have an open=true attribute after clicking it."
    241    );
    242  }
    243 
    244  info("> Wait for the popup to be shown.");
    245  await popupShown;
    246  // Also wait a tick in case someone else is subscribing to the "popupshown" event
    247  // and is doing synchronous work with it.
    248  await tick();
    249 }
    250 
    251 /**
    252 * Do not use this directly in a test. Prefer withPopupOpen.
    253 *
    254 * This function uses a keyboard shortcut to close the profiler popup.
    255 *
    256 * @param {Window} window
    257 * @return {Promise<void>}
    258 */
    259 async function _closePopup(window) {
    260  const popupHiddenPromise = waitForProfilerPopupEvent(window, "popuphidden");
    261  info("> Trigger an escape key to hide the popup");
    262  EventUtils.synthesizeKey("KEY_Escape");
    263 
    264  info("> Wait for the popup to be hidden.");
    265  await popupHiddenPromise;
    266  // Also wait a tick in case someone else is subscribing to the "popuphidden" event
    267  // and is doing synchronous work with it.
    268  await tick();
    269 }
    270 
    271 /**
    272 * Perform some action on the popup, and close it afterwards.
    273 *
    274 * @param {Window} window
    275 * @param {() => Promise<void>} callback
    276 */
    277 async function withPopupOpen(window, callback) {
    278  await _toggleOpenProfilerPopup(window);
    279  await callback();
    280  await _closePopup(window);
    281 }
    282 /* exported withPopupOpen */
    283 
    284 /**
    285 * This function opens the profiler popup, but also ensures that something else closes
    286 * it before the end of the test. This is useful for tests that trigger the profiler
    287 * popup to close through an implicit action, like opening a tab.
    288 *
    289 * @param {Window} window
    290 * @param {() => Promise<void>} callback
    291 */
    292 async function openPopupAndEnsureCloses(window, callback) {
    293  await _toggleOpenProfilerPopup(window);
    294  // We want to ensure the popup gets closed by the test, during the callback.
    295  const popupHiddenPromise = waitForProfilerPopupEvent(window, "popuphidden");
    296  await callback();
    297  info("> Verifying that the popup was closed by the test.");
    298  await popupHiddenPromise;
    299 }
    300 /* exported openPopupAndEnsureCloses */
    301 
    302 /**
    303 * This function overwrites the default profiler.firefox.com URL for tests. This
    304 * ensures that the tests do not attempt to access external URLs.
    305 * The origin needs to be on the allowlist in validateProfilerWebChannelUrl,
    306 * otherwise the WebChannel won't work. ("https://example.com" is on that list.)
    307 *
    308 * @param {string} origin - For example: https://example.com
    309 * @param {string} pathname - For example: /my/testing/frontend.html
    310 * @returns {Promise}
    311 */
    312 function setProfilerFrontendUrl(origin, pathname) {
    313  return SpecialPowers.pushPrefEnv({
    314    set: [
    315      // Make sure observer and testing function run in the same process
    316      ["devtools.performance.recording.ui-base-url", origin],
    317      ["devtools.performance.recording.ui-base-url-path", pathname],
    318    ],
    319  });
    320 }
    321 /* exported setProfilerFrontendUrl */
    322 
    323 /**
    324 * This function checks the document title of a tab to see what the state is.
    325 * This creates a simple messaging mechanism between the content page and the
    326 * test harness. This function runs in a loop every requestAnimationFrame, and
    327 * checks for a sucess title. In addition, an "initialTitle" and "errorTitle"
    328 * can be specified for nicer test output.
    329 *
    330 * @param {object}
    331 *   {
    332 *     initialTitle: string,
    333 *     successTitle: string,
    334 *     errorTitle: string
    335 *   }
    336 */
    337 async function checkTabLoadedProfile({
    338  initialTitle,
    339  successTitle,
    340  errorTitle,
    341 }) {
    342  const logPeriodically = createPeriodicLogger();
    343 
    344  info("Attempting to see if the selected tab can receive a profile.");
    345 
    346  return waitUntil(() => {
    347    switch (gBrowser.selectedTab.label) {
    348      case initialTitle:
    349        logPeriodically(`> Waiting for the profile to be received.`);
    350        return false;
    351      case successTitle:
    352        ok(true, "The profile was successfully injected to the page");
    353        BrowserTestUtils.removeTab(gBrowser.selectedTab);
    354        return true;
    355      case errorTitle:
    356        throw new Error(
    357          "The fake frontend indicated that there was an error injecting the profile."
    358        );
    359      default:
    360        logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
    361        return false;
    362    }
    363  });
    364 }
    365 /* exported checkTabLoadedProfile */
    366 
    367 /**
    368 * This function checks the url of a tab so we can assert the frontend's url
    369 * with our expected url. This function runs in a loop every
    370 * requestAnimationFrame, and checks for a initialTitle. Asserts as soon as it
    371 * finds that title. We don't have to look for success title or error title
    372 * since we only care about the url.
    373 *
    374 * @param {{
    375 *     initialTitle: string,
    376 *     successTitle: string,
    377 *     errorTitle: string,
    378 *     expectedUrl: string
    379 *   }}
    380 */
    381 async function waitForTabUrl({
    382  initialTitle,
    383  successTitle,
    384  errorTitle,
    385  expectedUrl,
    386 }) {
    387  const logPeriodically = createPeriodicLogger();
    388 
    389  info(`Waiting for the selected tab to have the url "${expectedUrl}".`);
    390 
    391  return waitUntil(() => {
    392    switch (gBrowser.selectedTab.label) {
    393      case initialTitle:
    394      case successTitle:
    395        if (gBrowser.currentURI.spec === expectedUrl) {
    396          ok(true, `The selected tab has the url ${expectedUrl}`);
    397          BrowserTestUtils.removeTab(gBrowser.selectedTab);
    398          return true;
    399        }
    400        throw new Error(
    401          `Found a different url on the fake frontend: ${gBrowser.currentURI.spec} (expecting ${expectedUrl})`
    402        );
    403      case errorTitle:
    404        throw new Error(
    405          "The fake frontend indicated that there was an error injecting the profile."
    406        );
    407      default:
    408        logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
    409        return false;
    410    }
    411  });
    412 }
    413 /* exported waitForTabUrl */
    414 
    415 /**
    416 * This function checks the document title of a tab as an easy way to pass
    417 * messages from a content page to the mochitest.
    418 *
    419 * @param {string} title
    420 */
    421 async function waitForTabTitle(title) {
    422  const logPeriodically = createPeriodicLogger();
    423 
    424  info(`Waiting for the selected tab to have the title "${title}".`);
    425 
    426  return waitUntil(() => {
    427    if (gBrowser.selectedTab.label === title) {
    428      ok(true, `The selected tab has the title ${title}`);
    429      return true;
    430    }
    431    logPeriodically(`> Waiting for the tab title to change.`);
    432    return false;
    433  });
    434 }
    435 /* exported waitForTabTitle */
    436 
    437 /**
    438 * Open about:profiling in a new tab, and output helpful log messages.
    439 *
    440 * @template T
    441 * @param {(Document, ChromeBrowser) => T} callback
    442 * @returns {Promise<T>}
    443 */
    444 function withAboutProfiling(callback) {
    445  info("Begin to open about:profiling in a new tab.");
    446  return BrowserTestUtils.withNewTab(
    447    "about:profiling",
    448    async contentBrowser => {
    449      info("about:profiling is now open in a tab.");
    450      await TestUtils.waitForCondition(
    451        () =>
    452          contentBrowser.contentDocument.getElementById("root")
    453            .firstElementChild,
    454        "Document's root has been populated"
    455      );
    456      return callback(contentBrowser.contentDocument, contentBrowser);
    457    }
    458  );
    459 }
    460 /* exported withAboutProfiling */
    461 
    462 /**
    463 * Open DevTools and view the performance-new tab. After running the callback, clean
    464 * up the test.
    465 *
    466 * @param {string} [url="about:blank"] url for the new tab
    467 * @param {(Document, Document) => unknown} callback: the first parameter is the
    468 *                                          devtools panel's document, the
    469 *                                          second parameter is the opened tab's
    470 *                                          document.
    471 * @param {Window} [aWindow] The browser's window object we target
    472 * @returns {Promise<void>}
    473 */
    474 async function withDevToolsPanel(url, callback, aWindow = window) {
    475  if (typeof url === "function") {
    476    aWindow = callback ?? window;
    477    callback = url;
    478    url = "about:blank";
    479  }
    480 
    481  const { gBrowser } = aWindow;
    482 
    483  const {
    484    gDevTools,
    485  } = require("resource://devtools/client/framework/devtools.js");
    486 
    487  info(`Create a new tab with url "${url}".`);
    488  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
    489 
    490  info("Begin to open the DevTools and the performance-new panel.");
    491  const toolbox = await gDevTools.showToolboxForTab(tab, {
    492    toolId: "performance",
    493  });
    494 
    495  const { document } = toolbox.getCurrentPanel().panelWin;
    496 
    497  info("The performance-new panel is now open and ready to use.");
    498  await callback(document, tab.linkedBrowser.contentDocument);
    499 
    500  info("About to remove the about:blank tab");
    501  await toolbox.destroy();
    502 
    503  // The previous asynchronous functions may resolve within a tick after opening a new tab.
    504  // We shouldn't remove the newly opened tab in the same tick.
    505  // Wait for the next tick here.
    506  await TestUtils.waitForTick();
    507 
    508  // Take care to register the TabClose event before we call removeTab, to avoid
    509  // race issues.
    510  const waitForClosingPromise = BrowserTestUtils.waitForTabClosing(tab);
    511  BrowserTestUtils.removeTab(tab);
    512  info("Requested closing the about:blank tab, waiting...");
    513  await waitForClosingPromise;
    514  info("The about:blank tab is now removed.");
    515 }
    516 /* exported withDevToolsPanel */
    517 
    518 /**
    519 * Start and stop the profiler to get the current active configuration. This is
    520 * done programmtically through the nsIProfiler interface, rather than through click
    521 * interactions, since the about:profiling page does not include buttons to control
    522 * the recording.
    523 *
    524 * @returns {object}
    525 */
    526 function getActiveConfiguration() {
    527  const BackgroundJSM = ChromeUtils.importESModule(
    528    "resource://devtools/client/performance-new/shared/background.sys.mjs"
    529  );
    530 
    531  const { startProfiler, stopProfiler } = BackgroundJSM;
    532 
    533  info("Start the profiler with the current about:profiling configuration.");
    534  startProfiler("aboutprofiling");
    535 
    536  // Immediately pause the sampling, to make sure the test runs fast. The profiler
    537  // only needs to be started to initialize the configuration.
    538  Services.profiler.Pause();
    539 
    540  const { activeConfiguration } = Services.profiler;
    541  if (!activeConfiguration) {
    542    throw new Error(
    543      "Expected to find an active configuration for the profile."
    544    );
    545  }
    546 
    547  info("Stop the profiler after getting the active configuration.");
    548  stopProfiler();
    549 
    550  return activeConfiguration;
    551 }
    552 /* exported getActiveConfiguration */
    553 
    554 /**
    555 * Start the profiler programmatically and check that the active configuration has
    556 * a feature enabled
    557 *
    558 * @param {string} feature
    559 * @return {boolean}
    560 */
    561 function activeConfigurationHasFeature(feature) {
    562  const { features } = getActiveConfiguration();
    563  return features.includes(feature);
    564 }
    565 /* exported activeConfigurationHasFeature */
    566 
    567 /**
    568 * Start the profiler programmatically and check that the active configuration is
    569 * tracking a thread.
    570 *
    571 * @param {string} thread
    572 * @return {boolean}
    573 */
    574 function activeConfigurationHasThread(thread) {
    575  const { threads } = getActiveConfiguration();
    576  return threads.includes(thread);
    577 }
    578 /* exported activeConfigurationHasThread */
    579 
    580 /**
    581 * Use user driven events to start the profiler, and then get the active configuration
    582 * of the profiler. This is similar to functions in the head.js file, but is specific
    583 * for the DevTools situation. The UI complains if the profiler stops unexpectedly.
    584 *
    585 * @param {Document} document
    586 * @param {string} feature
    587 * @returns {boolean}
    588 */
    589 async function devToolsActiveConfigurationHasFeature(document, feature) {
    590  info("Get the active configuration of the profiler via user driven events.");
    591  const start = await getActiveButtonFromText(document, "Start recording");
    592  info("Click the button to start recording.");
    593  start.click();
    594 
    595  // Get the cancel button first, so that way we know the profile has actually
    596  // been recorded.
    597  const cancel = await getActiveButtonFromText(document, "Cancel recording");
    598 
    599  const { activeConfiguration } = Services.profiler;
    600  if (!activeConfiguration) {
    601    throw new Error(
    602      "Expected to find an active configuration for the profile."
    603    );
    604  }
    605 
    606  info("Click the cancel button to discard the profile..");
    607  cancel.click();
    608 
    609  // Wait until the start button is back.
    610  await getActiveButtonFromText(document, "Start recording");
    611 
    612  return activeConfiguration.features.includes(feature);
    613 }
    614 /* exported devToolsActiveConfigurationHasFeature */
    615 
    616 /**
    617 * This adapts the expectation using the current build's available profiler
    618 * features.
    619 *
    620 * @param {string} fixture It can be either already trimmed or untrimmed.
    621 * @returns {string}
    622 */
    623 function _adaptCustomPresetExpectationToCustomBuild(fixture) {
    624  const supportedFeatures = Services.profiler.GetFeatures();
    625  info("Supported features are: " + supportedFeatures.join(", "));
    626 
    627  // Some platforms do not support stack walking, we can adjust the passed
    628  // fixture so that tests are passing in these platforms too.
    629  // Most notably MacOS outside of Nightly and DevEdition.
    630  if (!supportedFeatures.includes("stackwalk")) {
    631    info(
    632      "Supported features do not include stackwalk, let's remove the Native Stacks from the expected output."
    633    );
    634    fixture = fixture.replace(/^.*Native Stacks.*\n/m, "");
    635  }
    636 
    637  return fixture;
    638 }
    639 
    640 /**
    641 * Get the content of the preset description.
    642 *
    643 * @param {Element} devtoolsDocument
    644 * @returns {string}
    645 */
    646 function getDevtoolsCustomPresetContent(devtoolsDocument) {
    647  return devtoolsDocument.querySelector(".perf-presets-custom").innerText;
    648 }
    649 /* exported getDevtoolsCustomPresetContent */
    650 
    651 /**
    652 * This checks if the content of the preset description equals the fixture in
    653 * string form.
    654 *
    655 * @param {Element} devtoolsDocument
    656 * @param {string} fixture
    657 */
    658 function checkDevtoolsCustomPresetContent(devtoolsDocument, fixture) {
    659  // This removes all indentations and any start or end new line and other space characters.
    660  fixture = fixture.replace(/^\s+/gm, "").trim();
    661  // This removes unavailable features from the fixture content.
    662  fixture = _adaptCustomPresetExpectationToCustomBuild(fixture);
    663  is(getDevtoolsCustomPresetContent(devtoolsDocument), fixture);
    664 }
    665 /* exported checkDevtoolsCustomPresetContent */
    666 
    667 /**
    668 * Selects an element with some given text, then it walks up the DOM until it finds
    669 * an input or select element via a call to querySelector.
    670 *
    671 * @param {Document} document
    672 * @param {string} text
    673 * @param {HTMLInputElement}
    674 */
    675 async function getNearestInputFromText(document, text) {
    676  const textElement = await getElementFromDocumentByText(document, text);
    677  if (textElement.control) {
    678    // This is a label, just grab the input.
    679    return textElement.control;
    680  }
    681  // A non-label node
    682  let next = textElement;
    683  while ((next = next.parentElement)) {
    684    const input = next.querySelector("input, select");
    685    if (input) {
    686      return input;
    687    }
    688  }
    689  throw new Error("Could not find an input or select near the text element.");
    690 }
    691 /* exported getNearestInputFromText */
    692 
    693 /**
    694 * Grabs the closest button element from a given snippet of text, and make sure
    695 * the button is not disabled.
    696 *
    697 * @param {Document} document
    698 * @param {string} text
    699 * @param {HTMLButtonElement}
    700 */
    701 async function getActiveButtonFromText(document, text) {
    702  // This could select a span inside the button, or the button itself.
    703  let button = await getElementFromDocumentByText(document, text);
    704 
    705  while (button.tagName.toLowerCase() !== "button") {
    706    // Walk up until a button element is found.
    707    button = button.parentElement;
    708    if (!button) {
    709      throw new Error(`Unable to find a button from the text "${text}"`);
    710    }
    711  }
    712 
    713  await waitUntil(
    714    () => !button.disabled,
    715    "Waiting until the button is not disabled."
    716  );
    717 
    718  return button;
    719 }
    720 /* exported getActiveButtonFromText */
    721 
    722 /**
    723 * Wait until the profiler menu button is added.
    724 *
    725 * @returns Promise<void>
    726 */
    727 async function waitForProfilerMenuButton() {
    728  info("Checking if the profiler menu button is enabled.");
    729  await waitUntil(
    730    () => gBrowser.ownerDocument.getElementById("profiler-button"),
    731    "> Waiting until the profiler button is added to the browser."
    732  );
    733 }
    734 /* exported waitForProfilerMenuButton */
    735 
    736 /**
    737 * Make sure the profiler popup is disabled for the test.
    738 */
    739 async function makeSureProfilerPopupIsDisabled() {
    740  info("Make sure the profiler popup is dsiabled.");
    741 
    742  info("> Load the profiler menu button module.");
    743  const { ProfilerMenuButton } = ChromeUtils.importESModule(
    744    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
    745  );
    746 
    747  const isOriginallyInNavBar = ProfilerMenuButton.isInNavbar();
    748 
    749  if (isOriginallyInNavBar) {
    750    info("> The menu button is in the navbar, remove it for this test.");
    751    ProfilerMenuButton.remove();
    752  } else {
    753    info("> The menu button was not in the navbar yet.");
    754  }
    755 
    756  registerCleanupFunction(() => {
    757    info("Revert the profiler menu button to be back in its original place");
    758    if (isOriginallyInNavBar !== ProfilerMenuButton.isInNavbar()) {
    759      ProfilerMenuButton.remove();
    760    }
    761  });
    762 }
    763 /* exported makeSureProfilerPopupIsDisabled */
    764 
    765 /**
    766 * Open the WebChannel test document, that will enable the profiler popup via
    767 * WebChannel.
    768 *
    769 * @param {Function} callback
    770 */
    771 function withWebChannelTestDocument(callback) {
    772  return BrowserTestUtils.withNewTab(
    773    {
    774      gBrowser,
    775      url: "https://example.com/browser/devtools/client/performance-new/test/browser/webchannel.html",
    776    },
    777    callback
    778  );
    779 }
    780 /* exported withWebChannelTestDocument */
    781 
    782 // This has been stolen from the great library dom-testing-library.
    783 // See https://github.com/testing-library/dom-testing-library/blob/91b9dc3b6f5deea88028e97aab15b3b9f3289a2a/src/events.js#L104-L123
    784 // function written after some investigation here:
    785 // https://github.com/facebook/react/issues/10135#issuecomment-401496776
    786 function setNativeValue(element, value) {
    787  const { set: valueSetter } =
    788    Object.getOwnPropertyDescriptor(element, "value") || {};
    789  const prototype = Object.getPrototypeOf(element);
    790  const { set: prototypeValueSetter } =
    791    Object.getOwnPropertyDescriptor(prototype, "value") || {};
    792  if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
    793    prototypeValueSetter.call(element, value);
    794  } else {
    795    /* istanbul ignore if */
    796    // eslint-disable-next-line no-lonely-if -- Can't be ignored by istanbul otherwise
    797    if (valueSetter) {
    798      valueSetter.call(element, value);
    799    } else {
    800      throw new Error("The given element does not have a value setter");
    801    }
    802  }
    803 }
    804 /* exported setNativeValue */
    805 
    806 /**
    807 * Set a React-friendly input value. Doing this the normal way doesn't work.
    808 * This reuses the previous function setNativeValue stolen from
    809 * dom-testing-library.
    810 *
    811 * See https://github.com/facebook/react/issues/10135
    812 *
    813 * @param {HTMLInputElement} input
    814 * @param {string} value
    815 */
    816 function setReactFriendlyInputValue(input, value) {
    817  setNativeValue(input, value);
    818 
    819  // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
    820  input.dispatchEvent(new Event("change", { bubbles: true }));
    821 }
    822 /* exported setReactFriendlyInputValue */
    823 
    824 /**
    825 * The recording state is the internal state machine that represents the async
    826 * operations that are going on in the profiler. This function sets up a helper
    827 * that will obtain the Redux store and query this internal state. This is useful
    828 * for unit testing purposes.
    829 *
    830 * @param {Document} document
    831 */
    832 function setupGetRecordingState(document) {
    833  const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
    834  const store = document.defaultView.gStore;
    835  if (!store) {
    836    throw new Error("Could not find the redux store on the window object.");
    837  }
    838  return () => selectors.getRecordingState(store.getState());
    839 }
    840 /* exported setupGetRecordingState */