summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/test/browser/helpers.js
blob: d5cc3af19e8fc66483342802e7536b26f9828b24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * Wait for a single requestAnimationFrame tick.
 */
function tick() {
  return new Promise(resolve => requestAnimationFrame(resolve));
}

/**
 * It can be confusing when waiting for something asynchronously. This function
 * logs out a message periodically (every 1 second) in order to create helpful
 * log messages.
 * @param {string} message
 * @returns {Function}
 */
function createPeriodicLogger() {
  let startTime = Date.now();
  let lastCount = 0;
  let lastMessage = null;

  return message => {
    if (lastMessage === message) {
      // The messages are the same, check if we should log them.
      const now = Date.now();
      const count = Math.floor((now - startTime) / 1000);
      if (count !== lastCount) {
        info(
          `${message} (After ${count} ${count === 1 ? "second" : "seconds"})`
        );
        lastCount = count;
      }
    } else {
      // The messages are different, log them now, and reset the waiting time.
      info(message);
      startTime = Date.now();
      lastCount = 0;
      lastMessage = message;
    }
  };
}

/**
 * Wait until a condition is fullfilled.
 * @param {Function} condition
 * @param {string?} logMessage
 * @return The truthy result of the condition.
 */
async function waitUntil(condition, message) {
  const logPeriodically = createPeriodicLogger();

  // Loop through the condition.
  while (true) {
    if (message) {
      logPeriodically(message);
    }
    const result = condition();
    if (result) {
      return result;
    }

    await tick();
  }
}

/**
 * This function looks inside of a container for some element that has a label.
 * It runs in a loop every requestAnimationFrame until it finds the element. If
 * it doesn't find the element it throws an error.
 *
 * @param {Element} container
 * @param {string} label
 * @returns {Promise<HTMLElement>}
 */
function getElementByLabel(container, label) {
  return waitUntil(
    () => container.querySelector(`[label="${label}"]`),
    `Trying to find the button with the label "${label}".`
  );
}
/* exported getElementByLabel */

/**
 * This function looks inside of a container for some element that has a tooltip.
 * It runs in a loop every requestAnimationFrame until it finds the element. If
 * it doesn't find the element it throws an error.
 *
 * @param {Element} container
 * @param {string} tooltip
 * @returns {Promise<HTMLElement>}
 */
function getElementByTooltip(container, tooltip) {
  return waitUntil(
    () => container.querySelector(`[tooltiptext="${tooltip}"]`),
    `Trying to find the button with the tooltip "${tooltip}".`
  );
}
/* exported getElementByTooltip */

/**
 * This function will select a node from the XPath.
 * @returns {HTMLElement?}
 */
