Bug 1354679 - Show the paused debugger overlay when script gets paused; r=ochameau draft
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 07 Apr 2017 10:33:11 +0200
changeset 682186 91a0273d308e4ed8c7cd0a7fc8715898aad2df30
parent 682185 8f9fb8d914b47861e6264c81046c443c24934de5
child 682187 c0349f5d4cf33396382ce7bd2b84dd27249a3741
push id85041
push userbmo:ystartsev@mozilla.com
push dateWed, 18 Oct 2017 09:29:34 +0000
reviewersochameau
bugs1354679
milestone58.0a1
Bug 1354679 - Show the paused debugger overlay when script gets paused; r=ochameau A previous bug had introduced a new overlay (highlighter) that was aimed at being shown when the debugger would pause. This commit adds the necessary code to the ThreadActor for the overlay to be shown when script execution is paused, and hidden when script execution resumes. This commit also adds a test to cover this feature. MozReview-Commit-ID: 3LT5MbJgbYq
devtools/server/actors/highlighters/paused-debugger.js
devtools/server/actors/script.js
devtools/server/tests/unit/test_pausedOverlay.js
devtools/server/tests/unit/xpcshell.ini
devtools/shared/locales/en-US/debugger.properties
--- a/devtools/server/actors/highlighters/paused-debugger.js
+++ b/devtools/server/actors/highlighters/paused-debugger.js
@@ -71,17 +71,38 @@ PausedDebuggerOverlay.prototype = {
     this.markup.destroy();
     this.env = null;
   },
 
   getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
