summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance-new/test')
-rw-r--r--devtools/client/performance-new/test/browser/browser.ini48
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js28
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js78
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js63
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js32
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js72
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js128
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js54
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-rtl.js31
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js129
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js31
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-interrupted.js42
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-onboarding.js95
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-presets.js47
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-previously-started.js61
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-record-capture.js213
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-record-discard.js36
-rw-r--r--devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js389
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-profiler-states.js91
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js143
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-capture.js41
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-discard.js35
-rw-r--r--devtools/client/performance-new/test/browser/browser_split-toolbar-button.js180
-rw-r--r--devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button-preset.js52
-rw-r--r--devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js16
-rw-r--r--devtools/client/performance-new/test/browser/fake-frontend.html126
-rw-r--r--devtools/client/performance-new/test/browser/head.js40
-rw-r--r--devtools/client/performance-new/test/browser/helpers.js836
-rw-r--r--devtools/client/performance-new/test/browser/webchannel.html26
-rw-r--r--devtools/client/performance-new/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/performance-new/test/xpcshell/head.js12
-rw-r--r--devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js106
-rw-r--r--devtools/client/performance-new/test/xpcshell/test_remove_common_path_prefix.js121
-rw-r--r--devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js63
-rw-r--r--devtools/client/performance-new/test/xpcshell/xpcshell.ini8
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]