Bug 1385995 - Adding `Copy object` to the context menu which allows to copy the object/variable logged to the console. r=nchevobbe draft
authorabhinav <abhinav.koppula@gmail.com>
Thu, 28 Sep 2017 00:12:00 +0530
changeset 675613 58e4f4fd5dfb74e99163decc18b8f3b4b45ca8cf
parent 673986 65dac33a5682f3ec5a675e7f3314b0c1520a13fa
child 734661 a251edb81e4007e882dfae9cc517fab985d578cd
push id83191
push userbmo:abhinav.koppula@gmail.com
push dateThu, 05 Oct 2017 17:03:18 +0000
reviewersnchevobbe
bugs1385995
milestone58.0a1
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
devtools/client/locales/en-US/webconsole.properties
devtools/client/webconsole/jsterm.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_copy_object.js
devtools/client/webconsole/new-console-output/utils/context-menu.js
--- 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");