diff options
Diffstat (limited to 'devtools/client/performance-new/test')
35 files changed, 3479 insertions, 0 deletions
diff --git a/devtools/client/performance-new/test/browser/browser.ini b/devtools/client/performance-new/test/browser/browser.ini new file mode 100644 index 0000000000..9e9e905fac --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser.ini @@ -0,0 +1,48 @@ +[DEFAULT] +prefs = + # This sets up the WebChannel so that it can be used for our tests. + devtools.performance.recording.ui-base-url='http://example.com' +tags = devtools devtools-performance +subsuite = devtools +skip-if = + tsan # Bug 1804081, timeouts and data races in various tests + http3 # Bug 1829298 +support-files = + head.js + helpers.js + fake-frontend.html + webchannel.html + +[browser_aboutprofiling-env-restart-button.js] +[browser_aboutprofiling-entries.js] +[browser_aboutprofiling-features-disabled.js] +[browser_aboutprofiling-features.js] +[browser_aboutprofiling-interval.js] +[browser_aboutprofiling-threads.js] +[browser_aboutprofiling-threads-behavior.js] +[browser_aboutprofiling-presets.js] +[browser_aboutprofiling-presets-custom.js] +[browser_aboutprofiling-rtl.js] +[browser_devtools-interrupted.js] +[browser_devtools-onboarding.js] +[browser_devtools-presets.js] +[browser_devtools-previously-started.js] +[browser_devtools-record-capture.js] +https_first_disabled = true +[browser_devtools-record-discard.js] +[browser_webchannel-enable-menu-button.js] +https_first_disabled = true +[browser_webchannel-enable-menu-button-preset.js] +https_first_disabled = true +[browser_popup-profiler-states.js] +https_first_disabled = true +[browser_popup-record-capture.js] +https_first_disabled = true +[browser_popup-record-capture-view.js] +https_first_disabled = true +[browser_popup-record-discard.js] + +[browser_split-toolbar-button.js] +https_first_disabled = true + +[browser_interaction-between-interfaces.js] diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js new file mode 100644 index 0000000000..4415387b16 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js @@ -0,0 +1,28 @@ +/* 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 can modify the sampling interval."); + + await withAboutProfiling(async document => { + is( + getActiveConfiguration().capacity, + 128 * 1024 * 1024, + "The active configuration is set to a specific number initially. If this" + + " test fails here, then the magic numbers here may need to be adjusted." + ); + + info("Change the buffer input to an arbitrarily smaller value."); + const bufferInput = await getNearestInputFromText(document, "Buffer size:"); + setReactFriendlyInputValue(bufferInput, Number(bufferInput.value) * 0.1); + + is( + getActiveConfiguration().capacity, + 256 * 1024, + "The capacity changed to a smaller value." + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js new file mode 100644 index 0000000000..28ea3798a2 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js @@ -0,0 +1,78 @@ +/* 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 the popup offers to restart the browser to set an enviroment flag." + ); + + if (!Services.profiler.GetFeatures().includes("jstracer")) { + ok( + true, + "JS tracer is not supported on this platform, or is currently disabled. Skip the rest of the test." + ); + return; + } + + { + info("Ensure that JS Tracer is not currently enabled."); + ok( + !Services.env.get("JS_TRACE_LOGGING"), + "The JS_TRACE_LOGGING is not currently enabled." + ); + } + + ok( + false, + "This test was migrated from the initial popup implementation to " + + "about:profiling, however JS Tracer was disabled at the time. When " + + "re-enabling JS Tracer, please audit that this text works as expected, " + + "especially in the UI." + ); + + await withAboutProfiling(async document => { + { + info( + "Test that there is offer to restart the browser when first loading up the popup." + ); + const noRestartButton = maybeGetElementFromDocumentByText( + document, + "Restart" + ); + ok(!noRestartButton, "There is no button to restart the browser."); + } + + const jsTracerFeature = await getElementFromDocumentByText( + document, + "JSTracer" + ); + + { + info("Toggle the jstracer feature on."); + jsTracerFeature.click(); + + const restartButton = await getElementFromDocumentByText( + document, + "Restart" + ); + ok( + restartButton, + "There is now a button to offer to restart the browser" + ); + } + + { + info("Toggle the jstracer feature back off."); + jsTracerFeature.click(); + + const noRestartButton = maybeGetElementFromDocumentByText( + document, + "Restart" + ); + ok(!noRestartButton, "The offer to restart the browser goes away."); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js new file mode 100644 index 0000000000..ce91f42167 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js @@ -0,0 +1,63 @@ +/* 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 features that are disabled on the platform are disabled in about:profiling." + ); + + const supportedFeatures = Services.profiler.GetFeatures(); + const allFeatures = Services.profiler.GetAllFeatures(); + const unsupportedFeatures = allFeatures.filter( + feature => !supportedFeatures.includes(feature) + ); + + if (unsupportedFeatures.length === 0) { + ok(true, "This platform has no unsupported features. Skip this test."); + return; + } + + await withAboutProfiling(async document => { + { + info("Find and click a supported feature to toggle it."); + const [firstSupportedFeature] = supportedFeatures; + const checkbox = getFeatureCheckbox(document, firstSupportedFeature); + const initialValue = checkbox.checked; + info("Click the supported checkbox."); + checkbox.click(); + is( + initialValue, + !checkbox.checked, + "A supported feature can be toggled." + ); + checkbox.click(); + } + + { + info("Find and click an unsupported feature, it should be disabled."); + const [firstUnsupportedFeature] = unsupportedFeatures; + const checkbox = getFeatureCheckbox(document, firstUnsupportedFeature); + is(checkbox.checked, false, "The unsupported feature is not checked."); + + info("Click the unsupported checkbox."); + checkbox.click(); + is(checkbox.checked, false, "After clicking it, it's still not checked."); + } + }); +}); + +/** + * @param {HTMLDocument} document + * @param {string} feature + * @return {HTMLElement} + */ +function getFeatureCheckbox(document, feature) { + const element = document.querySelector(`input[value="${feature}"]`); + if (!element) { + throw new Error("Could not find the checkbox for the feature: " + feature); + } + return element; +} diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js new file mode 100644 index 0000000000..2ee07f5ad0 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js @@ -0,0 +1,32 @@ +/* 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 can be loaded, and the features changed."); + + ok( + Services.profiler.GetFeatures().includes("js"), + "This test assumes that the JavaScript feature is available on every platform." + ); + + await withAboutProfiling(async document => { + const jsInput = await getNearestInputFromText(document, "JavaScript"); + + ok( + activeConfigurationHasFeature("js"), + "By default, the JS feature is always enabled." + ); + ok(jsInput.checked, "The JavaScript input is checked when enabled."); + + jsInput.click(); + + ok( + !activeConfigurationHasFeature("js"), + "The JS feature can be toggled off." + ); + ok(!jsInput.checked, "The JS feature's input element is also toggled off."); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js new file mode 100644 index 0000000000..6dcc4e1156 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js @@ -0,0 +1,72 @@ +/* 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 can modify the sampling interval."); + + await withAboutProfiling(async document => { + const intervalInput = await getNearestInputFromText( + document, + "Sampling interval:" + ); + is( + getActiveConfiguration().interval, + 1, + "The active configuration's interval is set to a specific number initially." + ); + is( + intervalInput.getAttribute("aria-valuemin"), + "0.01", + "aria-valuemin has the expected value" + ); + is( + intervalInput.getAttribute("aria-valuemax"), + "100", + "aria-valuemax has the expected value" + ); + is( + intervalInput.getAttribute("aria-valuenow"), + "1", + "aria-valuenow has the expected value" + ); + + info( + "Increase the interval by an arbitrary amount. The input range will " + + "scale that to the final value presented to the profiler." + ); + setReactFriendlyInputValue(intervalInput, Number(intervalInput.value) + 1); + + is( + getActiveConfiguration().interval, + 2, + "The configuration's interval was able to be increased." + ); + is( + intervalInput.getAttribute("aria-valuenow"), + "2", + "aria-valuenow has the expected value" + ); + + intervalInput.focus(); + + info("Increase the interval with the keyboard"); + EventUtils.synthesizeKey("VK_RIGHT"); + await waitUntil(() => getActiveConfiguration().interval === 3); + is( + intervalInput.getAttribute("aria-valuenow"), + "3", + "aria-valuenow has the expected value" + ); + + info("Decrease the interval with the keyboard"); + EventUtils.synthesizeKey("VK_LEFT"); + await waitUntil(() => getActiveConfiguration().interval === 2); + is( + intervalInput.getAttribute("aria-valuenow"), + "2", + "aria-valuenow has the expected value" + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js new file mode 100644 index 0000000000..07680d7496 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js @@ -0,0 +1,128 @@ +/* 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 override the individual settings when changed." + ); + const supportedFeatures = Services.profiler.GetFeatures(); + + if (!supportedFeatures.includes("stackwalk")) { + ok(true, "This platform does not support stackwalking, skip this test."); + return; + } + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + supportedFeatures + ); + + await withAboutProfiling(async document => { + const webdevPreset = await getNearestInputFromText( + document, + "Web Developer" + ); + const customPreset = await getNearestInputFromText(document, "Custom"); + const stackwalkFeature = await getNearestInputFromText( + document, + "Native Stacks" + ); + const geckoMainThread = await getNearestInputFromText( + document, + "GeckoMain" + ); + + { + info("Check the defaults on the about:profiling page."); + ok( + webdevPreset.checked, + "By default the Web Developer preset is checked." + ); + ok(!customPreset.checked, "By default the custom preset is not checked."); + ok( + !stackwalkFeature.checked, + "Stack walking is not enabled for Web Developer." + ); + ok( + !activeConfigurationHasFeature("stackwalk"), + "Stack walking is not in the active configuration." + ); + ok( + geckoMainThread.checked, + "The GeckoMain thread is tracked for the Web Developer preset" + ); + ok( + activeConfigurationHasThread("GeckoMain"), + "The GeckoMain thread is in the active configuration." + ); + } + + { + info("Change some settings, which will move the preset over to Custom."); + + info("Click stack walking."); + stackwalkFeature.click(); + + info("Click the GeckoMain thread."); + geckoMainThread.click(); + } + + { + info("Check that the various settings were actually updated in the UI."); + ok( + !webdevPreset.checked, + "The Web Developer preset is no longer enabled." + ); + ok(customPreset.checked, "The Custom preset is now checked."); + ok(stackwalkFeature.checked, "Stack walking was enabled"); + ok( + activeConfigurationHasFeature("stackwalk"), + "Stack walking is in the active configuration." + ); + ok( + !geckoMainThread.checked, + "GeckoMain was removed from tracked threads." + ); + ok( + !activeConfigurationHasThread("GeckoMain"), + "The GeckoMain thread is not in the active configuration." + ); + } + + { + info( + "Click the Web Developer preset, which should revert the other settings." + ); + webdevPreset.click(); + } + + { + info( + "Now verify that everything was reverted back to the original settings." + ); + ok(webdevPreset.checked, "The Web Developer preset is checked again."); + ok(!customPreset.checked, "The custom preset is not checked."); + ok( + !stackwalkFeature.checked, + "Stack walking is reverted for the Web Developer preset." + ); + ok( + !activeConfigurationHasFeature("stackwalk"), + "Stack walking is not in the active configuration." + ); + ok( + geckoMainThread.checked, + "GeckoMain was added back to the tracked threads." + ); + ok( + activeConfigurationHasThread("GeckoMain"), + "The GeckoMain thread is in the active configuration." + ); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js new file mode 100644 index 0000000000..66ead70094 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js @@ -0,0 +1,54 @@ +/* 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; + } + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await withAboutProfiling(async document => { + const webdev = await getNearestInputFromText(document, "Web Developer"); + ok(webdev.checked, "By default the Web Developer preset is selected."); + + ok( + !activeConfigurationHasFeature("stackwalk"), + "Stackwalking is not enabled for the Web Developer workflow" + ); + + const graphics = await getNearestInputFromText(document, "Graphics"); + + ok(!graphics.checked, "The Graphics preset is not checked."); + graphics.click(); + ok( + graphics.checked, + "After clicking the input, the Graphics preset is now checked." + ); + + ok( + activeConfigurationHasFeature("stackwalk"), + "The graphics preset uses stackwalking." + ); + + const media = await getNearestInputFromText(document, "Media"); + + ok(!media.checked, "The media preset is not checked."); + media.click(); + ok( + media.checked, + "After clicking the input, the Media preset is now checked." + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-rtl.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-rtl.js new file mode 100644 index 0000000000..dfed3c432b --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-rtl.js @@ -0,0 +1,31 @@ +/* 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 () { + await withAboutProfiling(async document => { + is(document.dir, "ltr", "About profiling has the expected direction ltr"); + is( + document.documentElement.getAttribute("lang"), + "en-US", + "About profiling has the expected lang" + ); + }); +}); + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["intl.l10n.pseudo", "bidi"]], + }); + + await withAboutProfiling(async document => { + is(document.dir, "rtl", "About profiling has the expected direction rtl"); + is( + document.documentElement.getAttribute("lang"), + "en-US", + "About profiling has the expected lang" + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js new file mode 100644 index 0000000000..c2512dcc05 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js @@ -0,0 +1,129 @@ +/* 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 the behavior of thread toggling and the text summary works as expected." + ); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await withAboutProfiling(async document => { + const threadTextEl = await getNearestInputFromText( + document, + "Add custom threads by name:" + ); + + is( + getActiveConfiguration().threads.join(","), + "GeckoMain,Compositor,Renderer,DOM Worker", + "The threads starts out with the default" + ); + is( + threadTextEl.value, + "GeckoMain,Compositor,Renderer,DOM Worker", + "The threads starts out with the default in the thread text input" + ); + + await clickThreadCheckbox(document, "Compositor", "Toggle off"); + + is( + getActiveConfiguration().threads.join(","), + "GeckoMain,Renderer,DOM Worker", + "The threads have been updated" + ); + is( + threadTextEl.value, + "GeckoMain,Renderer,DOM Worker", + "The threads have been updated in the thread text input" + ); + + await clickThreadCheckbox(document, "DNS Resolver", "Toggle on"); + + is( + getActiveConfiguration().threads.join(","), + "GeckoMain,Renderer,DOM Worker,DNS Resolver", + "Another thread was added" + ); + is( + threadTextEl.value, + "GeckoMain,Renderer,DOM Worker,DNS Resolver", + "Another thread was in the thread text input" + ); + + const styleThreadCheckbox = await getNearestInputFromText( + document, + "StyleThread" + ); + ok(!styleThreadCheckbox.checked, "The style thread is not checked."); + + // Set the input box directly + setReactFriendlyInputValue( + threadTextEl, + "GeckoMain,DOM Worker,DNS Resolver,StyleThread" + ); + threadTextEl.dispatchEvent(new Event("blur", { bubbles: true })); + + ok(styleThreadCheckbox.checked, "The style thread is now checked."); + is( + getActiveConfiguration().threads.join(","), + "GeckoMain,DOM Worker,DNS Resolver,StyleThread", + "Another thread was added" + ); + is( + threadTextEl.value, + "GeckoMain,DOM Worker,DNS Resolver,StyleThread", + "Another thread was in the thread text input" + ); + + // The all threads checkbox has nested text elements, so it's not easy to select + // by its label value. Select it by ID. + const allThreadsCheckbox = document.querySelector( + "#perf-settings-thread-checkbox-all-threads" + ); + info(`Turning on "All Threads" by clicking it."`); + allThreadsCheckbox.click(); + + is( + getActiveConfiguration().threads.join(","), + "GeckoMain,DOM Worker,DNS Resolver,StyleThread,*", + "Asterisk was added" + ); + is( + threadTextEl.value, + "GeckoMain,DOM Worker,DNS Resolver,StyleThread,*", + "Asterisk was in the thread text input" + ); + + is(styleThreadCheckbox.disabled, true, "The Style Thread is now disabled."); + + // Remove the asterisk + setReactFriendlyInputValue( + threadTextEl, + "GeckoMain,DOM Worker,DNS Resolver,StyleThread" + ); + threadTextEl.dispatchEvent(new Event("blur", { bubbles: true })); + + ok(!allThreadsCheckbox.checked, "The all threads checkbox is not checked."); + is(styleThreadCheckbox.disabled, false, "The Style Thread is now enabled."); + }); +}); + +/** + * @param {Document} document + * @param {string} threadName + * @param {string} action - This is the intent of the click. + */ +async function clickThreadCheckbox(document, threadName, action) { + info(`${action} "${threadName}" by clicking it.`); + const checkbox = await getNearestInputFromText(document, threadName); + checkbox.click(); +} diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js new file mode 100644 index 0000000000..69b266207f --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js @@ -0,0 +1,31 @@ +/* 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 can be loaded, and the threads changed."); + + await withAboutProfiling(async document => { + const geckoMainInput = await getNearestInputFromText(document, "GeckoMain"); + + ok( + geckoMainInput.checked, + "The GeckoMain thread starts checked by default." + ); + + ok( + activeConfigurationHasThread("GeckoMain"), + "The profiler was started with the GeckoMain thread" + ); + + info("Click the GeckoMain checkbox."); + geckoMainInput.click(); + ok(!geckoMainInput.checked, "The GeckoMain thread UI is toggled off."); + + ok( + !activeConfigurationHasThread("GeckoMain"), + "The profiler was not started with the GeckoMain thread." + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js b/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js new file mode 100644 index 0000000000..f6b17366c3 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js @@ -0,0 +1,42 @@ +/* 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 what happens when a recording is interrupted by another tool."); + + const { stopProfiler: stopProfilerByAnotherTool } = ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); + + await withDevToolsPanel(async document => { + const getRecordingState = setupGetRecordingState(document); + + const startRecording = await getActiveButtonFromText( + document, + "Start recording" + ); + info("Click to start recording"); + startRecording.click(); + + info("Wait until the profiler UI has updated to show that it is ready."); + await getActiveButtonFromText(document, "Capture recording"); + + info("Stop the profiler by another tool."); + + stopProfilerByAnotherTool(); + + info("Check that the user was notified of this interruption."); + await getElementFromDocumentByText( + document, + "The recording was stopped by another tool." + ); + + is( + getRecordingState(), + "available-to-record", + "The client is ready to record again, even though it was interrupted." + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js b/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js new file mode 100644 index 0000000000..ac253b9562 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js @@ -0,0 +1,95 @@ +/* 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"; + +const ONBOARDING_PREF = "devtools.performance.new-panel-onboarding"; + +add_task(async function testWithOnboardingPreferenceFalse() { + info("Test that the onboarding message is displayed as expected."); + + info("Test the onboarding message when the preference is false"); + await SpecialPowers.pushPrefEnv({ + set: [[ONBOARDING_PREF, false]], + }); + await withDevToolsPanel(async document => { + { + // Wait for another UI element to be rendered before asserting the + // onboarding message. + await getActiveButtonFromText(document, "Start recording"); + ok( + !isOnboardingDisplayed(document), + "Onboarding message is not displayed" + ); + } + }); +}); + +add_task(async function testWithOnboardingPreferenceTrue() { + info("Test the onboarding message when the preference is true"); + await SpecialPowers.pushPrefEnv({ + set: [[ONBOARDING_PREF, true]], + }); + + await withDevToolsPanel(async document => { + await waitUntil( + () => isOnboardingDisplayed(document), + "Waiting for the onboarding message to be displayed" + ); + ok(true, "Onboarding message is displayed"); + await closeOnboardingMessage(document); + }); + + is( + Services.prefs.getBoolPref(ONBOARDING_PREF), + false, + "onboarding preference should be false after closing the message" + ); +}); + +add_task(async function testWithOnboardingPreferenceNotSet() { + info("Test the onboarding message when the preference is not set"); + await SpecialPowers.pushPrefEnv({ + clear: [[ONBOARDING_PREF]], + }); + + await withDevToolsPanel(async document => { + await waitUntil( + () => isOnboardingDisplayed(document), + "Waiting for the onboarding message to be displayed" + ); + ok(true, "Onboarding message is displayed"); + await closeOnboardingMessage(document); + }); + + is( + Services.prefs.getBoolPref(ONBOARDING_PREF), + false, + "onboarding preference should be false after closing the message" + ); +}); + +/** + * Helper to close the onboarding message by clicking on the close button. + */ +async function closeOnboardingMessage(document) { + const closeButton = await getActiveButtonFromText( + document, + "Close the onboarding message" + ); + info("Click the close button to hide the onboarding message."); + closeButton.click(); + + await waitUntil( + () => !isOnboardingDisplayed(document), + "Waiting for the onboarding message to disappear" + ); +} + +function isOnboardingDisplayed(document) { + return maybeGetElementFromDocumentByText( + document, + "Firefox Profiler is now integrated into Developer Tools" + ); +} 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 0000000000..383ca57088 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-presets.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 about:profiling presets configure the profiler"); + + if (!Services.profiler.GetFeatures().includes("stackwalk")) { + ok(true, "This platform does not support stackwalking, skip this test."); + return; + } + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + 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." + ); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js b/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js new file mode 100644 index 0000000000..41612c94e4 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js @@ -0,0 +1,61 @@ +/* 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 what happens if the profiler was previously started by another tool." + ); + + const { startProfiler } = ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); + + info("Start the profiler before DevTools is loaded."); + startProfiler("aboutprofiling"); + + await withDevToolsPanel(async document => { + const getRecordingState = setupGetRecordingState(document); + + // The initial state of the profiler UI is racy, as it calls out to the PerfFront + // to get the status of the profiler. This can race with the initialization of + // the test. Most of the the time the result is "not-yet-known", but rarely + // the PerfFront will win this race. Allow for both outcomes of the race in this + // test. + ok( + getRecordingState() === "not-yet-known" || + getRecordingState() === "recording", + "The component starts out in an unknown state or in a recording state." + ); + + const cancelRecording = await getActiveButtonFromText( + document, + "Cancel recording" + ); + + is( + getRecordingState(), + "recording", + "The profiler is reflecting the recording state." + ); + + info("Click the button to cancel the recording"); + cancelRecording.click(); + + is( + getRecordingState(), + "request-to-stop-profiler", + "We can request to stop the profiler." + ); + + await getActiveButtonFromText(document, "Start recording"); + + is( + getRecordingState(), + "available-to-record", + "The profiler is now available to record." + ); + }); +}); 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 0000000000..4f0accf3eb --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js @@ -0,0 +1,213 @@ +/* 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"; + +const FRONTEND_BASE_HOST = "http://example.com"; +const FRONTEND_BASE_PATH = + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html"; +const FRONTEND_BASE_URL = FRONTEND_BASE_HOST + FRONTEND_BASE_PATH; + +add_setup(async function setup() { + // The active tab view isn't enabled in all configurations. Let's make sure + // it's enabled in these tests. + SpecialPowers.pushPrefEnv({ + set: [["devtools.performance.recording.active-tab-view.enabled", true]], + }); +}); + +add_task(async function test() { + info( + "Test that DevTools can capture profiles. This function also unit tests the " + + "internal RecordingState of the client." + ); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await setProfilerFrontendUrl(FRONTEND_BASE_HOST, FRONTEND_BASE_PATH); + + await withDevToolsPanel(async document => { + const getRecordingState = setupGetRecordingState(document); + + // The initial state of the profiler UI is racy, as it calls out to the PerfFront + // to get the status of the profiler. This can race with the initialization of + // the test. Most of the the time the result is "not-yet-known", but rarely + // the PerfFront will win this race. Allow for both outcomes of the race in this + // test. + ok( + getRecordingState() === "not-yet-known" || + getRecordingState() === "available-to-record", + "The component starts out in an unknown state or is already available to record." + ); + + // First check for "firefox-platform" preset which will have no "view" query + // string because this is where our traditional "full" view opens up. + await setPresetCaptureAndAssertUrl({ + document, + preset: "firefox-platform", + expectedUrl: FRONTEND_BASE_URL, + getRecordingState, + }); + + // Now, let's check for "web-developer" preset. This will open up the frontend + // with "active-tab" view query string. Frontend will understand and open the active tab view for it. + await setPresetCaptureAndAssertUrl({ + document, + preset: "web-developer", + expectedUrl: FRONTEND_BASE_URL + "?view=active-tab&implementation=js", + getRecordingState, + }); + }); +}); + +add_task(async function test_in_private_window() { + info("Test that DevTools can capture profiles in a private window."); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await setProfilerFrontendUrl(FRONTEND_BASE_HOST, FRONTEND_BASE_PATH); + + info("Open a private window."); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + await withDevToolsPanel(async document => { + const getRecordingState = setupGetRecordingState(document); + + // The initial state of the profiler UI is racy, as it calls out to the PerfFront + // to get the status of the profiler. This can race with the initialization of + // the test. Most of the the time the result is "not-yet-known", but rarely + // the PerfFront will win this race. Allow for both outcomes of the race in this + // test. + ok( + getRecordingState() === "not-yet-known" || + getRecordingState() === "available-to-record", + "The component starts out in an unknown state or is already available to record." + ); + + // First check for "firefox-platform" preset which will have no "view" query + // string because this is where our traditional "full" view opens up. + // Note that this utility will check for a new tab in the main non-private + // window, which is exactly what we want here. + await setPresetCaptureAndAssertUrl({ + document, + preset: "firefox-platform", + expectedUrl: FRONTEND_BASE_URL, + getRecordingState, + }); + + // Now, let's check for "web-developer" preset. This will open up the frontend + // with "active-tab" view query string. Frontend will understand and open the active tab view for it. + await setPresetCaptureAndAssertUrl({ + document, + preset: "web-developer", + expectedUrl: FRONTEND_BASE_URL + "?view=active-tab&implementation=js", + getRecordingState, + }); + }, privateWindow); + + await BrowserTestUtils.closeWindow(privateWindow); +}); + +async function setPresetCaptureAndAssertUrl({ + document, + preset, + expectedUrl, + getRecordingState, +}) { + const presetsInDevtools = await getNearestInputFromText(document, "Settings"); + setReactFriendlyInputValue(presetsInDevtools, preset); + + const startRecording = await getActiveButtonFromText( + document, + "Start recording" + ); + + is( + getRecordingState(), + "available-to-record", + "After talking to the actor, we're ready to record." + ); + + info("Click the button to start recording"); + startRecording.click(); + + is( + getRecordingState(), + "request-to-start-recording", + "Clicking the start recording button sends in a request to start recording." + ); + + is( + document.defaultView.gToolbox.isHighlighted("performance"), + false, + "The Performance panel in not highlighted yet." + ); + + const captureRecording = await getActiveButtonFromText( + document, + "Capture recording" + ); + + is( + getRecordingState(), + "recording", + "Once the Capture recording button is available, the actor has started " + + "its recording" + ); + + is( + document.defaultView.gToolbox.isHighlighted("performance"), + true, + "The Performance Panel in the Devtools Tab is highlighted when the profiler " + + "is recording" + ); + + info("Click the button to capture the recording."); + captureRecording.click(); + + is( + getRecordingState(), + "request-to-get-profile-and-stop-profiler", + "We have requested to stop the profiler." + ); + + await getActiveButtonFromText(document, "Start recording"); + is( + getRecordingState(), + "available-to-record", + "The profiler is available to record again." + ); + + is( + document.defaultView.gToolbox.isHighlighted("performance"), + false, + "The Performance panel in not highlighted anymore when the profiler is stopped" + ); + + info( + "If the DevTools successfully injects a profile into the page, then the " + + "fake frontend will rename the title of the page." + ); + + await waitForTabUrl({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + expectedUrl, + }); +} 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 0000000000..a34df5def0 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js @@ -0,0 +1,36 @@ +/* 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 discard 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."); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js b/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js new file mode 100644 index 0000000000..1268cf818d --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js @@ -0,0 +1,389 @@ +/* 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/. */ +/* eslint-disable max-nested-callbacks */ +"use strict"; + +add_task(async function test_change_in_popup() { + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + info( + "Test that changing settings in the popup changes settings in the devtools panel and about:profiling too." + ); + + const browserWindow = window; + const browserDocument = document; + + await makeSureProfilerPopupIsEnabled(); + await withDevToolsPanel( + "about:profiling", + async (devtoolsDocument, aboutProfilingDocument) => { + await withPopupOpen(browserWindow, async () => { + const presetsInPopup = browserDocument.getElementById( + "PanelUI-profiler-presets" + ); + + const presetsInDevtools = await getNearestInputFromText( + devtoolsDocument, + "Settings" + ); + + const webdev = await getNearestInputFromText( + aboutProfilingDocument, + "Web Developer" + ); + const graphics = await getNearestInputFromText( + aboutProfilingDocument, + "Graphics" + ); + const media = await getNearestInputFromText( + aboutProfilingDocument, + "Media" + ); + + // Default situation + ok( + webdev.checked, + "By default the Web Developer preset is selected in the about:profiling interface." + ); + is( + presetsInDevtools.value, + "web-developer", + "The presets default to webdev mode in the devtools panel." + ); + is( + presetsInPopup.value, + "web-developer", + "The presets default to webdev mode in the popup." + ); + + // Select "graphics" using the popup + ok(!graphics.checked, "The Graphics preset is not checked."); + + presetsInPopup.menupopup.openPopup(); + presetsInPopup.menupopup.activateItem( + await getElementByLabel(presetsInPopup, "Graphics") + ); + + await TestUtils.waitForCondition( + () => !webdev.checked, + "After selecting the preset in the popup, waiting until the Web Developer preset isn't selected anymore in the about:profiling interface." + ); + await TestUtils.waitForCondition( + () => graphics.checked, + "After selecting the preset in the popup, waiting until the Graphics preset is checked in the about:profiling interface." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "graphics", + "After selecting the preset in the popup, waiting until the preset is changed to Graphics in the devtools panel too." + ); + await TestUtils.waitForCondition( + () => presetsInPopup.value === "graphics", + "After selecting the preset in the popup, waiting until the preset is changed to Graphics in the popup." + ); + + // Select "firefox frontend" using the popup + ok(!media.checked, "The Media preset is not checked."); + + presetsInPopup.menupopup.openPopup(); + presetsInPopup.menupopup.activateItem( + await getElementByLabel(presetsInPopup, "Media") + ); + + await TestUtils.waitForCondition( + () => !graphics.checked, + "After selecting the preset in the popup, waiting until the Graphics preset is not checked anymore in the about:profiling interface." + ); + await TestUtils.waitForCondition( + () => media.checked, + "After selecting the preset in the popup, waiting until the Media preset is checked in the about:profiling interface." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "media", + "After selecting the preset in the popup, waiting until the preset is changed to Firefox Front-end in the devtools panel." + ); + await TestUtils.waitForCondition( + () => presetsInPopup.value === "media", + "After selecting the preset in the popup, waiting until the preset is changed to Media in the popup." + ); + }); + } + ); +}); + +// In the following tests we don't look at changes in the popup. Indeed because +// the popup rerenders each time it's open, we don't need to mirror it. +add_task(async function test_change_in_about_profiling() { + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds, or after previous tests. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + info( + "Test that changing settings in about:profiling changes settings in the devtools panel too." + ); + + await withDevToolsPanel( + "about:profiling", + async (devtoolsDocument, aboutProfilingDocument) => { + const presetsInDevtools = await getNearestInputFromText( + devtoolsDocument, + "Settings" + ); + + const webdev = await getNearestInputFromText( + aboutProfilingDocument, + "Web Developer" + ); + const graphics = await getNearestInputFromText( + aboutProfilingDocument, + "Graphics" + ); + const media = await getNearestInputFromText( + aboutProfilingDocument, + "Media" + ); + const custom = await getNearestInputFromText( + aboutProfilingDocument, + "Custom" + ); + + // Default values + ok( + webdev.checked, + "By default the Web Developer preset is selected in the about:profiling interface." + ); + is( + presetsInDevtools.value, + "web-developer", + "The presets default to webdev mode in the devtools panel." + ); + + // Change the preset in about:profiling, check it changes also in the + // devtools panel. + ok(!graphics.checked, "The Graphics preset is not checked."); + graphics.click(); + ok( + graphics.checked, + "After clicking the input, the Graphics preset is now checked in about:profiling." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "graphics", + "The preset was changed to Graphics in the devtools panel too." + ); + + ok(!media.checked, "The Media preset is not checked."); + media.click(); + ok( + media.checked, + "After clicking the input, the Media preset is now checked in about:profiling." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "media", + "The preset was changed to Media in the devtools panel too." + ); + + // Now let's try to change some configuration! + info( + "Increase the interval by an arbitrary amount. The input range will " + + "scale that to the final value presented to the profiler." + ); + const intervalInput = await getNearestInputFromText( + aboutProfilingDocument, + "Sampling interval:" + ); + setReactFriendlyInputValue( + intervalInput, + Number(intervalInput.value) + 1 + ); + ok( + custom.checked, + "After changing the interval, the Custom preset is now checked in about:profiling." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "custom", + "The preset was changed to Custom in the devtools panel too." + ); + + ok( + getDevtoolsCustomPresetContent(devtoolsDocument).includes( + "Interval: 2 ms" + ), + "The new interval should be in the custom preset description" + ); + + // Let's check some thread as well + info("Change the threads values using the checkboxes"); + + const styleThreadInput = await getNearestInputFromText( + aboutProfilingDocument, + "StyleThread" + ); + ok( + !styleThreadInput.checked, + "The StyleThread thread isn't checked by default." + ); + + info("Click the StyleThread checkbox."); + styleThreadInput.click(); + + // For some reason, it's not possible to directly match "StyleThread". + const threadsLine = ( + await getElementFromDocumentByText(devtoolsDocument, "Threads") + ).parentElement; + await TestUtils.waitForCondition( + () => threadsLine.textContent.includes("StyleThread"), + "Waiting that StyleThread is displayed in the devtools panel." + ); + ok( + getDevtoolsCustomPresetContent(devtoolsDocument).includes( + "StyleThread" + ), + "The StyleThread thread should be listed in the custom preset description" + ); + styleThreadInput.click(); + await TestUtils.waitForCondition( + () => !threadsLine.textContent.includes("StyleThread"), + "Waiting until the StyleThread disappears from the devtools panel." + ); + ok( + !getDevtoolsCustomPresetContent(devtoolsDocument).includes( + "StyleThread" + ), + "The StyleThread thread should no longer be listed in the custom preset description" + ); + + info("Change the threads values using the input."); + const threadInput = await getNearestInputFromText( + aboutProfilingDocument, + "Add custom threads by name" + ); + + function setThreadInputValue(newThreadValue) { + // Actually set the new value. + setReactFriendlyInputValue(threadInput, newThreadValue); + // The settings are actually changed on the blur event. + threadInput.dispatchEvent(new FocusEvent("blur")); + } + + let newThreadValue = "GeckoMain,Foo"; + setThreadInputValue(newThreadValue); + await TestUtils.waitForCondition( + () => threadsLine.textContent.includes("Foo"), + "Waiting for Foo to be displayed in the devtools panel." + ); + + // The code detecting changes to the thread list has a fast path + // to detect that the list of threads has changed if the 2 lists + // have different lengths. Exercise the slower code path by changing + // the list of threads to a list with the same number of threads. + info("Change the thread list again to a list of the same length"); + newThreadValue = "GeckoMain,Dummy"; + is( + threadInput.value.split(",").length, + newThreadValue.split(",").length, + "The new value should have the same count of threads as the old value, please double check the test code." + ); + setThreadInputValue(newThreadValue); + checkDevtoolsCustomPresetContent( + devtoolsDocument, + ` + Interval: 2 ms + Threads: GeckoMain, Dummy + JavaScript + Native Stacks + CPU Utilization + Audio Callback Tracing + IPC Messages + Process CPU Utilization + ` + ); + } + ); +}); + +add_task(async function test_change_in_devtools_panel() { + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds, or after previous tests. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + info( + "Test that changing settings in the devtools panel changes settings in about:profiling too." + ); + + await withDevToolsPanel( + "about:profiling", + async (devtoolsDocument, aboutProfilingDocument) => { + const presetsInDevtools = await getNearestInputFromText( + devtoolsDocument, + "Settings" + ); + + const webdev = await getNearestInputFromText( + aboutProfilingDocument, + "Web Developer" + ); + const graphics = await getNearestInputFromText( + aboutProfilingDocument, + "Graphics" + ); + const media = await getNearestInputFromText( + aboutProfilingDocument, + "Media" + ); + + // Default values + ok( + webdev.checked, + "By default the Web Developer preset is selected in the about:profiling interface." + ); + is( + presetsInDevtools.value, + "web-developer", + "The presets default to webdev mode in the devtools panel." + ); + + // Change the preset in devtools panel, check it changes also in + // about:profiling. + ok( + !graphics.checked, + "The Graphics preset is not checked in about:profiling." + ); + + setReactFriendlyInputValue(presetsInDevtools, "graphics"); + await TestUtils.waitForCondition( + () => graphics.checked, + "After changing the preset in the devtools panel, the Graphics preset is now checked in about:profiling." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "graphics", + "The preset was changed to Graphics in the devtools panel too." + ); + + // Change another preset now + ok(!media.checked, "The Media preset is not checked."); + setReactFriendlyInputValue(presetsInDevtools, "media"); + await TestUtils.waitForCondition( + () => media.checked, + "After changing the preset in the devtools panel, the Media preset is now checked in about:profiling." + ); + await TestUtils.waitForCondition( + () => presetsInDevtools.value === "media", + "The preset was changed to Media in the devtools panel too." + ); + } + ); +}); diff --git a/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js b/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js new file mode 100644 index 0000000000..0d44a7d2ca --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js @@ -0,0 +1,91 @@ +/* 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 the states of the profiler button, e.g. inactive, active, and capturing." + ); + await setProfilerFrontendUrl( + "http://example.com", + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + await makeSureProfilerPopupIsEnabled(); + + const { toggleProfiler, captureProfile } = ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); + + const button = document.getElementById("profiler-button-button"); + if (!button) { + throw new Error("Could not find the profiler button."); + } + + info("The profiler button starts out inactive"); + await checkButtonState(button, { + tooltip: "Record a performance profile", + active: false, + paused: false, + }); + + info("Toggling the profiler turns on the active state"); + toggleProfiler("aboutprofiling"); + await checkButtonState(button, { + tooltip: "The profiler is recording a profile", + active: true, + paused: false, + }); + + info("Capturing a profile makes the button paused"); + captureProfile("aboutprofiling"); + + // The state "capturing" can be very quick, so waiting for the tooltip + // translation is racy. Let's only check the button's states. + await checkButtonState(button, { + active: false, + paused: true, + }); + + await waitUntil( + () => !button.classList.contains("profiler-paused"), + "Waiting until the profiler is no longer paused" + ); + + await checkButtonState(button, { + tooltip: "Record a performance profile", + active: false, + paused: false, + }); + + await checkTabLoadedProfile({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + }); +}); + +/** + * This check dives into the implementation details of the button, mainly + * because it's hard to provide a user-focused interpretation of button + * stylings. + */ +async function checkButtonState(button, { tooltip, active, paused }) { + is( + button.classList.contains("profiler-active"), + active, + `The expected profiler button active state is: ${active}` + ); + is( + button.classList.contains("profiler-paused"), + paused, + `The expected profiler button paused state is: ${paused}` + ); + + if (tooltip) { + // Let's also check the tooltip, but because the translation happens + // asynchronously, we need a waiting mechanism. + await getElementByTooltip(document, tooltip); + } +} diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js b/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js new file mode 100644 index 0000000000..144e63b63b --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js @@ -0,0 +1,143 @@ +/* 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"; + +const FRONTEND_BASE_HOST = "http://example.com"; +const FRONTEND_BASE_PATH = + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html"; +const FRONTEND_BASE_URL = FRONTEND_BASE_HOST + FRONTEND_BASE_PATH; + +add_setup(async function setup() { + // The active tab view isn't enabled in all configurations. Let's make sure + // it's enabled in these tests. + SpecialPowers.pushPrefEnv({ + set: [["devtools.performance.recording.active-tab-view.enabled", true]], + }); +}); + +add_task(async function test() { + info( + "Test that the profiler pop-up correctly opens the captured profile on the " + + "correct frontend view by adding proper view query string" + ); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await setProfilerFrontendUrl(FRONTEND_BASE_HOST, FRONTEND_BASE_PATH); + await makeSureProfilerPopupIsEnabled(); + + // First check for the "Media" preset which will have no "view" query + // string because it opens our traditional "full" view. + await openPopupAndAssertUrlForPreset({ + window, + preset: "Media", + expectedUrl: FRONTEND_BASE_URL, + }); + + // Now, let's check for "web-developer" preset. This will open up the frontend + // with "active-tab" view query string. Frontend will understand and open the active tab view for it. + await openPopupAndAssertUrlForPreset({ + window, + preset: "Web Developer", + expectedUrl: FRONTEND_BASE_URL + "?view=active-tab&implementation=js", + }); +}); + +add_task(async function test_in_private_window() { + info( + "Test that the profiler pop-up correctly opens the captured profile on the " + + "correct frontend view by adding proper view query string. This also tests " + + "that a tab is opened on the non-private window even when the popup is used " + + "in the private window." + ); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + Services.profiler.GetFeatures() + ); + + await setProfilerFrontendUrl(FRONTEND_BASE_HOST, FRONTEND_BASE_PATH); + await makeSureProfilerPopupIsEnabled(); + + info("Open a private window."); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // First check for the "Media" preset which will have no "view" query + // string because it opens our traditional "full" view. + // Note that this utility will check for a new tab in the main non-private + // window, which is exactly what we want here. + await openPopupAndAssertUrlForPreset({ + window: privateWindow, + preset: "Media", + expectedUrl: FRONTEND_BASE_URL, + }); + + // Now, let's check for "web-developer" preset. This will open up the frontend + // with "active-tab" view query string. Frontend will understand and open the active tab view for it. + await openPopupAndAssertUrlForPreset({ + window: privateWindow, + preset: "Web Developer", + expectedUrl: FRONTEND_BASE_URL + "?view=active-tab&implementation=js", + }); + + await BrowserTestUtils.closeWindow(privateWindow); +}); + +async function openPopupAndAssertUrlForPreset({ window, preset, expectedUrl }) { + // Let's capture a profile and assert newly created tab's url. + await openPopupAndEnsureCloses(window, async () => { + const { document } = window; + { + // Select the preset in the popup + const presetsInPopup = document.getElementById( + "PanelUI-profiler-presets" + ); + presetsInPopup.menupopup.openPopup(); + presetsInPopup.menupopup.activateItem( + await getElementByLabel(presetsInPopup, preset) + ); + + await TestUtils.waitForCondition( + () => presetsInPopup.label === preset, + `After selecting the preset in the popup, waiting until the preset is changed to ${preset} in the popup.` + ); + } + + { + const button = await getElementByLabel(document, "Start Recording"); + info("Click the button to start recording."); + button.click(); + } + + { + const button = await getElementByLabel(document, "Capture"); + info("Click the button to capture the recording."); + button.click(); + } + + info( + "If the profiler successfully captures a profile, it will create a new " + + "tab with the proper view query string depending on the preset." + ); + + await waitForTabUrl({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + expectedUrl, + }); + }); +} diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-capture.js b/devtools/client/performance-new/test/browser/browser_popup-record-capture.js new file mode 100644 index 0000000000..94da377614 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture.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 the profiler pop-up works end to end with profile recording and " + + "capture using the mouse and hitting buttons." + ); + await setProfilerFrontendUrl( + "http://example.com", + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + await makeSureProfilerPopupIsEnabled(); + await openPopupAndEnsureCloses(window, async () => { + { + const button = await getElementByLabel(document, "Start Recording"); + info("Click the button to start recording."); + button.click(); + } + + { + const button = await getElementByLabel(document, "Capture"); + info("Click the button to capture the recording."); + button.click(); + } + + info( + "If the profiler 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", + }); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-discard.js b/devtools/client/performance-new/test/browser/browser_popup-record-discard.js new file mode 100644 index 0000000000..e6ab5d38d1 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-discard.js @@ -0,0 +1,35 @@ +/* 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 the profiler popup recording can be discarded."); + await setProfilerFrontendUrl( + "http://example.com", + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + await makeSureProfilerPopupIsEnabled(); + await withPopupOpen(window, async () => { + { + const button = await getElementByLabel(document, "Start Recording"); + info("Click the button to start recording."); + button.click(); + } + + { + const button = await getElementByLabel(document, "Discard"); + info("Click the button to discard the recording."); + button.click(); + } + + { + const button = await getElementByLabel(document, "Start Recording"); + ok( + Boolean(button), + "The popup reverted back to be able to start recording again" + ); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js b/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js new file mode 100644 index 0000000000..d3631bde2f --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js @@ -0,0 +1,180 @@ +/* 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"; + +// This is the same value used by CustomizableUI tests. +const kForceOverflowWidthPx = 450; + +function isActive() { + return Services.profiler.IsActive(); +} + +/** + * Force focus to an element that isn't focusable. + * Toolbar buttons aren't focusable because if they were, clicking them would + * focus them, which is undesirable. Therefore, they're only made focusable + * when a user is navigating with the keyboard. This function forces focus as + * is done during toolbar keyboard navigation. + */ +function forceFocus(elem) { + elem.setAttribute("tabindex", "-1"); + elem.focus(); + elem.removeAttribute("tabindex"); +} + +async function waitForProfileAndCloseTab() { + await waitUntil( + () => !button.classList.contains("profiler-paused"), + "Waiting until the profiler is no longer paused" + ); + + await checkTabLoadedProfile({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + }); +} +var button; +var dropmarker; + +add_setup(async function () { + info( + "Add the profiler button to the toolbar and ensure capturing a profile loads a local url." + ); + await setProfilerFrontendUrl( + "http://example.com", + "/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + await makeSureProfilerPopupIsEnabled(); + button = document.getElementById("profiler-button-button"); + dropmarker = document.getElementById("profiler-button-dropmarker"); +}); + +add_task(async function click_icon() { + info("Test that the profiler icon starts and captures a profile."); + + ok(!dropmarker.hasAttribute("open"), "should start with the panel closed"); + ok(!isActive(), "should start with the profiler inactive"); + + button.click(); + await getElementByTooltip(document, "The profiler is recording a profile"); + ok(isActive(), "should have started the profiler"); + + button.click(); + // We're not testing for the tooltip "capturing a profile" because this might + // be racy. + await waitForProfileAndCloseTab(); + + // Back to the inactive state. + await getElementByTooltip(document, "Record a performance profile"); +}); + +add_task(async function click_dropmarker() { + info("Test that the profiler icon dropmarker opens the panel."); + + ok(!dropmarker.hasAttribute("open"), "should start with the panel closed"); + ok(!isActive(), "should start with the profiler inactive"); + + const popupShownPromise = waitForProfilerPopupEvent(window, "popupshown"); + dropmarker.click(); + await popupShownPromise; + + info("Ensure the panel is open and the profiler still inactive."); + ok(dropmarker.getAttribute("open") == "true", "panel should be open"); + ok(!isActive(), "profiler should still be inactive"); + await getElementByLabel(document, "Start Recording"); + + info("Press Escape to close the panel."); + const popupHiddenPromise = waitForProfilerPopupEvent(window, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await popupHiddenPromise; + ok(!dropmarker.hasAttribute("open"), "panel should be closed"); +}); + +add_task(async function click_overflowed_icon() { + info("Test that the profiler icon opens the panel when overflowed."); + + const overflowMenu = document.getElementById("widget-overflow"); + const profilerPanel = document.getElementById("PanelUI-profiler"); + + ok(!dropmarker.hasAttribute("open"), "should start with the panel closed"); + ok(!isActive(), "should start with the profiler inactive"); + + const navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + + info("Force the toolbar to overflow."); + const originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + info("Open the overflow menu."); + const chevron = document.getElementById("nav-bar-overflow-button"); + chevron.click(); + await TestUtils.waitForCondition(() => overflowMenu.state == "open"); + + info("Open the profiler panel."); + button.click(); + await TestUtils.waitForCondition(() => + profilerPanel?.hasAttribute("visible") + ); + + info("Ensure the panel is open and the profiler still inactive."); + ok(profilerPanel?.hasAttribute("visible"), "panel should be open"); + ok(!isActive(), "profiler should still be inactive"); + await getElementByLabel(document, "Start Recording"); + + info("Press Escape to close the panel."); + EventUtils.synthesizeKey("KEY_Escape"); + await TestUtils.waitForCondition(() => overflowMenu.state == "closed"); + ok(!dropmarker.hasAttribute("open"), "panel should be closed"); + + info("Undo the forced toolbar overflow."); + window.resizeTo(originalWindowWidth, window.outerHeight); + return TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +add_task(async function space_key() { + info("Test that the Space key starts and captures a profile."); + + ok(!dropmarker.hasAttribute("open"), "should start with the panel closed"); + ok(!isActive(), "should start with the profiler inactive"); + forceFocus(button); + + info("Press Space to start the profiler."); + EventUtils.synthesizeKey(" "); + ok(isActive(), "should have started the profiler"); + + info("Press Space again to capture the profile."); + EventUtils.synthesizeKey(" "); + await waitForProfileAndCloseTab(); +}); + +add_task(async function enter_key() { + info("Test that the Enter key starts and captures a profile."); + + ok(!dropmarker.hasAttribute("open"), "should start with the panel closed"); + ok(!isActive(), "should start with the profiler inactive"); + forceFocus(button); + + const isMacOS = Services.appinfo.OS === "Darwin"; + if (isMacOS) { + // On macOS, pressing Enter on a focused toolbarbutton does not fire a + // command event, so we do not expect Enter to start the profiler. + return; + } + + info("Pressing Enter should start the profiler."); + EventUtils.synthesizeKey("KEY_Enter"); + ok(isActive(), "should have started the profiler"); + + info("Pressing Enter again to capture the profile."); + EventUtils.synthesizeKey("KEY_Enter"); + await waitForProfileAndCloseTab(); +}); diff --git a/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button-preset.js b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button-preset.js new file mode 100644 index 0000000000..4732f8f037 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button-preset.js @@ -0,0 +1,52 @@ +/* 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 the WebChannel mechanism that it changes the preset to firefox-platform" + ); + await makeSureProfilerPopupIsDisabled(); + const supportedFeatures = Services.profiler.GetFeatures(); + + // This test assumes that the Web Developer preset is set by default, which is + // not the case on Nightly and custom builds. + BackgroundJSM.changePreset( + "aboutprofiling", + "web-developer", + supportedFeatures + ); + + await withAboutProfiling(async document => { + const webdevPreset = document.querySelector("input[value=web-developer]"); + const firefoxPreset = await document.querySelector( + "input[value=firefox-platform]" + ); + + // Check the presets now to make sure web-developer is selected right now. + ok(webdevPreset.checked, "By default the Web Developer preset is checked."); + ok( + !firefoxPreset.checked, + "By default the Firefox Platform preset is not checked." + ); + + // Enable the profiler menu button with web channel. + await withWebChannelTestDocument(async browser => { + await waitForTabTitle("WebChannel Page Ready"); + await waitForProfilerMenuButton(); + ok(true, "The profiler menu button was enabled by the WebChannel."); + }); + + // firefox-platform preset should be selected now. + ok( + !webdevPreset.checked, + "Web Developer preset should not be checked anymore." + ); + ok( + firefoxPreset.checked, + "Firefox Platform preset should now be checked after enabling the popup with web channel." + ); + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js new file mode 100644 index 0000000000..a1864c475d --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js @@ -0,0 +1,16 @@ +/* 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 the WebChannel mechanism works for turning on the menu button."); + await makeSureProfilerPopupIsDisabled(); + + await withWebChannelTestDocument(async browser => { + await waitForTabTitle("WebChannel Page Ready"); + await waitForProfilerMenuButton(); + ok(true, "The profiler menu button was enabled by the WebChannel."); + }); +}); diff --git a/devtools/client/performance-new/test/browser/fake-frontend.html b/devtools/client/performance-new/test/browser/fake-frontend.html new file mode 100644 index 0000000000..6742457113 --- /dev/null +++ b/devtools/client/performance-new/test/browser/fake-frontend.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<!-- 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/. --> +<html> + <head> + <meta charset="utf-8"/> + <title></title> + </head> + <body> + <script> + "use strict"; + // This file is used to test the injection of performance profiles into a front-end, + // specifically the mechanism used to inject into profiler.firefox.com. Rather + // than using some kind of complicated message passing scheme to talk to the test + // harness, modify the title of the page. The tests can easily read the window + // title to see if things worked as expected. + + // The following are the titles used to communicate the page's state to the tests. + // Keep these in sync with any tests that read them. + const initialTitle = "Waiting on the profile"; + const successTitle = "Profile received"; + const errorTitle = "Error" + + document.title = initialTitle; + + // A function which requests the profile from the browser using the GET_PROFILE + // WebChannel message. + function getProfile() { + return new Promise((resolve, reject) => { + const requestId = 0; + + function listener(event) { + window.removeEventListener( + "WebChannelMessageToContent", + listener, + true + ); + + const { id, message } = event.detail; + + if (id !== "profiler.firefox.com" || + !message || + typeof message !== "object" + ) { + console.error(message); + reject(new Error("A malformed WebChannel event was received.")); + return; + } + + if (!message.type) { + console.error(message); + reject(new Error("The WebChannel event indicates an error.")); + return; + } + + if (message.requestId === requestId) { + if (message.type === "SUCCESS_RESPONSE") { + resolve(message.response); + } else { + reject(new Error(message.error)); + } + } + } + + window.addEventListener("WebChannelMessageToContent", listener, true); + + window.dispatchEvent( + new CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: "profiler.firefox.com", + message: { type: "GET_PROFILE", requestId }, + }), + }) + ); + }) + } + + async function runTest() { + try { + // Get the profile. + const profile = await getProfile(); + + // Check that the profile is somewhat reasonable. It should be a gzipped + // profile, so we can only lightly check some properties about it, and check + // that it is an ArrayBuffer. + // + // After the check, modify the title of the document, so the tab title gets + // updated. This is an easy way to pass a message to the test script. + if ( + profile && + typeof profile === 'object' && + ( + // 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 (bug 1581963). + Object.keys(profile).includes("threads") + ) + ) { + // The profile looks good! + document.title = successTitle; + } else { + // The profile doesn't look right, surface the error to the terminal. + dump('The gecko profile was malformed in fake-frontend.html\n'); + dump(`Profile: ${JSON.stringify(profile)}\n`); + + // Also to the web console. + console.error(profile); + + // Report the error to the tab title. + document.title = errorTitle; + } + } catch (error) { + // Catch any error and notify the test. + document.title = errorTitle; + dump('An error was caught in fake-frontend.html\n'); + dump(`${error}\n`); + } + } + + runTest(); + </script> + </body> +</html> diff --git a/devtools/client/performance-new/test/browser/head.js b/devtools/client/performance-new/test/browser/head.js new file mode 100644 index 0000000000..af0a24c6d4 --- /dev/null +++ b/devtools/client/performance-new/test/browser/head.js @@ -0,0 +1,40 @@ +/* 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"; + +const BackgroundJSM = ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" +); + +registerCleanupFunction(() => { + BackgroundJSM.revertRecordingSettings(); +}); + +/** + * Allow tests to use "require". + */ +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); + +{ + if (Services.env.get("MOZ_PROFILER_SHUTDOWN")) { + throw new Error( + "These tests cannot be run with shutdown profiling as they rely on manipulating " + + "the state of the profiler." + ); + } + + if (Services.env.get("MOZ_PROFILER_STARTUP")) { + throw new Error( + "These tests cannot be run with startup profiling as they rely on manipulating " + + "the state of the profiler." + ); + } +} + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/performance-new/test/browser/helpers.js", + this +); diff --git a/devtools/client/performance-new/test/browser/helpers.js b/devtools/client/performance-new/test/browser/helpers.js new file mode 100644 index 0000000000..9e85bf5920 --- /dev/null +++ b/devtools/client/performance-new/test/browser/helpers.js @@ -0,0 +1,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.import( + "resource://devtools/client/performance-new/popup/menu-button.jsm.js" + ); + + 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.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); + + 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.import( + "resource://devtools/client/performance-new/popup/menu-button.jsm.js" + ); + + 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 */ diff --git a/devtools/client/performance-new/test/browser/webchannel.html b/devtools/client/performance-new/test/browser/webchannel.html new file mode 100644 index 0000000000..c1a0872cfe --- /dev/null +++ b/devtools/client/performance-new/test/browser/webchannel.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<!-- 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/. --> +<html> + <head> + <meta charset="utf-8"/> + <title></title> + </head> + <body> + This content page will send a WebChannel message to enable the profiler menu button. + <script> + "use strict"; + document.title = "WebChannel Page Ready"; + + window.dispatchEvent( + new CustomEvent('WebChannelMessageToChrome', { + detail: JSON.stringify({ + id: 'profiler.firefox.com', + message: { type: "ENABLE_MENU_BUTTON", requestId: 0 }, + }), + }) + ); + </script> + </body> +</html> diff --git a/devtools/client/performance-new/test/xpcshell/.eslintrc.js b/devtools/client/performance-new/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..b6aacf458f --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for xpcshell tests. + extends: "../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/client/performance-new/test/xpcshell/head.js b/devtools/client/performance-new/test/xpcshell/head.js new file mode 100644 index 0000000000..83812e9dc1 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/head.js @@ -0,0 +1,12 @@ +/* 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"; + +registerCleanupFunction(() => { + // Always clean up the prefs after every test. + const { revertRecordingSettings } = ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); + revertRecordingSettings(); +}); diff --git a/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js b/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js new file mode 100644 index 0000000000..821d9af2c2 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js @@ -0,0 +1,106 @@ +/* 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"; + +/** + * Tests the initial state of the background script for the popup. + */ + +function setupBackgroundJsm() { + return ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); +} + +add_task(function test() { + info("Test that we get the default preference values from the browser."); + const { getRecordingSettings } = setupBackgroundJsm(); + + const preferences = getRecordingSettings( + "aboutprofiling", + Services.profiler.GetFeatures() + ); + + Assert.notEqual( + preferences.entries, + undefined, + "The initial state has the default entries." + ); + Assert.notEqual( + preferences.interval, + undefined, + "The initial state has the default interval." + ); + Assert.notEqual( + preferences.features, + undefined, + "The initial state has the default features." + ); + Assert.equal( + preferences.features.includes("js"), + true, + "The js feature is initialized to the default." + ); + Assert.notEqual( + preferences.threads, + undefined, + "The initial state has the default threads." + ); + Assert.equal( + preferences.threads.includes("GeckoMain"), + true, + "The GeckoMain thread is initialized to the default." + ); + Assert.notEqual( + preferences.objdirs, + undefined, + "The initial state has the default objdirs." + ); + Assert.notEqual( + preferences.duration, + undefined, + "The duration is initialized to the duration." + ); +}); + +add_task(function test() { + info( + "Test that the state and features are properly validated. This ensures that as " + + "we add and remove features, the stored preferences do not cause the Gecko " + + "Profiler interface to crash with invalid values." + ); + const { getRecordingSettings, setRecordingSettings, changePreset } = + setupBackgroundJsm(); + + const supportedFeatures = Services.profiler.GetFeatures(); + + changePreset("aboutprofiling", "custom", supportedFeatures); + + Assert.ok( + getRecordingSettings("aboutprofiling", supportedFeatures).features.includes( + "js" + ), + "The js preference is present initially." + ); + + const settings = getRecordingSettings("aboutprofiling", supportedFeatures); + settings.features = settings.features.filter(feature => feature !== "js"); + settings.features.push("UNKNOWN_FEATURE_FOR_TESTS"); + setRecordingSettings("aboutprofiling", settings); + + Assert.ok( + !getRecordingSettings( + "aboutprofiling", + supportedFeatures + ).features.includes("UNKNOWN_FEATURE_FOR_TESTS"), + "The unknown feature is removed." + ); + Assert.ok( + !getRecordingSettings( + "aboutprofiling", + supportedFeatures + ).features.includes("js"), + "The js preference is still flipped from the default." + ); +}); diff --git a/devtools/client/performance-new/test/xpcshell/test_remove_common_path_prefix.js b/devtools/client/performance-new/test/xpcshell/test_remove_common_path_prefix.js new file mode 100644 index 0000000000..563f0c43b4 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/test_remove_common_path_prefix.js @@ -0,0 +1,121 @@ +/* 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"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + withCommonPathPrefixRemoved, +} = require("resource://devtools/client/performance-new/shared/utils.js"); + +add_task(function test() { + info( + "withCommonPathPrefixRemoved() removes the common prefix from an array " + + "of paths. This test ensures that the paths are correctly removed." + ); + + if (Services.appinfo.OS === "WINNT") { + info("Check Windows paths"); + + deepEqual(withCommonPathPrefixRemoved([]), [], "Windows empty paths"); + + deepEqual( + withCommonPathPrefixRemoved(["source\\file1.js", "source\\file2.js"]), + ["source\\file1.js", "source\\file2.js"], + "Windows relative paths" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "C:\\Users\\SomeUser\\Desktop\\source\\file1.js", + "D:\\Users\\SomeUser\\Desktop\\source\\file2.js", + ]), + [ + "C:\\Users\\SomeUser\\Desktop\\source\\file1.js", + "D:\\Users\\SomeUser\\Desktop\\source\\file2.js", + ], + "Windows multiple disks" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "C:\\Users\\SomeUser\\Desktop\\source\\file1.js", + "C:\\Users\\SomeUser\\Desktop\\source\\file2.js", + ]), + ["file1.js", "file2.js"], + "Windows full path match" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "C:\\Users\\SomeUser\\Desktop\\source\\file1.js", + "C:\\Users\\SomeUser\\file2.js", + ]), + ["Desktop\\source\\file1.js", "file2.js"], + "Windows path match at level 3" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "C:\\Users\\SomeUser\\Desktop\\source\\file1.js", + "C:\\Users\\SomeUser\\file2.js", + "C:\\Users\\file3.js", + ]), + ["SomeUser\\Desktop\\source\\file1.js", "SomeUser\\file2.js", "file3.js"], + "Windows path match at level 2" + ); + + deepEqual( + withCommonPathPrefixRemoved(["C:\\dev"]), + ["C:\\dev"], + "Windows path match at level 1" + ); + } else { + info("Check UNIX paths"); + + deepEqual(withCommonPathPrefixRemoved([]), [], "UNIX empty paths"); + + deepEqual( + withCommonPathPrefixRemoved(["source/file1.js", "source/file2.js"]), + ["source/file1.js", "source/file2.js"], + "UNIX relative paths" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "/home/someuser/Desktop/source/file1.js", + "/home/someuser/Desktop/source/file2.js", + ]), + ["file1.js", "file2.js"], + "UNIX full path match" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "/home/someuser/Desktop/source/file1.js", + "/home/someuser/file2.js", + ]), + ["Desktop/source/file1.js", "file2.js"], + "UNIX path match at level 3" + ); + + deepEqual( + withCommonPathPrefixRemoved([ + "/home/someuser/Desktop/source/file1.js", + "/home/someuser/file2.js", + "/home/file3.js", + ]), + ["someuser/Desktop/source/file1.js", "someuser/file2.js", "file3.js"], + "UNIX path match at level 2" + ); + + deepEqual( + withCommonPathPrefixRemoved(["/bin"]), + ["/bin"], + "UNIX path match at level 1" + ); + } +}); diff --git a/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js b/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js new file mode 100644 index 0000000000..0c1a03eb0e --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js @@ -0,0 +1,63 @@ +/* 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"; + +const { validateProfilerWebChannelUrl } = ChromeUtils.importESModule( + "resource:///modules/DevToolsStartup.sys.mjs" +); + +add_task(function test() { + info( + "Since the WebChannel can communicate with a content page, test that only " + + "trusted URLs can be used with this mechanism." + ); + + const { checkUrlIsValid, checkUrlIsInvalid } = setup(); + + info("Check all of the valid URLs"); + checkUrlIsValid("https://profiler.firefox.com"); + checkUrlIsValid("http://example.com"); + checkUrlIsValid("http://localhost:4242"); + checkUrlIsValid("http://localhost:32343434"); + checkUrlIsValid("http://localhost:4242/"); + checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.com"); + checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.com/"); + checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.app"); + checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.app/"); + checkUrlIsValid("https://main--perf-html.netlify.app/"); + + info("Check all of the invalid URLs"); + checkUrlIsInvalid("https://profiler.firefox.com/some-other-path"); + checkUrlIsInvalid("http://localhost:4242/some-other-path"); + checkUrlIsInvalid("http://profiler.firefox.com.example.com"); + checkUrlIsInvalid("http://mozilla.com"); + checkUrlIsInvalid("https://deploy-preview-1234--perf-html.netlify.dev"); + checkUrlIsInvalid("https://anything--perf-html.netlify.app/"); +}); + +function setup() { + function checkUrlIsValid(url) { + info(`Check that ${url} is valid`); + equal( + validateProfilerWebChannelUrl(url), + url, + `"${url}" is a valid WebChannel URL.` + ); + } + + function checkUrlIsInvalid(url) { + info(`Check that ${url} is invalid`); + equal( + validateProfilerWebChannelUrl(url), + "https://profiler.firefox.com", + `"${url}" was not valid, and was reset to the base URL.` + ); + } + + return { + checkUrlIsValid, + checkUrlIsInvalid, + }; +} diff --git a/devtools/client/performance-new/test/xpcshell/xpcshell.ini b/devtools/client/performance-new/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..b609fdf3d9 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = devtools +head = head.js +firefox-appdir = browser + +[test_popup_initial_state.js] +[test_remove_common_path_prefix.js] +[test_webchannel-urls.js] |