826 lines
26 KiB
JavaScript
826 lines
26 KiB
JavaScript
/* 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) {
|
|
return TestUtils.waitForCondition(condition, message);
|
|
}
|
|
|
|
/**
|
|
* 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(() => {
|
|
const element = getElementByXPath(document, xpath);
|
|
if (element && BrowserTestUtils.isVisible(element)) {
|
|
return element;
|
|
}
|
|
return null;
|
|
}, `Trying to find a visible 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();
|
|
|
|
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. ("https://example.com" is on that list.)
|
|
*
|
|
* @param {string} origin - For example: https://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, ChromeBrowser) => 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, contentBrowser);
|
|
}
|
|
);
|
|
}
|
|
/* 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.toLowerCase() !== "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: "https://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 */
|