+  /**
+   * Show this highlighter. This is the usual show method that all highlighters need to
+   * implement so they can be used via the CustomHighlighterActor mechanism.
+   * @param {DOMNode} node The context node for this highlighter.
+   * @param {Object} options See showOverlay for doc about this.
+   * @return {Boolean}
+   */
   show(node, options = {}) {
+    return this.showOverlay(options);
+  },
+
+  /**
+   * This contains the actual logic to show the highlighter. In comparison to the show
+   * method, it does not need a first DOMNode parameter which makes it simpler to be
+   * called by other server-side modules if needed.
+   * @param {Object} options A config object that can take 2 properties:
+   * - {String} reason The text that will be shown in the toolbar. If not given, the
+   *   toolbar will not be displayed.
+   * - {Boolean} onlyToolbar Set to true to only show the toolbar, and not the overlay.
+   * @return {Boolean}
+   */
+  showOverlay(options = {}) {
     if (this.env.isXUL) {
       return false;
     }
 
     // Show the highlighter's root element.
     let root = this.getElement("root");
     root.removeAttribute("hidden");
 
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -31,16 +31,19 @@ loader.lazyRequireGetter(this, "findCssS
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 loader.lazyRequireGetter(this, "BreakpointActor", "devtools/server/actors/breakpoint", true);
 loader.lazyRequireGetter(this, "setBreakpointAtEntryPoints", "devtools/server/actors/breakpoint", true);
 loader.lazyRequireGetter(this, "getSourceURL", "devtools/server/actors/source", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "FrameActor", "devtools/server/actors/frame", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
+loader.lazyGetter(this, "l10n",
+  () => Services.strings.createBundle("chrome://devtools-shared/locale/debugger.properties"));
+
 /**
  * A BreakpointActorMap is a map from locations to instances of BreakpointActor.
  */
 function BreakpointActorMap() {
   this._size = 0;
   this._actors = {};
 }
 
@@ -597,16 +600,17 @@ const ThreadActor = ActorClassWithSpec(t
     this._dbg.enabled = false;
     this._dbg = null;
   },
 
   /**
    * destroy the debugger and put the actor in the exited state.
    */
   exit: function () {
+    destroyPausedStateOverlays();
     this.destroy();
     this._state = "exited";
   },
 
   // Request handlers
   onAttach: function (request) {
     if (this.state === "exited") {
       return { type: "exited" };
@@ -741,16 +745,17 @@ const ThreadActor = ActorClassWithSpec(t
             reportError(error);
             return {
               error: "unknownError",
               message: error.message + "\n" + error.stack
             };
           })
           .then(pkt => {
             this.conn.send(pkt);
+            showPausedStateOverlay(this._parent);
           });
 
                     return undefined;
                   });
 
       this._pushThreadPause();
     } catch (e) {
       reportError(e, "Got an exception during TA__pauseAndRespond: ");
@@ -1030,16 +1035,18 @@ const ThreadActor = ActorClassWithSpec(t
     let resumeLimitHandled;
     if (request && request.resumeLimit) {
       resumeLimitHandled = this._handleResumeLimit(request);
     } else {
       this._clearSteppingHooks(this.youngestFrame);
       resumeLimitHandled = resolve(true);
     }
 
+    hidePausedStateOverlay(this._parent);
+
     return resumeLimitHandled.then(() => {
       if (request) {
         this._options.pauseOnExceptions = request.pauseOnExceptions;
         this._options.ignoreCaughtExceptions = request.ignoreCaughtExceptions;
         this.maybePauseOnExceptions();
         this._maybeListenToEvents(request);
       }
 
@@ -1890,16 +1897,18 @@ const ThreadActor = ActorClassWithSpec(t
       }
 
       packet.why = { type: "exception",
                      exception: createValueGrip(value, this._pausePool,
                                                 this.objectGrip)
       };
       this.conn.send(packet);
 
+      showPausedStateOverlay(this._parent);
+
       this._pushThreadPause();
     } catch (e) {
       reportError(e, "Got an exception during TA_onExceptionUnwind: ");
     }
 
     return undefined;
   },
 
@@ -2374,8 +2383,95 @@ exports.unwrapDebuggerObjectGlobal = wra
     // that any dead object proxies make themselves known.
     let global = wrappedGlobal.unsafeDereference();
     Object.getPrototypeOf(global) + "";
     return global;
   } catch (e) {
     return undefined;
   }
 };
+
+// Manage a collection of PausedDebuggerOverlays and their HighlighterEnvironments indexed
+// by TabActors (export it so it the paused state can be tested).
+const pausedStateOverlays = exports.pausedStateOverlays = new Map();
+
+/**
+ * Get the instance of the PausedDebuggerOverlay for a TabActor.
+ *
+ * @param {TabActor} tabActor
+ *        The parent of the threadActor.
+ *
+ * @return {PausedDebuggerOverlay}
+ *         The instance of the overlay, or null if it can't be created in this TabActor.
+ *         For instance, we can't insert an overlay when debugging the whole browser UI
+ *         from the Browser Toolbox or a worker.
+ */
+function getPausedStateOverlay(tabActor) {
+  if (pausedStateOverlays.has(tabActor)) {
+    return pausedStateOverlays.get(tabActor).overlay;
+  }
+
+  if (!tabActor.window || !tabActor.window.document) {
+    return null;
+  }
+
+  const principal = tabActor.window.document.nodePrincipal;
+  if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
+    return null;
+  }
+
+  require("devtools/server/actors/inspector");
+  const { HighlighterEnvironment } = require("devtools/server/actors/highlighters");
+  const { PausedDebuggerOverlay } = require("devtools/server/actors/highlighters/paused-debugger");
+
+  const environment = new HighlighterEnvironment();
+  environment.initFromTabActor(tabActor);
+
+  const overlay = new PausedDebuggerOverlay(environment);
+  pausedStateOverlays.set(tabActor, { overlay, environment });
+
+  return overlay;
+}
+
+/**
+ * Show the PausedDebuggerOverlay, creating it first if it doesn't exist.
+ *
+ * @param {TabActor} tabActor
+ *        The parent of the threadActor.
+ */
+function showPausedStateOverlay(tabActor) {
+  let overlay = getPausedStateOverlay(tabActor);
+  if (overlay) {
+    // TODO: for now, the same reason is always shown. In the future, we would like to
+    // show the same reason that is also shown inside the debugger panel UI (using the
+    // code in aReason.type). See the following issue for why we didn't do this already:
+    // https://github.com/devtools-html/debugger.html/pull/2581
+    overlay.show(null, {reason: l10n.GetStringFromName("debuggerPausedReason")});
+  }
+}
+
+/**
+ * Hide the PausedDebuggerOverlay, if it was shown before.
+ *
+ * @param {TabActor} tabActor
+ *        The parent of the threadActor.
+ */
+function hidePausedStateOverlay(tabActor) {
+  // Check if it even exists first, if not we don't need to do anything.
+  const overlayAndEnvironment = pausedStateOverlays.get(tabActor);
+  if (!overlayAndEnvironment) {
+    return;
+  }
+
+  overlayAndEnvironment.overlay.hide();
+}
+
+/**
+ * Destroy all PausedDebuggerOverlays instances and their HighlighterEnvironments too.
+ * This is needed when we exit the debugger.
+ */
+function destroyPausedStateOverlays() {
+  for (let { overlay, environment } of pausedStateOverlays.values()) {
+    overlay.destroy();
+    environment.destroy();
+  }
+  pausedStateOverlays.clear();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_pausedOverlay.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+const { pausedStateOverlays } = require("devtools/server/actors/script");
+
+/**
+ * Check that the paused overlay appears when the debugger pauses.
+ */
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gTabClient;
+
+// We use a mock overlay here, this test doesn't need to test the overlay itself, only the
+// fact that it gets shown and hidden at the right times.
+var isOverlayVisible = false;
+var mockOverlay = {
+  environment: {},
+  overlay: {
+    show: () => {
+      isOverlayVisible = true;
+    },
+    hide: () => {
+      isOverlayVisible = false;
+    }
+  }
+};
+
+function run_test() {
+  do_test_pending();
+
+  Task.spawn(function* () {
+    initTestDebuggerServer(DebuggerServer);
+    gDebuggee = addTestGlobal("test-overlay", DebuggerServer);
+    gClient = new DebuggerClient(DebuggerServer.connectPipe());
+    yield gClient.connect();
+
+    let [, tabClient, threadClient] = yield attachTestTabAndResume(gClient, "test-overlay");
+    gThreadClient = threadClient;
+    gTabClient = tabClient;
+
+    // Inject a mock overlay here. We need the TabActor reference in order to inject the
+    // mock into the pausedStateOverlays map (go over the transport boundary to do this).
+    pausedStateOverlays.set(
+      gClient._transport._serverConnection.getActor(gTabClient._actor), mockOverlay);
+
+    yield test_debugger_statement();
+    yield test_exception();
+    yield test_breakpoint();
+
+    yield gClient.close();
+    do_test_finished();
+  });
+}
+
+function* test_debugger_statement() {
+  ok(!isOverlayVisible, "The overlay is hidden at first");
+
+  yield executeOnNextTickAndWaitForPause(() => {
+    Cu.evalInSandbox("debugger;", gDebuggee);
+  }, gClient);
+
+  ok(isOverlayVisible, "The overlay is visible on debugger statements");
+
+  yield gThreadClient.resume();
+  ok(!isOverlayVisible, "The overlay is hidden on resume");
+}
+
+function* test_exception() {
+  yield gThreadClient.pauseOnExceptions(true, false);
+
+  ok(!isOverlayVisible, "The overlay is hidden at first");
+
+  yield executeOnNextTickAndWaitForPause(() => {
+    Cu.evalInSandbox("try { throw new Error('test error'); } catch (e) {}", gDebuggee);
+  }, gClient);
+
+  ok(isOverlayVisible,
+    "The overlay is visible when break on exceptions is set and there is an error");
+
+  yield gThreadClient.resume();
+  ok(!isOverlayVisible, "The overlay is hidden on resume");
+
+  yield gThreadClient.pauseOnExceptions(false);
+}
+
+function* test_breakpoint() {
+  ok(!isOverlayVisible, "The overlay is hidden at first");
+
+  executeSoon(() => {
+    Cu.evalInSandbox(
+      `var line0 = new Error().lineNumber;
+      function breakInHere() {
+        return 2;
+      }`,
+      gDebuggee
+    );
+  });
+
+  let { sources } = yield gThreadClient.getSources();
+  let source = gThreadClient.source(sources[sources.length - 1]);
+
+  yield setBreakpoint(source, { line: gDebuggee.line0 + 2 });
+
+  yield executeOnNextTickAndWaitForPause(() => {
+    gDebuggee.breakInHere();
+  }, gClient);
+
+  ok(isOverlayVisible, "The overlay is visible when a breakpoint is found");
+
+  yield gThreadClient.resume();
+  ok(!isOverlayVisible, "The overlay is hidden on resume");
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -62,16 +62,17 @@ support-files =
 [test_frameactor-02.js]
 [test_frameactor-03.js]
 [test_frameactor-04.js]
 [test_frameactor-05.js]
 [test_frameactor_wasm-01.js]
 [test_framearguments-01.js]
 [test_getRuleText.js]
 [test_getTextAtLineColumn.js]
+[test_pausedOverlay.js]
 [test_pauselifetime-01.js]
 [test_pauselifetime-02.js]
 [test_pauselifetime-03.js]
 [test_pauselifetime-04.js]
 [test_threadlifetime-01.js]
 [test_threadlifetime-02.js]
 [test_threadlifetime-03.js]
 [test_threadlifetime-04.js]
--- a/devtools/shared/locales/en-US/debugger.properties
+++ b/devtools/shared/locales/en-US/debugger.properties
@@ -52,8 +52,13 @@ clientSendOOBHash=My Cert: %1$S
 clientSendOOBToken=Token: %1$S
 
 # LOCALIZATION NOTE (serverReceiveOOBTitle): The title displayed on the dialog
 # that instructs the user to provide an authentication token from the client.
 serverReceiveOOBTitle=Provide Client Token
 # LOCALIZATION NOTE (serverReceiveOOBBody): Main text displayed on the dialog
 # that instructs the user to provide an authentication token from the client.
 serverReceiveOOBBody=The client should be displaying a token value.  Enter that token value here to complete authentication with this client.
+
+# LOCALIZATION NOTE (debuggerPausedReason): Text displayed inside the content page, in an
+# overlay, when the debugger is paused (so script execution is paused), explaining to the
+# user why the page is paused.
+debuggerPausedReason=Paused in debugger