function getElementByXPath(document, path) {
  return document.evaluate(
    path,
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
}
/* exported getElementByXPath */

/**
 * This function looks inside of a document for some element that contains
 * the given text. It runs in a loop every requestAnimationFrame until it
 * finds the element. If it doesn't find the element it throws an error.
 *
 * @param {HTMLDocument} document
 * @param {string} text
 * @returns {Promise<HTMLElement>}
 */
async function getElementFromDocumentByText(document, text) {
  // Fallback on aria-label if there are no results for the text xpath.
  const xpath = `//*[contains(text(), '${text}')] | //*[contains(@aria-label, '${text}')]`;
  return waitUntil(
    () => getElementByXPath(document, xpath),
    `Trying to find the element with the text "${text}".`
  );
}
/* exported getElementFromDocumentByText */

/**
 * This function is similar to getElementFromDocumentByText, but it immediately
 * returns and does not wait for an element to exist.
 * @param {HTMLDocument} document
 * @param {string} text
 * @returns {HTMLElement?}
 */
function maybeGetElementFromDocumentByText(document, text) {
  info(`Immediately trying to find the element with the text "${text}".`);
  const xpath = `//*[contains(text(), '${text}')]`;
  return getElementByXPath(document, xpath);
}
/* exported maybeGetElementFromDocumentByText */

/**
 * Make sure the profiler popup is enabled.
 */
async function makeSureProfilerPopupIsEnabled() {
  info("Make sure the profiler popup is enabled.");

  info("> Load the profiler menu button.");
  const { ProfilerMenuButton } = ChromeUtils.importESModule(
    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
  );

  if (!ProfilerMenuButton.isInNavbar()) {
    // Make sure the feature flag is enabled.
    SpecialPowers.pushPrefEnv({
      set: [["devtools.performance.popup.feature-flag", true]],
    });

    info("> The menu button is not in the nav bar, add it.");
    ProfilerMenuButton.addToNavbar(document);

    await waitUntil(
      () => gBrowser.ownerDocument.getElementById("profiler-button"),
      "> Waiting until the profiler button is added to the browser."
    );

    await SimpleTest.promiseFocus(gBrowser.ownerGlobal);

    registerCleanupFunction(() => {
      info(
        "Clean up after the test by disabling the profiler popup menu button."
      );
      if (!ProfilerMenuButton.isInNavbar()) {
        throw new Error(
          "Expected the profiler popup to still be in the navbar during the test cleanup."
        );
      }
      ProfilerMenuButton.remove();
    });
  } else {
    info("> The menu button was already enabled.");
  }
}
/* exported makeSureProfilerPopupIsEnabled */

/**
 * XUL popups will fire the popupshown and popuphidden events. These will fire for
 * any type of popup in the browser. This function waits for one of those events, and
 * checks that the viewId of the popup is PanelUI-profiler
 *
 * @param {Window} window
 * @param {"popupshown" | "popuphidden"} eventName
 * @returns {Promise<void>}
 */
function waitForProfilerPopupEvent(window, eventName) {
  return new Promise(resolve => {
    function handleEvent(event) {
      if (event.target.getAttribute("viewId") === "PanelUI-profiler") {
        window.removeEventListener(eventName, handleEvent);
        resolve();
      }
    }
    window.addEventListener(eventName, handleEvent);
  });
}
/* exported waitForProfilerPopupEvent */

/**
 * Do not use this directly in a test. Prefer withPopupOpen and openPopupAndEnsureCloses.
 *
 * This function toggles the profiler menu button, and then uses user gestures
 * to click it open. It waits a tick to make sure it has a chance to initialize.
 * @param {Window} window
 * @return {Promise<void>}
 */
async function _toggleOpenProfilerPopup(window) {
  info("Toggle open the profiler popup.");

  info("> Find the profiler menu button.");
  const profilerDropmarker = window.document.getElementById(
    "profiler-button-dropmarker"
  );
  if (!profilerDropmarker) {
    throw new Error(
      "Could not find the profiler button dropmarker in the toolbar."
    );
  }

  const popupShown = waitForProfilerPopupEvent(window, "popupshown");

  info("> Trigger a click on the profiler button dropmarker.");
  await EventUtils.synthesizeMouseAtCenter(profilerDropmarker, {}, window);

  if (profilerDropmarker.getAttribute("open") !== "true") {
    throw new Error(
      "This test assumes that the button will have an open=true attribute after clicking it."
    );
  }

  info("> Wait for the popup to be shown.");
  await popupShown;
  // Also wait a tick in case someone else is subscribing to the "popupshown" event
  // and is doing synchronous work with it.
  await tick();
}

/**
 * Do not use this directly in a test. Prefer withPopupOpen.
 *
 * This function uses a keyboard shortcut to close the profiler popup.
 * @param {Window} window
 * @return {Promise<void>}
 */
async function _closePopup(window) {
  const popupHiddenPromise = waitForProfilerPopupEvent(window, "popuphidden");
  info("> Trigger an escape key to hide the popup");
  EventUtils.synthesizeKey("KEY_Escape");

  info("> Wait for the popup to be hidden.");
  await popupHiddenPromise;
  // Also wait a tick in case someone else is subscribing to the "popuphidden" event
  // and is doing synchronous work with it.
  await tick();
}

/**
 * Perform some action on the popup, and close it afterwards.
 * @param {Window} window
 * @param {() => Promise<void>} callback
 */
async function withPopupOpen(window, callback) {
  await _toggleOpenProfilerPopup(window);
  await callback();
  await _closePopup(window);
}
/* exported withPopupOpen */

/**
 * This function opens the profiler popup, but also ensures that something else closes
 * it before the end of the test. This is useful for tests that trigger the profiler
 * popup to close through an implicit action, like opening a tab.
 *
 * @param {Window} window
 * @param {() => Promise<void>} callback
 */
async function openPopupAndEnsureCloses(window, callback) {
  await _toggleOpenProfilerPopup(window);
  // We want to ensure the popup gets closed by the test, during the callback.
  const popupHiddenPromise = waitForProfilerPopupEvent(window, "popuphidden");
  await callback();
  info("> Verifying that the popup was closed by the test.");
  await popupHiddenPromise;
}
/* exported openPopupAndEnsureCloses */

/**
 * This function overwrites the default profiler.firefox.com URL for tests. This
 * ensures that the tests do not attempt to access external URLs.
 * The origin needs to be on the allowlist in validateProfilerWebChannelUrl,
 * otherwise the WebChannel won't work. ("http://example.com" is on that list.)
 *
 * @param {string} origin - For example: http://example.com
 * @param {string} pathname - For example: /my/testing/frontend.html
 * @returns {Promise}
 */
function setProfilerFrontendUrl(origin, pathname) {
  return SpecialPowers.pushPrefEnv({
    set: [
      // Make sure observer and testing function run in the same process
      ["devtools.performance.recording.ui-base-url", origin],
      ["devtools.performance.recording.ui-base-url-path", pathname],
    ],
  });
}
/* exported setProfilerFrontendUrl */

/**
 * This function checks the document title of a tab to see what the state is.
 * This creates a simple messaging mechanism between the content page and the
 * test harness. This function runs in a loop every requestAnimationFrame, and
 * checks for a sucess title. In addition, an "initialTitle" and "errorTitle"
 * can be specified for nicer test output.
 * @param {object}
 *   {
 *     initialTitle: string,
 *     successTitle: string,
 *     errorTitle: string
 *   }
 */
async function checkTabLoadedProfile({
  initialTitle,
  successTitle,
  errorTitle,
}) {
  const logPeriodically = createPeriodicLogger();

  info("Attempting to see if the selected tab can receive a profile.");

  return waitUntil(() => {
    switch (gBrowser.selectedTab.label) {
      case initialTitle:
        logPeriodically(`> Waiting for the profile to be received.`);
        return false;
      case successTitle:
        ok(true, "The profile was successfully injected to the page");
        BrowserTestUtils.removeTab(gBrowser.selectedTab);
        return true;
      case errorTitle:
        throw new Error(
          "The fake frontend indicated that there was an error injecting the profile."
        );
      default:
        logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
        return false;
    }
  });
}
/* exported checkTabLoadedProfile */

/**
 * This function checks the url of a tab so we can assert the frontend's url
 * with our expected url. This function runs in a loop every
 * requestAnimationFrame, and checks for a initialTitle. Asserts as soon as it
 * finds that title. We don't have to look for success title or error title
 * since we only care about the url.
 * @param {{
 *     initialTitle: string,
 *     successTitle: string,
 *     errorTitle: string,
 *     expectedUrl: string
 *   }}
 */
async function waitForTabUrl({
  initialTitle,
  successTitle,
  errorTitle,
  expectedUrl,
}) {
  const logPeriodically = createPeriodicLogger();

  info(`Waiting for the selected tab to have the url "${expectedUrl}".`);

  return waitUntil(() => {
    switch (gBrowser.selectedTab.label) {
      case initialTitle:
      case successTitle:
        if (gBrowser.currentURI.spec === expectedUrl) {
          ok(true, `The selected tab has the url ${expectedUrl}`);
          BrowserTestUtils.removeTab(gBrowser.selectedTab);
          return true;
        }
        throw new Error(
          `Found a different url on the fake frontend: ${gBrowser.currentURI.spec} (expecting ${expectedUrl})`
        );
      case errorTitle:
        throw new Error(
          "The fake frontend indicated that there was an error injecting the profile."
        );
      default:
        logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
        return false;
    }
  });
}
/* exported waitForTabUrl */

/**
 * This function checks the document title of a tab as an easy way to pass
 * messages from a content page to the mochitest.
 * @param {string} title
 */
async function waitForTabTitle(title) {
  const logPeriodically = createPeriodicLogger();

  info(`Waiting for the selected tab to have the title "${title}".`);

  return waitUntil(() => {
    if (gBrowser.selectedTab.label === title) {
      ok(true, `The selected tab has the title ${title}`);
      return true;
    }
    logPeriodically(`> Waiting for the tab title to change.`);
    return false;
  });
}
/* exported waitForTabTitle */

/**
 * Open about:profiling in a new tab, and output helpful log messages.
 *
 * @template T
 * @param {(Document) => T} callback
 * @returns {Promise<T>}
 */
function withAboutProfiling(callback) {
  info("Begin to open about:profiling in a new tab.");
  return BrowserTestUtils.withNewTab(
    "about:profiling",
    async contentBrowser => {
      info("about:profiling is now open in a tab.");
      await TestUtils.waitForCondition(
        () =>
          contentBrowser.contentDocument.getElementById("root")
            .firstElementChild,
        "Document's root has been populated"
      );
      return callback(contentBrowser.contentDocument);
    }
  );
}
/* exported withAboutProfiling */

/**
 * Open DevTools and view the performance-new tab. After running the callback, clean
 * up the test.
 *
 * @param {string} [url="about:blank"] url for the new tab
 * @param {(Document, Document) => unknown} callback: the first parameter is the
 *                                          devtools panel's document, the
 *                                          second parameter is the opened tab's
 *                                          document.
 * @param {Window} [aWindow] The browser's window object we target
 * @returns {Promise<void>}
 */
async function withDevToolsPanel(url, callback, aWindow = window) {
  if (typeof url === "function") {
    aWindow = callback ?? window;
    callback = url;
    url = "about:blank";
  }

  const { gBrowser } = aWindow;

  const {
    gDevTools,
  } = require("resource://devtools/client/framework/devtools.js");

  info(`Create a new tab with url "${url}".`);
  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);

  info("Begin to open the DevTools and the performance-new panel.");
  const toolbox = await gDevTools.showToolboxForTab(tab, {
    toolId: "performance",
  });

  const { document } = toolbox.getCurrentPanel().panelWin;

  info("The performance-new panel is now open and ready to use.");
  await callback(document, tab.linkedBrowser.contentDocument);

  info("About to remove the about:blank tab");
  await toolbox.destroy();

  // The previous asynchronous functions may resolve within a tick after opening a new tab.
  // We shouldn't remove the newly opened tab in the same tick.
  // Wait for the next tick here.
  await TestUtils.waitForTick();

  // Take care to register the TabClose event before we call removeTab, to avoid
  // race issues.
  const waitForClosingPromise = BrowserTestUtils.waitForTabClosing(tab);
  BrowserTestUtils.removeTab(tab);
  info("Requested closing the about:blank tab, waiting...");
  await waitForClosingPromise;
  info("The about:blank tab is now removed.");
}
/* exported withDevToolsPanel */

