diff --git a/devtools/client/performance-new/test/browser/browser.ini b/devtools/client/performance-new/test/browser/browser.ini index 338624b205ee2..35cf447946bd3 100644 --- a/devtools/client/performance-new/test/browser/browser.ini +++ b/devtools/client/performance-new/test/browser/browser.ini @@ -18,7 +18,9 @@ support-files = [browser_aboutprofiling-threads-behavior.js] [browser_aboutprofiling-presets.js] [browser_aboutprofiling-presets-custom.js] +[browser_devtools-presets.js] +[browser_devtools-record-capture.js] +[browser_devtools-record-discard.js] [browser_webchannel-enable-menu-button.js] - [browser_popup-record-capture.js] [browser_popup-record-discard.js] diff --git a/devtools/client/performance-new/test/browser/browser_devtools-presets.js b/devtools/client/performance-new/test/browser/browser_devtools-presets.js new file mode 100644 index 0000000000000..10a10f21f38e6 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-presets.js @@ -0,0 +1,45 @@ +/* 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"; + +add_task(async function test() { + info("Test that about:profiling presets configure the profiler"); + + if (!Services.profiler.GetFeatures().includes("stackwalk")) { + ok(true, "This platform does not support stackwalking, skip this test."); + return; + } + + await withDevToolsPanel(async document => { + { + const presets = await getNearestInputFromText(document, "Settings"); + + is(presets.value, "web-developer", "The presets default to webdev mode."); + ok( + !(await devToolsActiveConfigurationHasFeature(document, "stackwalk")), + "Stack walking is not used in Web Developer mode." + ); + } + + { + const presets = await getNearestInputFromText(document, "Settings"); + setReactFriendlyInputValue(presets, "firefox-platform"); + is( + presets.value, + "firefox-platform", + "The preset was changed to Firefox Platform" + ); + ok( + await devToolsActiveConfigurationHasFeature(document, "stackwalk"), + "Stack walking is used in Firefox Platform mode." + ); + } + }); + + const { revertRecordingPreferences } = ChromeUtils.import( + "resource://devtools/client/performance-new/popup/background.jsm.js" + ); + + revertRecordingPreferences(); +}); diff --git a/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js new file mode 100644 index 0000000000000..76ea31787d817 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js @@ -0,0 +1,47 @@ +/* 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"; + +add_task(async function test() { + info("Test that DevTools can capture profiles."); + + await setProfilerFrontendUrl( + "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + + await withDevToolsPanel(async document => { + { + const button = await getActiveButtonFromText(document, "Start recording"); + info("Click the button to start recording"); + button.click(); + } + + { + const button = await getActiveButtonFromText( + document, + "Capture recording" + ); + info("Click the button to capture the recording."); + button.click(); + } + + info( + "If the DevTools successfully injects a profile into the page, then the " + + "fake frontend will rename the title of the page." + ); + + await checkTabLoadedProfile({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + }); + }); + + const { revertRecordingPreferences } = ChromeUtils.import( + "resource://devtools/client/performance-new/popup/background.jsm.js" + ); + + revertRecordingPreferences(); +}); diff --git a/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js b/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js new file mode 100644 index 0000000000000..acc766022e05f --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js @@ -0,0 +1,41 @@ +/* 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"; + +add_task(async function test() { + info("Test that DevTools can capture profiles."); + + await setProfilerFrontendUrl( + "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + + await withDevToolsPanel(async document => { + { + const button = await getActiveButtonFromText(document, "Start recording"); + info("Click the button to start recording"); + button.click(); + } + + { + const button = await getActiveButtonFromText( + document, + "Cancel recording" + ); + info("Click the button to discard to profile."); + button.click(); + } + + { + const button = await getActiveButtonFromText(document, "Start recording"); + ok(Boolean(button), "The start recording button is available again."); + } + }); + + const { revertRecordingPreferences } = ChromeUtils.import( + "resource://devtools/client/performance-new/popup/background.jsm.js" + ); + + revertRecordingPreferences(); +}); diff --git a/devtools/client/performance-new/test/browser/fake-frontend.html b/devtools/client/performance-new/test/browser/fake-frontend.html index 3839d1b442d12..9817b87e5d85c 100644 --- a/devtools/client/performance-new/test/browser/fake-frontend.html +++ b/devtools/client/performance-new/test/browser/fake-frontend.html @@ -40,7 +40,14 @@ if ( profile && typeof profile === 'object' && - profile instanceof ArrayBuffer + ( + // The popup injects the compressed profile as an ArrayBuffer. + (profile instanceof ArrayBuffer) || + // DevTools injects the profile as just the plain object, although + // maybe in the future it could also do it as a compressed profile + // to make this faster. + Object.keys(profile).includes("threads") + ) ) { // The profile looks good! document.title = successTitle; diff --git a/devtools/client/performance-new/test/browser/head.js b/devtools/client/performance-new/test/browser/head.js index a43bdd69cb34b..b042bb641055e 100644 --- a/devtools/client/performance-new/test/browser/head.js +++ b/devtools/client/performance-new/test/browser/head.js @@ -208,7 +208,11 @@ async function toggleOpenProfilerPopup() { * @returns {Promise} */ function setProfilerFrontendUrl(url) { - info("Setting the profiler URL to the fake frontend."); + info( + "Setting the profiler URL to the fake frontend. Note that this doesn't currently " + + "support the WebChannels, so expect a few error messages about the WebChannel " + + "URLs not being correct." + ); return SpecialPowers.pushPrefEnv({ set: [ // Make sure observer and testing function run in the same process @@ -331,6 +335,41 @@ function withAboutProfiling(callback) { ); } +/** + * Open DevTools and view the performance-new tab. After running the callback, clean + * up the test. + * + * @template T + * @param {(Document) => T} callback + * @returns {Promise} + */ +async function withDevToolsPanel(callback) { + SpecialPowers.pushPrefEnv({ + set: [["devtools.performance.new-panel-enabled", "true"]], + }); + + const { gDevTools } = require("devtools/client/framework/devtools"); + const { TargetFactory } = require("devtools/client/framework/target"); + + info("Create a new about:blank tab."); + const tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + + info("Begin to open the DevTools and the performance-new panel."); + const target = await TargetFactory.forTab(tab); + const toolbox = await gDevTools.showToolbox(target, "performance"); + + const { document } = toolbox.getCurrentPanel().panelWin; + + info("The performance-new panel is now open and ready to use."); + await callback(document); + + info("About to remove the about:blank tab"); + await toolbox.destroy(); + BrowserTestUtils.removeTab(tab); + info("The about:blank tab is now removed."); + await new Promise(resolve => setTimeout(resolve, 500)); +} + /** * Start and stop the profiler to get the current active configuration. This is * done programmtically through the nsIProfiler interface, rather than through click @@ -389,8 +428,43 @@ function activeConfigurationHasThread(thread) { } /** - * Grabs the associated input from the element, or it walks up the DOM from a text - * element and tries to query select an input. + * 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); +} + +/** + * 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 @@ -405,12 +479,40 @@ async function getNearestInputFromText(document, text) { // A non-label node let next = textElement; while ((next = next.parentElement)) { - const input = next.querySelector("input"); + const input = next.querySelector("input, select"); if (input) { return input; } } - throw new Error("Could not find an input near text element."); + throw new Error("Could not find an input or select near the text element."); +} + +/** + * 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; } /** @@ -473,20 +575,21 @@ function withWebChannelTestDocument(callback) { /** * Set a React-friendly input value. Doing this the normal way doesn't work. * - * See: https://github.com/facebook/react/issues/10135#issuecomment-314441175 + * See https://github.com/facebook/react/issues/10135#issuecomment-500929024 + * + * @param {HTMLInputElement} input + * @param {string} value */ -function setReactFriendlyInputValue(element, value) { - const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set; - const prototype = Object.getPrototypeOf(element); - const prototypeValueSetter = Object.getOwnPropertyDescriptor( - prototype, - "value" - ).set; - - if (valueSetter && valueSetter !== prototypeValueSetter) { - prototypeValueSetter.call(element, value); - } else { - valueSetter.call(element, value); +function setReactFriendlyInputValue(input, value) { + const previousValue = input.value; + + input.value = value; + + const tracker = input._valueTracker; + if (tracker) { + tracker.setValue(previousValue); } - element.dispatchEvent(new Event("input", { bubbles: true })); + + // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324 + input.dispatchEvent(new Event("change", { bubbles: true })); }