summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/performance-new/test
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance-new/test')
-rw-r--r--devtools/client/performance-new/test/.eslintrc.js6
-rw-r--r--devtools/client/performance-new/test/browser/.eslintrc.js9
-rw-r--r--devtools/client/performance-new/test/browser/browser.ini43
-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.js81
-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.js32
-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.js60
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js126
-rw-r--r--devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js38
-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-private-window.js57
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-record-capture.js90
-rw-r--r--devtools/client/performance-new/test/browser/browser_devtools-record-discard.js35
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-private-browsing.js48
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-profiler-states.js81
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js67
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-capture.js40
-rw-r--r--devtools/client/performance-new/test/browser/browser_popup-record-discard.js34
-rw-r--r--devtools/client/performance-new/test/browser/browser_split-toolbar-button.js123
-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.html74
-rw-r--r--devtools/client/performance-new/test/browser/head.js728
-rw-r--r--devtools/client/performance-new/test/browser/webchannel.html27
-rw-r--r--devtools/client/performance-new/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/performance-new/test/xpcshell/head.js14
-rw-r--r--devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js110
-rw-r--r--devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js63
-rw-r--r--devtools/client/performance-new/test/xpcshell/xpcshell.ini7
34 files changed, 2511 insertions, 0 deletions
diff --git a/devtools/client/performance-new/test/.eslintrc.js b/devtools/client/performance-new/test/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/client/performance-new/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/performance-new/test/browser/.eslintrc.js b/devtools/client/performance-new/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..8e6d7b4f8a
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/.eslintrc.js
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/performance-new/test/browser/browser.ini b/devtools/client/performance-new/test/browser/browser.ini
new file mode 100644
index 0000000000..5b517fa429
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser.ini
@@ -0,0 +1,43 @@
+[DEFAULT]
+prefs =
+ # This sets up the WebChannel so that it can be used for our tests.
+ devtools.performance.recording.ui-base-url='http://example.com'
+tags = devtools devtools-performance
+subsuite = devtools
+support-files =
+ head.js
+ fake-frontend.html
+ webchannel.html
+
+[browser_aboutprofiling-env-restart-button.js]
+[browser_aboutprofiling-entries.js]
+[browser_aboutprofiling-features-disabled.js]
+[browser_aboutprofiling-features.js]
+skip-if = tsan # Frequently times out on TSan
+[browser_aboutprofiling-interval.js]
+[browser_aboutprofiling-threads.js]
+[browser_aboutprofiling-threads-behavior.js]
+[browser_aboutprofiling-presets.js]
+[browser_aboutprofiling-presets-custom.js]
+[browser_devtools-interrupted.js]
+[browser_devtools-onboarding.js]
+[browser_devtools-presets.js]
+[browser_devtools-previously-started.js]
+[browser_devtools-private-window.js]
+[browser_devtools-record-capture.js]
+[browser_devtools-record-discard.js]
+[browser_webchannel-enable-menu-button.js]
+[browser_popup-profiler-states.js]
+[browser_popup-record-capture.js]
+[browser_popup-record-capture-view.js]
+[browser_popup-record-discard.js]
+
+# The popupshown and popuphidden events are not firing correctly on linux, as of
+# Bug 1625044. It could be because of the opening of a second private browsing
+# window. There should be good enough coverage of this feature with it disabled
+# on Linux. This bug appears to have been around for awhile see:
+# Bug 941073. Also see: 1178420, 1115757, 1401049, 1269392
+[browser_popup-private-browsing.js]
+skip-if = os == 'linux'
+
+[browser_split-toolbar-button.js]
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js
new file mode 100644
index 0000000000..4415387b16
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-entries.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling can modify the sampling interval.");
+
+ await withAboutProfiling(async document => {
+ is(
+ getActiveConfiguration().capacity,
+ 128 * 1024 * 1024,
+ "The active configuration is set to a specific number initially. If this" +
+ " test fails here, then the magic numbers here may need to be adjusted."
+ );
+
+ info("Change the buffer input to an arbitrarily smaller value.");
+ const bufferInput = await getNearestInputFromText(document, "Buffer size:");
+ setReactFriendlyInputValue(bufferInput, Number(bufferInput.value) * 0.1);
+
+ is(
+ getActiveConfiguration().capacity,
+ 256 * 1024,
+ "The capacity changed to a smaller value."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js
new file mode 100644
index 0000000000..95f9b4d9c9
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-env-restart-button.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that the popup offers to restart the browser to set an enviroment flag."
+ );
+
+ if (!Services.profiler.GetFeatures().includes("jstracer")) {
+ ok(
+ true,
+ "JS tracer is not supported on this platform, or is currently disabled. Skip the rest of the test."
+ );
+ return;
+ }
+
+ {
+ info("Ensure that JS Tracer is not currently enabled.");
+ const {
+ getEnvironmentVariable,
+ } = require("devtools/client/performance-new/browser");
+ ok(
+ !getEnvironmentVariable("JS_TRACE_LOGGING"),
+ "The JS_TRACE_LOGGING is not currently enabled."
+ );
+ }
+
+ ok(
+ false,
+ "This test was migrated from the initial popup implementation to " +
+ "about:profiling, however JS Tracer was disabled at the time. When " +
+ "re-enabling JS Tracer, please audit that this text works as expected, " +
+ "especially in the UI."
+ );
+
+ await withAboutProfiling(async document => {
+ {
+ info(
+ "Test that there is offer to restart the browser when first loading up the popup."
+ );
+ const noRestartButton = maybeGetElementFromDocumentByText(
+ document,
+ "Restart"
+ );
+ ok(!noRestartButton, "There is no button to restart the browser.");
+ }
+
+ const jsTracerFeature = await getElementFromDocumentByText(
+ document,
+ "JSTracer"
+ );
+
+ {
+ info("Toggle the jstracer feature on.");
+ jsTracerFeature.click();
+
+ const restartButton = await getElementFromDocumentByText(
+ document,
+ "Restart"
+ );
+ ok(
+ restartButton,
+ "There is now a button to offer to restart the browser"
+ );
+ }
+
+ {
+ info("Toggle the jstracer feature back off.");
+ jsTracerFeature.click();
+
+ const noRestartButton = maybeGetElementFromDocumentByText(
+ document,
+ "Restart"
+ );
+ ok(!noRestartButton, "The offer to restart the browser goes away.");
+ }
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js
new file mode 100644
index 0000000000..ce91f42167
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features-disabled.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that features that are disabled on the platform are disabled in about:profiling."
+ );
+
+ const supportedFeatures = Services.profiler.GetFeatures();
+ const allFeatures = Services.profiler.GetAllFeatures();
+ const unsupportedFeatures = allFeatures.filter(
+ feature => !supportedFeatures.includes(feature)
+ );
+
+ if (unsupportedFeatures.length === 0) {
+ ok(true, "This platform has no unsupported features. Skip this test.");
+ return;
+ }
+
+ await withAboutProfiling(async document => {
+ {
+ info("Find and click a supported feature to toggle it.");
+ const [firstSupportedFeature] = supportedFeatures;
+ const checkbox = getFeatureCheckbox(document, firstSupportedFeature);
+ const initialValue = checkbox.checked;
+ info("Click the supported checkbox.");
+ checkbox.click();
+ is(
+ initialValue,
+ !checkbox.checked,
+ "A supported feature can be toggled."
+ );
+ checkbox.click();
+ }
+
+ {
+ info("Find and click an unsupported feature, it should be disabled.");
+ const [firstUnsupportedFeature] = unsupportedFeatures;
+ const checkbox = getFeatureCheckbox(document, firstUnsupportedFeature);
+ is(checkbox.checked, false, "The unsupported feature is not checked.");
+
+ info("Click the unsupported checkbox.");
+ checkbox.click();
+ is(checkbox.checked, false, "After clicking it, it's still not checked.");
+ }
+ });
+});
+
+/**
+ * @param {HTMLDocument} document
+ * @param {string} feature
+ * @return {HTMLElement}
+ */
+function getFeatureCheckbox(document, feature) {
+ const element = document.querySelector(`input[value="${feature}"]`);
+ if (!element) {
+ throw new Error("Could not find the checkbox for the feature: " + feature);
+ }
+ return element;
+}
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js
new file mode 100644
index 0000000000..2ee07f5ad0
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-features.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling can be loaded, and the features changed.");
+
+ ok(
+ Services.profiler.GetFeatures().includes("js"),
+ "This test assumes that the JavaScript feature is available on every platform."
+ );
+
+ await withAboutProfiling(async document => {
+ const jsInput = await getNearestInputFromText(document, "JavaScript");
+
+ ok(
+ activeConfigurationHasFeature("js"),
+ "By default, the JS feature is always enabled."
+ );
+ ok(jsInput.checked, "The JavaScript input is checked when enabled.");
+
+ jsInput.click();
+
+ ok(
+ !activeConfigurationHasFeature("js"),
+ "The JS feature can be toggled off."
+ );
+ ok(!jsInput.checked, "The JS feature's input element is also toggled off.");
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js
new file mode 100644
index 0000000000..2b8cea19c8
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-interval.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling can modify the sampling interval.");
+
+ await withAboutProfiling(async document => {
+ is(
+ getActiveConfiguration().interval,
+ 1,
+ "The active configuration's interval is set to a specific number initially."
+ );
+
+ info(
+ "Increase the interval by an arbitrary amount. The input range will " +
+ "scale that to the final value presented to the profiler."
+ );
+ const intervalInput = await getNearestInputFromText(
+ document,
+ "Sampling interval:"
+ );
+ setReactFriendlyInputValue(intervalInput, Number(intervalInput.value) + 8);
+
+ is(
+ getActiveConfiguration().interval,
+ 2,
+ "The configuration's interval was able to be increased."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js
new file mode 100644
index 0000000000..07680d7496
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets-custom.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that about:profiling presets override the individual settings when changed."
+ );
+ const supportedFeatures = Services.profiler.GetFeatures();
+
+ if (!supportedFeatures.includes("stackwalk")) {
+ ok(true, "This platform does not support stackwalking, skip this test.");
+ return;
+ }
+ // This test assumes that the Web Developer preset is set by default, which is
+ // not the case on Nightly and custom builds.
+ BackgroundJSM.changePreset(
+ "aboutprofiling",
+ "web-developer",
+ supportedFeatures
+ );
+
+ await withAboutProfiling(async document => {
+ const webdevPreset = await getNearestInputFromText(
+ document,
+ "Web Developer"
+ );
+ const customPreset = await getNearestInputFromText(document, "Custom");
+ const stackwalkFeature = await getNearestInputFromText(
+ document,
+ "Native Stacks"
+ );
+ const geckoMainThread = await getNearestInputFromText(
+ document,
+ "GeckoMain"
+ );
+
+ {
+ info("Check the defaults on the about:profiling page.");
+ ok(
+ webdevPreset.checked,
+ "By default the Web Developer preset is checked."
+ );
+ ok(!customPreset.checked, "By default the custom preset is not checked.");
+ ok(
+ !stackwalkFeature.checked,
+ "Stack walking is not enabled for Web Developer."
+ );
+ ok(
+ !activeConfigurationHasFeature("stackwalk"),
+ "Stack walking is not in the active configuration."
+ );
+ ok(
+ geckoMainThread.checked,
+ "The GeckoMain thread is tracked for the Web Developer preset"
+ );
+ ok(
+ activeConfigurationHasThread("GeckoMain"),
+ "The GeckoMain thread is in the active configuration."
+ );
+ }
+
+ {
+ info("Change some settings, which will move the preset over to Custom.");
+
+ info("Click stack walking.");
+ stackwalkFeature.click();
+
+ info("Click the GeckoMain thread.");
+ geckoMainThread.click();
+ }
+
+ {
+ info("Check that the various settings were actually updated in the UI.");
+ ok(
+ !webdevPreset.checked,
+ "The Web Developer preset is no longer enabled."
+ );
+ ok(customPreset.checked, "The Custom preset is now checked.");
+ ok(stackwalkFeature.checked, "Stack walking was enabled");
+ ok(
+ activeConfigurationHasFeature("stackwalk"),
+ "Stack walking is in the active configuration."
+ );
+ ok(
+ !geckoMainThread.checked,
+ "GeckoMain was removed from tracked threads."
+ );
+ ok(
+ !activeConfigurationHasThread("GeckoMain"),
+ "The GeckoMain thread is not in the active configuration."
+ );
+ }
+
+ {
+ info(
+ "Click the Web Developer preset, which should revert the other settings."
+ );
+ webdevPreset.click();
+ }
+
+ {
+ info(
+ "Now verify that everything was reverted back to the original settings."
+ );
+ ok(webdevPreset.checked, "The Web Developer preset is checked again.");
+ ok(!customPreset.checked, "The custom preset is not checked.");
+ ok(
+ !stackwalkFeature.checked,
+ "Stack walking is reverted for the Web Developer preset."
+ );
+ ok(
+ !activeConfigurationHasFeature("stackwalk"),
+ "Stack walking is not in the active configuration."
+ );
+ ok(
+ geckoMainThread.checked,
+ "GeckoMain was added back to the tracked threads."
+ );
+ ok(
+ activeConfigurationHasThread("GeckoMain"),
+ "The GeckoMain thread is in the active configuration."
+ );
+ }
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js
new file mode 100644
index 0000000000..03bd0cfa27
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-presets.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling presets configure the profiler");
+
+ if (!Services.profiler.GetFeatures().includes("stackwalk")) {
+ ok(true, "This platform does not support stackwalking, skip this test.");
+ return;
+ }
+ // This test assumes that the Web Developer preset is set by default, which is
+ // not the case on Nightly and custom builds.
+ BackgroundJSM.changePreset(
+ "aboutprofiling",
+ "web-developer",
+ Services.profiler.GetFeatures()
+ );
+
+ await withAboutProfiling(async document => {
+ const webdev = await getNearestInputFromText(document, "Web Developer");
+ ok(webdev.checked, "By default the Web Developer preset is selected.");
+
+ ok(
+ !activeConfigurationHasFeature("stackwalk"),
+ "Stackwalking is not enabled for the Web Developer workflow"
+ );
+
+ const platform = await getNearestInputFromText(
+ document,
+ "Firefox Platform"
+ );
+
+ ok(!platform.checked, "The Firefox Platform preset is not checked.");
+ platform.click();
+ ok(
+ platform.checked,
+ "After clicking the input, the Firefox Platform preset is now checked."
+ );
+
+ ok(
+ activeConfigurationHasFeature("stackwalk"),
+ "The Firefox Platform preset uses stackwalking."
+ );
+
+ const frontEnd = await getNearestInputFromText(
+ document,
+ "Firefox Front-End"
+ );
+
+ ok(!frontEnd.checked, "The Firefox front-end preset is not checked.");
+ frontEnd.click();
+ ok(
+ frontEnd.checked,
+ "After clicking the input, the Firefox front-end preset is now checked."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js
new file mode 100644
index 0000000000..a42d9465fa
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads-behavior.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test the behavior of thread toggling and the text summary works as expected."
+ );
+
+ // This test assumes that the Web Developer preset is set by default, which is
+ // not the case on Nightly and custom builds.
+ BackgroundJSM.changePreset(
+ "aboutprofiling",
+ "web-developer",
+ Services.profiler.GetFeatures()
+ );
+
+ await withAboutProfiling(async document => {
+ const threadTextEl = await getNearestInputFromText(
+ document,
+ "Add custom threads by name:"
+ );
+
+ is(
+ getActiveConfiguration().threads.join(","),
+ "GeckoMain,Compositor,Renderer,DOM Worker",
+ "The threads starts out with the default"
+ );
+ is(
+ threadTextEl.value,
+ "GeckoMain,Compositor,Renderer,DOM Worker",
+ "The threads starts out with the default in the thread text input"
+ );
+
+ await clickThreadCheckbox(document, "Compositor", "Toggle off");
+
+ is(
+ getActiveConfiguration().threads.join(","),
+ "GeckoMain,Renderer,DOM Worker",
+ "The threads have been updated"
+ );
+ is(
+ threadTextEl.value,
+ "GeckoMain,Renderer,DOM Worker",
+ "The threads have been updated in the thread text input"
+ );
+
+ await clickThreadCheckbox(document, "DNS Resolver", "Toggle on");
+
+ is(
+ getActiveConfiguration().threads.join(","),
+ "GeckoMain,Renderer,DOM Worker,DNS Resolver",
+ "Another thread was added"
+ );
+ is(
+ threadTextEl.value,
+ "GeckoMain,Renderer,DOM Worker,DNS Resolver",
+ "Another thread was in the thread text input"
+ );
+
+ const styleThreadCheckbox = await getNearestInputFromText(
+ document,
+ "StyleThread"
+ );
+ ok(!styleThreadCheckbox.checked, "The style thread is not checked.");
+
+ // Set the input box directly
+ setReactFriendlyInputValue(
+ threadTextEl,
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread"
+ );
+ threadTextEl.dispatchEvent(new Event("blur", { bubbles: true }));
+
+ ok(styleThreadCheckbox.checked, "The style thread is now checked.");
+ is(
+ getActiveConfiguration().threads.join(","),
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread",
+ "Another thread was added"
+ );
+ is(
+ threadTextEl.value,
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread",
+ "Another thread was in the thread text input"
+ );
+
+ // The all threads checkbox has nested text elements, so it's not easy to select
+ // by its label value. Select it by ID.
+ const allThreadsCheckbox = document.querySelector(
+ "#perf-settings-thread-checkbox-all-threads"
+ );
+ info(`Turning on "All Threads" by clicking it."`);
+ allThreadsCheckbox.click();
+
+ is(
+ getActiveConfiguration().threads.join(","),
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread,*",
+ "Asterisk was added"
+ );
+ is(
+ threadTextEl.value,
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread,*",
+ "Asterisk was in the thread text input"
+ );
+
+ // Remove the asterisk
+ setReactFriendlyInputValue(
+ threadTextEl,
+ "GeckoMain,DOM Worker,DNS Resolver,StyleThread"
+ );
+ threadTextEl.dispatchEvent(new Event("blur", { bubbles: true }));
+
+ ok(!allThreadsCheckbox.checked, "The all threads checkbox is not checked.");
+ });
+});
+
+/**
+ * @param {Document} document
+ * @param {string} threadName
+ * @param {string} action - This is the intent of the click.
+ */
+async function clickThreadCheckbox(document, threadName, action) {
+ info(`${action} "${threadName}" by clicking it.`);
+ const checkbox = await getNearestInputFromText(document, threadName);
+ checkbox.click();
+}
diff --git a/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js
new file mode 100644
index 0000000000..c1e8db5c4f
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_aboutprofiling-threads.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling can be loaded, and the threads changed.");
+
+ await withAboutProfiling(async document => {
+ const geckoMainLabel = await getElementFromDocumentByText(
+ document,
+ "GeckoMain"
+ );
+ const geckoMainInput = geckoMainLabel.querySelector("input");
+ if (!geckoMainInput) {
+ throw new Error("Unable to find the input from the GeckoMain label.");
+ }
+
+ ok(
+ geckoMainInput.checked,
+ "The GeckoMain thread starts checked by default."
+ );
+
+ ok(
+ activeConfigurationHasThread("GeckoMain"),
+ "The profiler was started with the GeckoMain thread"
+ );
+
+ info("Click the GeckoMain checkbox.");
+ geckoMainInput.click();
+ ok(!geckoMainInput.checked, "The GeckoMain thread UI is toggled off.");
+
+ ok(
+ !activeConfigurationHasThread("GeckoMain"),
+ "The profiler was not started with the GeckoMain thread."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js b/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js
new file mode 100644
index 0000000000..87394c5ac7
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-interrupted.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test() {
+ info("Test what happens when a recording is interrupted by another tool.");
+
+ const { stopProfiler: stopProfilerByAnotherTool } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+
+ await withDevToolsPanel(async document => {
+ const getRecordingState = setupGetRecordingState(document);
+
+ const startRecording = await getActiveButtonFromText(
+ document,
+ "Start recording"
+ );
+ info("Click to start recording");
+ startRecording.click();
+
+ info("Wait until the profiler UI has updated to show that it is ready.");
+ await getActiveButtonFromText(document, "Capture recording");
+
+ info("Stop the profiler by another tool.");
+
+ stopProfilerByAnotherTool();
+
+ info("Check that the user was notified of this interruption.");
+ await getElementFromDocumentByText(
+ document,
+ "The recording was stopped by another tool."
+ );
+
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "The client is ready to record again, even though it was interrupted."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js b/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js
new file mode 100644
index 0000000000..ac253b9562
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-onboarding.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ONBOARDING_PREF = "devtools.performance.new-panel-onboarding";
+
+add_task(async function testWithOnboardingPreferenceFalse() {
+ info("Test that the onboarding message is displayed as expected.");
+
+ info("Test the onboarding message when the preference is false");
+ await SpecialPowers.pushPrefEnv({
+ set: [[ONBOARDING_PREF, false]],
+ });
+ await withDevToolsPanel(async document => {
+ {
+ // Wait for another UI element to be rendered before asserting the
+ // onboarding message.
+ await getActiveButtonFromText(document, "Start recording");
+ ok(
+ !isOnboardingDisplayed(document),
+ "Onboarding message is not displayed"
+ );
+ }
+ });
+});
+
+add_task(async function testWithOnboardingPreferenceTrue() {
+ info("Test the onboarding message when the preference is true");
+ await SpecialPowers.pushPrefEnv({
+ set: [[ONBOARDING_PREF, true]],
+ });
+
+ await withDevToolsPanel(async document => {
+ await waitUntil(
+ () => isOnboardingDisplayed(document),
+ "Waiting for the onboarding message to be displayed"
+ );
+ ok(true, "Onboarding message is displayed");
+ await closeOnboardingMessage(document);
+ });
+
+ is(
+ Services.prefs.getBoolPref(ONBOARDING_PREF),
+ false,
+ "onboarding preference should be false after closing the message"
+ );
+});
+
+add_task(async function testWithOnboardingPreferenceNotSet() {
+ info("Test the onboarding message when the preference is not set");
+ await SpecialPowers.pushPrefEnv({
+ clear: [[ONBOARDING_PREF]],
+ });
+
+ await withDevToolsPanel(async document => {
+ await waitUntil(
+ () => isOnboardingDisplayed(document),
+ "Waiting for the onboarding message to be displayed"
+ );
+ ok(true, "Onboarding message is displayed");
+ await closeOnboardingMessage(document);
+ });
+
+ is(
+ Services.prefs.getBoolPref(ONBOARDING_PREF),
+ false,
+ "onboarding preference should be false after closing the message"
+ );
+});
+
+/**
+ * Helper to close the onboarding message by clicking on the close button.
+ */
+async function closeOnboardingMessage(document) {
+ const closeButton = await getActiveButtonFromText(
+ document,
+ "Close the onboarding message"
+ );
+ info("Click the close button to hide the onboarding message.");
+ closeButton.click();
+
+ await waitUntil(
+ () => !isOnboardingDisplayed(document),
+ "Waiting for the onboarding message to disappear"
+ );
+}
+
+function isOnboardingDisplayed(document) {
+ return maybeGetElementFromDocumentByText(
+ document,
+ "Firefox Profiler is now integrated into Developer Tools"
+ );
+}
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-presets.js b/devtools/client/performance-new/test/browser/browser_devtools-presets.js
new file mode 100644
index 0000000000..383ca57088
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-presets.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+add_task(async function test() {
+ info("Test that about:profiling presets configure the profiler");
+
+ if (!Services.profiler.GetFeatures().includes("stackwalk")) {
+ ok(true, "This platform does not support stackwalking, skip this test.");
+ return;
+ }
+
+ // This test assumes that the Web Developer preset is set by default, which is
+ // not the case on Nightly and custom builds.
+ BackgroundJSM.changePreset(
+ "aboutprofiling",
+ "web-developer",
+ Services.profiler.GetFeatures()
+ );
+
+ await withDevToolsPanel(async document => {
+ {
+ const presets = await getNearestInputFromText(document, "Settings");
+
+ is(presets.value, "web-developer", "The presets default to webdev mode.");
+ ok(
+ !(await devToolsActiveConfigurationHasFeature(document, "stackwalk")),
+ "Stack walking is not used in Web Developer mode."
+ );
+ }
+
+ {
+ const presets = await getNearestInputFromText(document, "Settings");
+ setReactFriendlyInputValue(presets, "firefox-platform");
+ is(
+ presets.value,
+ "firefox-platform",
+ "The preset was changed to Firefox Platform"
+ );
+ ok(
+ await devToolsActiveConfigurationHasFeature(document, "stackwalk"),
+ "Stack walking is used in Firefox Platform mode."
+ );
+ }
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js b/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js
new file mode 100644
index 0000000000..4428178541
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-previously-started.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test what happens if the profiler was previously started by another tool."
+ );
+
+ const { startProfiler } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+
+ info("Start the profiler before DevTools is loaded.");
+ startProfiler("aboutprofiling");
+
+ await withDevToolsPanel(async document => {
+ const getRecordingState = setupGetRecordingState(document);
+
+ // The initial state of the profiler UI is racy, as it calls out to the PerfFront
+ // to get the status of the profiler. This can race with the initialization of
+ // the test. Most of the the time the result is "not-yet-known", but rarely
+ // the PerfFront will win this race. Allow for both outcomes of the race in this
+ // test.
+ ok(
+ getRecordingState() === "not-yet-known" ||
+ getRecordingState() === "recording",
+ "The component starts out in an unknown state or in a recording state."
+ );
+
+ const cancelRecording = await getActiveButtonFromText(
+ document,
+ "Cancel recording"
+ );
+
+ is(
+ getRecordingState(),
+ "recording",
+ "The profiler is reflecting the recording state."
+ );
+
+ info("Click the button to cancel the recording");
+ cancelRecording.click();
+
+ is(
+ getRecordingState(),
+ "request-to-stop-profiler",
+ "We can request to stop the profiler."
+ );
+
+ await getActiveButtonFromText(document, "Start recording");
+
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "The profiler is now available to record."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-private-window.js b/devtools/client/performance-new/test/browser/browser_devtools-private-window.js
new file mode 100644
index 0000000000..01864225b0
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-private-window.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test opening a private browsing window while the profiler is active.");
+
+ await withDevToolsPanel(async document => {
+ const getRecordingState = setupGetRecordingState(document);
+
+ const startRecording = await getActiveButtonFromText(
+ document,
+ "Start recording"
+ );
+
+ ok(!startRecording.disabled, "The start recording button is not disabled.");
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "The panel is available to record."
+ );
+
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await getElementFromDocumentByText(
+ document,
+ "The profiler is disabled when Private Browsing is enabled"
+ );
+
+ ok(
+ startRecording.disabled,
+ "The start recording button is disabled when a private browsing window is open."
+ );
+
+ is(
+ getRecordingState(),
+ "locked-by-private-browsing",
+ "The client knows about the private window."
+ );
+
+ info("Closing the private window");
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ info("Finally wait for the start recording button to become active again.");
+ await getActiveButtonFromText(document, "Start recording");
+
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "The panel is available to record again."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js
new file mode 100644
index 0000000000..51ec553e92
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-record-capture.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that DevTools can capture profiles. This function also unit tests the " +
+ "internal RecordingState of the client."
+ );
+
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+
+ await withDevToolsPanel(async document => {
+ const getRecordingState = setupGetRecordingState(document);
+
+ // The initial state of the profiler UI is racy, as it calls out to the PerfFront
+ // to get the status of the profiler. This can race with the initialization of
+ // the test. Most of the the time the result is "not-yet-known", but rarely
+ // the PerfFront will win this race. Allow for both outcomes of the race in this
+ // test.
+ ok(
+ getRecordingState() === "not-yet-known" ||
+ getRecordingState() === "available-to-record",
+ "The component starts out in an unknown state or is already available to record."
+ );
+
+ const startRecording = await getActiveButtonFromText(
+ document,
+ "Start recording"
+ );
+
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "After talking to the actor, we're ready to record."
+ );
+
+ info("Click the button to start recording");
+ startRecording.click();
+
+ is(
+ getRecordingState(),
+ "request-to-start-recording",
+ "Clicking the start recording button sends in a request to start recording."
+ );
+
+ const captureRecording = await getActiveButtonFromText(
+ document,
+ "Capture recording"
+ );
+
+ is(
+ getRecordingState(),
+ "recording",
+ "Once the Capture recording button is available, the actor has started " +
+ "its recording"
+ );
+
+ info("Click the button to capture the recording.");
+ captureRecording.click();
+
+ is(
+ getRecordingState(),
+ "request-to-get-profile-and-stop-profiler",
+ "We have requested to stop the profiler."
+ );
+
+ await getActiveButtonFromText(document, "Start recording");
+ is(
+ getRecordingState(),
+ "available-to-record",
+ "The profiler is available to record again."
+ );
+
+ info(
+ "If the DevTools successfully injects a profile into the page, then the " +
+ "fake frontend will rename the title of the page."
+ );
+
+ await checkTabLoadedProfile({
+ initialTitle: "Waiting on the profile",
+ successTitle: "Profile received",
+ errorTitle: "Error",
+ });
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js b/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js
new file mode 100644
index 0000000000..e3d3433fd1
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_devtools-record-discard.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test that DevTools can capture profiles.");
+
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+
+ await withDevToolsPanel(async document => {
+ {
+ const button = await getActiveButtonFromText(document, "Start recording");
+ info("Click the button to start recording");
+ button.click();
+ }
+
+ {
+ const button = await getActiveButtonFromText(
+ document,
+ "Cancel recording"
+ );
+ info("Click the button to discard to profile.");
+ button.click();
+ }
+
+ {
+ const button = await getActiveButtonFromText(document, "Start recording");
+ ok(Boolean(button), "The start recording button is available again.");
+ }
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js b/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js
new file mode 100644
index 0000000000..ad9b84cbcf
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_popup-private-browsing.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that the profiler popup gets disabled when a private browsing window is open."
+ );
+ await makeSureProfilerPopupIsEnabled();
+
+ const getRecordingButton = () =>
+ getElementByLabel(document, "Start Recording");
+
+ const getDisabledMessage = () =>
+ getElementFromDocumentByText(
+ document,
+ "The profiler is currently disabled"
+ );
+
+ await withPopupOpen(window, async () => {
+ ok(await getRecordingButton(), "The start recording button is available");
+ });
+
+ info("Open a private browsing window.");
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Switch back to the main window and open the popup again.");
+ window.focus();
+ await withPopupOpen(window, async () => {
+ ok(await getDisabledMessage(), "The disabled message is displayed.");
+ });
+
+ info("Close the private window");
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ info("Make sure the first window is focused, and open the popup back up.");
+ window.focus();
+ await withPopupOpen(window, async () => {
+ ok(
+ await getRecordingButton(),
+ "The start recording button is available once again."
+ );
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js b/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js
new file mode 100644
index 0000000000..799d1aefa0
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_popup-profiler-states.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test the states of the profiler button, e.g. inactive, active, and capturing."
+ );
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+ await makeSureProfilerPopupIsEnabled();
+
+ const { toggleProfiler, captureProfile } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+
+ const button = document.getElementById("profiler-button-button");
+ if (!button) {
+ throw new Error("Could not find the profiler button.");
+ }
+
+ info("The profiler button starts out inactive");
+ checkButtonState(button, {
+ tooltip: "Record a performance profile",
+ active: false,
+ paused: false,
+ });
+
+ info("Toggling the profiler turns on the active state");
+ toggleProfiler("aboutprofiling");
+ checkButtonState(button, {
+ tooltip: "The profiler is recording a profile",
+ active: true,
+ paused: false,
+ });
+
+ info("Capturing a profile makes the button paused");
+ captureProfile("aboutprofiling");
+ checkButtonState(button, {
+ tooltip: "The profiler is capturing a profile",
+ active: false,
+ paused: true,
+ });
+
+ waitUntil(
+ () => !button.classList.contains("profiler-paused"),
+ "Waiting until the profiler is no longer paused"
+ );
+
+ await checkTabLoadedProfile({
+ initialTitle: "Waiting on the profile",
+ successTitle: "Profile received",
+ errorTitle: "Error",
+ });
+});
+
+/**
+ * This check dives into the implementation details of the button, mainly
+ * because it's hard to provide a user-focused interpretation of button
+ * stylings.
+ */
+function checkButtonState(button, { tooltip, active, paused }) {
+ is(
+ button.getAttribute("tooltiptext"),
+ tooltip,
+ `The tooltip for the button is "${tooltip}".`
+ );
+ is(
+ button.classList.contains("profiler-active"),
+ active,
+ `The expected profiler button active state is: ${active}`
+ );
+ is(
+ button.classList.contains("profiler-paused"),
+ paused,
+ `The expected profiler button paused state is: ${paused}`
+ );
+}
diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js b/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js
new file mode 100644
index 0000000000..aabed66563
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture-view.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const FRONTEND_BASE_URL =
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html";
+
+add_task(async function test() {
+ info(
+ "Test that the profiler pop-up correctly opens the captured profile on the " +
+ "correct frontend view by adding proper view query string"
+ );
+ await setProfilerFrontendUrl(FRONTEND_BASE_URL);
+ await makeSureProfilerPopupIsEnabled();
+
+ // First check for "firefox-platform" preset which will have no "view" query
+ // string because this is where our traditional "full" view opens up.
+ await openPopupAndAssertUrlForPreset({
+ preset: "firefox-platform",
+ expectedUrl: FRONTEND_BASE_URL,
+ });
+
+ // Now, let's check for "web-developer" preset. This will open up the frontend
+ // with "active-tab" view query string. Frontend will understand and open the active tab view for it.
+ await openPopupAndAssertUrlForPreset({
+ preset: "web-developer",
+ expectedUrl: FRONTEND_BASE_URL + "?view=active-tab",
+ });
+});
+
+async function openPopupAndAssertUrlForPreset({ preset, expectedUrl }) {
+ // First, switch to the preset we want to test.
+ BackgroundJSM.changePreset(
+ "aboutprofiling",
+ preset,
+ [] // We don't need any features for this test.
+ );
+
+ // Let's capture a profile and assert newly created tab's url.
+ await openPopupAndEnsureCloses(window, async () => {
+ {
+ const button = await getElementByLabel(document, "Start Recording");
+ info("Click the button to start recording.");
+ button.click();
+ }
+
+ {
+ const button = await getElementByLabel(document, "Capture");
+ info("Click the button to capture the recording.");
+ button.click();
+ }
+
+ info(
+ "If the profiler successfully captures a profile, it will create a new " +
+ "tab with the proper view query string depending on the preset."
+ );
+
+ await waitForTabUrl({
+ initialTitle: "Waiting on the profile",
+ successTitle: "Profile received",
+ errorTitle: "Error",
+ expectedUrl,
+ });
+ });
+}
diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-capture.js b/devtools/client/performance-new/test/browser/browser_popup-record-capture.js
new file mode 100644
index 0000000000..8289014770
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_popup-record-capture.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info(
+ "Test that the profiler pop-up works end to end with profile recording and " +
+ "capture using the mouse and hitting buttons."
+ );
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+ await makeSureProfilerPopupIsEnabled();
+ await openPopupAndEnsureCloses(window, async () => {
+ {
+ const button = await getElementByLabel(document, "Start Recording");
+ info("Click the button to start recording.");
+ button.click();
+ }
+
+ {
+ const button = await getElementByLabel(document, "Capture");
+ info("Click the button to capture the recording.");
+ button.click();
+ }
+
+ info(
+ "If the profiler successfully injects a profile into the page, then the " +
+ "fake frontend will rename the title of the page."
+ );
+
+ await checkTabLoadedProfile({
+ initialTitle: "Waiting on the profile",
+ successTitle: "Profile received",
+ errorTitle: "Error",
+ });
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_popup-record-discard.js b/devtools/client/performance-new/test/browser/browser_popup-record-discard.js
new file mode 100644
index 0000000000..d8f4ec9f47
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_popup-record-discard.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test that the profiler popup recording can be discarded.");
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+ await makeSureProfilerPopupIsEnabled();
+ await withPopupOpen(window, async () => {
+ {
+ const button = await getElementByLabel(document, "Start Recording");
+ info("Click the button to start recording.");
+ button.click();
+ }
+
+ {
+ const button = await getElementByLabel(document, "Discard");
+ info("Click the button to discard the recording.");
+ button.click();
+ }
+
+ {
+ const button = await getElementByLabel(document, "Start Recording");
+ ok(
+ Boolean(button),
+ "The popup reverted back to be able to start recording again"
+ );
+ }
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js b/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js
new file mode 100644
index 0000000000..a4efd5c74e
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_split-toolbar-button.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function isActive() {
+ return Services.profiler.IsActive();
+}
+
+/**
+ * Force focus to an element that isn't focusable.
+ * Toolbar buttons aren't focusable because if they were, clicking them would
+ * focus them, which is undesirable. Therefore, they're only made focusable
+ * when a user is navigating with the keyboard. This function forces focus as
+ * is done during toolbar keyboard navigation.
+ */
+function forceFocus(elem) {
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ elem.removeAttribute("tabindex");
+}
+
+async function waitForProfileAndCloseTab() {
+ await waitUntil(
+ () => !button.classList.contains("profiler-paused"),
+ "Waiting until the profiler is no longer paused"
+ );
+
+ await checkTabLoadedProfile({
+ initialTitle: "Waiting on the profile",
+ successTitle: "Profile received",
+ errorTitle: "Error",
+ });
+}
+var button;
+var dropmarker;
+
+add_task(async function setup() {
+ info(
+ "Add the profiler button to the toolbar and ensure capturing a profile loads a local url."
+ );
+ await setProfilerFrontendUrl(
+ "http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
+ );
+ await makeSureProfilerPopupIsEnabled();
+ button = document.getElementById("profiler-button-button");
+ dropmarker = document.getElementById("profiler-button-dropmarker");
+});
+
+add_task(async function click_icon() {
+ info("Test that the profiler icon starts and captures a profile.");
+
+ ok(!dropmarker.hasAttribute("open"), "should start with the panel closed");
+ ok(!isActive(), "should start with the profiler inactive");
+
+ button.click();
+ ok(isActive(), "should have started the profiler");
+
+ button.click();
+ await waitForProfileAndCloseTab();
+});
+
+add_task(async function click_dropmarker() {
+ info("Test that the profiler icon dropmarker opens the panel.");
+
+ ok(!dropmarker.hasAttribute("open"), "should start with the panel closed");
+ ok(!isActive(), "should start with the profiler inactive");
+
+ const popupShownPromise = waitForProfilerPopupEvent("popupshown");
+ dropmarker.click();
+ await popupShownPromise;
+
+ info("Ensure the panel is open and the profiler still inactive.");
+ ok(dropmarker.getAttribute("open") == "true", "panel should be open");
+ ok(!isActive(), "profiler should still be inactive");
+ await getElementByLabel(document, "Start Recording");
+
+ info("Press Escape to close the panel.");
+ const popupHiddenPromise = waitForProfilerPopupEvent("popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await popupHiddenPromise;
+ ok(!dropmarker.hasAttribute("open"), "panel should be closed");
+});
+
+add_task(async function space_key() {
+ info("Test that the Space key starts and captures a profile.");
+
+ ok(!dropmarker.hasAttribute("open"), "should start with the panel closed");
+ ok(!isActive(), "should start with the profiler inactive");
+ forceFocus(button);
+
+ info("Press Space to start the profiler.");
+ EventUtils.synthesizeKey(" ");
+ ok(isActive(), "should have started the profiler");
+
+ info("Press Space again to capture the profile.");
+ EventUtils.synthesizeKey(" ");
+ await waitForProfileAndCloseTab();
+});
+
+add_task(async function enter_key() {
+ info("Test that the Enter key starts and captures a profile.");
+
+ ok(!dropmarker.hasAttribute("open"), "should start with the panel closed");
+ ok(!isActive(), "should start with the profiler inactive");
+ forceFocus(button);
+
+ const isMacOS = Services.appinfo.OS === "Darwin";
+ if (isMacOS) {
+ // On macOS, pressing Enter on a focused toolbarbutton does not fire a
+ // command event, so we do not expect Enter to start the profiler.
+ return;
+ }
+
+ info("Pressing Enter should start the profiler.");
+ EventUtils.synthesizeKey("KEY_Enter");
+ ok(isActive(), "should have started the profiler");
+
+ info("Pressing Enter again to capture the profile.");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await waitForProfileAndCloseTab();
+});
diff --git a/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js
new file mode 100644
index 0000000000..a1864c475d
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/browser_webchannel-enable-menu-button.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test() {
+ info("Test the WebChannel mechanism works for turning on the menu button.");
+ await makeSureProfilerPopupIsDisabled();
+
+ await withWebChannelTestDocument(async browser => {
+ await waitForTabTitle("WebChannel Page Ready");
+ await waitForProfilerMenuButton();
+ ok(true, "The profiler menu button was enabled by the WebChannel.");
+ });
+});
diff --git a/devtools/client/performance-new/test/browser/fake-frontend.html b/devtools/client/performance-new/test/browser/fake-frontend.html
new file mode 100644
index 0000000000..9817b87e5d
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/fake-frontend.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title></title>
+ </head>
+ <body>
+ <script>
+ "use strict";
+ // This file is used to test the injection of performance profiles into a front-end,
+ // specifically the mechanism used to inject into profiler.firefox.com. Rather
+ // than using some kind of complicated message passing scheme to talk to the test
+ // harness, modify the title of the page. The tests can easily read the window
+ // title to see if things worked as expected.
+
+ // The following are the titles used to communicate the page's state to the tests.
+ // Keep these in sync with any tests that read them.
+ const initialTitle = "Waiting on the profile";
+ const successTitle = "Profile received";
+ const errorTitle = "Error"
+
+ document.title = initialTitle;
+
+ // The following gets called by the frame script, and provides an API to
+ // receive the profile.
+ window.connectToGeckoProfiler = async (geckoProfiler) => {
+ try {
+ // Get the profile.
+ const profile = await geckoProfiler.getProfile();
+
+ // Check that the profile is somewhat reasonable. It should be a gzipped
+ // profile, so we can only lightly check some properties about it, and check
+ // that it is an ArrayBuffer.
+ //
+ // After the check, modify the title of the document, so the tab title gets
+ // updated. This is an easy way to pass a message to the test script.
+ if (
+ profile &&
+ typeof profile === 'object' &&
+ (
+ // The popup injects the compressed profile as an ArrayBuffer.
+ (profile instanceof ArrayBuffer) ||
+ // DevTools injects the profile as just the plain object, although
+ // maybe in the future it could also do it as a compressed profile
+ // to make this faster.
+ Object.keys(profile).includes("threads")
+ )
+ ) {
+ // The profile looks good!
+ document.title = successTitle;
+ } else {
+ // The profile doesn't look right, surface the error to the terminal.
+ dump('The gecko profile was malformed in fake-frontend.html\n');
+ dump(`Profile: ${JSON.stringify(profile)}\n`);
+
+ // Also to the web console.
+ console.error(profile);
+
+ // Report the error to the tab title.
+ document.title = errorTitle;
+ }
+ } catch (error) {
+ // Catch any error and notify the test.
+ document.title = errorTitle;
+ dump('An error was caught in fake-frontend.html\n');
+ dump(`${error}\n`);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/performance-new/test/browser/head.js b/devtools/client/performance-new/test/browser/head.js
new file mode 100644
index 0000000000..ab0715367f
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/head.js
@@ -0,0 +1,728 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const BackgroundJSM = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+);
+
+registerCleanupFunction(() => {
+ BackgroundJSM.revertRecordingPreferences();
+});
+
+/**
+ * Allow tests to use "require".
+ */
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+{
+ const {
+ getEnvironmentVariable,
+ } = require("devtools/client/performance-new/browser");
+
+ if (getEnvironmentVariable("MOZ_PROFILER_SHUTDOWN")) {
+ throw new Error(
+ "These tests cannot be run with shutdown profiling as they rely on manipulating " +
+ "the state of the profiler."
+ );
+ }
+
+ if (getEnvironmentVariable("MOZ_PROFILER_STARTUP")) {
+ throw new Error(
+ "These tests cannot be run with startup profiling as they rely on manipulating " +
+ "the state of the profiler."
+ );
+ }
+}
+
+/**
+ * Wait for a single requestAnimationFrame tick.
+ */
+function tick() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+/**
+ * It can be confusing when waiting for something asynchronously. This function
+ * logs out a message periodically (every 1 second) in order to create helpful
+ * log messages.
+ * @param {string} message
+ * @returns {Function}
+ */
+function createPeriodicLogger() {
+ let startTime = Date.now();
+ let lastCount = 0;
+ let lastMessage = null;
+
+ return message => {
+ if (lastMessage === message) {
+ // The messages are the same, check if we should log them.
+ const now = Date.now();
+ const count = Math.floor((now - startTime) / 1000);
+ if (count !== lastCount) {
+ info(
+ `${message} (After ${count} ${count === 1 ? "second" : "seconds"})`
+ );
+ lastCount = count;
+ }
+ } else {
+ // The messages are different, log them now, and reset the waiting time.
+ info(message);
+ startTime = Date.now();
+ lastCount = 0;
+ lastMessage = message;
+ }
+ };
+}
+
+/**
+ * Wait until a condition is fullfilled.
+ * @param {Function} condition
+ * @param {string?} logMessage
+ * @return The truthy result of the condition.
+ */
+async function waitUntil(condition, message) {
+ const logPeriodically = createPeriodicLogger();
+
+ // Loop through the condition.
+ while (true) {
+ if (message) {
+ logPeriodically(message);
+ }
+ const result = condition();
+ if (result) {
+ return result;
+ }
+
+ await tick();
+ }
+}
+
+/**
+ * This function looks inside of a document for some element that has a label.
+ * It runs in a loop every requestAnimationFrame until it finds the element. If
+ * it doesn't find the element it throws an error.
+ *
+ * @param {string} label
+ * @returns {Promise<HTMLElement>}
+ */
+function getElementByLabel(document, label) {
+ return waitUntil(
+ () => document.querySelector(`[label="${label}"]`),
+ `Trying to find the button with the label "${label}".`
+ );
+}
+
+/**
+ * This function will select a node from the XPath.
+ * @returns {HTMLElement?}
+ */
+function getElementByXPath(document, path) {
+ return document.evaluate(
+ path,
+ document,
+ null,
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
+ null
+ ).singleNodeValue;
+}
+
+/**
+ * This function looks inside of a document for some element that contains
+ * the given text. It runs in a loop every requestAnimationFrame until it
+ * finds the element. If it doesn't find the element it throws an error.
+ *
+ * @param {HTMLDocument} document
+ * @param {string} text
+ * @returns {Promise<HTMLElement>}
+ */
+async function getElementFromDocumentByText(document, text) {
+ // Fallback on aria-label if there are no results for the text xpath.
+ const xpath = `//*[contains(text(), '${text}')] | //*[contains(@aria-label, '${text}')]`;
+ return waitUntil(
+ () => getElementByXPath(document, xpath),
+ `Trying to find the element with the text "${text}".`
+ );
+}
+
+/**
+ * This function is similar to getElementFromDocumentByText, but it immediately
+ * returns and does not wait for an element to exist.
+ * @param {HTMLDocument} document
+ * @param {string} text
+ * @returns {HTMLElement?}
+ */
+function maybeGetElementFromDocumentByText(document, text) {
+ info(`Immediately trying to find the element with the text "${text}".`);
+ const xpath = `//*[contains(text(), '${text}')]`;
+ return getElementByXPath(document, xpath);
+}
+
+/**
+ * Make sure the profiler popup is enabled.
+ */
+async function makeSureProfilerPopupIsEnabled() {
+ info("Make sure the profiler popup is enabled.");
+
+ info("> Load the profiler menu button.");
+ const { ProfilerMenuButton } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
+ );
+
+ if (!ProfilerMenuButton.isInNavbar()) {
+ // Make sure the feature flag is enabled.
+ SpecialPowers.pushPrefEnv({
+ set: [["devtools.performance.popup.feature-flag", true]],
+ });
+
+ info("> The menu button is not in the nav bar, add it.");
+ ProfilerMenuButton.addToNavbar(document);
+
+ await waitUntil(
+ () => gBrowser.ownerDocument.getElementById("profiler-button"),
+ "> Waiting until the profiler button is added to the browser."
+ );
+
+ await SimpleTest.promiseFocus(gBrowser.ownerGlobal);
+
+ registerCleanupFunction(() => {
+ info(
+ "Clean up after the test by disabling the profiler popup menu button."
+ );
+ if (!ProfilerMenuButton.isInNavbar()) {
+ throw new Error(
+ "Expected the profiler popup to still be in the navbar during the test cleanup."
+ );
+ }
+ ProfilerMenuButton.remove();
+ });
+ } else {
+ info("> The menu button was already enabled.");
+ }
+}
+
+/**
+ * XUL popups will fire the popupshown and popuphidden events. These will fire for
+ * any type of popup in the browser. This function waits for one of those events, and
+ * checks that the viewId of the popup is PanelUI-profiler
+ *
+ * @param {"popupshown" | "popuphidden"} eventName
+ * @returns {Promise<void>}
+ */
+function waitForProfilerPopupEvent(eventName) {
+ return new Promise(resolve => {
+ function handleEvent(event) {
+ if (event.target.getAttribute("viewId") === "PanelUI-profiler") {
+ window.removeEventListener(eventName, handleEvent);
+ resolve();
+ }
+ }
+ window.addEventListener(eventName, handleEvent);
+ });
+}
+
+/**
+ * Do not use this directly in a test. Prefer withPopupOpen and openPopupAndEnsureCloses.
+ *
+ * This function toggles the profiler menu button, and then uses user gestures
+ * to click it open. It waits a tick to make sure it has a chance to initialize.
+ * @return {Promise<void>}
+ */
+async function _toggleOpenProfilerPopup(window) {
+ info("Toggle open the profiler popup.");
+
+ info("> Find the profiler menu button.");
+ const profilerDropmarker = document.getElementById(
+ "profiler-button-dropmarker"
+ );
+ if (!profilerDropmarker) {
+ throw new Error(
+ "Could not find the profiler button dropmarker in the toolbar."
+ );
+ }
+
+ const popupShown = waitForProfilerPopupEvent("popupshown");
+
+ info("> Trigger a click on the profiler button dropmarker.");
+ await EventUtils.synthesizeMouseAtCenter(profilerDropmarker, {});
+
+ if (profilerDropmarker.getAttribute("open") !== "true") {
+ throw new Error(
+ "This test assumes that the button will have an open=true attribute after clicking it."
+ );
+ }
+
+ info("> Wait for the popup to be shown.");
+ await popupShown;
+ // Also wait a tick in case someone else is subscribing to the "popupshown" event
+ // and is doing synchronous work with it.
+ await tick();
+}
+
+/**
+ * Do not use this directly in a test. Prefer withPopupOpen.
+ *
+ * This function uses a keyboard shortcut to close the profiler popup.
+ * @return {Promise<void>}
+ */
+async function _closePopup(window) {
+ const popupHiddenPromise = waitForProfilerPopupEvent("popuphidden");
+ info("> Trigger an escape key to hide the popup");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("> Wait for the popup to be hidden.");
+ await popupHiddenPromise;
+ // Also wait a tick in case someone else is subscribing to the "popuphidden" event
+ // and is doing synchronous work with it.
+ await tick();
+}
+
+/**
+ * Perform some action on the popup, and close it afterwards.
+ * @param {Window} window
+ * @param {() => Promise<void>} callback
+ */
+async function withPopupOpen(window, callback) {
+ await _toggleOpenProfilerPopup(window);
+ await callback();
+ await _closePopup(window);
+}
+
+/**
+ * This function opens the profiler popup, but also ensures that something else closes
+ * it before the end of the test. This is useful for tests that trigger the profiler
+ * popup to close through an implicit action, like opening a tab.
+ *
+ * @param {Window} window
+ * @param {() => Promise<void>} callback
+ */
+async function openPopupAndEnsureCloses(window, callback) {
+ await _toggleOpenProfilerPopup(window);
+ // We want to ensure the popup gets closed by the test, during the callback.
+ const popupHiddenPromise = waitForProfilerPopupEvent("popuphidden");
+ await callback();
+ info("> Verifying that the popup was closed by the test.");
+ await popupHiddenPromise;
+}
+
+/**
+ * This function overwrites the default profiler.firefox.com URL for tests. This
+ * ensures that the tests do not attempt to access external URLs.
+ * @param {string} url
+ * @returns {Promise}
+ */
+function setProfilerFrontendUrl(url) {
+ info(
+ "Setting the profiler URL to the fake frontend. Note that this doesn't currently " +
+ "support the WebChannels, so expect a few error messages about the WebChannel " +
+ "URLs not being correct."
+ );
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure observer and testing function run in the same process
+ ["devtools.performance.recording.ui-base-url", url],
+ ["devtools.performance.recording.ui-base-url-path", ""],
+ ],
+ });
+}
+
+/**
+ * This function checks the document title of a tab to see what the state is.
+ * This creates a simple messaging mechanism between the content page and the
+ * test harness. This function runs in a loop every requestAnimationFrame, and
+ * checks for a sucess title. In addition, an "initialTitle" and "errorTitle"
+ * can be specified for nicer test output.
+ * @param {object}
+ * {
+ * initialTitle: string,
+ * successTitle: string,
+ * errorTitle: string
+ * }
+ */
+async function checkTabLoadedProfile({
+ initialTitle,
+ successTitle,
+ errorTitle,
+}) {
+ const logPeriodically = createPeriodicLogger();
+
+ info("Attempting to see if the selected tab can receive a profile.");
+
+ return waitUntil(() => {
+ switch (gBrowser.selectedTab.label) {
+ case initialTitle:
+ logPeriodically(`> Waiting for the profile to be received.`);
+ return false;
+ case successTitle:
+ ok(true, "The profile was successfully injected to the page");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ return true;
+ case errorTitle:
+ throw new Error(
+ "The fake frontend indicated that there was an error injecting the profile."
+ );
+ default:
+ logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
+ return false;
+ }
+ });
+}
+
+/**
+ * This function checks the url of a tab so we can assert the frontend's url
+ * with our expected url. This function runs in a loop every
+ * requestAnimationFrame, and checks for a initialTitle. Asserts as soon as it
+ * finds that title. We don't have to look for success title or error title
+ * since we only care about the url.
+ * @param {{
+ * initialTitle: string,
+ * successTitle: string,
+ * errorTitle: string,
+ * expectedUrl: string
+ * }}
+ */
+async function waitForTabUrl({
+ initialTitle,
+ successTitle,
+ errorTitle,
+ expectedUrl,
+}) {
+ const logPeriodically = createPeriodicLogger();
+
+ info(`Waiting for the selected tab to have the url "${expectedUrl}".`);
+
+ return waitUntil(() => {
+ switch (gBrowser.selectedTab.label) {
+ case initialTitle:
+ case successTitle:
+ if (gBrowser.currentURI.spec === expectedUrl) {
+ ok(true, `The selected tab has the url ${expectedUrl}`);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ return true;
+ }
+ throw new Error(
+ `Found a different url on the fake frontend: ${gBrowser.currentURI.spec}`
+ );
+ case errorTitle:
+ throw new Error(
+ "The fake frontend indicated that there was an error injecting the profile."
+ );
+ default:
+ logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
+ return false;
+ }
+ });
+}
+
+/**
+ * This function checks the document title of a tab as an easy way to pass
+ * messages from a content page to the mochitest.
+ * @param {string} title
+ */
+async function waitForTabTitle(title) {
+ const logPeriodically = createPeriodicLogger();
+
+ info(`Waiting for the selected tab to have the title "${title}".`);
+
+ return waitUntil(() => {
+ if (gBrowser.selectedTab.label === title) {
+ ok(true, `The selected tab has the title ${title}`);
+ return true;
+ }
+ logPeriodically(`> Waiting for the tab title to change.`);
+ return false;
+ });
+}
+
+/**
+ * Open about:profiling in a new tab, and output helpful log messages.
+ *
+ * @template T
+ * @param {(Document) => T} callback
+ * @returns {Promise<T>}
+ */
+function withAboutProfiling(callback) {
+ info("Begin to open about:profiling in a new tab.");
+ return BrowserTestUtils.withNewTab(
+ "about:profiling",
+ async contentBrowser => {
+ info("about:profiling is now open in a tab.");
+ return callback(contentBrowser.contentDocument);
+ }
+ );
+}
+
+/**
+ * Open DevTools and view the performance-new tab. After running the callback, clean
+ * up the test.
+ *
+ * @template T
+ * @param {(Document) => T} callback
+ * @returns {Promise<T>}
+ */
+async function withDevToolsPanel(callback) {
+ SpecialPowers.pushPrefEnv({
+ set: [["devtools.performance.new-panel-enabled", "true"]],
+ });
+
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { TargetFactory } = require("devtools/client/framework/target");
+
+ info("Create a new about:blank tab.");
+ const tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+
+ info("Begin to open the DevTools and the performance-new panel.");
+ const target = await TargetFactory.forTab(tab);
+ const toolbox = await gDevTools.showToolbox(target, "performance");
+
+ const { document } = toolbox.getCurrentPanel().panelWin;
+
+ info("The performance-new panel is now open and ready to use.");
+ await callback(document);
+
+ info("About to remove the about:blank tab");
+ await toolbox.destroy();
+ BrowserTestUtils.removeTab(tab);
+ info("The about:blank tab is now removed.");
+ await new Promise(resolve => setTimeout(resolve, 500));
+}
+
+/**
+ * Start and stop the profiler to get the current active configuration. This is
+ * done programmtically through the nsIProfiler interface, rather than through click
+ * interactions, since the about:profiling page does not include buttons to control
+ * the recording.
+ *
+ * @returns {Object}
+ */
+function getActiveConfiguration() {
+ const { startProfiler, stopProfiler } = BackgroundJSM;
+
+ info("Start the profiler with the current about:profiling configuration.");
+ startProfiler("aboutprofiling");
+
+ // Immediately pause the sampling, to make sure the test runs fast. The profiler
+ // only needs to be started to initialize the configuration.
+ Services.profiler.Pause();
+
+ const { activeConfiguration } = Services.profiler;
+ if (!activeConfiguration) {
+ throw new Error(
+ "Expected to find an active configuration for the profile."
+ );
+ }
+
+ info("Stop the profiler after getting the active configuration.");
+ stopProfiler();
+
+ return activeConfiguration;
+}
+
+/**
+ * Start the profiler programmatically and check that the active configuration has
+ * a feature enabled
+ *
+ * @param {string} feature
+ * @return {boolean}
+ */
+function activeConfigurationHasFeature(feature) {
+ const { features } = getActiveConfiguration();
+ return features.includes(feature);
+}
+
+/**
+ * Start the profiler programmatically and check that the active configuration is
+ * tracking a thread.
+ *
+ * @param {string} thread
+ * @return {boolean}
+ */
+function activeConfigurationHasThread(thread) {
+ const { threads } = getActiveConfiguration();
+ return threads.includes(thread);
+}
+
+/**
+ * Use user driven events to start the profiler, and then get the active configuration
+ * of the profiler. This is similar to functions in the head.js file, but is specific
+ * for the DevTools situation. The UI complains if the profiler stops unexpectedly.
+ *
+ * @param {Document} document
+ * @param {string} feature
+ * @returns {boolean}
+ */
+async function devToolsActiveConfigurationHasFeature(document, feature) {
+ info("Get the active configuration of the profiler via user driven events.");
+ const start = await getActiveButtonFromText(document, "Start recording");
+ info("Click the button to start recording.");
+ start.click();
+
+ // Get the cancel button first, so that way we know the profile has actually
+ // been recorded.
+ const cancel = await getActiveButtonFromText(document, "Cancel recording");
+
+ const { activeConfiguration } = Services.profiler;
+ if (!activeConfiguration) {
+ throw new Error(
+ "Expected to find an active configuration for the profile."
+ );
+ }
+
+ info("Click the cancel button to discard the profile..");
+ cancel.click();
+
+ // Wait until the start button is back.
+ await getActiveButtonFromText(document, "Start recording");
+
+ return activeConfiguration.features.includes(feature);
+}
+
+/**
+ * Selects an element with some given text, then it walks up the DOM until it finds
+ * an input or select element via a call to querySelector.
+ *
+ * @param {Document} document
+ * @param {string} text
+ * @param {HTMLInputElement}
+ */
+async function getNearestInputFromText(document, text) {
+ const textElement = await getElementFromDocumentByText(document, text);
+ if (textElement.control) {
+ // This is a label, just grab the input.
+ return textElement.control;
+ }
+ // A non-label node
+ let next = textElement;
+ while ((next = next.parentElement)) {
+ const input = next.querySelector("input, select");
+ if (input) {
+ return input;
+ }
+ }
+ throw new Error("Could not find an input or select near the text element.");
+}
+
+/**
+ * Grabs the closest button element from a given snippet of text, and make sure
+ * the button is not disabled.
+ *
+ * @param {Document} document
+ * @param {string} text
+ * @param {HTMLButtonElement}
+ */
+async function getActiveButtonFromText(document, text) {
+ // This could select a span inside the button, or the button itself.
+ let button = await getElementFromDocumentByText(document, text);
+
+ while (button.tagName !== "button") {
+ // Walk up until a button element is found.
+ button = button.parentElement;
+ if (!button) {
+ throw new Error(`Unable to find a button from the text "${text}"`);
+ }
+ }
+
+ await waitUntil(
+ () => !button.disabled,
+ "Waiting until the button is not disabled."
+ );
+
+ return button;
+}
+
+/**
+ * Wait until the profiler menu button is added.
+ *
+ * @returns Promise<void>
+ */
+async function waitForProfilerMenuButton() {
+ info("Checking if the profiler menu button is enabled.");
+ await waitUntil(
+ () => gBrowser.ownerDocument.getElementById("profiler-button"),
+ "> Waiting until the profiler button is added to the browser."
+ );
+}
+
+/**
+ * Make sure the profiler popup is disabled for the test.
+ */
+async function makeSureProfilerPopupIsDisabled() {
+ info("Make sure the profiler popup is dsiabled.");
+
+ info("> Load the profiler menu button module.");
+ const { ProfilerMenuButton } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
+ );
+
+ const isOriginallyInNavBar = ProfilerMenuButton.isInNavbar();
+
+ if (isOriginallyInNavBar) {
+ info("> The menu button is in the navbar, remove it for this test.");
+ ProfilerMenuButton.remove();
+ } else {
+ info("> The menu button was not in the navbar yet.");
+ }
+
+ registerCleanupFunction(() => {
+ info("Revert the profiler menu button to be back in its original place");
+ if (isOriginallyInNavBar !== ProfilerMenuButton.isInNavbar()) {
+ ProfilerMenuButton.remove();
+ }
+ });
+}
+
+/**
+ * Open the WebChannel test document, that will enable the profiler popup via
+ * WebChannel.
+ * @param {Function} callback
+ */
+function withWebChannelTestDocument(callback) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "http://example.com/browser/devtools/client/performance-new/test/browser/webchannel.html",
+ },
+ callback
+ );
+}
+
+/**
+ * Set a React-friendly input value. Doing this the normal way doesn't work.
+ *
+ * See https://github.com/facebook/react/issues/10135#issuecomment-500929024
+ *
+ * @param {HTMLInputElement} input
+ * @param {string} value
+ */
+function setReactFriendlyInputValue(input, value) {
+ const previousValue = input.value;
+
+ input.value = value;
+
+ const tracker = input._valueTracker;
+ if (tracker) {
+ tracker.setValue(previousValue);
+ }
+
+ // 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+}
+
+/**
+ * The recording state is the internal state machine that represents the async
+ * operations that are going on in the profiler. This function sets up a helper
+ * that will obtain the Redux store and query this internal state. This is useful
+ * for unit testing purposes.
+ *
+ * @param {Document} document
+ */
+function setupGetRecordingState(document) {
+ const selectors = require("devtools/client/performance-new/store/selectors");
+ const store = document.defaultView.gStore;
+ if (!store) {
+ throw new Error("Could not find the redux store on the window object.");
+ }
+ return () => selectors.getRecordingState(store.getState());
+}
diff --git a/devtools/client/performance-new/test/browser/webchannel.html b/devtools/client/performance-new/test/browser/webchannel.html
new file mode 100644
index 0000000000..607c6dce12
--- /dev/null
+++ b/devtools/client/performance-new/test/browser/webchannel.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title></title>
+ </head>
+ <body>
+ This content page will send a WebChannel message to enable the profiler menu button.
+ <script>
+ "use strict";
+ document.title = "WebChannel Page Ready";
+
+ window.dispatchEvent(
+ new CustomEvent('WebChannelMessageToChrome', {
+ detail: JSON.stringify({
+ id: 'profiler.firefox.com',
+ message: { type: "ENABLE_MENU_BUTTON" },
+ requestId: 0,
+ }),
+ })
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/performance-new/test/xpcshell/.eslintrc.js b/devtools/client/performance-new/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..b6aacf458f
--- /dev/null
+++ b/devtools/client/performance-new/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for xpcshell tests.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/performance-new/test/xpcshell/head.js b/devtools/client/performance-new/test/xpcshell/head.js
new file mode 100644
index 0000000000..fb1cc73b6a
--- /dev/null
+++ b/devtools/client/performance-new/test/xpcshell/head.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+registerCleanupFunction(() => {
+ // Always clean up the prefs after every test.
+ const { revertRecordingPreferences } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+ revertRecordingPreferences();
+});
diff --git a/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js b/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js
new file mode 100644
index 0000000000..bd75fb9c65
--- /dev/null
+++ b/devtools/client/performance-new/test/xpcshell/test_popup_initial_state.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Tests the initial state of the background script for the popup.
+ */
+
+function setupBackgroundJsm() {
+ return ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ );
+}
+
+add_task(function test() {
+ info("Test that we get the default preference values from the browser.");
+ const { getRecordingPreferences } = setupBackgroundJsm();
+
+ const preferences = getRecordingPreferences(
+ "aboutprofiling",
+ Services.profiler.GetFeatures()
+ );
+
+ Assert.notEqual(
+ preferences.entries,
+ undefined,
+ "The initial state has the default entries."
+ );
+ Assert.notEqual(
+ preferences.interval,
+ undefined,
+ "The initial state has the default interval."
+ );
+ Assert.notEqual(
+ preferences.features,
+ undefined,
+ "The initial state has the default features."
+ );
+ Assert.equal(
+ preferences.features.includes("js"),
+ true,
+ "The js feature is initialized to the default."
+ );
+ Assert.notEqual(
+ preferences.threads,
+ undefined,
+ "The initial state has the default threads."
+ );
+ Assert.equal(
+ preferences.threads.includes("GeckoMain"),
+ true,
+ "The GeckoMain thread is initialized to the default."
+ );
+ Assert.notEqual(
+ preferences.objdirs,
+ undefined,
+ "The initial state has the default objdirs."
+ );
+ Assert.notEqual(
+ preferences.duration,
+ undefined,
+ "The duration is initialized to the duration."
+ );
+});
+
+add_task(function test() {
+ info(
+ "Test that the state and features are properly validated. This ensures that as " +
+ "we add and remove features, the stored preferences do not cause the Gecko " +
+ "Profiler interface to crash with invalid values."
+ );
+ const {
+ getRecordingPreferences,
+ setRecordingPreferences,
+ changePreset,
+ } = setupBackgroundJsm();
+
+ const supportedFeatures = Services.profiler.GetFeatures();
+
+ changePreset("aboutprofiling", "custom", supportedFeatures);
+
+ Assert.ok(
+ getRecordingPreferences(
+ "aboutprofiling",
+ supportedFeatures
+ ).features.includes("js"),
+ "The js preference is present initially."
+ );
+
+ const settings = getRecordingPreferences("aboutprofiling", supportedFeatures);
+ settings.features = settings.features.filter(feature => feature !== "js");
+ settings.features.push("UNKNOWN_FEATURE_FOR_TESTS");
+ setRecordingPreferences("aboutprofiling", settings);
+
+ Assert.ok(
+ !getRecordingPreferences(
+ "aboutprofiling",
+ supportedFeatures
+ ).features.includes("UNKNOWN_FEATURE_FOR_TESTS"),
+ "The unknown feature is removed."
+ );
+ Assert.ok(
+ !getRecordingPreferences(
+ "aboutprofiling",
+ supportedFeatures
+ ).features.includes("js"),
+ "The js preference is still flipped from the default."
+ );
+});
diff --git a/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js b/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js
new file mode 100644
index 0000000000..5f9582af0f
--- /dev/null
+++ b/devtools/client/performance-new/test/xpcshell/test_webchannel-urls.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { validateProfilerWebChannelUrl } = ChromeUtils.import(
+ "resource:///modules/DevToolsStartup.jsm"
+);
+
+add_task(function test() {
+ info(
+ "Since the WebChannel can communicate with a content page, test that only " +
+ "trusted URLs can be used with this mechanism."
+ );
+
+ const { checkUrlIsValid, checkUrlIsInvalid } = setup();
+
+ info("Check all of the valid URLs");
+ checkUrlIsValid("https://profiler.firefox.com");
+ checkUrlIsValid("http://example.com");
+ checkUrlIsValid("http://localhost:4242");
+ checkUrlIsValid("http://localhost:32343434");
+ checkUrlIsValid("http://localhost:4242/");
+ checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.com");
+ checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.com/");
+ checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.app");
+ checkUrlIsValid("https://deploy-preview-1234--perf-html.netlify.app/");
+ checkUrlIsValid("https://main--perf-html.netlify.app/");
+
+ info("Check all of the invalid URLs");
+ checkUrlIsInvalid("https://profiler.firefox.com/some-other-path");
+ checkUrlIsInvalid("http://localhost:4242/some-other-path");
+ checkUrlIsInvalid("http://profiler.firefox.com.example.com");
+ checkUrlIsInvalid("http://mozilla.com");
+ checkUrlIsInvalid("https://deploy-preview-1234--perf-html.netlify.dev");
+ checkUrlIsInvalid("https://anything--perf-html.netlify.app/");
+});
+
+function setup() {
+ function checkUrlIsValid(url) {
+ info(`Check that ${url} is valid`);
+ equal(
+ validateProfilerWebChannelUrl(url),
+ url,
+ `"${url}" is a valid WebChannel URL.`
+ );
+ }
+
+ function checkUrlIsInvalid(url) {
+ info(`Check that ${url} is invalid`);
+ equal(
+ validateProfilerWebChannelUrl(url),
+ "https://profiler.firefox.com",
+ `"${url}" was not valid, and was reset to the base URL.`
+ );
+ }
+
+ return {
+ checkUrlIsValid,
+ checkUrlIsInvalid,
+ };
+}
diff --git a/devtools/client/performance-new/test/xpcshell/xpcshell.ini b/devtools/client/performance-new/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..6aff202a65
--- /dev/null
+++ b/devtools/client/performance-new/test/xpcshell/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+
+[test_popup_initial_state.js]
+[test_webchannel-urls.js]