Bug 1419083 - Add an "Open in sidebar" context menu entry on ObjectInspector. r=nchevobbe

MozReview-Commit-ID: 9a2fBjpZ6zE

--HG--
extra : rebase_source : f533989d8f01a4f96133efbc98ef57e31806da46
This commit is contained in:
Mike Park 2017-12-05 16:05:13 -05:00
parent 33d26976c8
commit 35bbe45a57
14 changed files with 257 additions and 38 deletions

View File

@ -251,6 +251,12 @@ webconsole.menu.copyObject.accesskey=o
webconsole.menu.selectAll.label=Select all
webconsole.menu.selectAll.accesskey=A
# LOCALIZATION NOTE (webconsole.menu.openInSidebar.label)
# Label used for a context-menu item displayed for object/variable logs. Clicking on it
# opens the webconsole sidebar for the logged variable.
webconsole.menu.openInSidebar.label=Open in sidebar
webconsole.menu.openInSidebar.accesskey=V
# LOCALIZATION NOTE (webconsole.clearButton.tooltip)
# Label used for the tooltip on the clear logs button in the console top toolbar bar.
# Clicking on it will clear the content of the console.

View File

@ -1217,6 +1217,7 @@ body #output-container {
display: grid;
grid-template-rows: auto 1fr;
width: 100%;
overflow: hidden;
}
.webconsole-sidebar-toolbar {
@ -1228,6 +1229,7 @@ body #output-container {
.sidebar-contents {
grid-row: 2 / 3;
overflow: scroll;
}
.webconsole-sidebar-toolbar .sidebar-close-button {

View File

@ -7,6 +7,7 @@
"use strict";
const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
const { getMessage } = require("devtools/client/webconsole/new-console-output/selectors/messages");
const Services = require("Services");
const {
@ -15,7 +16,8 @@ const {
PERSIST_TOGGLE,
PREFS,
SELECT_NETWORK_MESSAGE_TAB,
SIDEBAR_TOGGLE,
SIDEBAR_CLOSE,
SHOW_OBJECT_IN_SIDEBAR,
TIMESTAMPS_TOGGLE,
} = require("devtools/client/webconsole/new-console-output/constants");
@ -59,9 +61,26 @@ function initialize() {
};
}
function sidebarToggle(show) {
function sidebarClose(show) {
return {
type: SIDEBAR_TOGGLE,
type: SIDEBAR_CLOSE,
};
}
function showObjectInSidebar(actorId, messageId) {
return (dispatch, getState) => {
let { parameters } = getMessage(getState(), messageId);
if (Array.isArray(parameters)) {
for (let parameter of parameters) {
if (parameter.actor === actorId) {
dispatch({
type: SHOW_OBJECT_IN_SIDEBAR,
grip: parameter
});
return;
}
}
}
};
}
@ -70,6 +89,7 @@ module.exports = {
initialize,
persistToggle,
selectNetworkMessageTab,
sidebarToggle,
sidebarClose,
showObjectInSidebar,
timestampsToggle,
};

View File

@ -32,7 +32,6 @@ class FilterBar extends Component {
filterBarVisible: PropTypes.bool.isRequired,
persistLogs: PropTypes.bool.isRequired,
filteredMessagesCount: PropTypes.object.isRequired,
sidebarToggle: PropTypes.bool,
};
}
@ -40,7 +39,6 @@ class FilterBar extends Component {
super(props);
this.onClickMessagesClear = this.onClickMessagesClear.bind(this);
this.onClickFilterBarToggle = this.onClickFilterBarToggle.bind(this);
this.onClickSidebarToggle = this.onClickSidebarToggle.bind(this);
this.onClickRemoveAllFilters = this.onClickRemoveAllFilters.bind(this);
this.onClickRemoveTextFilter = this.onClickRemoveTextFilter.bind(this);
this.onSearchInput = this.onSearchInput.bind(this);
@ -87,10 +85,6 @@ class FilterBar extends Component {
this.props.dispatch(actions.filterBarToggle());
}
onClickSidebarToggle() {
this.props.dispatch(actions.sidebarToggle());
}
onClickRemoveAllFilters() {
this.props.dispatch(actions.defaultFiltersReset());
}
@ -226,7 +220,6 @@ class FilterBar extends Component {
filterBarVisible,
persistLogs,
filteredMessagesCount,
sidebarToggle,
} = this.props;
let children = [
@ -261,13 +254,6 @@ class FilterBar extends Component {
onChange: this.onChangePersistToggle,
checked: persistLogs,
}),
sidebarToggle ?
dom.button({
className: "devtools-button webconsole-sidebar-button",
title: l10n.getStr("webconsole.toggleFilterButton.tooltip"),
onClick: this.onClickSidebarToggle
}, "Toggle Sidebar")
: null,
)
];
@ -298,7 +284,6 @@ function mapStateToProps(state) {
filterBarVisible: uiState.filterBarVisible,
persistLogs: uiState.persistLogs,
filteredMessagesCount: getFilteredMessagesCount(state),
sidebarToggle: state.prefs.sidebarToggle,
};
}

