Bug 1424721 - Allow long strings and invisible-to-debugger objects to be stored as global variables. draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Mon, 18 Dec 2017 12:52:31 +0100
changeset 712655 5dd75ccebcecdcbd303c387cba809a03c7fbf45c
parent 712645 5572465c08a9ce0671dcd01c721f9356fcd53a65
child 744099 870d68374ffe7508a3df9a0fe304a9c3606c1f5e
push id93386
push userbmo:oriol-bugzilla@hotmail.com
push dateMon, 18 Dec 2017 12:08:18 +0000
bugs1424721
milestone59.0a1
Bug 1424721 - Allow long strings and invisible-to-debugger objects to be stored as global variables. MozReview-Commit-ID: IZFKgror7F6
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_store_as_global.js
devtools/server/actors/object.js
devtools/server/actors/webconsole.js
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_store_as_global.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_store_as_global.js
@@ -7,85 +7,79 @@
 // clicking on messages that are associated with an object actor.
 
 "use strict";
 
 const TEST_URI = `data:text/html;charset=utf-8,<script>
   window.bar = { baz: 1 };
   console.log("foo");
   console.log("foo", window.bar);
-  console.log(["foo", window.bar, 2]);
+  window.array = ["foo", window.bar, 2];
+  console.log(window.array);
+  window.longString = "foo" + "a".repeat(1e4);
+  console.log(window.longString);
 </script>`;
 
 add_task(async function() {
   let hud = await openNewTabAndConsole(TEST_URI);
 
-  let [msgWithText, msgWithObj, msgNested] =
-    await waitFor(() => findMessages(hud, "foo"));
-  ok(msgWithText && msgWithObj && msgNested, "Three messages should have appeared");
-
-  let text = msgWithText.querySelector(".objectBox-string");
-  let objInMsgWithObj = msgWithObj.querySelector(".objectBox-object");
-  let textInMsgWithObj = msgWithObj.querySelector(".objectBox-string");
-
-  // The third message has an object nested in an array, the array is therefore the top
-  // object, the object is the nested object.
-  let topObjInMsg = msgNested.querySelector(".objectBox-array");
-  let nestedObjInMsg = msgNested.querySelector(".objectBox-object");
+  let messages = await waitFor(() => findMessages(hud, "foo"));
+  is(messages.length, 4, "Four messages should have appeared");
+  let [msgWithText, msgWithObj, msgNested, msgLongStr] = messages;
+  let varIdx = 0;
 
   info("Check store as global variable is disabled for text only messages");
-  let menuPopup = await openContextMenu(hud, text);
-  let storeMenuItem = menuPopup.querySelector("#console-menu-store");
-  ok(storeMenuItem.disabled, "store as global variable is disabled for text message");
-  await hideContextMenu(hud);
+  await storeAsVariable(hud, msgWithText, "string");
 
   info("Check store as global variable is disabled for text in complex messages");
-  menuPopup = await openContextMenu(hud, textInMsgWithObj);
-  storeMenuItem = menuPopup.querySelector("#console-menu-store");
-  ok(storeMenuItem.disabled,
-    "store as global variable is disabled for text in complex message");
-  await hideContextMenu(hud);
+  await storeAsVariable(hud, msgWithObj, "string");
 
   info("Check store as global variable is enabled for objects in complex messages");
-  await storeAsVariable(hud, objInMsgWithObj);
-
-  is(hud.jsterm.getInputValue(), "temp0", "Input was set");
-
-  let executedResult = await hud.jsterm.execute();
-  ok(executedResult.textContent.includes("{ baz: 1 }"),
-     "Correct variable assigned into console");
+  await storeAsVariable(hud, msgWithObj, "object", varIdx++, "window.bar");
 
   info("Check store as global variable is enabled for top object in nested messages");
-  await storeAsVariable(hud, topObjInMsg);
-
-  is(hud.jsterm.getInputValue(), "temp1", "Input was set");
-
-  executedResult = await hud.jsterm.execute();
-  ok(executedResult.textContent.includes(`[ "foo", {\u2026}, 2 ]`),
-     "Correct variable assigned into console " + executedResult.textContent);
+  await storeAsVariable(hud, msgNested, "array", varIdx++, "window.array");
 
   info("Check store as global variable is enabled for nested object in nested messages");
-  await storeAsVariable(hud, nestedObjInMsg);
+  await storeAsVariable(hud, msgNested, "object", varIdx++, "window.bar");
 
-  is(hud.jsterm.getInputValue(), "temp2", "Input was set");
+  info("Check store as global variable is enabled for long strings");
+  await storeAsVariable(hud, msgLongStr, "string", varIdx++, "window.longString");
 
-  executedResult = await hud.jsterm.execute();
-  ok(executedResult.textContent.includes("{ baz: 1 }"),
-     "Correct variable assigned into console " + executedResult.textContent);
+  info("Check store as global variable is enabled for invisible-to-debugger objects");
+  let onMessageInvisible = waitForMessage(hud, "foo");
+  ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+    let obj = Cu.Sandbox(Cu.getObjectPrincipal(content), {invisibleToDebugger: true});
+    content.wrappedJSObject.invisibleToDebugger = obj;
+    content.console.log("foo", obj);
+  });
+  let msgInvisible = (await onMessageInvisible).node;
+  await storeAsVariable(hud, msgInvisible, "object", varIdx++, "window.invisibleToDebugger");
 });
 
