Bug 1424721 - Allow long strings and invisible-to-debugger objects to be stored as global variables.
MozReview-Commit-ID: IZFKgror7F6
--- 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) {