Bug 1630957 - Provide settings menu for debugger UI to allow disabling JavaScript r=jlast,Honza
☠☠ backed out by 401003622904 ☠ ☠
authorDavid Walsh <dwalsh@mozilla.com>
Tue, 21 Apr 2020 21:55:42 +0000
changeset 525324 3fc026e3b0e70ae939db76d08f108d3dc8be6077
parent 525323 da168cc5c08f8ec33691f141c55e987e20906f20
child 525325 208d2317ac56072d9aa5f97ae2dbee09b1f97cf3
push id113646
push userdwalsh@mozilla.com
push dateWed, 22 Apr 2020 01:59:13 +0000
treeherderautoland@3fc026e3b0e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast, Honza
bugs1630957
milestone77.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 1630957 - Provide settings menu for debugger UI to allow disabling JavaScript r=jlast,Honza Differential Revision: https://phabricator.services.mozilla.com/D71365
devtools/client/debugger/babel.config.js
devtools/client/debugger/jest-test.config.js
devtools/client/debugger/src/client/firefox/types.js
devtools/client/debugger/src/components/App.js
devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
devtools/client/debugger/src/main.development.js
devtools/client/debugger/src/test/fixtures/Chrome.js
devtools/client/debugger/src/test/fixtures/ChromeUtils.js
devtools/client/debugger/src/test/fixtures/Services.js
devtools/client/debugger/src/test/tests-setup.js
devtools/client/debugger/src/utils/bootstrap.js
devtools/client/debugger/src/utils/prefs.js
devtools/client/debugger/test/mochitest/browser.ini
devtools/client/debugger/test/mochitest/browser_dbg-settings-disable-javascript.js
devtools/client/debugger/test/mochitest/helpers.js
devtools/client/locales/en-US/debugger.properties
--- a/devtools/client/debugger/babel.config.js
+++ b/devtools/client/debugger/babel.config.js
@@ -11,16 +11,17 @@ module.exports = {
   overrides: [
     {
       test: [
         "./src",
         "./packages/*/index.js",
         "./packages/*/src",
         /[/\\]node_modules[/\\]devtools-/,
         /[/\\]node_modules[/\\]react-aria-components[/\\]/,
+        "../../shared",
       ],
       presets: [
         "@babel/preset-react",
         [
           "@babel/preset-env",
           {
             targets: {
               browsers: ["last 1 Chrome version", "last 1 Firefox version"],
--- a/devtools/client/debugger/jest-test.config.js
+++ b/devtools/client/debugger/jest-test.config.js
@@ -36,12 +36,14 @@ module.exports = {
   snapshotSerializers: [
     "jest-serializer-babel-ast",
     "enzyme-to-json/serializer",
   ],
   moduleNameMapper: {
     "\\.css$": "<rootDir>/src/test/__mocks__/styleMock.js",
     "\\.svg$": "<rootDir>/src/test/__mocks__/svgMock.js",
     "^Services": "<rootDir>/src/test/fixtures/Services",
+    "^chrome": "<rootDir>/src/test/fixtures/Chrome",
+    "^ChromeUtils": "<rootDir>/src/test/fixtures/ChromeUtils",
     // Map all require("devtools/...") to the real devtools root.
     "^devtools\\/(.*)": "<rootDir>/../../$1",
   },
 };
--- a/devtools/client/debugger/src/client/firefox/types.js
+++ b/devtools/client/debugger/src/client/firefox/types.js
@@ -426,9 +426,10 @@ export type Panel = {|
   emit: (eventName: string) => void,
   openLink: (url: URL) => void,
   openInspector: () => void,
   openElementInInspector: (grip: Object) => void,
   openConsoleAndEvaluate: (input: string) => void,
   highlightDomElement: (grip: Object) => void,
   unHighlightDomElement: (grip: Object) => void,
   getToolboxStore: () => any,
+  panelWin: Object,
 |};
--- a/devtools/client/debugger/src/components/App.js
+++ b/devtools/client/debugger/src/components/App.js
@@ -53,24 +53,27 @@ import PrimaryPanes from "./PrimaryPanes
 import Editor from "./Editor";
 import SecondaryPanes from "./SecondaryPanes";
 import WelcomeBox from "./WelcomeBox";
 import EditorTabs from "./Editor/Tabs";
 import EditorFooter from "./Editor/Footer";
 import QuickOpenModal from "./QuickOpenModal";
 import WhyPaused from "./SecondaryPanes/WhyPaused";
 
-type OwnProps = {||};
+type OwnProps = {
+  toolboxDoc: Object,
+};
 type Props = {
   selectedSource: ?Source,
   orientation: OrientationType,
   startPanelCollapsed: boolean,
   endPanelCollapsed: boolean,
   activeSearch: ?ActiveSearchType,
   quickOpenEnabled: boolean,
+  toolboxDoc: Object,
   setActiveSearch: typeof actions.setActiveSearch,
   closeActiveSearch: typeof actions.closeActiveSearch,
   closeProjectSearch: typeof actions.closeProjectSearch,
   openQuickOpen: typeof actions.openQuickOpen,
   closeQuickOpen: typeof actions.closeQuickOpen,
   setOrientation: typeof actions.setOrientation,
 };
 
@@ -93,19 +96,23 @@ class App extends Component<Props, State
     super(props);
     this.state = {
       shortcutsModalEnabled: false,
       startPanelSize: 0,
       endPanelSize: 0,
     };
   }
 
-  getChildContext = () => {
-    return { shortcuts, l10n: L10N };
-  };
+  getChildContext() {
+    return {
+      toolboxDoc: this.props.toolboxDoc,
+      shortcuts,
+      l10n: L10N,
+    };
+  }
 
   componentDidMount() {
     horizontalLayoutBreakpoint.addListener(this.onLayoutChange);
     verticalLayoutBreakpoint.addListener(this.onLayoutChange);
     this.setOrientation();
 
     shortcuts.on(L10N.getStr("symbolSearch.search.key2"), (_, e) =>
       this.toggleQuickOpenModal(_, e, "@")
@@ -326,16 +333,17 @@ class App extends Component<Props, State
           {this.renderShortcutsModal()}
         </A11yIntention>
       </div>
     );
   }
 }
 
 App.childContextTypes = {
+  toolboxDoc: PropTypes.object,
   shortcuts: PropTypes.object,
   l10n: PropTypes.object,
 };
 
 const mapStateToProps = state => ({
   selectedSource: getSelectedSource(state),
   startPanelCollapsed: getPaneCollapse(state, "start"),
   endPanelCollapsed: getPaneCollapse(state, "end"),
--- a/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
@@ -4,33 +4,40 @@
 
 // @flow
 
 import PropTypes from "prop-types";
 import React, { Component } from "react";
 
 import { connect } from "../../utils/connect";
 import classnames from "classnames";
-import { features } from "../../utils/prefs";
+import { features, javascriptPrefs } from "../../utils/prefs";
 import {
   getIsWaitingOnBreak,
   getSkipPausing,
   getCurrentThread,
   isTopFrameSelected,
   getThreadContext,
 } from "../../selectors";
 import { formatKeyShortcut } from "../../utils/text";
 import actions from "../../actions";
 import { debugBtn } from "../shared/Button/CommandBarButton";
 import AccessibleImage from "../shared/AccessibleImage";
 import "./CommandBar.css";
 
 import { appinfo } from "devtools-services";
 import type { ThreadContext } from "../../types";
 
+// $FlowIgnore
+const MenuButton = require("devtools/client/shared/components/menu/MenuButton");
+// $FlowIgnore
+const MenuItem = require("devtools/client/shared/components/menu/MenuItem");
+// $FlowIgnore
+const MenuList = require("devtools/client/shared/components/menu/MenuList");
+
 const isMacOS = appinfo.OS === "Darwin";
 
 // NOTE: the "resume" command will call either the resume or breakOnNext action
 // depending on whether or not the debugger is paused or running
 const COMMANDS = ["resume", "stepOver", "stepIn", "stepOut"];
 type CommandActionType = "resume" | "stepOver" | "stepIn" | "stepOut";
 
 const KEYS = {
@@ -223,33 +230,67 @@ class CommandBar extends Component<Props
         }
         onClick={toggleSkipPausing}
       >
         <AccessibleImage className="disable-pausing" />
       </button>
     );
   }
 
+  renderSettingsButton() {
+    const { toolboxDoc } = this.context;
+
+    return (
+      <MenuButton
+        menuId="debugger-settings-menu-button"
+        toolboxDoc={toolboxDoc}
+        className="devtools-button command-bar-button debugger-settings-menu-button"
+        title={L10N.getStr("settings.label")}
+      >
+        {() => this.renderSettingsMenuItems()}
+      </MenuButton>
+    );
+  }
+
+  renderSettingsMenuItems() {
+    return (
+      <MenuList id="debugger-settings-menu-list">
+        <MenuItem
+          key="debugger-settings-menu-item-disable-javascript"
+          className="menu-item debugger-settings-menu-item-disable-javascript"
+          checked={!javascriptPrefs.enableJavaScript}
+          label={L10N.getStr("settings.disableJavaScript.label")}
+          tooltip={L10N.getStr("settings.disableJavaScript.tooltip")}
+          onClick={() => {
+            javascriptPrefs.enableJavaScript = !javascriptPrefs.enableJavaScript;
+          }}
+        />
+      </MenuList>
+    );
+  }
+
   render() {
     return (
       <div
         className={classnames("command-bar", {
           vertical: !this.props.horizontal,
         })}
       >
         {this.renderStepButtons()}
         <div className="filler" />
         {this.renderSkipPausingButton()}
+        {this.renderSettingsButton()}
       </div>
     );
   }
 }
 
 CommandBar.contextTypes = {
   shortcuts: PropTypes.object,
+  toolboxDoc: PropTypes.object,
 };
 
 const mapStateToProps = state => ({
   cx: getThreadContext(state),
   isWaitingOnBreak: getIsWaitingOnBreak(state, getCurrentThread(state)),
   skipPausing: getSkipPausing(state),
   topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
 });
--- a/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
@@ -14,26 +14,40 @@
   min-width: 30px;
 }
 
 .command-bar-button:disabled {
   opacity: 0.6;
   cursor: default;
 }
 
-.command-bar-button:not(.disabled):hover {
+.command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:not(:disabled):not([aria-expanded="true"]):hover {
   background: var(--theme-toolbar-background-hover);
 }
 
-.theme-dark .command-bar-button:not(.disabled):hover {
+.theme-dark .command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:not(:disabled):not([aria-expanded="true"]):hover {
   background: var(--theme-toolbar-hover);
 }
 
 :root.theme-dark .command-bar-button {
   color: var(--theme-body-color);
 }
 
 .command-bar-button > * {
   width: 16px;
   height: 16px;
   display: inline-block;
   vertical-align: middle;
 }
+
+/**
+ * Settings icon and menu
+ */
+.devtools-button.debugger-settings-menu-button {
+  border-radius: 0;
+  margin: 0;
+}
+
+.devtools-button.debugger-settings-menu-button::before {
+  background-image: url("chrome://devtools/skin/images/settings.svg");
+}
--- a/devtools/client/debugger/src/main.development.js
+++ b/devtools/client/debugger/src/main.development.js
@@ -28,10 +28,11 @@ bootstrap(React, ReactDOM).then(connecti
     openConsoleAndEvaluate: input => alert(`console.log: ${input}`),
     highlightDomElement: (grip: Object) =>
       console.log("highlighting dom element"),
     unHighlightDomElement: (grip: Object) =>
       console.log("unhighlighting dom element"),
     getToolboxStore: () => {
       throw new Error("Cannot connect to Toolbox store when running Launchpad");
     },
+    panelWin: window,
   });
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/src/test/fixtures/Chrome.js
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+  Cu: {},
+  components: {},
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/src/test/fixtures/ChromeUtils.js
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {};
--- a/devtools/client/debugger/src/test/fixtures/Services.js
+++ b/devtools/client/debugger/src/test/fixtures/Services.js
@@ -1,9 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 module.exports = {
   appinfo: "",
+  prefs: { getBoolPref: () => {}, addObserver: () => {} },
+  obs: { addObserver: () => {} },
 };
--- a/devtools/client/debugger/src/test/tests-setup.js
+++ b/devtools/client/debugger/src/test/tests-setup.js
@@ -3,17 +3,16 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 // $FlowIgnore
 global.Worker = require("workerjs");
 
 import path from "path";
-// import getConfig from "../../bin/getConfig";
 import { readFileSync } from "fs";
 import Enzyme from "enzyme";
 // $FlowIgnore
 import Adapter from "enzyme-adapter-react-16";
 import { setupHelper } from "../utils/dbg";
 import { prefs } from "../utils/prefs";
 
 import { startSourceMapWorker, stopSourceMapWorker } from "devtools-source-map";
@@ -46,16 +45,53 @@ function getL10nBundle() {
   }
 }
 
 global.DebuggerConfig = {};
 global.L10N = require("devtools-launchpad").L10N;
 global.L10N.setBundle(getL10nBundle());
 global.jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
 global.performance = { now: () => 0 };
+global.isWorker = false;
+
+global.define = function() {};
+global.loader = {
+  lazyServiceGetter: () => {},
+  lazyGetter: (context, name, fn) => {
+    try {
+      global[name] = fn();
+    } catch (_) {}
+  },
+  lazyRequireGetter: (context, name, _path, destruct) => {
+    if (
+      !_path ||
+      _path.startsWith("resource://") ||
+      _path.match(/server\/actors/)
+    ) {
+      return;
+    }
+
+    const excluded = [
+      "Debugger",
+      "devtools/shared/event-emitter",
+      "devtools/client/shared/autocomplete-popup",
+      "devtools/client/framework/devtools",
+      "devtools/client/shared/keycodes",
+      "devtools/client/shared/sourceeditor/editor",
+      "devtools/client/shared/telemetry",
+      "devtools/shared/screenshot/save",
+      "devtools/client/shared/focus",
+    ];
+    if (!excluded.includes(_path)) {
+      // $FlowIgnore
+      const module = require(_path);
+      global[name] = destruct ? module[name] : module;
+    }
+  },
+};
 
 const { URL } = require("url");
 global.URL = URL;
 
 global.indexedDB = mockIndexeddDB();
 
 Enzyme.configure({ adapter: new Adapter() });
 
--- a/devtools/client/debugger/src/utils/bootstrap.js
+++ b/devtools/client/debugger/src/utils/bootstrap.js
@@ -37,24 +37,26 @@ function renderPanel(component, store, p
   root.className = "launchpad-root theme-body";
   root.style.setProperty("flex", "1");
   const mount = document.querySelector("#mount");
   if (!mount) {
     return;
   }
   mount.appendChild(root);
 
+  const toolboxDoc = panel.panelWin.parent.document;
+
   ReactDOM.render(
     React.createElement(
       Provider,
       { store },
       React.createElement(
         ToolboxProvider,
         { store: panel.getToolboxStore() },
-        React.createElement(component)
+        React.createElement(component, { toolboxDoc })
       )
     ),
     root
   );
 }
 
 type Workers = {
   sourceMaps: typeof SourceMaps,
--- a/devtools/client/debugger/src/utils/prefs.js
+++ b/devtools/client/debugger/src/utils/prefs.js
@@ -72,16 +72,17 @@ if (isDevelopment()) {
   pref("devtools.debugger.features.event-listeners-breakpoints", true);
   pref("devtools.debugger.features.dom-mutation-breakpoints", true);
   pref("devtools.debugger.features.log-points", true);
   pref("devtools.debugger.features.inline-preview", true);
   pref("devtools.debugger.features.overlay-step-buttons", true);
   pref("devtools.debugger.features.watchpoints", true);
   pref("devtools.debugger.features.frame-step", true);
   pref("devtools.editor.tabsize", 2);
+  pref("javascript.enabled", true);
 }
 
 export const prefs = new PrefsHelper("devtools", {
   fission: ["Bool", "browsertoolbox.fission"],
   logging: ["Bool", "debugger.logging"],
   editorWrapping: ["Bool", "debugger.ui.editor-wrapping"],
   alphabetizeOutline: ["Bool", "debugger.alphabetize-outline"],
   autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
@@ -116,16 +117,20 @@ export const prefs = new PrefsHelper("de
   projectDirectoryRoot: ["Char", "debugger.project-directory-root", ""],
   skipPausing: ["Bool", "debugger.skip-pausing"],
   mapScopes: ["Bool", "debugger.map-scopes-enabled"],
   logActions: ["Bool", "debugger.log-actions"],
   logEventBreakpoints: ["Bool", "debugger.log-event-breakpoints"],
   indentSize: ["Int", "editor.tabsize"],
 });
 
+export const javascriptPrefs = new PrefsHelper("javascript", {
+  enableJavaScript: ["Bool", "enabled"],
+});
+
 // The pref may not be defined. Defaulting to null isn't viable (cursor never blinks).
 // Can't use CodeMirror.defaults here because it's loaded later.
 // Hardcode the fallback value to that of CodeMirror.defaults.cursorBlinkRate.
 prefs.cursorBlinkRate = Services.prefs.getIntPref("ui.caretBlinkTime", 530);
 
 export const features = new PrefsHelper("devtools.debugger.features", {
   asyncStepping: ["Bool", "async-stepping"],
   wasm: ["Bool", "wasm"],
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -198,8 +198,9 @@ skip-if = (os == 'linux' && debug) || (o
 skip-if = debug
 [browser_dbg-toolbox-unselected-pause.js]
 skip-if = asan # Bug 1591064
 [browser_dbg-toolbox-workers.js]
 skip-if = asan || !nightly_build # Bug 1591064, parent intercept mode is needed bug 1588154
 [browser_dbg-wrong-fetch.js]
 [browser_dbg-worker-nested.js]
 [browser_dbg-step-in-navigate.js]
+[browser_dbg-settings-disable-javascript.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-settings-disable-javascript.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+async function toggleJavaScript(dbg, shouldBeCheckedAtStart) {
+  const menuItemClassName = ".debugger-settings-menu-item-disable-javascript";
+
+  const menuButton = findElementWithSelector(dbg, ".debugger-settings-menu-button");
+  menuButton.click();
+
+  // Wait for the menu to show before trying to click the item
+  const { parent } = dbg.panel.panelWin;
+  const { document } = parent;
+
+  const menuItem = document.querySelector(menuItemClassName);
+  is(
+    !!menuItem.getAttribute("aria-checked"), 
+    shouldBeCheckedAtStart,
+    "Item is checked before clicking"
+  );
+  menuItem.click();
+}
+
+// Tests that using the Settings menu to enable and disable JavaScript
+// updates the pref properly
+add_task(async function() {
+  const dbg = await initDebugger("doc-scripts.html", "simple1.js");
+
+  info("Clicking the disable javascript button in the settings menu");
+  await toggleJavaScript(dbg, false);
+  is(Services.prefs.getBoolPref("javascript.enabled"), false, "JavaScript is disabled");
+
+  info("Clicking the disable javascript button in the settings menu to reenable JavaScript");
+  await toggleJavaScript(dbg, true);
+  is(Services.prefs.getBoolPref("javascript.enabled"), true, "JavaScript is enabled");
+});
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -576,16 +576,17 @@ async function clearDebuggerPreferences(
   Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions");
   Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
   Services.prefs.clearUserPref("devtools.debugger.expressions");
   Services.prefs.clearUserPref("devtools.debugger.breakpoints-visible");
   Services.prefs.clearUserPref("devtools.debugger.call-stack-visible");
   Services.prefs.clearUserPref("devtools.debugger.scopes-visible");
   Services.prefs.clearUserPref("devtools.debugger.skip-pausing");
   Services.prefs.clearUserPref("devtools.debugger.map-scopes-enabled");
+  Services.prefs.clearUserPref("javascript.enabled");
   await pushPref("devtools.debugger.log-actions", true);
   for (const pref of prefs) {
     await pushPref(...pref);
   }
 }
 
 /**
  * Intilializes the debugger.
--- a/devtools/client/locales/en-US/debugger.properties
+++ b/devtools/client/locales/en-US/debugger.properties
@@ -474,16 +474,27 @@ downloadFile.accesskey=d
 # LOCALIZATION NOTE (inlinePreview.show.label): Context menu item
 # for showing the inline preview blocks
 inlinePreview.show.label=Show inline preview
 
 # LOCALIZATION NOTE (inlinePreview.hide.label): Context menu item
 # for hiding the inline preview block
 inlinePreview.hide.label=Hide inline preview
 
+# LOCALIZATION NOTE (settings.label): Label for Settings button
+settings.label=Debugger Settings
+
+# LOCALIZATION NOTE (settings.disableJavaScript.label): Context menu item
+# tooltip for disabling JavaScript
+settings.disableJavaScript.label=Disable JavaScript
+
+# LOCALIZATION NOTE (settings.disableJavaScript.tooltip): Context menu item
+# tooltip for disabling JavaScript
+settings.disableJavaScript.tooltip=Disables JavaScript in the current tab (Requires refresh)
+
 # LOCALIZATION NOTE (preview.noProperties): Label shown in the preview
 # popup when there are no properties to show.
 preview.noProperties=No properties
 
 # LOCALIZATION NOTE (framework.disableGrouping): This is the text that appears in the
 # context menu to disable framework grouping.
 framework.disableGrouping=Disable framework grouping
 framework.disableGrouping.accesskey=u