-async function storeAsVariable(hud, element) {
-  info("Check store as global variable is enabled");
+async function storeAsVariable(hud, msg, type, varIdx, equalTo) {
+  let element = msg.querySelector(".objectBox-" + type);
   let menuPopup = await openContextMenu(hud, element);
   let storeMenuItem = menuPopup.querySelector("#console-menu-store");
-  ok(!storeMenuItem.disabled,
-    "store as global variable is enabled for object in complex message");
+
+  if (varIdx == null) {
+    ok(storeMenuItem.disabled, "store as global variable is disabled");
+    await hideContextMenu(hud);
+    return;
+  }
+
+  ok(!storeMenuItem.disabled, "store as global variable is enabled");
 
   info("Click on store as global variable");
   let onceInputSet = hud.jsterm.once("set-input-value");
   storeMenuItem.click();
 
   info("Wait for console input to be updated with the temp variable");
   await onceInputSet;
 
   info("Wait for context menu to be hidden");
   await hideContextMenu(hud);
+
+  is(hud.jsterm.getInputValue(), "temp" + varIdx, "Input was set");
+
+  let equal = await hud.jsterm.requestEvaluation("temp" + varIdx + " === " + equalTo);
+  is(equal.result, true, "Correct variable assigned into console.");
 }
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -64,16 +64,20 @@ function ObjectActor(obj, {
     getGlobalDebugObject
   };
   this.iterators = new Set();
 }
 
 ObjectActor.prototype = {
   actorPrefix: "obj",
 
+  rawValue: function () {
+    return this.obj.unsafeDereference();
+  },
+
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
   grip: function () {
     let g = {
       "type": "object",
       "actor": this.actorID,
       "class": this.obj.class,
@@ -2265,16 +2269,20 @@ function makeDebuggeeValueIfNeeded(obj, 
 function LongStringActor(string) {
   this.string = string;
   this.stringLength = string.length;
 }
 
 LongStringActor.prototype = {
   actorPrefix: "longString",
 
+  rawValue: function () {
+    return this.string;
+  },
+
   destroy: function () {
     // Because longStringActors is not a weak map, we won't automatically leave
     // it so we need to manually leave on destroy so that we don't leak
     // memory.
     this._releaseActor();
   },
 
   /**
@@ -2336,16 +2344,20 @@ LongStringActor.prototype.requestTypes =
 function ArrayBufferActor(buffer) {
   this.buffer = buffer;
   this.bufferLength = buffer.byteLength;
 }
 
 ArrayBufferActor.prototype = {
   actorPrefix: "arrayBuffer",
 
+  rawValue: function () {
+    return this.buffer;
+  },
+
   destroy: function () {
   },
 
   grip() {
     return {
       "type": "arrayBuffer",
       "length": this.bufferLength,
       "actor": this.actorID
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -37,16 +37,20 @@ if (isWorker) {
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
 } else {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/listeners", true);
   loader.lazyRequireGetter(this, "ConsoleReflowListener", "devtools/server/actors/webconsole/listeners", true);
   loader.lazyRequireGetter(this, "ContentProcessListener", "devtools/server/actors/webconsole/listeners", true);
 }
 
+function isObject(value) {
+  return Object(value) === value;
+}
+
 /**
  * The WebConsoleActor implements capabilities needed for the Web Console
  * feature.
  *
  * @constructor
  * @param object connection
  *        The connection to the client, DebuggerServerConnection.
  * @param object [parentActor]
@@ -438,18 +442,17 @@ WebConsoleActor.prototype =
    *        The value you want to get a debuggee value for.
    * @param boolean useObjectGlobal
    *        If |true| the object global is determined and added as a debuggee,
    *        otherwise |this.window| is used when makeDebuggeeValue() is invoked.
    * @return object
    *         Debuggee value for |value|.
    */
   makeDebuggeeValue: function (value, useObjectGlobal) {
-    let isObject = Object(value) === value;
-    if (useObjectGlobal && isObject) {
+    if (useObjectGlobal && isObject(value)) {
       try {
         let global = Cu.getGlobalForObject(value);
         let dbgGlobal = this.dbg.makeGlobalObjectReference(global);
         return dbgGlobal.makeDebuggeeValue(value);
       } catch (ex) {
         // The above can throw an exception if value is not an actual object
         // or 'Object in compartment marked as invisible to Debugger'
       }
@@ -1315,27 +1318,35 @@ WebConsoleActor.prototype =
 
     // If we have an object to bind to |_self|, create a Debugger.Object
     // referring to that object, belonging to dbg.
     let bindSelf = null;
     if (options.bindObjectActor || options.selectedObjectActor) {
       let objActor = this.getActorByID(options.bindObjectActor ||
                                        options.selectedObjectActor);
       if (objActor) {
-        let jsObj = objActor.obj.unsafeDereference();
-        // If we use the makeDebuggeeValue method of jsObj's own global, then
-        // we'll get a D.O that sees jsObj as viewed from its own compartment -
-        // that is, without wrappers. The evalWithBindings call will then wrap
-        // jsObj appropriately for the evaluation compartment.
-        let global = Cu.getGlobalForObject(jsObj);
-        let _dbgWindow = dbg.makeGlobalObjectReference(global);
-        bindSelf = dbgWindow.makeDebuggeeValue(jsObj);
+        let jsVal = objActor.rawValue();
 
-        if (options.bindObjectActor) {
-          dbgWindow = _dbgWindow;
+        if (isObject(jsVal)) {
+          // If we use the makeDebuggeeValue method of jsVal's own global, then
+          // we'll get a D.O that sees jsVal as viewed from its own compartment -
+          // that is, without wrappers. The evalWithBindings call will then wrap
+          // jsVal appropriately for the evaluation compartment.
+          bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
+          if (options.bindObjectActor) {
+            let global = Cu.getGlobalForObject(jsVal);
+            try {
+              let _dbgWindow = dbg.makeGlobalObjectReference(global);
+              dbgWindow = _dbgWindow;
+            } catch (err) {
+              // The above will throw if `global` is invisible to debugger.
+            }
+          }
+        } else {
+          bindSelf = jsVal;
         }
       }
     }
 
     // Get the Web Console commands for the given debugger window.
     let helpers = this._getWebConsoleCommands(dbgWindow);
     let bindings = helpers.sandbox;
     if (bindSelf) {