/**
 * Start and stop the profiler to get the current active configuration. This is
 * done programmtically through the nsIProfiler interface, rather than through click
 * interactions, since the about:profiling page does not include buttons to control
 * the recording.
 *
 * @returns {Object}
 */
function getActiveConfiguration() {
  const BackgroundJSM = ChromeUtils.importESModule(
    "resource://devtools/client/performance-new/shared/background.sys.mjs"
  );

  const { startProfiler, stopProfiler } = BackgroundJSM;

  info("Start the profiler with the current about:profiling configuration.");
  startProfiler("aboutprofiling");

  // Immediately pause the sampling, to make sure the test runs fast. The profiler
  // only needs to be started to initialize the configuration.
  Services.profiler.Pause();

  const { activeConfiguration } = Services.profiler;
  if (!activeConfiguration) {
    throw new Error(
      "Expected to find an active configuration for the profile."
    );
  }

  info("Stop the profiler after getting the active configuration.");
  stopProfiler();

  return activeConfiguration;
}
/* exported getActiveConfiguration */

/**
 * Start the profiler programmatically and check that the active configuration has
 * a feature enabled
 *
 * @param {string} feature
 * @return {boolean}
 */
function activeConfigurationHasFeature(feature) {
  const { features } = getActiveConfiguration();
  return features.includes(feature);
}
/* exported activeConfigurationHasFeature */