View File

@ -14,22 +14,24 @@ class SideBar extends Component {
static get propTypes() {
return {
dispatch: PropTypes.func.isRequired,
sidebarVisible: PropTypes.bool
sidebarVisible: PropTypes.bool,
grip: PropTypes.object,
};
}
constructor(props) {
super(props);
this.onClickSidebarToggle = this.onClickSidebarToggle.bind(this);
this.onClickSidebarClose = this.onClickSidebarClose.bind(this);
}
onClickSidebarToggle() {
this.props.dispatch(actions.sidebarToggle());
onClickSidebarClose() {
this.props.dispatch(actions.sidebarClose());
}
render() {
let {
sidebarVisible,
grip,
} = this.props;
let endPanel = dom.aside({
@ -40,12 +42,12 @@ class SideBar extends Component {
},
dom.button({
className: "devtools-button sidebar-close-button",
onClick: this.onClickSidebarToggle
onClick: this.onClickSidebarClose
})
),
dom.aside({
className: "sidebar-contents"
}, "Sidebar WIP")
}, JSON.stringify(grip, null, 2))
);
return (
@ -66,6 +68,7 @@ class SideBar extends Component {
function mapStateToProps(state, props) {
return {
sidebarVisible: state.ui.sidebarVisible,
grip: state.ui.gripInSidebar,
};
}

View File

@ -117,6 +117,7 @@ function ConsoleApiCall(props) {
indent,
timeStamp,
timestampsVisible,
parameters,
});
}

View File

@ -24,7 +24,8 @@ const actionTypes = {
PERSIST_TOGGLE: "PERSIST_TOGGLE",
REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
SIDEBAR_TOGGLE: "SIDEBAR_TOGGLE",
SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
};

View File

@ -119,11 +119,23 @@ NewConsoleOutputWrapper.prototype = {
? messageVariable.textContent : null;
// Retrieve closes actor id from the DOM.
let actorEl = target.closest("[data-link-actor-id]");
let actorEl = target.closest("[data-link-actor-id]") ||
target.querySelector("[data-link-actor-id]");
let actor = actorEl ? actorEl.dataset.linkActorId : null;
let rootObjectInspector = target.closest(".object-inspector");
let rootActor = rootObjectInspector ?
rootObjectInspector.querySelector("[data-link-actor-id]") : null;
let rootActorId = rootActor ? rootActor.dataset.linkActorId : null;
let sidebarTogglePref = store.getState().prefs.sidebarToggle;
let openSidebar = sidebarTogglePref ? (messageId) => {
store.dispatch(actions.showObjectInSidebar(rootActorId, messageId));
} : null;
let menu = createContextMenu(this.jsterm, this.parentNode,
{ actor, clipboardText, variableText, message, serviceContainer });
{ actor, clipboardText, variableText, message,
serviceContainer, openSidebar, rootActorId });
// Emit the "menu-open" event for testing.
menu.once("open", () => this.emit("menu-open"));

View File

@ -10,7 +10,8 @@ const {
INITIALIZE,
PERSIST_TOGGLE,
SELECT_NETWORK_MESSAGE_TAB,
SIDEBAR_TOGGLE,
SIDEBAR_CLOSE,
SHOW_OBJECT_IN_SIDEBAR,
TIMESTAMPS_TOGGLE,
MESSAGES_CLEAR,
} = require("devtools/client/webconsole/new-console-output/constants");
@ -26,6 +27,7 @@ const UiState = (overrides) => Object.freeze(Object.assign({
persistLogs: false,
sidebarVisible: false,
timestampsVisible: true,
gripInSidebar: null
}, overrides));
function ui(state = UiState(), action) {
@ -38,12 +40,20 @@ function ui(state = UiState(), action) {
return Object.assign({}, state, {timestampsVisible: action.visible});
case SELECT_NETWORK_MESSAGE_TAB:
return Object.assign({}, state, {networkMessageActiveTabId: action.id});
case SIDEBAR_TOGGLE:
return Object.assign({}, state, {sidebarVisible: !state.sidebarVisible});
case SIDEBAR_CLOSE:
return Object.assign({}, state, {
sidebarVisible: !state.sidebarVisible,
gripInSidebar: null
});
case INITIALIZE:
return Object.assign({}, state, {initialized: true});
case MESSAGES_CLEAR:
return Object.assign({}, state, {sidebarVisible: false});
return Object.assign({}, state, {sidebarVisible: false, gripInSidebar: null});
case SHOW_OBJECT_IN_SIDEBAR:
if (action.grip === state.gripInSidebar) {
return state;
}
return Object.assign({}, state, {sidebarVisible: true, gripInSidebar: action.grip});
}
return state;

View File

@ -260,6 +260,7 @@ 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_object_in_sidebar.js]
[browser_webconsole_context_menu_open_url.js]
[browser_webconsole_context_menu_store_as_global.js]
[browser_webconsole_csp_ignore_reflected_xss_message.js]

View File

@ -63,9 +63,19 @@ add_task(async function () {
});
async function showSidebar(hud) {
let toggleButton = hud.ui.document.querySelector(".webconsole-sidebar-button");
let onMessage = waitForMessage(hud, "Object");
ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
content.wrappedJSObject.console.log({a: 1});
});
await onMessage;
let objectNode = hud.ui.outputNode.querySelector(".object-inspector .objectBox");
let wrapper = hud.ui.document.querySelector(".webconsole-output-wrapper");
let onSidebarShown = waitForNodeMutation(wrapper, { childList: true });
toggleButton.click();
let contextMenu = await openContextMenu(hud, objectNode);
let openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar");
openInSidebar.click();
await onSidebarShown;
await hideContextMenu(hud);
}

