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 */