/**
 * Start the profiler programmatically and check that the active configuration is
 * tracking a thread.
 *
 * @param {string} thread
 * @return {boolean}
 */
function activeConfigurationHasThread(thread) {
  const { threads } = getActiveConfiguration();
  return threads.includes(thread);
}
/* exported activeConfigurationHasThread */

/**
 * Use user driven events to start the profiler, and then get the active configuration
 * of the profiler. This is similar to functions in the head.js file, but is specific
 * for the DevTools situation. The UI complains if the profiler stops unexpectedly.
 *
 * @param {Document} document
 * @param {string} feature
 * @returns {boolean}
 */
async function devToolsActiveConfigurationHasFeature(document, feature) {
  info("Get the active configuration of the profiler via user driven events.");
  const start = await getActiveButtonFromText(document, "Start recording");
  info("Click the button to start recording.");
  start.click();

  // Get the cancel button first, so that way we know the profile has actually
  // been recorded.
  const cancel = await getActiveButtonFromText(document, "Cancel recording");

  const { activeConfiguration } = Services.profiler;
  if (!activeConfiguration) {
    throw new Error(
      "Expected to find an active configuration for the profile."
    );
  }

  info("Click the cancel button to discard the profile..");
  cancel.click();

  // Wait until the start button is back.
  await getActiveButtonFromText(document, "Start recording");

  return activeConfiguration.features.includes(feature);
}
/* exported devToolsActiveConfigurationHasFeature */

