Bug 1385995 - Adding `Copy object` to the context menu which allows to copy the object/variable logged to the console. r=nchevobbe
MozReview-Commit-ID: EbOE9qrpikn
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -228,21 +228,27 @@ webconsole.menu.openInVarView.label=Open
webconsole.menu.openInVarView.accesskey=V
# LOCALIZATION NOTE (webconsole.menu.storeAsGlobalVar.label)
# Label used for a context-menu item displayed for object/variable logs. Clicking on it
# creates a new global variable pointing to the logged variable.
webconsole.menu.storeAsGlobalVar.label=Store as global variable
webconsole.menu.storeAsGlobalVar.accesskey=S
-# LOCALIZATION NOTE (webconsole.menu.copy.label)
+# LOCALIZATION NOTE (webconsole.menu.copyMessage.label)
# Label used for a context-menu item displayed for any log. Clicking on it will copy the
# content of the log (or the user selection, if any).
-webconsole.menu.copy.label=Copy
-webconsole.menu.copy.accesskey=C
+webconsole.menu.copyMessage.label=Copy message
+webconsole.menu.copyMessage.accesskey=C
+
+# LOCALIZATION NOTE (webconsole.menu.copyObject.label)
+# Label used for a context-menu item displayed for object/variable log. Clicking on it
+# will copy the object/variable.
+webconsole.menu.copyObject.label=Copy object
+webconsole.menu.copyObject.accesskey=o
# LOCALIZATION NOTE (webconsole.menu.selectAll.label)
# Label used for a context-menu item that will select all the content of the webconsole
# output.
webconsole.menu.selectAll.label=Select all
webconsole.menu.selectAll.accesskey=A
# LOCALIZATION NOTE (webconsole.clearButton.tooltip)
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -544,16 +544,31 @@ JSTerm.prototype = {
selectedObjectActor: options.selectedObjectActor,
};
this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions);
return deferred.promise;
},
/**
+ * Copy the object/variable by invoking the server
+ * which invokes the `copy(variable)` command and makes it
+ * available in the clipboard
+ * @param evalString - string which has the evaluation string to be copied
+ * @param options - object - Options for evaluation
+ * @return object
+ * A promise object that is resolved when the server response is
+ * received.
+ */
+ copyObject: function (evalString, evalOptions) {
+ return this.webConsoleClient.evaluateJSAsync(`copy(${evalString})`,
+ null, evalOptions);
+ },
+
+ /**
* Retrieve the FrameActor ID given a frame depth.
*
* @param number frame
* Frame depth.
* @return string|null
* The FrameActor ID for the given frame depth.
*/
getFrameActor: function (frame) {
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -96,22 +96,29 @@ NewConsoleOutputWrapper.prototype = {
// is available in the current scope and we can pass it into
// `createContextMenu` method.
serviceContainer.openContextMenu = (e, message) => {
let { screenX, screenY, target } = e;
let messageEl = target.closest(".message");
let clipboardText = messageEl ? messageEl.textContent : null;
+ let messageVariable = target.closest(".objectBox");
+ // Ensure that console.group and console.groupCollapsed commands are not captured
+ let variableText = (messageVariable
+ && !(messageEl.classList.contains("startGroup"))
+ && !(messageEl.classList.contains("startGroupCollapsed")))
+ ? messageVariable.textContent : null;
+
// Retrieve closes actor id from the DOM.
let actorEl = target.closest("[data-link-actor-id]");
let actor = actorEl ? actorEl.dataset.linkActorId : null;
let menu = createContextMenu(this.jsterm, this.parentNode,
- { actor, clipboardText, message, serviceContainer });
+ { actor, clipboardText, variableText, message, serviceContainer });
// Emit the "menu-open" event for testing.
menu.once("open", () => this.emit("menu-open"));
menu.popup(screenX, screenY, this.toolbox);
return menu;
};
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -37,16 +37,18 @@ support-files =
[browser_webconsole_console_group.js]
[browser_webconsole_console_table.js]
[browser_webconsole_context_menu_copy_entire_message.js]
subsuite = clipboard
skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
[browser_webconsole_context_menu_copy_link_location.js]
subsuite = clipboard
skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
+[browser_webconsole_context_menu_copy_object.js]
+subsuite = clipboard
[browser_webconsole_context_menu_open_url.js]
[browser_webconsole_context_menu_store_as_global.js]
[browser_webconsole_filters.js]
[browser_webconsole_filters_persist.js]
[browser_webconsole_init.js]
[browser_webconsole_input_focus.js]
[browser_webconsole_keyboard_accessibility.js]
[browser_webconsole_location_debugger_link.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_copy_object.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the "Copy object" menu item of the webconsole is enabled only when
+// 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]);
+ console.group("group");
+ console.groupCollapsed("collapsed");
+ console.groupEnd();
+ console.log(532);
+ console.log(true);
+ console.log(false);
+ console.log(undefined);
+ console.log(null);
+</script>`;
+const copyObjectMenuItemId = "#console-menu-copy-object";
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+
+ let [msgWithText, msgWithObj, msgNested] =
+ yield waitFor(() => findMessages(hud, "foo"));
+ ok(msgWithText && msgWithObj && msgNested, "Three messages should have appeared");
+
+ let [groupMsgObj] = yield waitFor(() => findMessages(hud, "group", ".message-body"));
+ let [collapsedGroupMsgObj] = yield waitFor(() =>
+ findMessages(hud, "collapsed", ".message-body"));
+ let [numberMsgObj] = yield waitFor(() => findMessages(hud, `532`, ".message-body"));
+ let [trueMsgObj] = yield waitFor(() => findMessages(hud, `true`, ".message-body"));
+ let [falseMsgObj] = yield waitFor(() => findMessages(hud, `false`, ".message-body"));
+ let [undefinedMsgObj] = yield waitFor(() => findMessages(hud, `undefined`,
+ ".message-body"));
+ let [nullMsgObj] = yield waitFor(() => findMessages(hud, `null`, ".message-body"));
+ ok(nullMsgObj, "One message with null value 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 consoleMessages = yield waitFor(() => findMessages(hud, "console.log(\"foo\");",
+ ".message-location"));
+ yield testCopyObjectMenuItemDisabled(hud, consoleMessages[0]);
+
+ info(`Check "Copy object" is enabled for text only messages
+ thus copying the text`);
+ yield testCopyObject(hud, text, `foo`, false);
+
+ info(`Check "Copy object" is enabled for text in complex messages
+ thus copying the text`);
+ yield testCopyObject(hud, textInMsgWithObj, `foo`, false);
+
+ info("Check `Copy object` is enabled for objects in complex messages");
+ yield testCopyObject(hud, objInMsgWithObj, `{"baz":1}`, true);
+
+ info("Check `Copy object` is enabled for top object in nested messages");
+ yield testCopyObject(hud, topObjInMsg, `["foo",{"baz":1},2]`, true);
+
+ info("Check `Copy object` is enabled for nested object in nested messages");
+ yield testCopyObject(hud, nestedObjInMsg, `{"baz":1}`, true);
+
+ info("Check `Copy object` is disabled on `console.group('group')` messages");
+ yield testCopyObjectMenuItemDisabled(hud, groupMsgObj);
+
+ info(`Check "Copy object" is disabled in "console.groupCollapsed('collapsed')"
+ messages`);
+ yield testCopyObjectMenuItemDisabled(hud, collapsedGroupMsgObj);
+
+ // Check for primitive objects
+ info("Check `Copy object` is enabled for numbers");
+ yield testCopyObject(hud, numberMsgObj, `532`, false);
+
+ info("Check `Copy object` is enabled for booleans");
+ yield testCopyObject(hud, trueMsgObj, `true`, false);
+ yield testCopyObject(hud, falseMsgObj, `false`, false);
+
+ info("Check `Copy object` is enabled for undefined and null");
+ yield testCopyObject(hud, undefinedMsgObj, `undefined`, false);
+ yield testCopyObject(hud, nullMsgObj, `null`, false);
+});
+
+function* testCopyObject(hud, element, expectedMessage, objectInput) {
+ info("Check `Copy object` is enabled");
+ let menuPopup = yield openContextMenu(hud, element);
+ let copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId);
+ ok(!copyObjectMenuItem.disabled,
+ "`Copy object` is enabled for object in complex message");
+
+ const validatorFn = data => {
+ let prettifiedMessage = prettyPrintMessage(expectedMessage, objectInput);
+ return data === prettifiedMessage;
+ };
+
+ info("Click on `Copy object`");
+ yield waitForClipboardPromise(() => copyObjectMenuItem.click(), validatorFn);
+
+ info("`Copy object` by using the access-key O");
+ menuPopup = yield openContextMenu(hud, element);
+ yield waitForClipboardPromise(() => synthesizeKeyShortcut("O"), validatorFn);
+}
+
+function* testCopyObjectMenuItemDisabled(hud, element) {
+ let menuPopup = yield openContextMenu(hud, element);
+ let copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId);
+ ok(copyObjectMenuItem.disabled, `"Copy object" is disabled for messages
+ with no variables/objects`);
+ yield hideContextMenu(hud);
+}
+
+function prettyPrintMessage(message, isObject) {
+ return isObject ? JSON.stringify(JSON.parse(message), null, 2) : message;
+}
--- a/devtools/client/webconsole/new-console-output/utils/context-menu.js
+++ b/devtools/client/webconsole/new-console-output/utils/context-menu.js
@@ -22,23 +22,26 @@ const { l10n } = require("devtools/clien
*
* @param {Object} jsterm
* The JSTerm instance used by the webconsole.
* @param {Element} parentNode
* The container of the new console frontend output wrapper.
* @param {Object} options
* - {String} actor (optional) actor id to use for context menu actions
* - {String} clipboardText (optional) text to "Copy" if no selection is available
+ * - {String} variableText (optional) which is the textual frontend
+ * representation of the variable
* - {Object} message (optional) message object containing metadata such as:
* - {String} source
* - {String} request
*/
function createContextMenu(jsterm, parentNode, {
actor,
clipboardText,
+ variableText,
message,
serviceContainer
}) {
let win = parentNode.ownerDocument.defaultView;
let selection = win.getSelection();
let { source, request } = message || {};
@@ -111,31 +114,50 @@ function createContextMenu(jsterm, paren
jsterm.setInputValue(res.result);
});
},
}));
// Copy message or grip.
menu.append(new MenuItem({
id: "console-menu-copy",
- label: l10n.getStr("webconsole.menu.copy.label"),
- accesskey: l10n.getStr("webconsole.menu.copy.accesskey"),
+ label: l10n.getStr("webconsole.menu.copyMessage.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyMessage.accesskey"),
// Disabled if there is no selection and no message element available to copy.
disabled: selection.isCollapsed && !clipboardText,
click: () => {
if (selection.isCollapsed) {
// If the selection is empty/collapsed, copy the text content of the
// message for which the context menu was opened.
clipboardHelper.copyString(clipboardText);
} else {
clipboardHelper.copyString(selection.toString());
}
},
}));
+ // Copy message object.
+ menu.append(new MenuItem({
+ id: "console-menu-copy-object",
+ label: l10n.getStr("webconsole.menu.copyObject.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyObject.accesskey"),
+ // Disabled if there is no actor and no variable text associated.
+ disabled: (!actor && !variableText),
+ click: () => {
+ if (actor) {
+ // The Debugger.Object of the OA will be bound to |_self| during evaluation,
+ jsterm.copyObject(`_self`, { selectedObjectActor: actor }).then((res) => {
+ clipboardHelper.copyString(res.helperResult.value);
+ });
+ } else {
+ clipboardHelper.copyString(variableText);
+ }
+ },
+ }));
+
// Select all.
menu.append(new MenuItem({
id: "console-menu-select",
label: l10n.getStr("webconsole.menu.selectAll.label"),
accesskey: l10n.getStr("webconsole.menu.selectAll.accesskey"),
disabled: false,
click: () => {
let webconsoleOutput = parentNode.querySelector(".webconsole-output");