summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/browser-toolbox/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/browser-toolbox/test')
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser.toml53
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js66
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js222
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js199
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js66
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js220
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector_webextension.js94
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_l10n_buttons.js88
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_navigate_tab.js85
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_netmonitor.js151
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js58
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js29
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_ruleview_stylesheet.js82
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_shouldprocessupdates.js42
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_unavailable_children.js144
-rw-r--r--devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_watchedByDevTools.js122
-rw-r--r--devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html14
-rw-r--r--devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html16
-rw-r--r--devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_ruleview_stylesheet.html12
-rw-r--r--devtools/client/framework/browser-toolbox/test/head.js13
-rw-r--r--devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js233
-rw-r--r--devtools/client/framework/browser-toolbox/test/style_browser_toolbox_ruleview_stylesheet.css3
22 files changed, 2012 insertions, 0 deletions
diff --git a/devtools/client/framework/browser-toolbox/test/browser.toml b/devtools/client/framework/browser-toolbox/test/browser.toml
new file mode 100644
index 0000000000..1fc6dcaa39
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser.toml
@@ -0,0 +1,53 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+skip-if = ["asan"] # UNTIL Bug 1591064 IS FIXED ALL NEW TESTS SHOULD BE SKIPPED ON ASAN
+support-files = [
+ "doc_browser_toolbox_fission_contentframe_inspector_frame.html",
+ "doc_browser_toolbox_fission_contentframe_inspector_page.html",
+ "doc_browser_toolbox_ruleview_stylesheet.html",
+ "style_browser_toolbox_ruleview_stylesheet.css",
+ "head.js",
+ "helpers-browser-toolbox.js",
+ "!/devtools/client/debugger/test/mochitest/shared-head.js",
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+]
+prefs = ["security.allow_unsafe_parent_loads=true"] # This is far from ideal. Bug 1565279 covers removing this pref flip.
+
+["browser_browser_toolbox.js"]
+
+["browser_browser_toolbox_debugger.js"]
+skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1756616
+
+["browser_browser_toolbox_evaluation_context.js"]
+
+["browser_browser_toolbox_fission_contentframe_inspector.js"]
+skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1604751
+
+["browser_browser_toolbox_fission_inspector.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_browser_toolbox_fission_inspector_webextension.js"]
+
+["browser_browser_toolbox_l10n_buttons.js"]
+
+["browser_browser_toolbox_navigate_tab.js"]
+
+["browser_browser_toolbox_netmonitor.js"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1777831
+
+["browser_browser_toolbox_print_preview.js"]
+
+["browser_browser_toolbox_rtl.js"]
+
+["browser_browser_toolbox_ruleview_stylesheet.js"]
+skip-if = ["os == 'mac' && fission"] # high frequency intermittent
+
+["browser_browser_toolbox_shouldprocessupdates.js"]
+
+["browser_browser_toolbox_unavailable_children.js"]
+
+["browser_browser_toolbox_watchedByDevTools.js"]
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js
new file mode 100644
index 0000000000..29d8856b05
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+add_task(async function () {
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({});
+
+ const hasCloseButton = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ return !!gToolbox.doc.getElementById("toolbox-close");
+ });
+ ok(!hasCloseButton, "Browser toolbox doesn't have a close button");
+
+ info("Trigger F5 key shortcut and ensure nothing happens");
+ info(
+ "If F5 triggers a full reload, the mochitest will stop here as firefox instance will be restarted"
+ );
+ const previousInnerWindowId =
+ window.browsingContext.currentWindowGlobal.innerWindowId;
+ function onUnload() {
+ ok(false, "The top level window shouldn't be reloaded/closed");
+ }
+ window.addEventListener("unload", onUnload);
+ await ToolboxTask.spawn(null, async () => {
+ const isMacOS = Services.appinfo.OS === "Darwin";
+ const { win } = gToolbox;
+ // Simulate CmdOrCtrl+R
+ win.dispatchEvent(
+ new win.KeyboardEvent("keydown", {
+ bubbles: true,
+ ctrlKey: !isMacOS,
+ metaKey: isMacOS,
+ keyCode: "r".charCodeAt(0),
+ })
+ );
+ // Simulate F5
+ win.dispatchEvent(
+ new win.KeyboardEvent("keydown", {
+ bubbles: true,
+ keyCode: win.KeyEvent.DOM_VK_F5,
+ })
+ );
+ });
+
+ // Let a chance to trigger the regression where the top level document closes or reloads
+ await wait(1000);
+
+ is(
+ window.browsingContext.currentWindowGlobal.innerWindowId,
+ previousInnerWindowId,
+ "Check the browser.xhtml wasn't reloaded when pressing F5"
+ );
+ window.removeEventListener("unload", onUnload);
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js
new file mode 100644
index 0000000000..edcba359e2
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test asserts that the new debugger works from the browser toolbox process
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test runner, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const { fetch } = require("resource://devtools/shared/DevToolsUtils.js");
+
+const debuggerHeadURL =
+ CHROME_URL_ROOT + "../../../debugger/test/mochitest/shared-head.js";
+
+add_task(async function runTest() {
+ let { content: debuggerHead } = await fetch(debuggerHeadURL);
+
+ // We remove its import of shared-head, which isn't available in browser toolbox process
+ // And isn't needed thanks to testHead's symbols
+ debuggerHead = debuggerHead.replace(
+ /Services.scriptloader.loadSubScript[^\)]*\);/g,
+ ""
+ );
+
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ // head.js uses this method
+ registerCleanupFunction: () => {},
+ waitForDispatch,
+ waitUntil,
+ });
+ await ToolboxTask.importScript(debuggerHead);
+
+ info("### First test breakpoint in the parent process script");
+ const s = Cu.Sandbox("http://mozilla.org");
+
+ // Use a unique id for the fake script name in order to be able to run
+ // this test more than once. That's because the Sandbox is not immediately
+ // destroyed and so the debugger would display only one file but not necessarily
+ // connected to the latest sandbox.
+ const id = new Date().getTime();
+
+ // Pass a fake URL to evalInSandbox. If we just pass a filename,
+ // Debugger is going to fail and only display root folder (`/`) listing.
+ // But it won't try to fetch this url and use sandbox content as expected.
+ const testUrl = `http://mozilla.org/browser-toolbox-test-${id}.js`;
+ Cu.evalInSandbox(
+ `this.plop = function plop() {
+ const foo = 1;
+ return foo;
+};`,
+ s,
+ "1.8",
+ testUrl,
+ 0
+ );
+
+ // Execute the function every second in order to trigger the breakpoint
+ const interval = setInterval(s.plop, 1000);
+
+ await ToolboxTask.spawn(testUrl, async _testUrl => {
+ /* global gToolbox, createDebuggerContext, waitForSources, waitForPaused,
+ addBreakpoint, assertPausedAtSourceAndLine, stepIn, findSource,
+ removeBreakpoint, resume, selectSource, assertNotPaused, assertBreakpoint,
+ assertTextContentOnLine, waitForResumed */
+ Services.prefs.clearUserPref("devtools.debugger.tabs");
+ Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
+
+ info("Waiting for debugger load");
+ await gToolbox.selectTool("jsdebugger");
+ const dbg = createDebuggerContext(gToolbox);
+
+ await waitForSources(dbg, _testUrl);
+
+ info("Loaded, selecting the test script to debug");
+ const fileName = _testUrl.match(/browser-toolbox-test.*\.js/)[0];
+ await selectSource(dbg, fileName);
+
+ info("Add a breakpoint and wait to be paused");
+ const onPaused = waitForPaused(dbg);
+ await addBreakpoint(dbg, fileName, 2);
+ await onPaused;
+
+ const source = findSource(dbg, fileName);
+ assertPausedAtSourceAndLine(dbg, source.id, 2);
+ assertTextContentOnLine(dbg, 2, "const foo = 1;");
+ is(
+ dbg.selectors.getBreakpointCount(),
+ 1,
+ "There is exactly one breakpoint"
+ );
+
+ await stepIn(dbg);
+
+ assertPausedAtSourceAndLine(dbg, source.id, 3);
+ assertTextContentOnLine(dbg, 3, "return foo;");
+ is(
+ dbg.selectors.getBreakpointCount(),
+ 1,
+ "We still have only one breakpoint after step-in"
+ );
+
+ // Remove the breakpoint before resuming in order to prevent hitting the breakpoint
+ // again during test closing.
+ await removeBreakpoint(dbg, source.id, 2);
+
+ await resume(dbg);
+
+ // Let a change for the interval to re-execute
+ await new Promise(r => setTimeout(r, 1000));
+
+ is(dbg.selectors.getBreakpointCount(), 0, "There is no more breakpoints");
+
+ assertNotPaused(dbg);
+ });
+
+ clearInterval(interval);
+
+ info("### Now test breakpoint in a privileged content process script");
+ const testUrl2 = `http://mozilla.org/content-process-test-${id}.js`;
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [testUrl2], testUrl => {
+ // Use a sandbox in order to have a URL to set a breakpoint
+ const s = Cu.Sandbox("http://mozilla.org");
+ Cu.evalInSandbox(
+ `this.foo = function foo() {
+ const plop = 1;
+ return plop;
+};`,
+ s,
+ "1.8",
+ testUrl,
+ 0
+ );
+ content.interval = content.setInterval(s.foo, 1000);
+ });
+ await ToolboxTask.spawn(testUrl2, async _testUrl => {
+ const dbg = createDebuggerContext(gToolbox);
+
+ const fileName = _testUrl.match(/content-process-test.*\.js/)[0];
+ await waitForSources(dbg, _testUrl);
+
+ await selectSource(dbg, fileName);
+
+ const onPaused = waitForPaused(dbg);
+ await addBreakpoint(dbg, fileName, 2);
+ await onPaused;
+
+ const source = findSource(dbg, fileName);
+ assertPausedAtSourceAndLine(dbg, source.id, 2);
+ assertTextContentOnLine(dbg, 2, "const plop = 1;");
+ await assertBreakpoint(dbg, 2);
+ is(dbg.selectors.getBreakpointCount(), 1, "We have exactly one breakpoint");
+
+ await stepIn(dbg);
+
+ assertPausedAtSourceAndLine(dbg, source.id, 3);
+ assertTextContentOnLine(dbg, 3, "return plop;");
+ is(
+ dbg.selectors.getBreakpointCount(),
+ 1,
+ "We still have only one breakpoint after step-in"
+ );
+
+ // Remove the breakpoint before resuming in order to prevent hitting the breakpoint
+ // again during test closing.
+ await removeBreakpoint(dbg, source.id, 2);
+
+ await resume(dbg);
+
+ // Let a change for the interval to re-execute
+ await new Promise(r => setTimeout(r, 1000));
+
+ is(dbg.selectors.getBreakpointCount(), 0, "There is no more breakpoints");
+
+ assertNotPaused(dbg);
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.clearInterval(content.interval);
+ });
+
+ info("Trying pausing in a content process that crashes");
+
+ const crashingUrl =
+ "data:text/html,<script>setTimeout(()=>{debugger;})</script>";
+ const crashingTab = await addTab(crashingUrl);
+ await ToolboxTask.spawn(crashingUrl, async url => {
+ const dbg = createDebuggerContext(gToolbox);
+ await waitForPaused(dbg);
+ const source = findSource(dbg, url);
+ assertPausedAtSourceAndLine(dbg, source.id, 1);
+ const thread = dbg.selectors.getThread(dbg.selectors.getCurrentThread());
+ is(thread.isTopLevel, false, "The current thread is not the top level one");
+ is(thread.targetType, "process", "The current thread is the tab one");
+ });
+
+ info(
+ "Crash the tab and ensure the debugger resumes and switch to the main thread"
+ );
+ await BrowserTestUtils.crashFrame(crashingTab.linkedBrowser);
+
+ await ToolboxTask.spawn(null, async () => {
+ const dbg = createDebuggerContext(gToolbox);
+ await waitForResumed(dbg);
+ const thread = dbg.selectors.getThread(dbg.selectors.getCurrentThread());
+ is(thread.isTopLevel, true, "The current thread is the top level one");
+ });
+
+ await removeTab(crashingTab);
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js
new file mode 100644
index 0000000000..34e18d15c5
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_evaluation_context.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// This test is used to test fission-like features via the Browser Toolbox:
+// - the evaluation context selector in the console show the right targets
+// - the iframe dropdown also show the right targets
+// - both are updated accordingly when toggle to parent-process only scope
+
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the console by default
+ await pushPref("devtools.browsertoolbox.panel", "webconsole");
+ await pushPref("devtools.webconsole.input.context", true);
+ // Force EFT to have targets for all WindowGlobals
+ await pushPref("devtools.every-frame-target.enabled", true);
+ // Enable Multiprocess Browser Toolbox
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // Open the test *before* opening the Browser toolbox in order to have the right target title.
+ // Once created, the target won't update its title, and so would be "New Tab", instead of "Test tab"
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=<html><title>Test tab</title></html>"
+ );
+
+ const ToolboxTask = await initBrowserToolboxTask();
+
+ await ToolboxTask.importFunctions({
+ waitUntil,
+ getContextLabels,
+ getFramesLabels,
+ });
+
+ const tabProcessID =
+ tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid;
+
+ const decodedTabURI = decodeURI(tab.linkedBrowser.currentURI.spec);
+
+ await ToolboxTask.spawn(
+ [tabProcessID, isFissionEnabled(), decodedTabURI],
+ async (processID, _isFissionEnabled, tabURI) => {
+ /* global gToolbox */
+ const { hud } = await gToolbox.getPanel("webconsole");
+
+ const evaluationContextSelectorButton = hud.ui.outputNode.querySelector(
+ ".webconsole-evaluation-selector-button"
+ );
+
+ is(
+ !!evaluationContextSelectorButton,
+ true,
+ "The evaluation context selector is visible"
+ );
+ is(
+ evaluationContextSelectorButton.innerText,
+ "Top",
+ "The button has the expected 'Top' text"
+ );
+
+ const labelTexts = getContextLabels(gToolbox);
+
+ const expectedTitle = _isFissionEnabled
+ ? `(pid ${processID}) https://example.com`
+ : `(pid ${processID}) web`;
+ ok(
+ labelTexts.includes(expectedTitle),
+ `${processID} content process visible in the execution context (${labelTexts})`
+ );
+
+ ok(
+ labelTexts.includes(`Test tab`),
+ `Test tab is visible in the execution context (${labelTexts})`
+ );
+
+ // Also assert the behavior of the iframe dropdown and the mode selector
+ info("Check the iframe dropdown, start by opening it");
+ const btn = gToolbox.doc.getElementById("command-button-frames");
+ btn.click();
+
+ const panel = gToolbox.doc.getElementById("command-button-frames-panel");
+ ok(panel, "popup panel has created.");
+ await waitUntil(
+ () => panel.classList.contains("tooltip-visible"),
+ "Wait for the menu to be displayed"
+ );
+
+ is(
+ getFramesLabels(gToolbox)[0],
+ "chrome://browser/content/browser.xhtml",
+ "The iframe dropdown lists first browser.xhtml, running in the parent process"
+ );
+ ok(
+ getFramesLabels(gToolbox).includes(tabURI),
+ "The iframe dropdown lists the tab document, running in the content process"
+ );
+
+ // Click on top frame to hide the iframe picker, so clicks on other elements can be registered.
+ gToolbox.doc.querySelector("#toolbox-frame-menu .command").click();
+
+ await waitUntil(
+ () => !panel.classList.contains("tooltip-visible"),
+ "Wait for the menu to be hidden"
+ );
+
+ info("Check that the ChromeDebugToolbar is displayed");
+ const chromeDebugToolbar = gToolbox.doc.querySelector(
+ ".chrome-debug-toolbar"
+ );
+ ok(!!chromeDebugToolbar, "ChromeDebugToolbar is displayed");
+ const chromeDebugToolbarScopeInputs = Array.from(
+ chromeDebugToolbar.querySelectorAll(`[name="chrome-debug-mode"]`)
+ );
+ is(
+ chromeDebugToolbarScopeInputs.length,
+ 2,
+ "There are 2 mode inputs in the chromeDebugToolbar"
+ );
+ const [
+ chromeDebugToolbarParentProcessModeInput,
+ chromeDebugToolbarMultiprocessModeInput,
+ ] = chromeDebugToolbarScopeInputs;
+ is(
+ chromeDebugToolbarParentProcessModeInput.value,
+ "parent-process",
+ "Got expected value for the first input"
+ );
+ is(
+ chromeDebugToolbarMultiprocessModeInput.value,
+ "everything",
+ "Got expected value for the second input"
+ );
+ ok(
+ chromeDebugToolbarMultiprocessModeInput.checked,
+ "The multiprocess mode is selected"
+ );
+
+ info(
+ "Click on the parent-process input and check that it restricts the targets"
+ );
+ chromeDebugToolbarParentProcessModeInput.click();
+ info("Wait for the iframe dropdown to hide the tab target");
+ await waitUntil(() => {
+ return !getFramesLabels(gToolbox).includes(tabURI);
+ });
+
+ info("Wait for the context selector to hide the tab context");
+ await waitUntil(() => {
+ return !getContextLabels(gToolbox).includes(`Test tab`);
+ });
+
+ ok(
+ !chromeDebugToolbarMultiprocessModeInput.checked,
+ "Now, the multiprocess mode is disabled…"
+ );
+ ok(
+ chromeDebugToolbarParentProcessModeInput.checked,
+ "…and the parent process mode is enabled"
+ );
+
+ info("Switch back to multiprocess mode");
+ chromeDebugToolbarMultiprocessModeInput.click();
+
+ info("Wait for the iframe dropdown to show again the tab target");
+ await waitUntil(() => {
+ return getFramesLabels(gToolbox).includes(tabURI);
+ });
+
+ info("Wait for the context selector to show again the tab context");
+ await waitUntil(() => {
+ return getContextLabels(gToolbox).includes(`Test tab`);
+ });
+ }
+ );
+
+ await ToolboxTask.destroy();
+});
+
+function getContextLabels(toolbox) {
+ // Note that the context menu is in the top level chrome document (toolbox.xhtml)
+ // instead of webconsole.xhtml.
+ const labels = toolbox.doc.querySelectorAll(
+ "#webconsole-console-evaluation-context-selector-menu-list li .label"
+ );
+ return Array.from(labels).map(item => item.textContent);
+}
+
+function getFramesLabels(toolbox) {
+ return Array.from(
+ toolbox.doc.querySelectorAll("#toolbox-frame-menu .command .label")
+ ).map(el => el.textContent);
+}
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js
new file mode 100644
index 0000000000..ba2ab2779c
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+/**
+ * Check that different-site iframes can be expanded in the Omniscient Browser
+ * Toolbox. The test is supposed to run successfully with or without fission.
+ * Pass --enable-fission to ./mach test to enable fission when running this
+ * test locally.
+ */
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ getNodeFrontInFrames,
+ selectNode,
+ // selectNodeInFrames depends on selectNode, getNodeFront, getNodeFrontInFrames.
+ selectNodeInFrames,
+ });
+
+ const tab = await addTab(
+ `https://example.com/browser/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html`
+ );
+
+ // Set a custom attribute on the tab's browser, in order to easily select it in the markup view
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ const testAttribute = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = await gToolbox.selectTool("inspector");
+ const onSidebarSelect = inspector.sidebar.once("select");
+ inspector.sidebar.select("computedview");
+ await onSidebarSelect;
+
+ info("Select the test element nested in the remote iframe");
+ const nodeFront = await selectNodeInFrames(
+ ['browser[remote="true"][test-tab]', "iframe", "#inside-iframe"],
+ inspector
+ );
+
+ return nodeFront.getAttribute("test-attribute");
+ });
+
+ is(
+ testAttribute,
+ "fission",
+ "Could successfully read attribute on a node inside a remote iframe"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js
new file mode 100644
index 0000000000..24c7fa7918
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// This test is used to test fission-like features via the Browser Toolbox:
+// - computed view is correct when selecting an element in a remote frame
+
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+ // Enable Multiprocess Browser Toolbox
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ getNodeFrontInFrames,
+ selectNode,
+ // selectNodeInFrames depends on selectNode, getNodeFront, getNodeFrontInFrames.
+ selectNodeInFrames,
+ });
+
+ // Open the tab *after* opening the Browser Toolbox in order to force creating the remote frames
+ // late and exercise frame target watching code.
+ const tab = await addTab(
+ `data:text/html,<div id="my-div" style="color: red">Foo</div><div id="second-div" style="color: blue">Foo</div>`
+ );
+ // Set a custom attribute on the tab's browser, in order to easily select it in the markup view
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ const color = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+ const onSidebarSelect = inspector.sidebar.once("select");
+ inspector.sidebar.select("computedview");
+ await onSidebarSelect;
+
+ await selectNodeInFrames(
+ ['browser[remote="true"][test-tab]', "#my-div"],
+ inspector
+ );
+
+ const view = inspector.getPanel("computedview").computedView;
+ function getProperty(name) {
+ const propertyViews = view.propertyViews;
+ for (const propView of propertyViews) {
+ if (propView.name == name) {
+ return propView;
+ }
+ }
+ return null;
+ }
+ const prop = getProperty("color");
+ return prop.valueNode.textContent;
+ });
+
+ is(
+ color,
+ "rgb(255, 0, 0)",
+ "The color property of the <div> within a tab isn't red"
+ );
+
+ info("Check that the node picker can be used on element in the content page");
+ await pickNodeInContentPage(
+ ToolboxTask,
+ tab,
+ "browser[test-tab]",
+ "#second-div"
+ );
+ const secondColor = await ToolboxTask.spawn(null, async () => {
+ const inspector = gToolbox.getPanel("inspector");
+
+ is(
+ inspector.selection.nodeFront.id,
+ "second-div",
+ "The expected element is selected in the inspector"
+ );
+
+ const view = inspector.getPanel("computedview").computedView;
+ function getProperty(name) {
+ const propertyViews = view.propertyViews;
+ for (const propView of propertyViews) {
+ if (propView.name == name) {
+ return propView;
+ }
+ }
+ return null;
+ }
+ const prop = getProperty("color");
+ return prop.valueNode.textContent;
+ });
+
+ is(
+ secondColor,
+ "rgb(0, 0, 255)",
+ "The color property of the <div> within a tab isn't blue"
+ );
+
+ info(
+ "Check that the node picker can be used for element in non-remote <browser>"
+ );
+ const nonRemoteUrl = "about:robots";
+ const nonRemoteTab = await addTab(nonRemoteUrl);
+ // Set a custom attribute on the tab's browser, in order to target it
+ nonRemoteTab.linkedBrowser.setAttribute("test-tab-non-remote", "");
+
+ // check that the browser element is indeed not remote. If that changes for about:robots,
+ // this should be replaced with another page
+ is(
+ nonRemoteTab.linkedBrowser.hasAttribute("remote"),
+ false,
+ "The <browser> element for about:robots is not remote"
+ );
+
+ await pickNodeInContentPage(
+ ToolboxTask,
+ nonRemoteTab,
+ "browser[test-tab-non-remote]",
+ "#errorTryAgain"
+ );
+
+ await ToolboxTask.spawn(null, async () => {
+ const inspector = gToolbox.getPanel("inspector");
+ is(
+ inspector.selection.nodeFront.id,
+ "errorTryAgain",
+ "The element inside a non-remote <browser> element is selected in the inspector"
+ );
+ });
+
+ await ToolboxTask.destroy();
+});
+
+async function pickNodeInContentPage(
+ ToolboxTask,
+ tab,
+ browserElementSelector,
+ contentElementSelector
+) {
+ await ToolboxTask.spawn(contentElementSelector, async _selector => {
+ const onPickerStarted = gToolbox.nodePicker.once("picker-started");
+
+ // Wait until the inspector front was initialized in the target that
+ // contains the element we want to pick.
+ // Otherwise, even if the picker is "started", the corresponding WalkerActor
+ // might not be listening to the correct pick events (WalkerActor::pick)
+ const onPickerReady = new Promise(resolve => {
+ gToolbox.nodePicker.on(
+ "inspector-front-ready-for-picker",
+ async function onFrontReady(walker) {
+ if (await walker.querySelector(walker.rootNode, _selector)) {
+ gToolbox.nodePicker.off(
+ "inspector-front-ready-for-picker",
+ onFrontReady
+ );
+ resolve();
+ }
+ }
+ );
+ });
+
+ gToolbox.nodePicker.start();
+ await onPickerStarted;
+ await onPickerReady;
+
+ const inspector = gToolbox.getPanel("inspector");
+
+ // Save the promises for later tasks, in order to start listening
+ // *before* hovering the element and wait for resolution *after* hovering.
+ this.onPickerStopped = gToolbox.nodePicker.once("picker-stopped");
+ this.onInspectorUpdated = inspector.once("inspector-updated");
+ });
+
+ // Retrieve the position of the element we want to pick in the content page
+ const { x, y } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [contentElementSelector],
+ _selector => {
+ const rect = content.document
+ .querySelector(_selector)
+ .getBoundingClientRect();
+ return { x: rect.x, y: rect.y };
+ }
+ );
+
+ // Synthesize the mouse event in the top level browsing context, but on the <browser>
+ // element containing the tab we're looking at, at the position where should be the
+ // content element.
+ // We need to do this to mimick what's actually done in node-picker.js
+ await EventUtils.synthesizeMouse(
+ document.querySelector(browserElementSelector),
+ x + 5,
+ y + 5,
+ {}
+ );
+
+ await ToolboxTask.spawn(null, async () => {
+ info(" # Waiting for picker stop");
+ await this.onPickerStopped;
+ info(" # Waiting for inspector-updated");
+ await this.onInspectorUpdated;
+
+ delete this.onPickerStopped;
+ delete this.onInspectorUpdated;
+ });
+}
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector_webextension.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector_webextension.js
new file mode 100644
index 0000000000..4f8a2f7535
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector_webextension.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that expanding a browser element of a webextension in the browser toolbox works
+// as expected (See Bug 1696862).
+
+add_task(async function () {
+ const extension = ExtensionTestUtils.loadExtension({
+ // manifest_version: 2,
+ manifest: {
+ sidebar_action: {
+ default_title: "SideBarExtensionTest",
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+ <!DOCTYPE html>
+ <html class="sidebar-extension-test">
+ <head>
+ <meta charset="utf-8">
+ <script src="sidebar.js"></script>
+ </head>
+ <body>
+ <h1 id="sidebar-extension-h1">Sidebar Extension Test</h1>
+ </body>
+ </html>`,
+ "sidebar.js": function () {
+ window.onload = () => {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage("sidebar-ready");
+ };
+ },
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("sidebar-ready");
+
+ ok(true, "Extension sidebar is displayed");
+
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+ // Enable Multiprocess Browser Toolbox
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ getNodeFrontInFrames,
+ selectNode,
+ // selectNodeInFrames depends on selectNode, getNodeFront, getNodeFrontInFrames.
+ selectNodeInFrames,
+ });
+
+ const nodeId = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+
+ const nodeFront = await selectNodeInFrames(
+ [
+ "browser#sidebar",
+ "browser#webext-panels-browser",
+ "html.sidebar-extension-test h1",
+ ],
+ inspector
+ );
+ return nodeFront.id;
+ });
+
+ is(
+ nodeId,
+ "sidebar-extension-h1",
+ "The Browser Toolbox can inspect a node in the webextension sidebar document"
+ );
+
+ await ToolboxTask.destroy();
+ await extension.unload();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_l10n_buttons.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_l10n_buttons.js
new file mode 100644
index 0000000000..abd2f3806a
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_l10n_buttons.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+/**
+ * In the browser toolbox there are options to switch the language to the "bidi" and
+ * "accented" languages. These are useful for making sure the browser is correctly
+ * localized. This test opens the browser toolbox, and checks that these buttons
+ * work.
+ */
+add_task(async function () {
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({ clickMeatballItem });
+
+ is(getPseudoLocale(), "", "Starts out as empty");
+
+ await ToolboxTask.spawn(null, () => clickMeatballItem("accented"));
+ is(getPseudoLocale(), "accented", "Enabled the accented pseudo-locale");
+
+ await ToolboxTask.spawn(null, () => clickMeatballItem("accented"));
+ is(getPseudoLocale(), "", "Disabled the accented pseudo-locale.");
+
+ await ToolboxTask.spawn(null, () => clickMeatballItem("bidi"));
+ is(getPseudoLocale(), "bidi", "Enabled the bidi pseudo-locale.");
+
+ await ToolboxTask.spawn(null, () => clickMeatballItem("bidi"));
+ is(getPseudoLocale(), "", "Disabled the bidi pseudo-locale.");
+
+ await ToolboxTask.spawn(null, () => clickMeatballItem("bidi"));
+ is(getPseudoLocale(), "bidi", "Enabled the bidi before closing.");
+
+ await ToolboxTask.destroy();
+
+ is(getPseudoLocale(), "", "After closing the pseudo-locale is disabled.");
+});
+
+/**
+ * Return the pseudo-locale preference of the debuggee browser (not the browser toolbox).
+ *
+ * Another option for this test would be to test the text and layout of the
+ * browser directly, but this could be brittle. Checking the preference will
+ * hopefully provide adequate coverage.
+ */
+function getPseudoLocale() {
+ return Services.prefs.getCharPref("intl.l10n.pseudo");
+}
+
+/**
+ * This function is a ToolboxTask and is cloned into the toolbox context. It opens the
+ * "meatball menu" in the browser toolbox, clicks one of the pseudo-locale
+ * options, and finally returns the pseudo-locale preference from the target browser.
+ *
+ * @param {"accented" | "bidi"} type
+ */
+function clickMeatballItem(type) {
+ return new Promise(resolve => {
+ /* global gToolbox */
+
+ dump(`Opening the meatball menu in the browser toolbox.\n`);
+ gToolbox.doc.getElementById("toolbox-meatball-menu-button").click();
+
+ gToolbox.doc.addEventListener(
+ "popupshown",
+ async () => {
+ const menuItem = gToolbox.doc.getElementById(
+ "toolbox-meatball-menu-pseudo-locale-" + type
+ );
+ dump(`Clicking the meatball menu item: "${type}".\n`);
+ menuItem.click();
+
+ // Request the pseudo-locale so that we know the preference actor is fully
+ // done setting the debuggee browser.
+ await gToolbox.getPseudoLocale();
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_navigate_tab.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_navigate_tab.js
new file mode 100644
index 0000000000..46a6564a39
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_navigate_tab.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that the Browser Toolbox still works after navigating a content tab
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+
+ await testNavigate("everything");
+ await testNavigate("parent-process");
+});
+
+async function testNavigate(browserToolboxScope) {
+ await pushPref("devtools.browsertoolbox.scope", browserToolboxScope);
+
+ const tab = await addTab(
+ `data:text/html,<div>NAVIGATE TEST - BEFORE: ${browserToolboxScope}</div>`
+ );
+ // Set the scope on the browser element to assert it easily in the Toolbox
+ // task.
+ tab.linkedBrowser.setAttribute("data-test-scope", browserToolboxScope);
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ selectNode,
+ });
+
+ const hasBrowserContainerTask = async ({ scope, hasNavigated }) => {
+ /* global gToolbox */
+ const inspector = await gToolbox.selectTool("inspector");
+ info("Select the test browser element in the inspector");
+ let selector = `browser[data-test-scope="${scope}"]`;
+ if (hasNavigated) {
+ selector += `[navigated="true"]`;
+ }
+ const nodeFront = await getNodeFront(selector, inspector);
+ await selectNode(nodeFront, inspector);
+ const browserContainer = inspector.markup.getContainer(nodeFront);
+ return !!browserContainer;
+ };
+
+ info("Select the test browser in the Browser Toolbox (before navigation)");
+ const hasContainerBeforeNavigation = await ToolboxTask.spawn(
+ { scope: browserToolboxScope, hasNavigated: false },
+ hasBrowserContainerTask
+ );
+ ok(
+ hasContainerBeforeNavigation,
+ "Found a valid container for the browser element before navigation"
+ );
+
+ info("Navigate the test tab to another data-uri");
+ await navigateTo(
+ `data:text/html,<div>NAVIGATE TEST - AFTER: ${browserToolboxScope}</div>`
+ );
+ tab.linkedBrowser.setAttribute("navigated", "true");
+
+ info("Select the test browser in the Browser Toolbox (after navigation)");
+ const hasContainerAfterNavigation = await ToolboxTask.spawn(
+ { scope: browserToolboxScope, hasNavigated: true },
+ hasBrowserContainerTask
+ );
+ ok(
+ hasContainerAfterNavigation,
+ "Found a valid container for the browser element after navigation"
+ );
+
+ await ToolboxTask.destroy();
+}
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_netmonitor.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_netmonitor.js
new file mode 100644
index 0000000000..56e38998ce
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_netmonitor.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global gToolbox */
+
+add_task(async function () {
+ // Disable several prefs to avoid network requests.
+ await pushPref("browser.safebrowsing.blockedURIs.enabled", false);
+ await pushPref("browser.safebrowsing.downloads.enabled", false);
+ await pushPref("browser.safebrowsing.malware.enabled", false);
+ await pushPref("browser.safebrowsing.phishing.enabled", false);
+ await pushPref("privacy.query_stripping.enabled", false);
+ await pushPref("extensions.systemAddon.update.enabled", false);
+
+ await pushPref("services.settings.server", "invalid://err");
+
+ // Define a set list of visible columns
+ await pushPref(
+ "devtools.netmonitor.visibleColumns",
+ JSON.stringify(["file", "url", "status"])
+ );
+
+ // Force observice all processes to see the content process requests
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+
+ await ToolboxTask.importFunctions({
+ waitUntil,
+ });
+
+ await ToolboxTask.spawn(null, async () => {
+ const { resourceCommand } = gToolbox.commands;
+
+ // Assert that the toolbox is not listening to network events
+ // before the netmonitor panel is opened.
+ is(
+ resourceCommand.isResourceWatched(resourceCommand.TYPES.NETWORK_EVENT),
+ false,
+ "The toolox is not watching for network event resources"
+ );
+
+ await gToolbox.selectTool("netmonitor");
+ const monitor = gToolbox.getCurrentPanel();
+ const { document, store, windowRequire } = monitor.panelWin;
+
+ const Actions = windowRequire(
+ "devtools/client/netmonitor/src/actions/index"
+ );
+
+ store.dispatch(Actions.batchEnable(false));
+
+ await waitUntil(
+ () => !!document.querySelector(".request-list-empty-notice")
+ );
+
+ is(
+ resourceCommand.isResourceWatched(resourceCommand.TYPES.NETWORK_EVENT),
+ true,
+ "The network panel is now watching for network event resources"
+ );
+
+ const emptyListNotice = document.querySelector(
+ ".request-list-empty-notice"
+ );
+
+ ok(
+ !!emptyListNotice,
+ "An empty notice should be displayed when the frontend is opened."
+ );
+
+ is(
+ emptyListNotice.innerText,
+ "Perform a request to see detailed information about network activity.",
+ "The reload and perfomance analysis details should not be visible in the browser toolbox"
+ );
+
+ is(
+ store.getState().requests.requests.length,
+ 0,
+ "The requests should be empty when the frontend is opened."
+ );
+
+ ok(
+ !document.querySelector(".requests-list-network-summary-button"),
+ "The perfomance analysis button should not be visible in the browser toolbox"
+ );
+ });
+
+ info("Trigger request in parent process and check that it shows up");
+ await fetch("https://example.org/document-builder.sjs?html=fromParent");
+
+ await ToolboxTask.spawn(null, async () => {
+ const monitor = gToolbox.getCurrentPanel();
+ const { document, store } = monitor.panelWin;
+
+ await waitUntil(
+ () => !document.querySelector(".request-list-empty-notice")
+ );
+ ok(true, "The empty notice is no longer displayed");
+ is(
+ store.getState().requests.requests.length,
+ 1,
+ "There's 1 request in the store"
+ );
+
+ const requests = Array.from(
+ document.querySelectorAll("tbody .requests-list-column.requests-list-url")
+ );
+ is(requests.length, 1, "One request displayed");
+ is(
+ requests[0].textContent,
+ "https://example.org/document-builder.sjs?html=fromParent",
+ "Expected request is displayed"
+ );
+ });
+
+ info("Trigger content process requests");
+ const urlImg = `${URL_ROOT_SSL}test-image.png?fromContent&${Date.now()}-${Math.random()}`;
+ await addTab(
+ `https://example.com/document-builder.sjs?html=${encodeURIComponent(
+ `<img src='${urlImg}'>`
+ )}`
+ );
+
+ await ToolboxTask.spawn(urlImg, async innerUrlImg => {
+ const monitor = gToolbox.getCurrentPanel();
+ const { document, store } = monitor.panelWin;
+
+ await waitUntil(() => store.getState().requests.requests.length >= 3);
+ ok(true, "Expected content requests are displayed");
+
+ const requests = Array.from(
+ document.querySelectorAll("tbody .requests-list-column.requests-list-url")
+ );
+ is(requests.length, 3, "Three requests displayed");
+ ok(
+ requests[1].textContent.includes(
+ `https://example.com/document-builder.sjs`
+ ),
+ "Request for the tab is displayed"
+ );
+ is(
+ requests[2].textContent,
+ innerUrlImg,
+ "Request for image image in tab is displayed"
+ );
+ });
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js
new file mode 100644
index 0000000000..81ff0808fb
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_print_preview.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that the MultiProcessBrowserToolbox can be opened when print preview is
+// started, and can select elements in the print preview document.
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+
+ // Enable Multiprocess Browser Toolbox
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // Open the tab *after* opening the Browser Toolbox in order to force creating the remote frames
+ // late and exercise frame target watching code.
+ await addTab(`data:text/html,<div id="test-div">PRINT PREVIEW TEST</div>`);
+
+ info("Start the print preview for the current tab");
+ document.getElementById("cmd_print").doCommand();
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ getNodeFrontInFrames,
+ selectNode,
+ // selectNodeInFrames depends on selectNode, getNodeFront, getNodeFrontInFrames.
+ selectNodeInFrames,
+ });
+
+ const hasCloseButton = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+
+ info("Select the #test-div element in the printpreview document");
+ await selectNodeInFrames(
+ ['browser[printpreview="true"]', "#test-div"],
+ inspector
+ );
+ return !!gToolbox.doc.getElementById("toolbox-close");
+ });
+ ok(!hasCloseButton, "Browser toolbox doesn't have a close button");
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js
new file mode 100644
index 0000000000..558be1a16c
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Test that DevTools panels are rendered in "rtl" (right-to-left) in the Browser Toolbox.
+add_task(async function () {
+ await pushPref("intl.l10n.pseudo", "bidi");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({});
+
+ const dir = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = await gToolbox.selectTool("inspector");
+ return inspector.panelDoc.dir;
+ });
+ is(dir, "rtl", "Inspector panel has the expected direction");
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_ruleview_stylesheet.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_ruleview_stylesheet.js
new file mode 100644
index 0000000000..60f30b44b4
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_ruleview_stylesheet.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// Check that CSS rules are displayed with the proper source label in the
+// browser toolbox.
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+ // Enable Multiprocess Browser Toolbox
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({
+ getNodeFront,
+ getNodeFrontInFrames,
+ getRuleViewLinkByIndex,
+ selectNode,
+ // selectNodeInFrames depends on selectNode, getNodeFront, getNodeFrontInFrames.
+ selectNodeInFrames,
+ waitUntil,
+ });
+
+ // This is a simple test page, which contains a <div> with a CSS rule `color: red`
+ // coming from a dedicated stylesheet.
+ const tab = await addTab(
+ `https://example.com/browser/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_ruleview_stylesheet.html`
+ );
+
+ // Set a custom attribute on the tab's browser, in order to easily select it in the markup view
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ info(
+ "Get the source label for a rule displayed in the Browser Toolbox ruleview"
+ );
+ const sourceLabel = await ToolboxTask.spawn(null, async () => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+
+ info("Select the rule view");
+ const onSidebarSelect = inspector.sidebar.once("select");
+ inspector.sidebar.select("ruleview");
+ await onSidebarSelect;
+
+ info("Select a DIV element in the test page");
+ await selectNodeInFrames(
+ ['browser[remote="true"][test-tab]', "div"],
+ inspector
+ );
+
+ info("Retrieve the sourceLabel for the rule at index 1");
+ const ruleView = inspector.getPanel("ruleview").view;
+ await waitUntil(() => getRuleViewLinkByIndex(ruleView, 1));
+ const sourceLabelEl = getRuleViewLinkByIndex(ruleView, 1).querySelector(
+ ".ruleview-rule-source-label"
+ );
+
+ return sourceLabelEl.textContent;
+ });
+
+ is(
+ sourceLabel,
+ "style_browser_toolbox_ruleview_stylesheet.css:1",
+ "source label has the expected value in the ruleview"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_shouldprocessupdates.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_shouldprocessupdates.js
new file mode 100644
index 0000000000..6f5e18791b
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_shouldprocessupdates.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+add_task(async function () {
+ // Running devtools should prevent processing updates. By setting this
+ // environment variable and then inspecting it from the launched devtools
+ // process, we can witness update processing being skipped.
+ Services.env.set("MOZ_TEST_PROCESS_UPDATES", "1");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({});
+
+ let result = await ToolboxTask.spawn(null, async () => {
+ const result = {
+ exists: Services.env.exists("MOZ_TEST_PROCESS_UPDATES"),
+ get: Services.env.get("MOZ_TEST_PROCESS_UPDATES"),
+ };
+ // Log so that we have a hope of debugging.
+ console.log("result", result);
+ return JSON.stringify(result);
+ });
+
+ result = JSON.parse(result);
+ ok(result.exists, "MOZ_TEST_PROCESS_UPDATES exists in subprocess");
+ is(
+ result.get,
+ "ShouldNotProcessUpdates(): DevToolsLaunching",
+ "MOZ_TEST_PROCESS_UPDATES is correct in subprocess"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_unavailable_children.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_unavailable_children.js
new file mode 100644
index 0000000000..5029c62306
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_unavailable_children.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// On debug test machine, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+// This test is used to test a badge displayed in the markup view under content
+// browser elements when switching from Multi Process mode to Parent Process
+// mode.
+
+add_task(async function () {
+ // Forces the Browser Toolbox to open on the inspector by default
+ await pushPref("devtools.browsertoolbox.panel", "inspector");
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=<div id=pick-me>Pickme"
+ );
+ tab.linkedBrowser.setAttribute("test-tab", "true");
+
+ const ToolboxTask = await initBrowserToolboxTask();
+
+ await ToolboxTask.importFunctions({
+ waitUntil,
+ getNodeFront,
+ selectNode,
+ });
+
+ const tabProcessID =
+ tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid;
+
+ const decodedTabURI = decodeURI(tab.linkedBrowser.currentURI.spec);
+
+ await ToolboxTask.spawn(
+ [tabProcessID, isFissionEnabled(), decodedTabURI],
+ async (processID, _isFissionEnabled, tabURI) => {
+ /* global gToolbox */
+ const inspector = gToolbox.getPanel("inspector");
+
+ info("Select the test browser element.");
+ await selectNode('browser[remote="true"][test-tab]', inspector);
+
+ info("Retrieve the node front for selected node.");
+ const browserNodeFront = inspector.selection.nodeFront;
+ ok(!!browserNodeFront, "Retrieved a node front for the browser");
+ is(browserNodeFront.displayName, "browser");
+
+ // Small helper to expand containers and return the child container
+ // matching the provided display name.
+ async function expandContainer(container, expectedChildName) {
+ info(`Expand the node expected to contain a ${expectedChildName}`);
+ await inspector.markup.expandNode(container.node);
+ await waitUntil(() => !!container.getChildContainers().length);
+
+ const children = container
+ .getChildContainers()
+ .filter(child => child.node.displayName === expectedChildName);
+ is(children.length, 1);
+ return children[0];
+ }
+
+ info("Check that the corresponding markup view container has children");
+ const browserContainer = inspector.markup.getContainer(browserNodeFront);
+ ok(browserContainer.hasChildren);
+ ok(
+ !browserContainer.node.childrenUnavailable,
+ "childrenUnavailable un-set"
+ );
+ ok(
+ !browserContainer.elt.querySelector(".unavailable-children"),
+ "The unavailable badge is not displayed"
+ );
+
+ // Store the asserts as a helper to reuse it later in the test.
+ async function assertMarkupView() {
+ info("Check that the children are #document > html > body > div");
+ let container = await expandContainer(browserContainer, "#document");
+ container = await expandContainer(container, "html");
+ container = await expandContainer(container, "body");
+ container = await expandContainer(container, "div");
+
+ info("Select the #pick-me div");
+ await selectNode(container.node, inspector);
+ is(inspector.selection.nodeFront.id, "pick-me");
+ }
+ await assertMarkupView();
+
+ const parentProcessScope = gToolbox.doc.querySelector(
+ 'input[name="chrome-debug-mode"][value="parent-process"]'
+ );
+
+ info("Switch to parent process only scope");
+ const onInspectorUpdated = inspector.once("inspector-updated");
+ parentProcessScope.click();
+ await onInspectorUpdated;
+
+ // Note: `getChildContainers` returns null when the container has no
+ // children, instead of an empty array.
+ await waitUntil(() => browserContainer.getChildContainers() === null);
+
+ ok(!browserContainer.hasChildren, "browser container has no children");
+ ok(browserContainer.node.childrenUnavailable, "childrenUnavailable set");
+ ok(
+ !!browserContainer.elt.querySelector(".unavailable-children"),
+ "The unavailable badge is displayed"
+ );
+
+ const everythingScope = gToolbox.doc.querySelector(
+ 'input[name="chrome-debug-mode"][value="everything"]'
+ );
+
+ info("Switch to multi process scope");
+ everythingScope.click();
+
+ info("Wait until browserContainer has children");
+ await waitUntil(() => browserContainer.hasChildren);
+ ok(browserContainer.hasChildren, "browser container has children");
+ ok(
+ !browserContainer.node.childrenUnavailable,
+ "childrenUnavailable un-set"
+ );
+ ok(
+ !browserContainer.elt.querySelector(".unavailable-children"),
+ "The unavailable badge is no longer displayed"
+ );
+
+ await assertMarkupView();
+ }
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_watchedByDevTools.js b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_watchedByDevTools.js
new file mode 100644
index 0000000000..0eadaaeffe
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_watchedByDevTools.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the "watchedByDevTools" flag is properly handled.
+ */
+
+const EXAMPLE_NET_URI =
+ "https://example.net/document-builder.sjs?html=<div id=net>net";
+const EXAMPLE_COM_URI =
+ "https://example.com/document-builder.sjs?html=<div id=com>com";
+const EXAMPLE_ORG_URI =
+ "https://example.org/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=<div id=org>org</div>";
+
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ is(
+ topWindow.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools isn't set on the parent process browsing context when DevTools aren't opened"
+ );
+
+ // Open 2 tabs that we can check the flag on
+ const tabNet = await addTab(EXAMPLE_NET_URI);
+ const tabCom = await addTab(EXAMPLE_COM_URI);
+
+ is(
+ tabNet.linkedBrowser.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools is not set on the .net tab"
+ );
+ is(
+ tabCom.linkedBrowser.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools is not set on the .com tab"
+ );
+
+ info("Open the BrowserToolbox so the parent process will be watched");
+ const ToolboxTask = await initBrowserToolboxTask();
+
+ is(
+ topWindow.browsingContext.watchedByDevTools,
+ true,
+ "watchedByDevTools is set when the browser toolbox is opened"
+ );
+
+ // Open a new tab when the browser toolbox is opened
+ const newTab = await addTab(EXAMPLE_COM_URI);
+
+ is(
+ tabNet.linkedBrowser.browsingContext.watchedByDevTools,
+ true,
+ "watchedByDevTools is set on the .net tab browsing context after opening the browser toolbox"
+ );
+ is(
+ tabCom.linkedBrowser.browsingContext.watchedByDevTools,
+ true,
+ "watchedByDevTools is set on the .com tab browsing context after opening the browser toolbox"
+ );
+
+ info(
+ "Check that adding watchedByDevTools is set on a tab that was added when the browser toolbox was opened"
+ );
+ is(
+ newTab.linkedBrowser.browsingContext.watchedByDevTools,
+ true,
+ "watchedByDevTools is set on the newly opened tab"
+ );
+
+ info(
+ "Check that watchedByDevTools persist when navigating to a page that creates a new browsing context"
+ );
+ const previousBrowsingContextId = newTab.linkedBrowser.browsingContext.id;
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ encodeURI(EXAMPLE_ORG_URI)
+ );
+ BrowserTestUtils.startLoadingURIString(newTab.linkedBrowser, EXAMPLE_ORG_URI);
+ await onBrowserLoaded;
+
+ isnot(
+ newTab.linkedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ newTab.linkedBrowser.browsingContext.watchedByDevTools,
+ true,
+ "watchedByDevTools is still set after navigating the tab to a page which forces a new browsing context"
+ );
+
+ info("Destroying browser toolbox");
+ await ToolboxTask.destroy();
+
+ is(
+ topWindow.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools was reset when the browser toolbox was closed"
+ );
+
+ is(
+ tabNet.linkedBrowser.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools was reset on the .net tab after closing the browser toolbox"
+ );
+ is(
+ tabCom.linkedBrowser.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools was reset on the .com tab after closing the browser toolbox"
+ );
+ is(
+ newTab.linkedBrowser.browsingContext.watchedByDevTools,
+ false,
+ "watchedByDevTools was reset on the tab opened while the browser toolbox was opened"
+ );
+});
diff --git a/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html
new file mode 100644
index 0000000000..1f365cc17f
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_browser_toolbox_fission_contentframe_inspector.js</title>
+ </head>
+
+ <body>
+ <div id="inside-iframe" test-attribute="fission">Inside iframe</div>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html
new file mode 100644
index 0000000000..853c4ec91c
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_page.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Frame for browser_browser_toolbox_fission_contentframe_inspector.js</title>
+ </head>
+
+ <body>
+ <!-- Here we use example.org, while the embedder is loaded with example.com (.org vs .com)
+ This ensures this frame will be a remote frame when fission is enabled. -->
+ <iframe src="https://example.org/browser/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_fission_contentframe_inspector_frame.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_ruleview_stylesheet.html b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_ruleview_stylesheet.html
new file mode 100644
index 0000000000..3fab2ff8a8
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/doc_browser_toolbox_ruleview_stylesheet.html
@@ -0,0 +1,12 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <link rel="stylesheet" type="text/css" href="style_browser_toolbox_ruleview_stylesheet.css">
+ </head>
+ <body>
+ <div>test div with "color: red" applied from a stylesheet</div>
+ </body>
+</html>
diff --git a/devtools/client/framework/browser-toolbox/test/head.js b/devtools/client/framework/browser-toolbox/test/head.js
new file mode 100644
index 0000000000..4ea18d547e
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/head.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ this
+);
diff --git a/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
new file mode 100644
index 0000000000..bc97c20c01
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js
@@ -0,0 +1,233 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-unused-vars, no-undef */
+
+"use strict";
+
+const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
+ "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
+);
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+/**
+ * Open up a browser toolbox and return a ToolboxTask object for interacting
+ * with it. ToolboxTask has the following methods:
+ *
+ * importFunctions(object)
+ *
+ * The object contains functions from this process which should be defined in
+ * the global evaluation scope of the toolbox. The toolbox cannot load testing
+ * files directly.
+ *
+ * destroy()
+ *
+ * Destroy the browser toolbox and make sure it exits cleanly.
+ *
+ * @param {Object}:
+ * - {Function} existingProcessClose: if truth-y, connect to an existing
+ * browser toolbox process rather than launching a new one and
+ * connecting to it. The given function is expected to return an
+ * object containing an `exitCode`, like `{exitCode}`, and will be
+ * awaited in the returned `destroy()` function. `exitCode` is
+ * asserted to be 0 (success).
+ */
+async function initBrowserToolboxTask({ existingProcessClose } = {}) {
+ if (AppConstants.ASAN) {
+ ok(
+ false,
+ "ToolboxTask cannot be used on ASAN builds. This test should be skipped (Bug 1591064)."
+ );
+ }
+
+ await pushPref("devtools.chrome.enabled", true);
+ await pushPref("devtools.debugger.remote-enabled", true);
+ await pushPref("devtools.browsertoolbox.enable-test-server", true);
+ await pushPref("devtools.debugger.prompt-connection", false);
+
+ // This rejection seems to affect all tests using the browser toolbox.
+ ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+ ).PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
+
+ let process;
+ let dbgProcess;
+ if (!existingProcessClose) {
+ [process, dbgProcess] = await new Promise(resolve => {
+ BrowserToolboxLauncher.init({
+ onRun: (_process, _dbgProcess) => resolve([_process, _dbgProcess]),
+ overwritePreferences: true,
+ });
+ });
+ ok(true, "Browser toolbox started");
+ is(
+ BrowserToolboxLauncher.getBrowserToolboxSessionState(),
+ true,
+ "Has session state"
+ );
+ } else {
+ ok(true, "Connecting to existing browser toolbox");
+ }
+
+ // The port of the DevToolsServer installed in the toolbox process is fixed.
+ // See browser-toolbox/window.js
+ let transport;
+ while (true) {
+ try {
+ transport = await DevToolsClient.socketConnect({
+ host: "localhost",
+ port: 6001,
+ webSocket: false,
+ });
+ break;
+ } catch (e) {
+ await waitForTime(100);
+ }
+ }
+ ok(true, "Got transport");
+
+ const client = new DevToolsClient(transport);
+ await client.connect();
+
+ const commands = await CommandsFactory.forMainProcess({ client });
+ const target = await commands.descriptorFront.getTarget();
+ const consoleFront = await target.getFront("console");
+
+ ok(true, "Connected");
+
+ await importFunctions({
+ info: msg => dump(msg + "\n"),
+ is: (a, b, description) => {
+ let msg =
+ "'" + JSON.stringify(a) + "' is equal to '" + JSON.stringify(b) + "'";
+ if (description) {
+ msg += " - " + description;
+ }
+ if (a !== b) {
+ msg = "FAILURE: " + msg;
+ dump(msg + "\n");
+ throw new Error(msg);
+ } else {
+ msg = "SUCCESS: " + msg;
+ dump(msg + "\n");
+ }
+ },
+ ok: (a, description) => {
+ let msg = "'" + JSON.stringify(a) + "' is true";
+ if (description) {
+ msg += " - " + description;
+ }
+ if (!a) {
+ msg = "FAILURE: " + msg;
+ dump(msg + "\n");
+ throw new Error(msg);
+ } else {
+ msg = "SUCCESS: " + msg;
+ dump(msg + "\n");
+ }
+ },
+ });
+
+ async function evaluateExpression(expression, options = {}) {
+ const onEvaluationResult = consoleFront.once("evaluationResult");
+ await consoleFront.evaluateJSAsync({ text: expression, ...options });
+ return onEvaluationResult;
+ }
+
+ /**
+ * Invoke the given function and argument(s) within the global evaluation scope
+ * of the toolbox. The evaluation scope predefines the name "gToolbox" for the
+ * toolbox itself.
+ *
+ * @param {value|Array<value>} arg
+ * If an Array is passed, we will consider it as the list of arguments
+ * to pass to `fn`. Otherwise we will consider it as the unique argument
+ * to pass to it.
+ * @param {Function} fn
+ * Function to call in the global scope within the browser toolbox process.
+ * This function will be stringified and passed to the process via RDP.
+ * @return {Promise<Value>}
+ * Return the primitive value returned by `fn`.
+ */
+ async function spawn(arg, fn) {
+ // Use JSON.stringify to ensure that we can pass strings
+ // as well as any JSON-able object.
+ const argString = JSON.stringify(Array.isArray(arg) ? arg : [arg]);
+ const rv = await evaluateExpression(`(${fn}).apply(null,${argString})`, {
+ // Use the following argument in order to ensure waiting for the completion
+ // of the promise returned by `fn` (in case this is an async method).
+ mapped: { await: true },
+ });
+ if (rv.exceptionMessage) {
+ throw new Error(`ToolboxTask.spawn failure: ${rv.exceptionMessage}`);
+ } else if (rv.topLevelAwaitRejected) {
+ throw new Error(`ToolboxTask.spawn await rejected`);
+ }
+ return rv.result;
+ }
+
+ async function importFunctions(functions) {
+ for (const [key, fn] of Object.entries(functions)) {
+ await evaluateExpression(`this.${key} = ${fn}`);
+ }
+ }
+
+ async function importScript(script) {
+ const response = await evaluateExpression(script);
+ if (response.hasException) {
+ ok(
+ false,
+ "ToolboxTask.spawn exception while importing script: " +
+ response.exceptionMessage
+ );
+ }
+ }
+
+ let destroyed = false;
+ async function destroy() {
+ // No need to do anything if `destroy` was already called.
+ if (destroyed) {
+ return;
+ }
+
+ const closePromise = existingProcessClose
+ ? existingProcessClose()
+ : dbgProcess.wait();
+ evaluateExpression("gToolbox.destroy()").catch(e => {
+ // Ignore connection close as the toolbox destroy may destroy
+ // everything quickly enough so that evaluate request is still pending
+ if (!e.message.includes("Connection closed")) {
+ throw e;
+ }
+ });
+
+ const { exitCode } = await closePromise;
+ ok(true, "Browser toolbox process closed");
+
+ is(exitCode, 0, "The remote debugger process died cleanly");
+
+ if (!existingProcessClose) {
+ is(
+ BrowserToolboxLauncher.getBrowserToolboxSessionState(),
+ false,
+ "No session state after closing"
+ );
+ }
+
+ await commands.destroy();
+ destroyed = true;
+ }
+
+ // When tests involving using this task fail, the spawned Browser Toolbox is not
+ // destroyed and might impact the next tests (e.g. pausing the content process before
+ // the debugger from the content toolbox does). So make sure to cleanup everything.
+ registerCleanupFunction(destroy);
+
+ return {
+ importFunctions,
+ importScript,
+ spawn,
+ destroy,
+ };
+}
diff --git a/devtools/client/framework/browser-toolbox/test/style_browser_toolbox_ruleview_stylesheet.css b/devtools/client/framework/browser-toolbox/test/style_browser_toolbox_ruleview_stylesheet.css
new file mode 100644
index 0000000000..538fa56f4a
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/test/style_browser_toolbox_ruleview_stylesheet.css
@@ -0,0 +1,3 @@
+div {
+ color: red;
+}