Bug 1780156 - [devtools] Add EvaluationContextSelector to BrowserConsole. r=ochameau.
☠☠ backed out by 43809ea6bc75 ☠ ☠
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 19 Jul 2022 14:02:58 +0000
changeset 624382 61c455e77edc90c30c68ffc2fb5668f2da59775e
parent 624381 2acbfa79b63eff4a30263ad78b97c35e5b1328b6
child 624383 6040e53370dce5879930402475b818d0589bb53a
push id166409
push usernchevobbe@mozilla.com
push dateTue, 19 Jul 2022 14:05:37 +0000
treeherderautoland@6040e53370dc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1780156
milestone104.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1780156 - [devtools] Add EvaluationContextSelector to BrowserConsole. r=ochameau. Differential Revision: https://phabricator.services.mozilla.com/D150090
devtools/client/webconsole/actions/input.js
devtools/client/webconsole/components/Input/EditorToolbar.js
devtools/client/webconsole/components/Input/EvaluationContextSelector.js
devtools/client/webconsole/components/Input/JSTerm.js
devtools/client/webconsole/test/browser/_browser_console.ini
devtools/client/webconsole/test/browser/browser_console_evaluation_context_selector.js
devtools/client/webconsole/test/browser/head.js
devtools/client/webconsole/webconsole-wrapper.js
--- a/devtools/client/webconsole/actions/input.js
+++ b/devtools/client/webconsole/actions/input.js
@@ -50,16 +50,22 @@ loader.lazyRequireGetter(
   true
 );
 loader.lazyRequireGetter(
   this,
   "createSimpleTableMessage",
   "devtools/client/webconsole/utils/messages",
   true
 );
+loader.lazyRequireGetter(
+  this,
+  "getSelectedTarget",
+  "devtools/shared/commands/target/selectors/targets",
+  true
+);
 
 const HELP_URL =
   "https://firefox-source-docs.mozilla.org/devtools-user/web_console/helpers/";
 
 async function getMappedExpression(hud, expression) {
   let mapResult;
   try {
     mapResult = await hud.getMappedExpression(expression);
@@ -108,17 +114,19 @@ function evaluateExpression(expression, 
     // Even if the evaluation fails,
     // we still need to pass the error response to onExpressionEvaluated.
     const onSettled = res => res;
 
     const response = await commands.scriptCommand
       .execute(expression, {
         frameActor: hud.getSelectedFrameActorID(),
         selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
-        selectedTargetFront: toolbox && toolbox.getSelectedTargetFront(),
+        selectedTargetFront: getSelectedTarget(
+          webConsoleUI.hud.commands.targetCommand.store.getState()
+        ),
         mapped,
       })
       .then(onSettled, onSettled);
 
     const serverConsoleCommandTimestamp = response.startTime;
 
     // In case of remote debugging, it might happen that the debuggee page does not have
     // the exact same clock time as the client. This could cause some ordering issues
@@ -239,17 +247,18 @@ function handleHelperResult(response) {
                 ),
               },
             ])
           );
           break;
         case "screenshotOutput":
           const { args, value } = helperResult;
           const targetFront =
-            toolbox?.getSelectedTargetFront() || hud.currentTarget;
+            getSelectedTarget(hud.commands.targetCommand.store.getState()) ||
+            hud.currentTarget;
           let screenshotMessages;
 
           // @backward-compat { version 87 } The screenshot-content actor isn't available
           // in older server.
           // With an old server, the console actor captures the screenshot when handling
           // the command, and send it to the client which only needs to save it to a file.
           // With a new server, the server simply acknowledges the command,
           // and the client will drive the whole screenshot process (capture and save).
