--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -459,21 +459,24 @@ function waitForContextMenu(popup, butto
onHidden && onHidden();
deferred.resolve(popup);
}
popup.addEventListener("popupshown", onPopupShown);
info("wait for the context menu to open");
- button.scrollIntoView();
+ synthesizeContextMenuEvent(button);
+ return deferred.promise;
+}
+
+function synthesizeContextMenuEvent(el) {
+ el.scrollIntoView();
let eventDetails = {type: "contextmenu", button: 2};
- EventUtils.synthesizeMouse(button, 5, 2, eventDetails,
- button.ownerDocument.defaultView);
- return deferred.promise;
+ EventUtils.synthesizeMouse(el, 5, 2, eventDetails, el.ownerDocument.defaultView);
}
/**
* Promise wrapper around SimpleTest.waitForClipboard
*/
function waitForClipboardPromise(setup, expected) {
return new Promise((resolve, reject) => {
SimpleTest.waitForClipboard(expected, setup, resolve, reject);
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -196,8 +196,46 @@ webconsole.find.key=CmdOrCtrl+F
# LOCALIZATION NOTE (webconsole.close.key)
# Key shortcut used to close the Browser console (doesn't work in regular web console)
webconsole.close.key=CmdOrCtrl+W
# LOCALIZATION NOTE (webconsole.clear.key*)
# Key shortcut used to clear the console output
webconsole.clear.key=Ctrl+Shift+L
webconsole.clear.keyOSX=Ctrl+L
+
+
+# LOCALIZATION NOTE (webconsole.menu.copyURL.label)
+# Label used for a context-menu item displayed for network message logs. Clicking on it
+# copies the URL displayed in the message to the clipboard.
+webconsole.menu.copyURL.label=Copy Link Location
+webconsole.menu.copyURL.accesskey=a
+
+# LOCALIZATION NOTE (webconsole.menu.openURL.label)
+# Label used for a context-menu item displayed for network message logs. Clicking on it
+# opens the URL displayed in a new browser tab.
+webconsole.menu.openURL.label=Open URL in New Tab
+webconsole.menu.openURL.accesskey=T
+
+# LOCALIZATION NOTE (webconsole.menu.openInVarView.label)
+# Label used for a context-menu item displayed for object/variable logs. Clicking on it
+# opens the webconsole variable view for the logged variable.
+webconsole.menu.openInVarView.label=Open in Variables View
+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)
+# 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
+
+# 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
+
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -53,16 +53,22 @@ const ConsoleOutput = createClass({
},
componentDidUpdate() {
if (this.shouldScrollBottom) {
scrollToBottom(this.outputNode);
}
},
+ onContextMenu(e) {
+ this.props.serviceContainer.openContextMenu(e);
+ e.stopPropagation();
+ e.preventDefault();
+ },
+
render() {
let {
dispatch,
autoscroll,
messages,
messagesUi,
messagesTableData,
serviceContainer,
@@ -94,16 +100,17 @@ const ConsoleOutput = createClass({
if (!timestampsVisible) {
classList.push("hideTimestamps");
}
return (
dom.div({
className: classList.join(" "),
+ onContextMenu: this.onContextMenu,
ref: node => {
this.outputNode = node;
},
}, messageNodes
)
);
}
});
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -30,23 +30,24 @@ function EvaluationResult(props) {
const {
source,
type,
level,
id: messageId,
exceptionDocURL,
frame,
timeStamp,
+ parameters,
} = message;
let messageBody;
if (message.messageText) {
messageBody = message.messageText;
} else {
- messageBody = GripMessageBody({grip: message.parameters, serviceContainer});
+ messageBody = GripMessageBody({grip: parameters, serviceContainer});
}
const topLevelClasses = ["cm-s-mozilla"];
const childProps = {
source,
type,
level,
@@ -54,13 +55,14 @@ function EvaluationResult(props) {
topLevelClasses,
messageBody,
messageId,
scrollToMessage: props.autoscroll,
serviceContainer,
exceptionDocURL,
frame,
timeStamp,
+ parameters,
};
return Message(childProps);
}
module.exports = EvaluationResult;
--- a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -52,13 +52,14 @@ function NetworkEventMessage(props) {
source,
type,
level,
indent,
topLevelClasses,
timeStamp,
messageBody,
serviceContainer,
+ request,
};
return Message(childProps);
}
module.exports = NetworkEventMessage;
--- a/devtools/client/webconsole/new-console-output/components/message.js
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -38,21 +38,24 @@ const Message = createClass({
messageBody: PropTypes.any.isRequired,
repeat: PropTypes.any,
frame: PropTypes.any,
attachment: PropTypes.any,
stacktrace: PropTypes.any,
messageId: PropTypes.string,
scrollToMessage: PropTypes.bool,
exceptionDocURL: PropTypes.string,
+ parameters: PropTypes.object,
+ request: PropTypes.object,
serviceContainer: PropTypes.shape({
emitNewMessage: PropTypes.func.isRequired,
onViewSourceInDebugger: PropTypes.func.isRequired,
onViewSourceInScratchpad: PropTypes.func.isRequired,
onViewSourceInStyleEditor: PropTypes.func.isRequired,
+ openContextMenu: PropTypes.func.isRequired,
sourceMapService: PropTypes.any,
}),
},
getDefaultProps: function () {
return {
indent: 0
};
@@ -72,16 +75,27 @@ const Message = createClass({
}
},
onLearnMoreClick: function () {
let {exceptionDocURL} = this.props;
this.props.serviceContainer.openLink(exceptionDocURL);
},
+ onContextMenu(e) {
+ let { serviceContainer, source, request } = this.props;
+ let messageInfo = {
+ source,
+ request,
+ };
+ serviceContainer.openContextMenu(e, messageInfo);
+ e.stopPropagation();
+ e.preventDefault();
+ },
+
render() {
const {
messageId,
open,
collapsible,
collapseTitle,
source,
type,
@@ -168,16 +182,17 @@ const Message = createClass({
className: "learn-more-link webconsole-learn-more-link",
title: exceptionDocURL.split("?")[0],
onClick: this.onLearnMoreClick,
}, `[${l10n.getStr("webConsoleMoreInfoLabel")}]`);
}
return dom.div({
className: topLevelClasses.join(" "),
+ onContextMenu: this.onContextMenu,
ref: node => {
this.messageNode = node;
}
},
timestampEl,
MessageIndent({indent}),
icon,
collapse,
--- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -20,15 +20,17 @@ VariablesViewLink.propTypes = {
};
function VariablesViewLink(props) {
const { className, object, children } = props;
return (
dom.a({
onClick: openVariablesView.bind(null, object),
+ // Context menu can use this actor id information to enable additional menu items.
+ "data-link-actor-id": object.actor,
className: className || "cm-variable",
draggable: false,
}, children)
);
}
module.exports = VariablesViewLink;
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -4,26 +4,30 @@
"use strict";
// React & Redux
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { createContextMenu } = require("devtools/client/webconsole/new-console-output/utils/context-menu");
const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+const EventEmitter = require("devtools/shared/event-emitter");
const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
const store = configureStore();
let queuedActions = [];
let throttledDispatchTimeout = false;
function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
+ EventEmitter.decorate(this);
+
this.parentNode = parentNode;
this.jsterm = jsterm;
this.toolbox = toolbox;
this.owner = owner;
this.document = document;
this.init = this.init.bind(this);
}
@@ -58,16 +62,35 @@ NewConsoleOutputWrapper.prototype = {
frame.url,
frame.line
),
onViewSourceInStyleEditor: frame => this.toolbox.viewSourceInStyleEditor.call(
this.toolbox,
frame.url,
frame.line
),
+ openContextMenu: (e, message) => {
+ let { screenX, screenY, target } = e;
+
+ let messageEl = target.closest(".message");
+ let clipboardText = messageEl ? messageEl.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 });
+
+ // Emit the "menu-open" event for testing.
+ menu.once("open", () => this.emit("menu-open"));
+ menu.popup(screenX, screenY, this.toolbox);
+
+ return menu;
+ },
openNetworkPanel: (requestId) => {
return this.toolbox.selectTool("netmonitor").then(panel => {
return panel.panelWin.NetMonitorController.inspectRequest(requestId);
});
},
sourceMapService: this.toolbox ? this.toolbox._sourceMapService : null,
openLink: url => this.jsterm.hud.owner.openLink.call(this.jsterm.hud.owner, url),
createElement: nodename => {
@@ -85,16 +108,17 @@ NewConsoleOutputWrapper.prototype = {
},
}
});
let filterBar = FilterBar({
serviceContainer: {
attachRefToHud
}
});
+
let provider = React.createElement(
Provider,
{ store },
React.DOM.div(
{className: "webconsole-output-wrapper"},
filterBar,
childComponent
));
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -15,16 +15,23 @@ support-files =
test-location-styleeditor-link-2.css
test-location-styleeditor-link.html
test-stacktrace-location-debugger-link.html
!/devtools/client/framework/test/shared-head.js
[browser_webconsole_batching.js]
[browser_webconsole_console_group.js]
[browser_webconsole_console_table.js]
+[browser_webconsole_context_menu_copy_entire_message.js]
+subsuite = clipboard
+[browser_webconsole_context_menu_copy_link_location.js]
+subsuite = clipboard
+[browser_webconsole_context_menu_open_in_var_view.js]
+[browser_webconsole_context_menu_open_url.js]
+[browser_webconsole_context_menu_store_as_global.js]
[browser_webconsole_filters.js]
[browser_webconsole_init.js]
[browser_webconsole_input_focus.js]
[browser_webconsole_keyboard_accessibility.js]
[browser_webconsole_location_debugger_link.js]
[browser_webconsole_location_scratchpad_link.js]
[browser_webconsole_location_styleeditor_link.js]
[browser_webconsole_nodes_highlight.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_copy_entire_message.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,<script>
+ window.logStuff = function () {
+ console.log("simple text message");
+ console.trace();
+ };
+</script>`;
+
+// Test the Copy menu item of the webconsole copies the expected clipboard text for
+// different log messages.
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.logStuff();
+ });
+
+ let messages = [];
+ for (let s of ["simple text message", "console.trace()"]) {
+ messages.push(yield waitFor(() => findMessage(hud, s)));
+ }
+
+ for (let message of messages) {
+ let menuPopup = yield openContextMenu(hud, message);
+ let copyMenuItem = menuPopup.querySelector("#console-menu-copy");
+ ok(copyMenuItem, "copy menu item is enabled");
+
+ yield waitForClipboardPromise(
+ () => copyMenuItem.click(),
+ data => data === message.textContent
+ );
+
+ ok(true, "Clipboard text was found and saved");
+
+ // TODO: The copy menu item & this test should be improved for the new console.
+ // This is tracked by https://bugzilla.mozilla.org/show_bug.cgi?id=1329606 .
+
+ // The rest of this test was copied from the old console frontend. The copy menu item
+ // logic is not on par with the one from the old console yet.
+
+ // let lines = clipboardText.split("\n");
+ // ok(lines.length > 0, "There is at least one newline in the message");
+ // is(lines.pop(), "", "There is a newline at the end");
+ // is(lines.length, result.lines, `There are ${result.lines} lines in the message`);
+
+ // // Test the first line for "timestamp message repeat file:line"
+ // let firstLine = lines.shift();
+ // ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine),
+ // "The message's first line has the right format:\n" + firstLine);
+
+ // // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col"
+ // for (let line of lines) {
+ // ok(/^\t.+ .+:\d+:\d+$/.test(line),
+ // "The stack trace line has the right format:\n" + line);
+ // }
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_copy_link_location.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the Copy Link Location menu item of the webconsole is displayed for network
+// messages and copies the expected URL.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "new-console-output/test/mochitest/test-console.html?_date=" + Date.now();
+const CONTEXT_MENU_ID = "#console-menu-copy-url";
+
+add_task(function* () {
+ // Enable net messages in the console for this test.
+ yield pushPref("devtools.webconsole.filter.net", true);
+
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ info("Test Copy URL menu item for text log");
+
+ info("Logging a text message in the content window");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.console.log("simple text message");
+ });
+ let message = yield waitFor(() => findMessage(hud, "simple text message"));
+ ok(message, "Text log found in the console");
+
+ info("Open and check the context menu for the logged text message");
+ let menuPopup = yield openContextMenu(hud, message);
+ let copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID);
+ ok(!copyURLItem, "Copy URL menu item is hidden for a simple text message");
+
+ yield hideContextMenu(hud);
+ hud.jsterm.clearOutput();
+
+ info("Test Copy URL menu item for network log");
+
+ info("Reload the content window to produce a network log");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.location.reload();
+ });
+
+ message = yield waitFor(() => findMessage(hud, "test-console.html"));
+ ok(message, "Network log found in the console");
+
+ info("Open and check the context menu for the logged network message");
+ menuPopup = yield openContextMenu(hud, message);
+ copyURLItem = menuPopup.querySelector(CONTEXT_MENU_ID);
+ ok(copyURLItem, "Copy url menu item is available in context menu");
+
+ info("Click on Copy URL menu item and wait for clipboard to be updated");
+ yield waitForClipboardPromise(() => copyURLItem.click(), TEST_URI);
+ ok(true, "Expected text was copied to the clipboard.");
+
+ yield hideContextMenu(hud);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_open_in_var_view.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the Open in Variables View menu item of the webconsole is enabled only when
+// clicking on messages that can be opened in the variables view.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,<script>
+ console.log("foo");
+ console.log("foo", window);
+</script>`;
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+
+ let [msgWithText, msgWithObj] = yield waitFor(() => findMessages(hud, "foo"));
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let text = msgWithText.querySelector(".objectBox-string");
+ let objInMsgWithObj = msgWithObj.querySelector(".cm-variable");
+ let textInMsgWithObj = msgWithObj.querySelector(".objectBox-string");
+
+ info("Check open in variables view is disabled for text only messages");
+ let menuPopup = yield openContextMenu(hud, text);
+ let openMenuItem = menuPopup.querySelector("#console-menu-open");
+ ok(openMenuItem.disabled, "open in variables view is disabled for text message");
+ yield hideContextMenu(hud);
+
+ info("Check open in variables view is enabled for objects in complex messages");
+ menuPopup = yield openContextMenu(hud, objInMsgWithObj);
+ openMenuItem = menuPopup.querySelector("#console-menu-open");
+ ok(!openMenuItem.disabled,
+ "open in variables view is enabled for object in complex message");
+ yield hideContextMenu(hud);
+
+ info("Check open in variables view is disabled for text in complex messages");
+ menuPopup = yield openContextMenu(hud, textInMsgWithObj);
+ openMenuItem = menuPopup.querySelector("#console-menu-open");
+ ok(openMenuItem.disabled,
+ "open in variables view is disabled for text in complex message");
+ yield hideContextMenu(hud);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_open_url.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the Open URL in new Tab menu item is displayed for network logs and works as
+// expected.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "new-console-output/test/mochitest/test-console.html";
+
+add_task(function* () {
+ // Enable net messages in the console for this test.
+ yield pushPref("devtools.webconsole.filter.net", true);
+
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ info("Test Open URL menu item for text log");
+
+ info("Logging a text message in the content window");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.console.log("simple text message");
+ });
+ let message = yield waitFor(() => findMessage(hud, "simple text message"));
+ ok(message, "Text log found in the console");
+
+ info("Open and check the context menu for the logged text message");
+ let menuPopup = yield openContextMenu(hud, message);
+ let openUrlItem = menuPopup.querySelector("#console-menu-open-url");
+ ok(!openUrlItem, "Open URL menu item is not available");
+
+ yield hideContextMenu(hud);
+ hud.jsterm.clearOutput();
+
+ info("Test Open URL menu item for network log");
+
+ info("Reload the content window to produce a network log");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.location.reload();
+ });
+ message = yield waitFor(() => findMessage(hud, "test-console.html"));
+ ok(message, "Network log found in the console");
+
+ info("Open and check the context menu for the logged network message");
+ menuPopup = yield openContextMenu(hud, message);
+ openUrlItem = menuPopup.querySelector("#console-menu-open-url");
+ ok(openUrlItem, "Open URL menu item is available");
+
+ let currentTab = gBrowser.selectedTab;
+ let tabLoaded = listenToTabLoad();
+ info("Click on Open URL menu item and wait for new tab to open");
+ openUrlItem.click();
+ yield hideContextMenu(hud);
+ let newTab = yield tabLoaded;
+ let newTabHref = newTab.linkedBrowser._contentWindow.location.href;
+ is(newTabHref, TEST_URI, "Tab was opened with the expected URL");
+
+ info("Remove the new tab and select the previous tab back");
+ gBrowser.removeTab(newTab);
+ gBrowser.selectedTab = currentTab;
+});
+
+/**
+ * Simple helper to wrap a tab load listener in a promise.
+ */
+function listenToTabLoad() {
+ return new Promise((resolve) => {
+ gBrowser.tabContainer.addEventListener("TabOpen", function onTabOpen(evt) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
+ let newTab = evt.target;
+ newTab.linkedBrowser.addEventListener("load", function onTabLoad() {
+ newTab.linkedBrowser.removeEventListener("load", onTabLoad, true);
+ resolve(newTab);
+ }, true);
+ }, true);
+ });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_store_as_global.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the "Store as global variable" 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]);
+</script>`;
+
+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 text = msgWithText.querySelector(".objectBox-string");
+ let objInMsgWithObj = msgWithObj.querySelector(".cm-variable");
+ 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 > .cm-variable");
+ let nestedObjInMsg = msgNested.querySelector(".objectBox-object > .cm-variable");
+
+ info("Check store as global variable is disabled for text only messages");
+ let menuPopup = yield openContextMenu(hud, text);
+ let storeMenuItem = menuPopup.querySelector("#console-menu-store");
+ ok(storeMenuItem.disabled, "store as global variable is disabled for text message");
+ yield hideContextMenu(hud);
+
+ info("Check store as global variable is disabled for text in complex messages");
+ menuPopup = yield openContextMenu(hud, textInMsgWithObj);
+ storeMenuItem = menuPopup.querySelector("#console-menu-store");
+ ok(storeMenuItem.disabled,
+ "store as global variable is disabled for text in complex message");
+ yield hideContextMenu(hud);
+
+ info("Check store as global variable is enabled for objects in complex messages");
+ yield storeAsVariable(hud, objInMsgWithObj);
+
+ is(hud.jsterm.getInputValue(), "temp0", "Input was set");
+
+ let executedResult = yield hud.jsterm.execute();
+ ok(executedResult.textContent.includes("{ baz: 1 }"),
+ "Correct variable assigned into console");
+
+ info("Check store as global variable is enabled for top object in nested messages");
+ yield storeAsVariable(hud, topObjInMsg);
+
+ is(hud.jsterm.getInputValue(), "temp1", "Input was set");
+
+ executedResult = yield hud.jsterm.execute();
+ ok(executedResult.textContent.includes("[ \"foo\", Object, 2 ]"),
+ "Correct variable assigned into console " + executedResult.textContent);
+
+ info("Check store as global variable is enabled for nested object in nested messages");
+ yield storeAsVariable(hud, nestedObjInMsg);
+
+ is(hud.jsterm.getInputValue(), "temp2", "Input was set");
+
+ executedResult = yield hud.jsterm.execute();
+ ok(executedResult.textContent.includes("{ baz: 1 }"),
+ "Correct variable assigned into console " + executedResult.textContent);
+});
+
+function* storeAsVariable(hud, element) {
+ info("Check store as global variable is enabled");
+ let menuPopup = yield openContextMenu(hud, element);
+ let storeMenuItem = menuPopup.querySelector("#console-menu-store");
+ ok(!storeMenuItem.disabled,
+ "store as global variable is enabled for object in complex message");
+
+ 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");
+ yield onceInputSet;
+
+ info("Wait for context menu to be hidden");
+ yield hideContextMenu(hud);
+}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -1,14 +1,15 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from ../../../../framework/test/shared-head.js */
-/* exported WCUL10n, openNewTabAndConsole, waitForMessages, waitFor, findMessage */
+/* exported WCUL10n, openNewTabAndConsole, waitForMessages, waitFor, findMessage,
+ openContextMenu, hideContextMenu */
"use strict";
// shared-head.js handles imports, constants, and utility functions
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
this);
@@ -135,8 +136,44 @@ function findMessage(hud, text, selector
function findMessages(hud, text, selector = ".message") {
const messages = hud.ui.experimentalOutputNode.querySelectorAll(selector);
const elements = Array.prototype.filter.call(
messages,
(el) => el.textContent.includes(text)
);
return elements;
}
+
+/**
+ * Simulate a context menu event on the provided element, and wait for the console context
+ * menu to open. Returns a promise that resolves the menu popup element.
+ *
+ * @param object hud
+ * The web console.
+ * @param element element
+ * The dom element on which the context menu event should be synthesized.
+ * @return promise
+ */
+function* openContextMenu(hud, element) {
+ let onConsoleMenuOpened = hud.ui.newConsoleOutput.once("menu-open");
+ synthesizeContextMenuEvent(element);
+ yield onConsoleMenuOpened;
+ return hud.ui.newConsoleOutput.toolbox.doc.getElementById("webconsole-menu");
+}
+
+/**
+ * Hide the webconsole context menu popup. Returns a promise that will resolve when the
+ * context menu popup is hidden or immediately if the popup can't be found.
+ *
+ * @param object hud
+ * The web console.
+ * @return promise
+ */
+function hideContextMenu(hud) {
+ let popup = hud.ui.newConsoleOutput.toolbox.doc.getElementById("webconsole-menu");
+ if (!popup) {
+ return Promise.resolve();
+ }
+
+ let onPopupHidden = once(popup, "popuphidden");
+ popup.hidePopup();
+ return onPopupHidden;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/context-menu.js
@@ -0,0 +1,143 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+const { MESSAGE_SOURCE } = require("devtools/client/webconsole/new-console-output/constants");
+
+const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+/**
+ * Create a Menu instance for the webconsole.
+ *
+ * @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
+ * - {Object} message (optional) message object containing metadata such as:
+ * - {String} source
+ * - {String} request
+ */
+function createContextMenu(jsterm, parentNode, { actor, clipboardText, message }) {
+ let win = parentNode.ownerDocument.defaultView;
+ let selection = win.getSelection();
+
+ let { source, request } = message || {};
+
+ let menu = new Menu({
+ id: "webconsole-menu"
+ });
+
+ // Copy URL for a network request.
+ menu.append(new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ clipboardHelper.copyString(request.url);
+ },
+ }));
+
+ // Open URL in a new tab for a network request.
+ menu.append(new MenuItem({
+ id: "console-menu-open-url",
+ label: l10n.getStr("webconsole.menu.openURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ mainWindow.openUILinkIn(request.url, "tab");
+ },
+ }));
+
+ // Open in variables view.
+ menu.append(new MenuItem({
+ id: "console-menu-open",
+ label: l10n.getStr("webconsole.menu.openInVarView.label"),
+ accesskey: l10n.getStr("webconsole.menu.openInVarView.accesskey"),
+ disabled: !actor,
+ click: () => {
+ openVariablesView(actor);
+ },
+ }));
+
+ // Store as global variable.
+ menu.append(new MenuItem({
+ id: "console-menu-store",
+ label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
+ accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
+ disabled: !actor,
+ click: () => {
+ let evalString = `{ let i = 0;
+ while (this.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ this["temp" + i] = _self;
+ "temp" + i;
+ }`;
+ let options = {
+ selectedObjectActor: actor,
+ };
+
+ jsterm.requestEvaluation(evalString, options).then((res) => {
+ jsterm.focus();
+ 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"),
+ // 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());
+ }
+ },
+ }));
+
+ // 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");
+ selection.selectAllChildren(webconsoleOutput);
+ },
+ }));
+
+ return menu;
+}
+
+exports.createContextMenu = createContextMenu;
--- a/devtools/client/webconsole/new-console-output/utils/moz.build
+++ b/devtools/client/webconsole/new-console-output/utils/moz.build
@@ -1,10 +1,11 @@
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
+ 'context-menu.js',
'id-generator.js',
'messages.js',
'variables-view.js',
)