/**
 * This adapts the expectation using the current build's available profiler
 * features.
 * @param {string} fixture It can be either already trimmed or untrimmed.
 * @returns {string}
 */
function _adaptCustomPresetExpectationToCustomBuild(fixture) {
  const supportedFeatures = Services.profiler.GetFeatures();
  info("Supported features are: " + supportedFeatures.join(", "));

  // Some platforms do not support stack walking, we can adjust the passed
  // fixture so that tests are passing in these platforms too.
  // Most notably MacOS outside of Nightly and DevEdition.
  if (!supportedFeatures.includes("stackwalk")) {
    info(
      "Supported features do not include stackwalk, let's remove the Native Stacks from the expected output."
    );
    fixture = fixture.replace(/^.*Native Stacks.*\n/m, "");
  }

  return fixture;
}

/**
 * Get the content of the preset description.
 * @param {Element} devtoolsDocument
 * @returns {string}
 */
function getDevtoolsCustomPresetContent(devtoolsDocument) {
  return devtoolsDocument.querySelector(".perf-presets-custom").innerText;
}
/* exported getDevtoolsCustomPresetContent */

/**
 * This checks if the content of the preset description equals the fixture in
 * string form.
 * @param {Element} devtoolsDocument
 * @param {string} fixture
 */
function checkDevtoolsCustomPresetContent(devtoolsDocument, fixture) {
  // This removes all indentations and any start or end new line and other space characters.
  fixture = fixture.replace(/^\s+/gm, "").trim();
  // This removes unavailable features from the fixture content.
  fixture = _adaptCustomPresetExpectationToCustomBuild(fixture);
  is(getDevtoolsCustomPresetContent(devtoolsDocument), fixture);
}
/* exported checkDevtoolsCustomPresetContent */

/**
 * Selects an element with some given text, then it walks up the DOM until it finds
 * an input or select element via a call to querySelector.
 *
 * @param {Document} document
 * @param {string} text
 * @param {HTMLInputElement}
 */
async function getNearestInputFromText(document, text) {
  const textElement = await getElementFromDocumentByText(document, text);
  if (textElement.control) {
    // This is a label, just grab the input.
    return textElement.control;
  }
  // A non-label node
  let next = textElement;
  while ((next = next.parentElement)) {
    const input = next.querySelector("input, select");
    if (input) {
      return input;
    }
  }
  throw new Error("Could not find an input or select near the text element.");
}
/* exported getNearestInputFromText */

/**
 * Grabs the closest button element from a given snippet of text, and make sure
 * the button is not disabled.
 *
 * @param {Document} document
 * @param {string} text
 * @param {HTMLButtonElement}
 */