@@ -366,24 +375,17 @@ function setInputValue(value) {
  * Request an eager evaluation from the server.
  *
  * @param {String} expression: The expression to evaluate.
  * @param {Boolean} force: When true, will request an eager evaluation again, even if
  *                         the expression is the same one than the one that was used in
  *                         the previous evaluation.
  */
 function terminalInputChanged(expression, force = false) {
-  return async ({
-    dispatch,
-    webConsoleUI,
-    hud,
-    toolbox,
-    commands,
-    getState,
-  }) => {
+  return async ({ dispatch, webConsoleUI, hud, commands, getState }) => {
     const prefs = getAllPrefs(getState());
     if (!prefs.eagerEvaluation) {
       return null;
     }
 
     const { terminalInput = "" } = getState().history;
 
     // Only re-evaluate if the expression did change.
@@ -412,17 +414,19 @@ function terminalInputChanged(expression
     }
 
     let mapped;
     ({ expression, mapped } = await getMappedExpression(hud, expression));
 
     const response = await commands.scriptCommand.execute(expression, {
       frameActor: hud.getSelectedFrameActorID(),
       selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
-      selectedTargetFront: toolbox && toolbox.getSelectedTargetFront(),
+      selectedTargetFront: getSelectedTarget(
+        hud.commands.targetCommand.store.getState()
+      ),
       mapped,
       eager: true,
     });
 
     return dispatch({
       type: SET_TERMINAL_EAGER_RESULT,
       result: getEagerEvaluationResult(response),
     });
--- a/devtools/client/webconsole/components/Input/EditorToolbar.js
+++ b/devtools/client/webconsole/components/Input/EditorToolbar.js
@@ -55,20 +55,17 @@ class EditorToolbar extends Component {
       actions.reverseSearchInputToggle({
         initialValue: serviceContainer.getInputSelection(),
         access: "editor-toolbar-icon",
       })
     );
   }
 
   renderEvaluationContextSelector() {
-    if (
-      !this.props.webConsoleUI.wrapper.toolbox ||
-      !this.props.showEvaluationContextSelector
-    ) {
+    if (!this.props.showEvaluationContextSelector) {
       return null;
     }
 
     return EvaluationContextSelector({
       webConsoleUI: this.props.webConsoleUI,
     });
   }
 
--- a/devtools/client/webconsole/components/Input/EvaluationContextSelector.js
+++ b/devtools/client/webconsole/components/Input/EvaluationContextSelector.js
@@ -227,23 +227,27 @@ class EvaluationContextSelector extends 
       return l10n.getStr("webconsole.input.selector.top");
     }
 
     return selectedTarget.name;
   }
 
   render() {
     const { webConsoleUI, targets, selectedTarget } = this.props;
+
+    // Don't render if there's only one target.
+    // Also bail out if the console is being destroyed (where WebConsoleUI.wrapper gets
+    // nullified).
+    if (targets.length <= 1 || !webConsoleUI.wrapper) {
+      return null;
+    }
+
     const doc = webConsoleUI.document;
     const { toolbox } = webConsoleUI.wrapper;
 
-    if (targets.length <= 1) {
-      return null;
-    }
-
     return MenuButton(
       {
         menuId: "webconsole-input-evaluationsButton",
         toolboxDoc: toolbox ? toolbox.doc : doc,
         label: this.getLabel(),
         className:
           "webconsole-evaluation-selector-button devtools-button devtools-dropdown-button" +
           (selectedTarget && !selectedTarget.isTopLevel
--- a/devtools/client/webconsole/components/Input/JSTerm.js
+++ b/devtools/client/webconsole/components/Input/JSTerm.js
@@ -1484,21 +1484,17 @@ class JSTerm extends Component {
       title: l10n.getFormatStr("webconsole.input.openEditorButton.tooltip2", [
         isMacOS ? "Cmd + B" : "Ctrl + B",
       ]),
       onClick: this.props.editorToggle,
     });
   }
 
   renderEvaluationContextSelector() {
-    if (
-      !this.props.webConsoleUI.wrapper.toolbox ||
-      this.props.editorMode ||
-      !this.props.showEvaluationContextSelector
-    ) {
+    if (this.props.editorMode || !this.props.showEvaluationContextSelector) {
       return null;
     }
 
     return EvaluationContextSelector(this.props);
   }
 
   renderEditorOnboarding() {
     if (!this.props.showEditorOnboarding) {
--- a/devtools/client/webconsole/test/browser/_browser_console.ini
+++ b/devtools/client/webconsole/test/browser/_browser_console.ini
@@ -35,16 +35,17 @@ skip-if = true # Bug 1437843
 skip-if = (os == "linux") || (os == "win") || (os == "mac" && !debug) # Bug 1440059, disabled for all build types, Bug 1609460
 [browser_console_context_menu_export_console_output.js]
 [browser_console_dead_objects.js]
 [browser_console_devtools_loader_exception.js]
 [browser_console_eager_eval.js]
 [browser_console_enable_network_monitoring.js]
 skip-if = verify
 [browser_console_error_source_click.js]
+[browser_console_evaluation_context_selector.js]
 [browser_console_filters.js]
 [browser_console_ignore_debugger_statement.js]
 [browser_console_jsterm_await.js]
 [browser_console_nsiconsolemessage.js]
 [browser_console_open_or_focus.js]
 [browser_console_restore.js]
 skip-if = verify
 [browser_console_screenshot.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_evaluation_context_selector.js
@@ -0,0 +1,189 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function() {
+  await pushPref("devtools.chrome.enabled", true);
+
+  const hud = await BrowserConsoleManager.toggleBrowserConsole();
+
+  const evaluationContextSelectorButton = await waitFor(() =>
+    hud.ui.outputNode.querySelector(".webconsole-evaluation-selector-button")
+  );
+
+  if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+    is(
+      evaluationContextSelectorButton,
+      null,
+      "context selector is only displayed when Fission or EFT is enabled"
+    );
+    return;
+  }
+
+  ok(
+    evaluationContextSelectorButton,
+    "The evaluation context selector is visible"
+  );
+  is(
+    evaluationContextSelectorButton.innerText,
+    "Top",
+    "The button has the expected 'Top' text"
+  );
+  is(
+    evaluationContextSelectorButton.classList.contains(
+      "webconsole-evaluation-selector-button-non-top"
+    ),
+    false,
+    "The non-top class isn't applied"
+  );
+
+  await executeAndWaitForResultMessage(
+    hud,
+    "document.location",
+    "chrome://browser/content/browser.xhtml"
+  );
+  ok(true, "The evaluation was done in the top context");
+
+  setInputValue(hud, "document.location.host");
+  await waitForEagerEvaluationResult(hud, `"browser"`);
+
+  info("Check the context selector menu");
+  checkContextSelectorMenuItemAt(hud, 0, {
+    label: "Top",
+    tooltip: "chrome://browser/content/browser.xhtml",
+    checked: true,
+  });
+  checkContextSelectorMenuItemAt(hud, 1, {
+    separator: true,
+  });
+
+  info(
+    "Add a tab with a worker and check both the document and the worker are displayed in the context selector"
+  );
+  const documentFile = "test-evaluate-worker.html";
+  const documentWithWorkerUrl =
+    "https://example.com/browser/devtools/client/webconsole/test/browser/" +
+    documentFile;
+  const tab = await addTab(documentWithWorkerUrl);
+
+  const documentIndex = await waitFor(() => {
+    const i = getContextSelectorItems(hud).findIndex(el =>
+      el.querySelector(".label")?.innerText?.endsWith(documentFile)
+    );
+    return i == -1 ? null : i;
+  });
+
+  info("Select the document target");
+  selectTargetInContextSelector(hud, documentWithWorkerUrl);
+
+  await waitFor(() =>
+    evaluationContextSelectorButton.innerText.includes(documentFile)
+  );
+  ok(true, "The context was set to the selected document");
+  is(
+    evaluationContextSelectorButton.classList.contains(
+      "webconsole-evaluation-selector-button-non-top"
+    ),
+    true,
+    "The non-top class is applied"
+  );
+
+  checkContextSelectorMenuItemAt(hud, documentIndex, {
+    label: documentWithWorkerUrl,
+    tooltip: documentWithWorkerUrl,
+    checked: true,
+  });
+
+  await waitForEagerEvaluationResult(hud, `"example.com"`);
+  ok(true, "The instant evaluation result is updated in the document context");
+
+  await executeAndWaitForResultMessage(
+    hud,
+    "document.location",
+    documentWithWorkerUrl
+  );
+  ok(true, "The evaluation is done in the document context");
+
+  // set input text so we can watch for instant evaluation result update
+  setInputValue(hud, "globalThis.location.href");
+  await waitForEagerEvaluationResult(hud, `"${documentWithWorkerUrl}"`);
+
+  info("Select the worker target");
+  const workerFile = "test-evaluate-worker.js";
+  const workerUrl =
+    "https://example.com/browser/devtools/client/webconsole/test/browser/" +
+    workerFile;
+  const workerIndex = await waitFor(() => {
+    const i = getContextSelectorItems(hud).findIndex(el =>
+      el.querySelector(".label")?.innerText?.endsWith(workerFile)
+    );
+    return i == -1 ? null : i;
+  });
+  selectTargetInContextSelector(hud, workerFile);
+
+  await waitFor(() =>
+    evaluationContextSelectorButton.innerText.includes(workerFile)
+  );
+  ok(true, "The context was set to the selected worker");
+
+  await waitForEagerEvaluationResult(hud, `"${workerUrl}"`);
+  ok(true, "The instant evaluation result is updated in the worker context");
+
+  checkContextSelectorMenuItemAt(hud, workerIndex, {
+    label: workerFile,
+    tooltip: workerFile,
+    checked: true,
+  });
+
+  await executeAndWaitForResultMessage(
+    hud,
+    "globalThis.location",
+    `WorkerLocation`
+  );
+  ok(true, "The evaluation is done in the worker context");
+
+  // set input text so we can watch for instant evaluation result update
+  setInputValue(hud, "document.location.host");
+  await waitForEagerEvaluationResult(
+    hud,
+    `ReferenceError: document is not defined`
+  );
+
+  info(
+    "Remove the tab and make sure both the document and worker target are removed from the context selector"
+  );
+  await removeTab(tab);
+
+  await waitFor(() => evaluationContextSelectorButton.innerText == "Top");
+  ok(true, "The context is set back to Top");
+
+  checkContextSelectorMenuItemAt(hud, 0, {
+    label: "Top",
+    tooltip: "chrome://browser/content/browser.xhtml",
+    checked: true,
+  });
+
+  is(
+    getContextSelectorItems(hud).every(el => {
+      const label = el.querySelector(".label")?.innerText;
+      return (
+        !label ||
+        (label !== "test-evaluate-worker.html" && label !== workerFile)
+      );
+    }),
+    true,
+    "the document and worker targets were removed"
+  );
+
+  await waitForEagerEvaluationResult(hud, `"browser"`);
+  ok(true, "The instant evaluation was done in the top context");
+
+  await executeAndWaitForResultMessage(
+    hud,
+    "document.location",
+    "chrome://browser/content/browser.xhtml"
+  );
+  ok(true, "The evaluation was done in the top context");
+});
--- a/devtools/client/webconsole/test/browser/head.js
+++ b/devtools/client/webconsole/test/browser/head.js
@@ -1832,55 +1832,63 @@ function getContextSelectorItems(hud) {
   return Array.from(list.querySelectorAll("li.menuitem button, hr"));
 }
 
 /**
  * Check that the evaluation context selector menu has the expected item, in the expected
  * state.
  *
  * @param {WebConsole} hud
- * @param {Array<Object>} expected: An array of object which can have the following shape:
- *         - {String} label: The label of the target
- *         - {String} tooltip: The tooltip of the target element in the menu
- *         - {Boolean} checked: if the target should be selected or not
- *         - {Boolean} separator: if the element is a simple separator
+ * @param {Array<Object>} expected: An array of object (see checkContextSelectorMenuItemAt
+ *                        for expected properties)
  */
 function checkContextSelectorMenu(hud, expected) {
   const items = getContextSelectorItems(hud);
 
   is(
     items.length,
     expected.length,
     "The context selector menu has the expected number of items"
   );
 
-  expected.forEach(({ label, tooltip, checked, separator }, i) => {
-    const el = items[i];
-
-    if (separator === true) {
-      is(
-        el.getAttribute("role"),
-        "menuseparator",
-        "The element is a separator"
-      );
-      return;
-    }
+  expected.forEach((expectedItem, i) => {
+    checkContextSelectorMenuItemAt(hud, i, expectedItem);
+  });
+}
 
-    const elChecked = el.getAttribute("aria-checked") === "true";
-    const elTooltip = el.getAttribute("title");
-    const elLabel = el.querySelector(".label").innerText;
+/**
+ * Check that the evaluation context selector menu has the expected item at the specified index.
+ *
+ * @param {WebConsole} hud
+ * @param {Number} index
+ * @param {Object} expected
+ * @param {String} expected.label: The label of the target
+ * @param {String} expected.tooltip: The tooltip of the target element in the menu
+ * @param {Boolean} expected.checked: if the target should be selected or not
+ * @param {Boolean} expected.separator: if the element is a simple separator
+ */
+function checkContextSelectorMenuItemAt(hud, index, expected) {
+  const el = getContextSelectorItems(hud).at(index);
 
-    is(elLabel, label, `The item has the expected label`);
-    is(elTooltip, tooltip, `Item "${label}" has the expected tooltip`);
-    is(
-      elChecked,
-      checked,
-      `Item "${label}" is ${checked ? "checked" : "unchecked"}`
-    );
-  });
+  if (expected.separator === true) {
+    is(el.getAttribute("role"), "menuseparator", "The element is a separator");
+    return;
+  }
+
+  const elChecked = el.getAttribute("aria-checked") === "true";
+  const elTooltip = el.getAttribute("title");
+  const elLabel = el.querySelector(".label").innerText;
+
+  is(elLabel, expected.label, `The item has the expected label`);
+  is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`);
+  is(
+    elChecked,
+    expected.checked,
+    `Item "${elLabel}" is ${expected.checked ? "checked" : "unchecked"}`
+  );
 }
 
 /**
  * Select a target in the context selector.
  *
  * @param {WebConsole} hud
  * @param {String} targetLabel: The label of the target to select.
  */
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -45,28 +45,26 @@ loader.lazyRequireGetter(
 );
 
 // Localized strings for (devtools/client/locales/en-US/startup.properties)
 loader.lazyGetter(this, "L10N", function() {
   const { LocalizationHelper } = require("devtools/shared/l10n");
   return new LocalizationHelper("devtools/client/locales/startup.properties");
 });
 
-function renderApp({ app, store, toolbox, root }) {
+function renderApp({ app, store, commands, root }) {
   return ReactDOM.render(
     createElement(
       Provider,
       { store },
-      toolbox
-        ? createElement(
-            createProvider(toolbox.commands.targetCommand.storeId),
-            { store: toolbox.commands.targetCommand.store },
-            app
-          )
-        : app
+      createElement(
+        createProvider(commands.targetCommand.storeId),
+        { store: commands.targetCommand.store },
+        app
+      )
     ),
     root
   );
 }
 
 let store = null;
 
 class WebConsoleWrapper {
@@ -130,17 +128,17 @@ class WebConsoleWrapper {
       );
 
       // Render the root Application component.
       if (this.parentNode) {
         this.body = renderApp({
           app,
           store,
           root: this.parentNode,
-          toolbox: this.toolbox,
+          commands: this.hud.commands,
         });
       } else {
         // If there's no parentNode, we are in a test. So we can resolve immediately.
         resolve();
       }
     });
   }