--- 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
@@ -197,16 +197,20 @@ webconsole.find.key=CmdOrCtrl+F
# 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
+webconsole.menu.copyURL.label=Copy Link Location
+# According to webconsole.dtd, this is a, but a is already the access key for
+# selectAll ...
+webconsole.menu.copyURL.accesskey=a
webconsole.menu.openInVarView.label=Open in Variables View
webconsole.menu.openInVarView.accesskey=V
webconsole.menu.storeAsGlobalVar.label=Store as global variable
webconsole.menu.storeAsGlobalVar.accesskey=S
webconsole.menu.copy.label=Copy
webconsole.menu.copy.accesskey=C
webconsole.menu.selectAll.label=Select all
webconsole.menu.selectAll.accesskey=A
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -10,17 +10,18 @@
// define. We need to do that before loading Rep.
if (typeof define === "undefined") {
require("amd-loader");
}
// React
const {
createFactory,
- PropTypes
+ PropTypes,
+ DOM: dom
} = require("devtools/client/shared/vendor/react");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
const StringRep = createFactories(require("devtools/client/shared/components/reps/string").StringRep).rep;
const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link"));
const { Grip } = require("devtools/client/shared/components/reps/grip");
const { MODE } = require("devtools/client/shared/components/reps/constants");
@@ -29,16 +30,17 @@ GripMessageBody.displayName = "GripMessa
GripMessageBody.propTypes = {
grip: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]).isRequired,
serviceContainer: PropTypes.shape({
createElement: PropTypes.func.isRequired,
+ addContextMenuInfo: PropTypes.func.isRequired,
}),
userProvidedStyle: PropTypes.string,
};
GripMessageBody.defaultProps = {
mode: MODE.LONG,
};
@@ -52,33 +54,36 @@ function GripMessageBody(props) {
let onDOMNodeMouseOver;
let onDOMNodeMouseOut;
if (serviceContainer) {
onDOMNodeMouseOver = (object) => serviceContainer.highlightDomElement(object);
onDOMNodeMouseOut = serviceContainer.unHighlightDomElement;
}
- return (
+ return dom.span({
+ onContextMenu: (e) => props.serviceContainer.addContextMenuInfo({ grip })
+ }, (
// @TODO once there is a longString rep, also turn off quotes for those.
typeof grip === "string"
? StringRep({
object: grip,
useQuotes: false,
mode: props.mode,
style: styleObject
})
: Rep({
object: grip,
objectLink: VariablesViewLink,
onDOMNodeMouseOver,
onDOMNodeMouseOut,
defaultRep: Grip,
mode: props.mode,
})
+ )
);
}
function cleanupStyle(userProvidedStyle, createElement) {
// Regular expression that matches the allowed CSS property names.
const allowedStylesRegex = new RegExp(
"^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
"margin|padding|text|transition|outline|white-space|word|writing|" +
--- 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
@@ -37,32 +37,36 @@ function EvaluationResult(props) {
timeStamp,
parameters,
} = message;
let messageBody;
if (message.messageText) {
messageBody = message.messageText;
} else {
- messageBody = GripMessageBody({grip: parameters, serviceContainer});
+ messageBody = GripMessageBody({grip: parameters, serviceContainer, yolo: true});
}
+ console.log(message);
+
const topLevelClasses = ["cm-s-mozilla"];
const childProps = {
source,
type,
level,
indent,
topLevelClasses,
messageBody,
messageId,
scrollToMessage: props.autoscroll,
serviceContainer,
exceptionDocURL,
frame,
timeStamp,
- parameters,
+ contextMenuInfo: {
+ 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,16 @@ function NetworkEventMessage(props) {
source,
type,
level,
indent,
topLevelClasses,
timeStamp,
messageBody,
serviceContainer,
+ contextMenuInfo: {
+ 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
@@ -37,21 +37,21 @@ 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.any,
+ contextMenuInfo: PropTypes.object,
serviceContainer: PropTypes.shape({
emitNewMessage: PropTypes.func.isRequired,
onViewSourceInDebugger: PropTypes.func.isRequired,
- openContextMenu: PropTypes.func.isRequired,
+ addContextMenuInfo: PropTypes.func.isRequired,
sourceMapService: PropTypes.any,
}),
},
getDefaultProps: function () {
return {
indent: 0
};
@@ -66,19 +66,18 @@ const Message = createClass({
// did not emit for them.
if (this.props.serviceContainer) {
this.props.serviceContainer.emitNewMessage(
this.messageNode, this.props.messageId);
}
}
},
- onContextMenu: function (e) {
- this.props.serviceContainer.openContextMenu(e, this.props);
- e.stopPropagation();
+ onContextMenu: function () {
+ this.props.serviceContainer.addContextMenuInfo({ message: this.props });
},
onLearnMoreClick: function () {
let {exceptionDocURL} = this.props;
this.props.serviceContainer.openLink(exceptionDocURL);
},
render() {
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -7,29 +7,33 @@
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.contextMenuInfo = {};
this.init = this.init.bind(this);
}
NewConsoleOutputWrapper.prototype = {
init: function () {
const attachRefToHud = (id, node) => {
this.jsterm.hud[id] = node;
@@ -45,19 +49,34 @@ NewConsoleOutputWrapper.prototype = {
}]));
},
hudProxyClient: this.jsterm.hud.proxy.client,
onViewSourceInDebugger: frame => this.toolbox.viewSourceInDebugger.call(
this.toolbox,
frame.url,
frame.line
),
- openContextMenu: (e, message) => {
- let { screenX, screenY, target } = e;
- let menu = createContextMenu(target, message, this.jsterm, this.parentNode);
+ addContextMenuInfo: ({ message, grip }) => {
+ if (message) {
+ this.contextMenuInfo.message = message;
+ }
+
+ if (grip) {
+ this.contextMenuInfo.grip = grip;
+ }
+ },
+ openContextMenu: (e) => {
+ let { grip, message } = this.contextMenuInfo;
+ let { screenX, screenY } = e;
+ let messageElement = e.target.closest(".message");
+ let menu = createContextMenu(messageElement, message, grip, this.jsterm,
+ this.parentNode);
+ 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);
});
},
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -1,23 +1,34 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
head.js
test-batching.html
test-console.html
+ test-console-context-menu.html
test-console-filters.html
test-console-group.html
test-console-table.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]
+subsuite = clipboard
+[browser_webconsole_context_menu_open_url.js]
+subsuite = clipboard
+[browser_webconsole_context_menu_store_as_global.js]
+subsuite = clipboard
[browser_webconsole_filters.js]
[browser_webconsole_init.js]
[browser_webconsole_input_focus.js]
[browser_webconsole_keyboard_accessibility.js]
[browser_webconsole_nodes_highlight.js]
[browser_webconsole_observer_notifications.js]
[browser_webconsole_timestamps.js]
[browser_webconsole_vview_close_on_esc_key.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,88 @@
+/* -*- 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/ */
+
+/* globals goDoCommand */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "new-console-output/test/mochitest/test-console-context-menu.html";
+
+// Test copying of the entire console message when right-clicked
+// with no other text selected. See Bug 1100562.
+
+add_task(function* () {
+ let outputNode;
+ let contextMenu;
+
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ let button = content.wrappedJSObject.document.getElementById("testTrace");
+ button.click();
+ });
+
+ yield new Promise(r => setTimeout(r, 60000));
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "bug 1100562",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ lines: 1,
+ },
+ {
+ name: "console.trace output",
+ consoleTrace: true,
+ lines: 3,
+ },
+ ]
+ });
+
+ outputNode.focus();
+
+ for (let result of results) {
+ let message = [...result.matched][0];
+
+ yield waitForContextMenu(contextMenu, message, () => {
+ let copyItem = contextMenu.querySelector("#cMenu_copy");
+ copyItem.doCommand();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+ });
+
+ let clipboardText;
+
+ yield waitForClipboardPromise(
+ () => goDoCommand("cmd_copy"),
+ (str) => {
+ clipboardText = str;
+ return message.textContent == clipboardText;
+ }
+ );
+
+ ok(clipboardText, "Clipboard text was found and saved");
+
+ 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");
+
+ // 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");
+ }
+ }
+});
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,63 @@
+/* -*- 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 for the "Copy link location" context menu item shown when you right
+// click network requests in the output.
+
+"use strict";
+
+requestLongerTimeout(4);
+
+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* () {
+ yield pushPref("devtools.webconsole.filter.net", true);
+
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ let output = hud.outputNode;
+
+ hud.jsterm.clearOutput();
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.console.log("bug 638949");
+ });
+
+ let message = yield waitFor(() => findMessage(hud, "bug 638949"));
+
+ // Test that the "Copy Link Location" menu item is hidden for non-network
+ // messages.
+ let menuPopup = yield openContextMenu(hud, message);
+
+ // Copy URL context menu item is not rendered for non URL things
+ let isHidden = !menuPopup.querySelector(CONTEXT_MENU_ID);
+ ok(isHidden, CONTEXT_MENU_ID + " is hidden");
+
+ yield hideContextMenu(hud);
+
+ hud.jsterm.clearOutput();
+ // Reloading will produce network logging
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ content.wrappedJSObject.location.reload();
+ });
+
+ // Test that the "Copy Link Location" command is enabled and works
+ // as expected for any network-related message.
+ // This command should copy only the URL.
+ message = yield waitFor(() => findMessage(hud, "test-console.html"));
+ output.focus();
+ hud.ui.output.selectMessage(message);
+
+ info("expected clipboard value: " + message.url);
+
+ menuPopup = yield openContextMenu(hud, message);
+ let copyItem = menuPopup.querySelector(CONTEXT_MENU_ID);
+
+ ok(copyItem, "Copy url menu item is available in context menu");
+ copyItem.click();
+
+ yield waitForClipboardPromise(() => copyItem.click(), TEST_URI);
+ 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,55 @@
+/* -*- 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/ */
+
+// Tests that the "Open in Variables View" context menu item is enabled
+// only for objects.
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ console.log("foo");
+ console.log("foo", window);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openNewTabAndConsole(TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let message = yield waitFor(() => findMessage(hud, "foo"));
+
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let openInVarViewItem = contextMenu.querySelector("#menu_openInVarView");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(openInVarViewItem.disabled === false, "The \"Open In Variables View\" " +
+ "context menu item should be available for objects");
+ }, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled for texts");
+ });
+});
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,142 @@
+/* -*- 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/ */
+
+// This is a test for the Open URL context menu item
+// that is shown for network requests
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+const COMMAND_NAME = "consoleCmd_openURL";
+const CONTEXT_MENU_ID = "#menu_openURL";
+
+var HUD = null, outputNode = null, contextMenu = null;
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+
+ yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ let results = yield consoleOpened();
+ yield onConsoleMessage(results);
+
+ let results2 = yield testOnNetActivity();
+ let msg = yield onNetworkMessage(results2);
+
+ yield testOnNetActivityContextMenu(msg);
+
+ Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo");
+
+ HUD = null;
+ outputNode = null;
+ contextMenu = null;
+});
+
+function consoleOpened() {
+ outputNode = HUD.outputNode;
+ contextMenu = HUD.iframeWindow.document.getElementById("output-contextmenu");
+
+ HUD.jsterm.clearOutput();
+
+ content.console.log("bug 764572");
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug 764572",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onConsoleMessage(results) {
+ outputNode.focus();
+ outputNode.selectedItem = [...results[0].matched][0];
+
+ // Check if the command is disabled non-network messages.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+
+ let isDisabled = !controller || !controller.isCommandEnabled(COMMAND_NAME);
+ ok(isDisabled, COMMAND_NAME + " should be disabled.");
+
+ return waitForContextMenu(contextMenu, outputNode.selectedItem, () => {
+ let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " should be hidden.");
+ });
+}
+
+function testOnNetActivity() {
+ HUD.jsterm.clearOutput();
+
+ // Reload the url to show net activity in console.
+ content.location.reload();
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onNetworkMessage(results) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ let msg = [...results[0].matched][0];
+ ok(msg, "network message");
+ HUD.ui.output.selectMessage(msg);
+
+ let currentTab = gBrowser.selectedTab;
+ let newTab = null;
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(evt) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true);
+ newTab = evt.target;
+ newTab.linkedBrowser.addEventListener("load", onTabLoaded, true);
+ }, true);
+
+ function onTabLoaded() {
+ newTab.linkedBrowser.removeEventListener("load", onTabLoaded, true);
+ gBrowser.removeTab(newTab);
+ gBrowser.selectedTab = currentTab;
+ executeSoon(deferred.resolve.bind(null, msg));
+ }
+
+ // Check if the command is enabled for a network message.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+ ok(controller.isCommandEnabled(COMMAND_NAME),
+ COMMAND_NAME + " should be enabled.");
+
+ // Try to open the URL.
+ goDoCommand(COMMAND_NAME);
+
+ return deferred.promise;
+}
+
+function testOnNetActivityContextMenu(msg) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ HUD.ui.output.selectMessage(msg);
+
+ info("net activity context menu");
+
+ waitForContextMenu(contextMenu, msg, () => {
+ let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isShown, CONTEXT_MENU_ID + " should be shown.");
+ }).then(deferred.resolve);
+
+ return deferred.promise;
+}
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,66 @@
+/* -*- 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/ */
+
+// Tests the "Store as global variable" context menu item feature.
+// It should be work, and be enabled only for objects
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ window.bar = { baz: 1 };
+ console.log("foo");
+ console.log("foo", window.bar);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let storeAsGlobalItem = contextMenu.querySelector("#menu_storeAsGlobal");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+ let onceInputSet = hud.jsterm.once("set-input-value");
+
+ info("Waiting for context menu on the object");
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(storeAsGlobalItem.disabled === false, "The \"Store as global\" " +
+ "context menu item should be available for objects");
+ storeAsGlobalItem.click();
+ }, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ info("Waiting for context menu on the text node");
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled for texts");
+ });
+
+ info("Waiting for input to be set");
+ yield onceInputSet;
+
+ 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");
+
+});
--- 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/test/mochitest/test-console-context-menu.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test page for context menu</title>
+ <script type="text/javascript">
+ var fooObj = {
+ testProp: "testValue"
+ };
+
+ function test() {
+ var str = "Dolske Digs Bacon, Now and Forevermore."
+ for (var i=0; i < 5; i++) {
+ console.log(str);
+ }
+ }
+
+ function testTrace() {
+ console.log("bug 1100562");
+ console.trace();
+ }
+
+ console.info("INLINE SCRIPT:");
+ test();
+ console.warn("I'm warning you, he will eat up all yr bacon.");
+ console.error("Error Message");
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Heads Up Display Demo</h1>
+ <button onclick="test();">Log stuff about Dolske</button>
+ <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button>
+ <div id="myDiv"></div>
+ </body>
+</html>
--- a/devtools/client/webconsole/new-console-output/utils/context-menu.js
+++ b/devtools/client/webconsole/new-console-output/utils/context-menu.js
@@ -4,54 +4,77 @@
* 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 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");
-function createContextMenu(target, message, jsterm, parentNode) {
- let messageElement = target.closest(".message-body");
+function createContextMenu(messageElement, message, grip, jsterm, parentNode) {
+ let messageBody = messageElement.querySelector(".message-body");
let win = parentNode.ownerDocument.defaultView;
let selection = win.getSelection();
- let menu = new Menu();
+ let menu = new Menu({
+ id: "webconsole-menu"
+ });
+
+ let info = message && message.contextMenuInfo;
+ let actor = grip && grip.actor;
+ if (!actor) {
+ actor = info && info.parameters && info.parameters.actor;
+ }
+ // Open in variables view menu item.
+ menu.append(new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ visible: message && message.source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!info.request) {
+ return;
+ }
+ clipboardHelper.copyString(info.request.url);
+ },
+ }));
// Open in variables view menu item.
menu.append(new MenuItem({
id: "console-menu-open",
label: l10n.getStr("webconsole.menu.openInVarView.label"),
accesskey: l10n.getStr("webconsole.menu.openInVarView.accesskey"),
- disabled: !message || !message.parameters || !message.parameters.actor,
+ disabled: !actor,
click: () => {
- openVariablesView(message.parameters);
+ openVariablesView(actor);
},
}));
// Store as global variable menu item.
menu.append(new MenuItem({
id: "console-menu-store",
label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
- disabled: !message || !message.parameters || !message.parameters.actor,
+ 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: message.parameters.actor,
+ selectedObjectActor: actor,
};
jsterm.requestEvaluation(evalString, options).then((res) => {
jsterm.focus();
jsterm.setInputValue(res.result);
});
},
}));