View File

@ -0,0 +1,90 @@
/* -*- 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 in sidebar" context menu entry is active for
// the correct objects and opens the sidebar when clicked.
"use strict";
const TEST_URI =
"data:text/html;charset=utf8," +
"<script>console.log({a:1},100,{b:1},'foo',false,null,undefined);</script>";
add_task(async function () {
// Should be removed when sidebar work is complete
await pushPref("devtools.webconsole.sidebarToggle", true);
let hud = await openNewTabAndConsole(TEST_URI);
let message = findMessage(hud, "foo");
let [objectA, objectB] =
message.querySelectorAll(".object-inspector .objectBox-object");
let number = findMessage(hud, "100", ".objectBox");
let string = findMessage(hud, "foo", ".objectBox");
let bool = findMessage(hud, "false", ".objectBox");
let nullMessage = findMessage(hud, "null", ".objectBox");
let undefinedMsg = findMessage(hud, "undefined", ".objectBox");
info("Showing sidebar for {a:1}");
await showSidebarWithContextMenu(hud, objectA, true);
let sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
ok(sidebarText.includes('"a":'), "Sidebar is shown for {a:1}");
info("Showing sidebar for {a:1} again");
await showSidebarWithContextMenu(hud, objectA, false);
ok(hud.ui.document.querySelector(".sidebar"),
"Sidebar is still shown after clicking on same object");
is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
"Sidebar is not updated after clicking on same object");
info("Showing sidebar for {b:1}");
await showSidebarWithContextMenu(hud, objectB, false);
isnot(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
"Sidebar is updated for {b:1}");
sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
ok(sidebarText.includes('"b":'), "Sidebar contents shown for {b:1}");
info("Checking context menu entry is disabled for number");
let numberContextMenuEnabled = await isContextMenuEntryEnabled(hud, number);
ok(!numberContextMenuEnabled, "Context menu entry is disabled for number");
info("Checking context menu entry is disabled for string");
let stringContextMenuEnabled = await isContextMenuEntryEnabled(hud, string);
ok(!stringContextMenuEnabled, "Context menu entry is disabled for string");
info("Checking context menu entry is disabled for bool");
let boolContextMenuEnabled = await isContextMenuEntryEnabled(hud, bool);
ok(!boolContextMenuEnabled, "Context menu entry is disabled for bool");
info("Checking context menu entry is disabled for null message");
let nullContextMenuEnabled = await isContextMenuEntryEnabled(hud, nullMessage);
ok(!nullContextMenuEnabled, "Context menu entry is disabled for nullMessage");
info("Checking context menu entry is disabled for undefined message");
let undefinedContextMenuEnabled = await isContextMenuEntryEnabled(hud, undefinedMsg);
ok(!undefinedContextMenuEnabled, "Context menu entry is disabled for undefinedMsg");
});
async function showSidebarWithContextMenu(hud, node, expectMutation) {
let wrapper = hud.ui.document.querySelector(".webconsole-output-wrapper");
let onSidebarShown = waitForNodeMutation(wrapper, { childList: true });
let contextMenu = await openContextMenu(hud, node);
let openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar");
openInSidebar.click();
if (expectMutation) {
await onSidebarShown;
}
await hideContextMenu(hud);
}
async function isContextMenuEntryEnabled(hud, node) {
let contextMenu = await openContextMenu(hud, node);
let openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar");
let enabled = !openInSidebar.attributes.disabled;
await hideContextMenu(hud);
return enabled;
}

View File

@ -7,6 +7,8 @@ const expect = require("expect");
const actions = require("devtools/client/webconsole/new-console-output/actions/index");
const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
const { getAllMessagesById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
describe("Testing UI", () => {
let store;
@ -17,16 +19,16 @@ describe("Testing UI", () => {
describe("Toggle sidebar", () => {
it("sidebar is toggled on and off", () => {
store.dispatch(actions.sidebarToggle());
store.dispatch(actions.sidebarClose());
expect(store.getState().ui.sidebarVisible).toEqual(true);
store.dispatch(actions.sidebarToggle());
store.dispatch(actions.sidebarClose());
expect(store.getState().ui.sidebarVisible).toEqual(false);
});
});
describe("Hide sidebar on clear", () => {
it("sidebar is hidden on clear", () => {
store.dispatch(actions.sidebarToggle());
store.dispatch(actions.sidebarClose());
expect(store.getState().ui.sidebarVisible).toEqual(true);
store.dispatch(actions.messagesClear());
expect(store.getState().ui.sidebarVisible).toEqual(false);
@ -34,4 +36,64 @@ describe("Testing UI", () => {
expect(store.getState().ui.sidebarVisible).toEqual(false);
});
});
describe("Show object in sidebar", () => {
it("sidebar is shown with correct object", () => {
const packet = stubPackets.get("inspect({a: 1})");
const message = stubPreparedMessages.get("inspect({a: 1})");
store.dispatch(actions.messageAdd(packet));
const messages = getAllMessagesById(store.getState());
const actorId = message.parameters[0].actor;
const messageId = messages.first().id;
store.dispatch(actions.showObjectInSidebar(actorId, messageId));
expect(store.getState().ui.sidebarVisible).toEqual(true);
expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
});
it("sidebar is not updated for the same object", () => {
const packet = stubPackets.get("inspect({a: 1})");
const message = stubPreparedMessages.get("inspect({a: 1})");
store.dispatch(actions.messageAdd(packet));
const messages = getAllMessagesById(store.getState());
const actorId = message.parameters[0].actor;
const messageId = messages.first().id;
store.dispatch(actions.showObjectInSidebar(actorId, messageId));
expect(store.getState().ui.sidebarVisible).toEqual(true);
expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
let state = store.getState().ui;
store.dispatch(actions.showObjectInSidebar(actorId, messageId));
expect(store.getState().ui).toEqual(state);
});
it("sidebar shown and updated for new object", () => {
const packet = stubPackets.get("inspect({a: 1})");
const message = stubPreparedMessages.get("inspect({a: 1})");
store.dispatch(actions.messageAdd(packet));
const messages = getAllMessagesById(store.getState());
const actorId = message.parameters[0].actor;
const messageId = messages.first().id;
store.dispatch(actions.showObjectInSidebar(actorId, messageId));
expect(store.getState().ui.sidebarVisible).toEqual(true);
expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
const newPacket = stubPackets.get("new Date(0)");
const newMessage = stubPreparedMessages.get("new Date(0)");
store.dispatch(actions.messageAdd(newPacket));
const newMessages = getAllMessagesById(store.getState());
const newActorId = newMessage.parameters[0].actor;
const newMessageId = newMessages.last().id;
store.dispatch(actions.showObjectInSidebar(newActorId, newMessageId));
expect(store.getState().ui.sidebarVisible).toEqual(true);
expect(store.getState().ui.gripInSidebar).toEqual(newMessage.parameters[0]);
});
});
});

View File

@ -32,13 +32,18 @@ const { l10n } = require("devtools/client/webconsole/new-console-output/utils/me
* - {Object} message (optional) message object containing metadata such as:
* - {String} source
* - {String} request
* - {Function} openSidebar (optional) function that will open the object
* inspector sidebar
* - {String} rootActorId (optional) actor id for the root object being clicked on
*/
function createContextMenu(jsterm, parentNode, {
actor,
clipboardText,
variableText,
message,
serviceContainer
serviceContainer,
openSidebar,
rootActorId,
}) {
let win = parentNode.ownerDocument.defaultView;
let selection = win.getSelection();
@ -165,6 +170,17 @@ function createContextMenu(jsterm, parentNode, {
},
}));
// Open object in sidebar.
if (openSidebar) {
menu.append(new MenuItem({
id: "console-menu-open-sidebar",
label: l10n.getStr("webconsole.menu.openInSidebar.label"),
acesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"),
disabled: !rootActorId,
click: () => openSidebar(message.messageId),
}));
}
return menu;
}