diff options
Diffstat (limited to 'devtools/client/performance-new/test')
34 files changed, 2511 insertions, 0 deletions
diff --git a/devtools/client/performance-new/test/.eslintrc.js b/devtools/client/performance-new/test/.eslintrc.js new file mode 100644 index 0000000000..3d0bd99e1b --- /dev/null +++ b/devtools/client/performance-new/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../.eslintrc.mochitests.js", +}; diff --git a/devtools/client/performance-new/test/browser/.eslintrc.js b/devtools/client/performance-new/test/browser/.eslintrc.js new file mode 100644 index 0000000000..8e6d7b4f8a --- /dev/null +++ b/devtools/client/performance-new/test/browser/.eslintrc.js @@ -0,0 +1,9 @@ +/* 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"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../../.eslintrc.mochitests.js", +}; 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..5b517fa429 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser.ini @@ -0,0 +1,43 @@ +[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 +support-files = + head.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] +skip-if = tsan # Frequently times out on TSan +[browser_aboutprofiling-interval.js] +[browser_aboutprofiling-threads.js] +[browser_aboutprofiling-threads-behavior.js] +[browser_aboutprofiling-presets.js] +[browser_aboutprofiling-presets-custom.js] +[browser_devtools-interrupted.js] +[browser_devtools-onboarding.js] +[browser_devtools-presets.js] +[browser_devtools-previously-started.js] +[browser_devtools-private-window.js] +[browser_devtools-record-capture.js] +[browser_devtools-record-discard.js] +[browser_webchannel-enable-menu-button.js] +[browser_popup-profiler-states.js] +[browser_popup-record-capture.js] +[browser_popup-record-capture-view.js] +[browser_popup-record-discard.js] + +# The popupshown and popuphidden events are not firing correctly on linux, as of +# Bug 1625044. It could be because of the opening of a second private browsing +# window. There should be good enough coverage of this feature with it disabled +# on Linux. This bug appears to have been around for awhile see: +# Bug 941073. Also see: 1178420, 1115757, 1401049, 1269392 +[browser_popup-private-browsing.js] +skip-if = os == 'linux' + +[browser_split-toolbar-button.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..95f9b4d9c9 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js @@ -0,0 +1,81 @@ +/* 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."); + const { + getEnvironmentVariable, + } = require("devtools/client/performance-new/browser"); + ok( + !getEnvironmentVariable("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..2b8cea19c8 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.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 modify the sampling interval."); + + await withAboutProfiling(async document => { + is( + getActiveConfiguration().interval, + 1, + "The active configuration's interval is set to a specific number initially." + ); + + 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( + document, + "Sampling interval:" + ); + setReactFriendlyInputValue(intervalInput, Number(intervalInput.value) + 8); + + is( + getActiveConfiguration().interval, + 2, + "The configuration's interval was able to be increased." + ); + }); +}); 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..03bd0cfa27 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js @@ -0,0 +1,60 @@ +/* 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 platform = await getNearestInputFromText( + document, + "Firefox Platform" + ); + + ok(!platform.checked, "The Firefox Platform preset is not checked."); + platform.click(); + ok( + platform.checked, + "After clicking the input, the Firefox Platform preset is now checked." + ); + + ok( + activeConfigurationHasFeature("stackwalk"), + "The Firefox Platform preset uses stackwalking." + ); + + const frontEnd = await getNearestInputFromText( + document, + "Firefox Front-End" + ); + + ok(!frontEnd.checked, "The Firefox front-end preset is not checked."); + frontEnd.click(); + ok( + frontEnd.checked, + "After clicking the input, the Firefox front-end preset is now checked." + ); + }); +}); 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..a42d9465fa --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js @@ -0,0 +1,126 @@ +/* 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" + ); + + // 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."); + }); +}); + +/** + * @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..c1e8db5c4f --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js @@ -0,0 +1,38 @@ +/* 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 geckoMainLabel = await getElementFromDocumentByText( + document, + "GeckoMain" + ); + const geckoMainInput = geckoMainLabel.querySelector("input"); + if (!geckoMainInput) { + throw new Error("Unable to find the input from the GeckoMain label."); + } + + 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..87394c5ac7 --- /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/popup/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..4428178541 --- /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/popup/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-private-window.js b/devtools/client/performance-new/test/browser/browser_devtools-private-window.js new file mode 100644 index 0000000000..01864225b0 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-private-window.js @@ -0,0 +1,57 @@ +/* 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 opening a private browsing window while the profiler is active."); + + await withDevToolsPanel(async document => { + const getRecordingState = setupGetRecordingState(document); + + const startRecording = await getActiveButtonFromText( + document, + "Start recording" + ); + + ok(!startRecording.disabled, "The start recording button is not disabled."); + is( + getRecordingState(), + "available-to-record", + "The panel is available to record." + ); + + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + await getElementFromDocumentByText( + document, + "The profiler is disabled when Private Browsing is enabled" + ); + + ok( + startRecording.disabled, + "The start recording button is disabled when a private browsing window is open." + ); + + is( + getRecordingState(), + "locked-by-private-browsing", + "The client knows about the private window." + ); + + info("Closing the private window"); + await BrowserTestUtils.closeWindow(privateWindow); + + info("Finally wait for the start recording button to become active again."); + await getActiveButtonFromText(document, "Start recording"); + + is( + getRecordingState(), + "available-to-record", + "The panel is available to record again." + ); + }); +}); 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..51ec553e92 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test() { + info( + "Test that DevTools can capture profiles. This function also unit tests the " + + "internal RecordingState of the client." + ); + + await setProfilerFrontendUrl( + "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + + 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." + ); + + 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." + ); + + const captureRecording = await getActiveButtonFromText( + document, + "Capture recording" + ); + + is( + getRecordingState(), + "recording", + "Once the Capture recording button is available, the actor has started " + + "its 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." + ); + + info( + "If the DevTools successfully injects a profile into the page, then the " + + "fake frontend will rename the title of the page." + ); + + await checkTabLoadedProfile({ + initialTitle: "Waiting on the profile", + successTitle: "Profile received", + errorTitle: "Error", + }); + }); +}); 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..e3d3433fd1 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_devtools-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 DevTools can capture profiles."); + + await setProfilerFrontendUrl( + "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html" + ); + + await withDevToolsPanel(async document => { + { + const button = await getActiveButtonFromText(document, "Start recording"); + info("Click the button to start recording"); + button.click(); + } + + { + const button = await getActiveButtonFromText( + document, + "Cancel recording" + ); + info("Click the button to discard to profile."); + button.click(); + } + + { + const button = await getActiveButtonFromText(document, "Start recording"); + ok(Boolean(button), "The start recording button is available again."); + } + }); +}); diff --git a/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js b/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js new file mode 100644 index 0000000000..ad9b84cbcf --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js @@ -0,0 +1,48 @@ +/* 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 gets disabled when a private browsing window is open." + ); + await makeSureProfilerPopupIsEnabled(); + + const getRecordingButton = () => + getElementByLabel(document, "Start Recording"); + + const getDisabledMessage = () => + getElementFromDocumentByText( + document, + "The profiler is currently disabled" + ); + + await withPopupOpen(window, async () => { + ok(await getRecordingButton(), "The start recording button is available"); + }); + + info("Open a private browsing window."); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Switch back to the main window and open the popup again."); + window.focus(); + await withPopupOpen(window, async () => { + ok(await getDisabledMessage(), "The disabled message is displayed."); + }); + + info("Close the private window"); + await BrowserTestUtils.closeWindow(privateWindow); + + info("Make sure the first window is focused, and open the popup back up."); + window.focus(); + await withPopupOpen(window, async () => { + ok( + await getRecordingButton(), + "The start recording button is available once again." + ); + }); +}); 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..799d1aefa0 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js @@ -0,0 +1,81 @@ +/* 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/popup/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"); + checkButtonState(button, { + tooltip: "Record a performance profile", + active: false, + paused: false, + }); + + info("Toggling the profiler turns on the active state"); + toggleProfiler("aboutprofiling"); + checkButtonState(button, { + tooltip: "The profiler is recording a profile", + active: true, + paused: false, + }); + + info("Capturing a profile makes the button paused"); + captureProfile("aboutprofiling"); + checkButtonState(button, { + tooltip: "The profiler is capturing a profile", + active: false, + paused: true, + }); + + 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", + }); +}); + +/** + * This check dives into the implementation details of the button, mainly + * because it's hard to provide a user-focused interpretation of button + * stylings. + */ +function checkButtonState(button, { tooltip, active, paused }) { + is( + button.getAttribute("tooltiptext"), + tooltip, + `The tooltip for the button is "${tooltip}".` + ); + 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}` + ); +} 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..aabed66563 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js @@ -0,0 +1,67 @@ +/* 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_URL = + "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"; + +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" + ); + await setProfilerFrontendUrl(FRONTEND_BASE_URL); + await makeSureProfilerPopupIsEnabled(); + + // First check for "firefox-platform" preset which will have no "view" query + // string because this is where our traditional "full" view opens up. + await openPopupAndAssertUrlForPreset({ + preset: "firefox-platform", + 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({ + preset: "web-developer", + expectedUrl: FRONTEND_BASE_URL + "?view=active-tab", + }); +}); + +async function openPopupAndAssertUrlForPreset({ preset, expectedUrl }) { + // First, switch to the preset we want to test. + BackgroundJSM.changePreset( + "aboutprofiling", + preset, + [] // We don't need any features for this test. + ); + + // Let's capture a profile and assert newly created tab's url. + 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 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..8289014770 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture.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"; + +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..d8f4ec9f47 --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_popup-record-discard.js @@ -0,0 +1,34 @@ +/* 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..a4efd5c74e --- /dev/null +++ b/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js @@ -0,0 +1,123 @@ +/* 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"; + +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_task(async function setup() { + 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(); + ok(isActive(), "should have started the profiler"); + + button.click(); + await waitForProfileAndCloseTab(); +}); + +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("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("popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await popupHiddenPromise; + ok(!dropmarker.hasAttribute("open"), "panel should be closed"); +}); + +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.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..9817b87e5d --- /dev/null +++ b/devtools/client/performance-new/test/browser/fake-frontend.html @@ -0,0 +1,74 @@ +<!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; + + // The following gets called by the frame script, and provides an API to + // receive the profile. + window.connectToGeckoProfiler = async (geckoProfiler) => { + try { + // Get the profile. + const profile = await geckoProfiler.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. + 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`); + } + } + </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..ab0715367f --- /dev/null +++ b/devtools/client/performance-new/test/browser/head.js @@ -0,0 +1,728 @@ +/* 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/popup/background.jsm.js" +); + +registerCleanupFunction(() => { + BackgroundJSM.revertRecordingPreferences(); +}); + +/** + * Allow tests to use "require". + */ +const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); + +{ + const { + getEnvironmentVariable, + } = require("devtools/client/performance-new/browser"); + + if (getEnvironmentVariable("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 (getEnvironmentVariable("MOZ_PROFILER_STARTUP")) { + throw new Error( + "These tests cannot be run with startup profiling as they rely on manipulating " + + "the state of the profiler." + ); + } +} + +/** + * 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 document 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 {string} label + * @returns {Promise<HTMLElement>} + */ +function getElementByLabel(document, label) { + return waitUntil( + () => document.querySelector(`[label="${label}"]`), + `Trying to find the button with the label "${label}".` + ); +} + +/** + * 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; +} + +/** + * 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}".` + ); +} + +/** + * 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); +} + +/** + * 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."); + } +} + +/** + * 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 {"popupshown" | "popuphidden"} eventName + * @returns {Promise<void>} + */ +function waitForProfilerPopupEvent(eventName) { + return new Promise(resolve => { + function handleEvent(event) { + if (event.target.getAttribute("viewId") === "PanelUI-profiler") { + window.removeEventListener(eventName, handleEvent); + resolve(); + } + } + window.addEventListener(eventName, handleEvent); + }); +} + +/** + * 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. + * @return {Promise<void>} + */ +async function _toggleOpenProfilerPopup(window) { + info("Toggle open the profiler popup."); + + info("> Find the profiler menu button."); + const profilerDropmarker = document.getElementById( + "profiler-button-dropmarker" + ); + if (!profilerDropmarker) { + throw new Error( + "Could not find the profiler button dropmarker in the toolbar." + ); + } + + const popupShown = waitForProfilerPopupEvent("popupshown"); + + info("> Trigger a click on the profiler button dropmarker."); + await EventUtils.synthesizeMouseAtCenter(profilerDropmarker, {}); + + 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. + * @return {Promise<void>} + */ +async function _closePopup(window) { + const popupHiddenPromise = waitForProfilerPopupEvent("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); +} + +/** + * 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("popuphidden"); + await callback(); + info("> Verifying that the popup was closed by the test."); + await popupHiddenPromise; +} + +/** + * This function overwrites the default profiler.firefox.com URL for tests. This + * ensures that the tests do not attempt to access external URLs. + * @param {string} url + * @returns {Promise} + */ +function setProfilerFrontendUrl(url) { + info( + "Setting the profiler URL to the fake frontend. Note that this doesn't currently " + + "support the WebChannels, so expect a few error messages about the WebChannel " + + "URLs not being correct." + ); + return SpecialPowers.pushPrefEnv({ + set: [ + // Make sure observer and testing function run in the same process + ["devtools.performance.recording.ui-base-url", url], + ["devtools.performance.recording.ui-base-url-path", ""], + ], + }); +} + +/** + * 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; + } + }); +} + +/** + * 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}` + ); + 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; + } + }); +} + +/** + * 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; + }); +} + +/** + * 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."); + return callback(contentBrowser.contentDocument); + } + ); +} + +/** + * Open DevTools and view the performance-new tab. After running the callback, clean + * up the test. + * + * @template T + * @param {(Document) => T} callback + * @returns {Promise<T>} + */ +async function withDevToolsPanel(callback) { + SpecialPowers.pushPrefEnv({ + set: [["devtools.performance.new-panel-enabled", "true"]], + }); + + const { gDevTools } = require("devtools/client/framework/devtools"); + const { TargetFactory } = require("devtools/client/framework/target"); + + info("Create a new about:blank tab."); + const tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + + info("Begin to open the DevTools and the performance-new panel."); + const target = await TargetFactory.forTab(tab); + const toolbox = await gDevTools.showToolbox(target, "performance"); + + const { document } = toolbox.getCurrentPanel().panelWin; + + info("The performance-new panel is now open and ready to use."); + await callback(document); + + info("About to remove the about:blank tab"); + await toolbox.destroy(); + BrowserTestUtils.removeTab(tab); + info("The about:blank tab is now removed."); + await new Promise(resolve => setTimeout(resolve, 500)); +} + +/** + * Start and stop the profiler to get the current active configuration. This is + * done programmtically through the nsIProfiler interface, rather than through click + * interactions, since the about:profiling page does not include buttons to control + * the recording. + * + * @returns {Object} + */ +function getActiveConfiguration() { + 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; +} + +/** + * 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); +} + +/** + * 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); +} + +/** + * Use user driven events to start the profiler, and then get the active configuration + * of the profiler. This is similar to functions in the head.js file, but is specific + * for the DevTools situation. The UI complains if the profiler stops unexpectedly. + * + * @param {Document} document + * @param {string} feature + * @returns {boolean} + */ +async function devToolsActiveConfigurationHasFeature(document, feature) { + info("Get the active configuration of the profiler via user driven events."); + const start = await getActiveButtonFromText(document, "Start recording"); + info("Click the button to start recording."); + start.click(); + + // Get the cancel button first, so that way we know the profile has actually + // been recorded. + const cancel = await getActiveButtonFromText(document, "Cancel recording"); + + const { activeConfiguration } = Services.profiler; + if (!activeConfiguration) { + throw new Error( + "Expected to find an active configuration for the profile." + ); + } + + info("Click the cancel button to discard the profile.."); + cancel.click(); + + // Wait until the start button is back. + await getActiveButtonFromText(document, "Start recording"); + + return activeConfiguration.features.includes(feature); +} + +/** + * Selects an element with some given text, then it walks up the DOM until it finds + * an input or select element via a call to querySelector. + * + * @param {Document} document + * @param {string} text + * @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."); +} + +/** + * 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; +} + +/** + * 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." + ); +} + +/** + * 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(); + } + }); +} + +/** + * 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 + ); +} + +/** + * Set a React-friendly input value. Doing this the normal way doesn't work. + * + * See https://github.com/facebook/react/issues/10135#issuecomment-500929024 + * + * @param {HTMLInputElement} input + * @param {string} value + */ +function setReactFriendlyInputValue(input, value) { + const previousValue = input.value; + + input.value = value; + + const tracker = input._valueTracker; + if (tracker) { + tracker.setValue(previousValue); + } + + // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324 + input.dispatchEvent(new Event("change", { bubbles: true })); +} + +/** + * 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("devtools/client/performance-new/store/selectors"); + 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()); +} 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..607c6dce12 --- /dev/null +++ b/devtools/client/performance-new/test/browser/webchannel.html @@ -0,0 +1,27 @@ +<!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..fb1cc73b6a --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/head.js @@ -0,0 +1,14 @@ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +registerCleanupFunction(() => { + // Always clean up the prefs after every test. + const { revertRecordingPreferences } = ChromeUtils.import( + "resource://devtools/client/performance-new/popup/background.jsm.js" + ); + revertRecordingPreferences(); +}); 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..bd75fb9c65 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js @@ -0,0 +1,110 @@ +/* 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/popup/background.jsm.js" + ); +} + +add_task(function test() { + info("Test that we get the default preference values from the browser."); + const { getRecordingPreferences } = setupBackgroundJsm(); + + const preferences = getRecordingPreferences( + "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 { + getRecordingPreferences, + setRecordingPreferences, + changePreset, + } = setupBackgroundJsm(); + + const supportedFeatures = Services.profiler.GetFeatures(); + + changePreset("aboutprofiling", "custom", supportedFeatures); + + Assert.ok( + getRecordingPreferences( + "aboutprofiling", + supportedFeatures + ).features.includes("js"), + "The js preference is present initially." + ); + + const settings = getRecordingPreferences("aboutprofiling", supportedFeatures); + settings.features = settings.features.filter(feature => feature !== "js"); + settings.features.push("UNKNOWN_FEATURE_FOR_TESTS"); + setRecordingPreferences("aboutprofiling", settings); + + Assert.ok( + !getRecordingPreferences( + "aboutprofiling", + supportedFeatures + ).features.includes("UNKNOWN_FEATURE_FOR_TESTS"), + "The unknown feature is removed." + ); + Assert.ok( + !getRecordingPreferences( + "aboutprofiling", + supportedFeatures + ).features.includes("js"), + "The js preference is still flipped from the default." + ); +}); 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..5f9582af0f --- /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.import( + "resource:///modules/DevToolsStartup.jsm" +); + +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..6aff202a65 --- /dev/null +++ b/devtools/client/performance-new/test/xpcshell/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +tags = devtools +head = head.js +firefox-appdir = browser + +[test_popup_initial_state.js] +[test_webchannel-urls.js] |