async function getActiveButtonFromText(document, text) {
  // This could select a span inside the button, or the button itself.
  let button = await getElementFromDocumentByText(document, text);

  while (button.tagName !== "button") {
    // Walk up until a button element is found.
    button = button.parentElement;
    if (!button) {
      throw new Error(`Unable to find a button from the text "${text}"`);
    }
  }

  await waitUntil(
    () => !button.disabled,
    "Waiting until the button is not disabled."
  );

  return button;
}
/* exported getActiveButtonFromText */

/**
 * Wait until the profiler menu button is added.
 *
 * @returns Promise<void>
 */
async function waitForProfilerMenuButton() {
  info("Checking if the profiler menu button is enabled.");
  await waitUntil(
    () => gBrowser.ownerDocument.getElementById("profiler-button"),
    "> Waiting until the profiler button is added to the browser."
  );
}
/* exported waitForProfilerMenuButton */

/**
 * Make sure the profiler popup is disabled for the test.
 */
async function makeSureProfilerPopupIsDisabled() {
  info("Make sure the profiler popup is dsiabled.");

  info("> Load the profiler menu button module.");
  const { ProfilerMenuButton } = ChromeUtils.importESModule(
    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
  );

  const isOriginallyInNavBar = ProfilerMenuButton.isInNavbar();

  if (isOriginallyInNavBar) {
    info("> The menu button is in the navbar, remove it for this test.");
    ProfilerMenuButton.remove();
  } else {
    info("> The menu button was not in the navbar yet.");
  }

  registerCleanupFunction(() => {
    info("Revert the profiler menu button to be back in its original place");
    if (isOriginallyInNavBar !== ProfilerMenuButton.isInNavbar()) {
      ProfilerMenuButton.remove();
    }
  });
}
/* exported makeSureProfilerPopupIsDisabled */

/**
 * Open the WebChannel test document, that will enable the profiler popup via
 * WebChannel.
 * @param {Function} callback
 */
function withWebChannelTestDocument(callback) {
  return BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: "http://example.com/browser/devtools/client/performance-new/test/browser/webchannel.html",
    },
    callback
  );
}
/* exported withWebChannelTestDocument */

// This has been stolen from the great library dom-testing-library.
// See https://github.com/testing-library/dom-testing-library/blob/91b9dc3b6f5deea88028e97aab15b3b9f3289a2a/src/events.js#L104-L123
// function written after some investigation here:
// https://github.com/facebook/react/issues/10135#issuecomment-401496776
function setNativeValue(element, value) {
  const { set: valueSetter } =
    Object.getOwnPropertyDescriptor(element, "value") || {};
  const prototype = Object.getPrototypeOf(element);
  const { set: prototypeValueSetter } =
    Object.getOwnPropertyDescriptor(prototype, "value") || {};
  if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
    prototypeValueSetter.call(element, value);
  } else {
    /* istanbul ignore if */
    // eslint-disable-next-line no-lonely-if -- Can't be ignored by istanbul otherwise
    if (valueSetter) {
      valueSetter.call(element, value);
    } else {
      throw new Error("The given element does not have a value setter");
    }
  }
}
/* exported setNativeValue */

/**
 * Set a React-friendly input value. Doing this the normal way doesn't work.
 * This reuses the previous function setNativeValue stolen from
 * dom-testing-library.
 *
 * See https://github.com/facebook/react/issues/10135
 *
 * @param {HTMLInputElement} input
 * @param {string} value
 */
function setReactFriendlyInputValue(input, value) {
  setNativeValue(input, value);

  // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
  input.dispatchEvent(new Event("change", { bubbles: true }));
}
/* exported setReactFriendlyInputValue */

/**
 * The recording state is the internal state machine that represents the async
 * operations that are going on in the profiler. This function sets up a helper
 * that will obtain the Redux store and query this internal state. This is useful
 * for unit testing purposes.
 *
 * @param {Document} document
 */
function setupGetRecordingState(document) {
  const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
  const store = document.defaultView.gStore;
  if (!store) {
    throw new Error("Could not find the redux store on the window object.");
  }
  return () => selectors.getRecordingState(store.getState());
}
/* exported setupGetRecordingState */