Bug 1093953 - (Part 1) Make CSS warnings expandable to show affected DOM elements. r=Honza
authorRazvan Caliman <rcaliman@mozilla.com>
Fri, 03 May 2019 13:01:27 +0000
changeset 472497 835c16acd375943236bcac8d6d2ac6ff0925725b
parent 472496 1002276e26e22f1251c0d59d242759362dd8fccc
child 472498 26a53be19f261e246e1e2ef372395b2c81d772aa
push id113025
push usermalexandru@mozilla.com
push dateFri, 03 May 2019 22:03:16 +0000
treeherdermozilla-inbound@b2c30fabdfea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1093953, 1537876
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1093953 - (Part 1) Make CSS warnings expandable to show affected DOM elements. r=Honza This patch builds on [Bug 1537876](https://bugzilla.mozilla.org/show_bug.cgi?id=1537876) which associates CSS selectors with error messages where applicable. This patch introduces a new React component, `CSSWarning`, for messages of type CSS. It forks the`PageError` component which was shared for `LOG` messages of type `JAVASCRIPT` and type `CSS`. The `CSSWarning` component is expandable when the message has an associated CSS selector. When expanded, it runs a `document.querySelectorAll()` command to list all elements matching the selector. Clicking on any of the elements in the result jumps to the Inspector and select the corresponding node in the markup view. Not all errors have associated CSS selectors. Not all selectors match elements. The errors/warnings are a result of the CSS Parser; there is no guarantee that the CSS rule is used anywhere on the document. The query may return an empty `NodeList`. Differential Revision: https://phabricator.services.mozilla.com/D28457
devtools/client/locales/en-US/webconsole.properties
devtools/client/themes/webconsole.css
devtools/client/webconsole/actions/messages.js
devtools/client/webconsole/components/ConsoleOutput.js
devtools/client/webconsole/components/Message.js
devtools/client/webconsole/components/MessageContainer.js
devtools/client/webconsole/components/message-types/CSSWarning.js
devtools/client/webconsole/components/message-types/NetworkEventMessage.js
devtools/client/webconsole/components/message-types/moz.build
devtools/client/webconsole/constants.js
devtools/client/webconsole/reducers/messages.js
devtools/client/webconsole/selectors/messages.js
devtools/client/webconsole/types.js
devtools/client/webconsole/utils/messages.js
devtools/client/webconsole/webconsole-wrapper.js
devtools/server/actors/webconsole.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -353,8 +353,13 @@ webconsole.confirmDialog.getter.label=In
 # Label used for the confirm button in the "invoke getter" dialog that appears in the
 # console when a user tries to autocomplete a property with a getter.
 webconsole.confirmDialog.getter.invokeButtonLabel=Invoke
 
 # LOCALIZATION NOTE (webconsole.group.contentBlocked)
 # Label used as the group header in the console output when content blocking is enabled
 # and that we have several warning messages about resources being blocked.
 webconsole.group.contentBlocked=Content blocked messages
+
+# LOCALIZATION NOTE (webconsole.cssWarningElements.label)
+# Label for the list of HTML elements matching the selector associated
+# with the CSS warning. Parameters: %S is the CSS selector.
+webconsole.cssWarningElements.label=Elements matching selector: %S
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -247,16 +247,21 @@ a {
   min-width: 0px;
   margin: var(--console-output-vertical-padding) 0;
 }
 
 .message-body-wrapper .table-widget-body {
   overflow: visible;
 }
 
+.message-body-wrapper .elements-label {
+  color: var(--location-color);
+  margin: calc(var(--console-output-vertical-padding) * 2) 0;
+}
+
 /* The bubble that shows the number of times a message is repeated */
 .message-repeats,
 .warning-group-badge {
   flex-shrink: 0;
   margin: 2px 5px 0 5px;
   padding: 0 6px;
   height: 1.25em;
   border-radius: 40px;
--- a/devtools/client/webconsole/actions/messages.js
+++ b/devtools/client/webconsole/actions/messages.js
@@ -16,16 +16,17 @@ const {
   MESSAGES_ADD,
   NETWORK_MESSAGE_UPDATE,
   NETWORK_UPDATE_REQUEST,
   MESSAGES_CLEAR,
   MESSAGES_CLEAR_LOGPOINT,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
+  MESSAGE_UPDATE_PAYLOAD,
   MESSAGE_TABLE_RECEIVE,
   PAUSED_EXCECUTION_POINT,
   PRIVATE_MESSAGES_CLEAR,
 } = require("../constants");
 
 const defaultIdGenerator = new IdGenerator();
 
 function messagesAdd(packets, idGenerator = null) {
@@ -88,16 +89,39 @@ function messageOpen(id) {
 
 function messageClose(id) {
   return {
     type: MESSAGE_CLOSE,
     id,
   };
 }
 
+/**
+ * Make a query on the server to get a list of DOM elements matching the given
+ * CSS selectors and set the result as a message's additional data payload.
+ *
+ * @param {String} id
+ *        Message ID
+ * @param {String} cssSelectors
+ *        CSS selectors string to use in the querySelectorAll() call
+ * @return {[type]} [description]
+ */
+function messageGetMatchingElements(id, cssSelectors) {
+  return ({ dispatch, services }) => {
+    services
+      .requestEvaluation(`document.querySelectorAll('${cssSelectors}')`)
+      .then(response => {
+        dispatch(messageUpdatePayload(id, response.result));
+      })
+      .catch(err => {
+        console.error(err);
+      });
+  };
+}
+
 function messageTableDataGet(id, client, dataType) {
   return ({dispatch}) => {
     let fetchObjectActorData;
     if (["Map", "WeakMap", "Set", "WeakSet"].includes(dataType)) {
       fetchObjectActorData = (cb) => client.enumEntries(cb);
     } else {
       fetchObjectActorData = (cb) => client.enumProperties({
         ignoreNonIndexedProperties: dataType === "Array",
@@ -118,16 +142,32 @@ function messageTableDataGet(id, client,
 function messageTableDataReceive(id, data) {
   return {
     type: MESSAGE_TABLE_RECEIVE,
     id,
     data,
   };
 }
 
+/**
+ * Associate additional data with a message without mutating the original message object.
+ *
+ * @param {String} id
+ *        Message ID
+ * @param {Object} data
+ *        Object with arbitrary data.
+ */
+function messageUpdatePayload(id, data) {
+  return {
+    type: MESSAGE_UPDATE_PAYLOAD,
+    id,
+    data,
+  };
+}
+
 function networkMessageUpdate(packet, idGenerator = null, response) {
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
   }
 
   const message = prepareMessage(packet, idGenerator);
 
   return {
@@ -146,16 +186,18 @@ function networkUpdateRequest(id, data) 
 }
 
 module.exports = {
   messagesAdd,
   messagesClear,
   messagesClearLogpoint,
   messageOpen,
   messageClose,
+  messageGetMatchingElements,
   messageTableDataGet,
+  messageUpdatePayload,
   networkMessageUpdate,
   networkUpdateRequest,
   privateMessagesClear,
   // for test purpose only.
   messageTableDataReceive,
   setPauseExecutionPoint,
 };
--- a/devtools/client/webconsole/components/ConsoleOutput.js
+++ b/devtools/client/webconsole/components/ConsoleOutput.js
@@ -6,16 +6,17 @@
 const { Component, createElement } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
 const {initialize} = require("devtools/client/webconsole/actions/ui");
 
 const {
   getAllMessagesById,
   getAllMessagesUiById,
+  getAllMessagesPayloadById,
   getAllMessagesTableDataById,
   getAllNetworkMessagesUpdateById,
   getVisibleMessages,
   getPausedExecutionPoint,
   getAllRepeatById,
   getAllWarningGroupsById,
   isMessageInWarningGroup,
 } = require("devtools/client/webconsole/selectors/messages");
@@ -53,16 +54,17 @@ class ConsoleOutput extends Component {
       messagesUi: PropTypes.array.isRequired,
       serviceContainer: PropTypes.shape({
         attachRefToWebConsoleUI: PropTypes.func.isRequired,
         openContextMenu: PropTypes.func.isRequired,
         sourceMapService: PropTypes.object,
       }),
       dispatch: PropTypes.func.isRequired,
       timestampsVisible: PropTypes.bool,
+      messagesPayload: PropTypes.object.isRequired,
       messagesTableData: PropTypes.object.isRequired,
       messagesRepeat: PropTypes.object.isRequired,
       warningGroups: PropTypes.object.isRequired,
       isInWarningGroup: PropTypes.func,
       networkMessagesUpdate: PropTypes.object.isRequired,
       visibleMessages: PropTypes.array.isRequired,
       networkMessageActiveTabId: PropTypes.string.isRequired,
       onFirstMeaningfulPaint: PropTypes.func.isRequired,
@@ -168,16 +170,17 @@ class ConsoleOutput extends Component {
   }
 
   render() {
     let {
       dispatch,
       visibleMessages,
       messages,
       messagesUi,
+      messagesPayload,
       messagesTableData,
       messagesRepeat,
       warningGroups,
       isInWarningGroup,
       networkMessagesUpdate,
       networkMessageActiveTabId,
       serviceContainer,
       timestampsVisible,
@@ -198,16 +201,17 @@ class ConsoleOutput extends Component {
 
     const messageNodes = visibleMessages.map((messageId) =>
       createElement(MessageContainer, {
         dispatch,
         key: messageId,
         messageId,
         serviceContainer,
         open: messagesUi.includes(messageId),
+        payload: messagesPayload.get(messageId),
         tableData: messagesTableData.get(messageId),
         timestampsVisible,
         repeat: messagesRepeat[messageId],
         badge: warningGroups.has(messageId) ? warningGroups.get(messageId).length : null,
         inWarningGroup: isInWarningGroup
           ? isInWarningGroup(messages.get(messageId))
           : false,
         networkMessageUpdate: networkMessagesUpdate[messageId],
@@ -247,16 +251,17 @@ function isScrolledToBottom(outputNode, 
 
 function mapStateToProps(state, props) {
   return {
     initialized: state.ui.initialized,
     pausedExecutionPoint: getPausedExecutionPoint(state),
     messages: getAllMessagesById(state),
     visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
+    messagesPayload: getAllMessagesPayloadById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     messagesRepeat: getAllRepeatById(state),
     warningGroups: getAllWarningGroupsById(state),
     isInWarningGroup: state.prefs.groupWarnings
       ? message => isMessageInWarningGroup(state, message)
       : null,
     networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
     timestampsVisible: state.ui.timestampsVisible,
--- a/devtools/client/webconsole/components/Message.js
+++ b/devtools/client/webconsole/components/Message.js
@@ -22,16 +22,17 @@ loader.lazyRequireGetter(this, "PropType
 loader.lazyRequireGetter(this, "SmartTrace", "devtools/client/shared/components/SmartTrace");
 
 class Message extends Component {
   static get propTypes() {
     return {
       open: PropTypes.bool,
       collapsible: PropTypes.bool,
       collapseTitle: PropTypes.string,
+      onToggle: PropTypes.func,
       source: PropTypes.string.isRequired,
       type: PropTypes.string.isRequired,
       level: PropTypes.string.isRequired,
       indent: PropTypes.number.isRequired,
       inWarningGroup: PropTypes.bool,
       topLevelClasses: PropTypes.array.isRequired,
       messageBody: PropTypes.any.isRequired,
       repeat: PropTypes.any,
@@ -99,18 +100,23 @@ class Message extends Component {
   }
 
   onLearnMoreClick(e) {
     const {exceptionDocURL} = this.props;
     this.props.serviceContainer.openLink(exceptionDocURL, e);
   }
 
   toggleMessage(e) {
-    const { open, dispatch, messageId } = this.props;
-    if (open) {
+    const { open, dispatch, messageId, onToggle } = this.props;
+
+    // If defined on props, we let the onToggle() method handle the toggling,
+    // otherwise we toggle the message open/closed ourselves.
+    if (onToggle) {
+      onToggle(messageId, e);
+    } else if (open) {
       dispatch(actions.messageClose(messageId));
     } else {
       dispatch(actions.messageOpen(messageId));
     }
   }
 
   onContextMenu(e) {
     const { serviceContainer, source, request, messageId } = this.props;
--- a/devtools/client/webconsole/components/MessageContainer.js
+++ b/devtools/client/webconsole/components/MessageContainer.js
@@ -14,29 +14,31 @@ loader.lazyRequireGetter(this, "isWarnin
 const {
   MESSAGE_SOURCE,
   MESSAGE_TYPE,
 } = require("devtools/client/webconsole/constants");
 
 const componentMap = new Map([
   ["ConsoleApiCall", require("./message-types/ConsoleApiCall")],
   ["ConsoleCommand", require("./message-types/ConsoleCommand")],
+  ["CSSWarning", require("./message-types/CSSWarning")],
   ["DefaultRenderer", require("./message-types/DefaultRenderer")],
   ["EvaluationResult", require("./message-types/EvaluationResult")],
   ["NetworkEventMessage", require("./message-types/NetworkEventMessage")],
   ["PageError", require("./message-types/PageError")],
   ["WarningGroup", require("./message-types/WarningGroup")],
 ]);
 
 class MessageContainer extends Component {
   static get propTypes() {
     return {
       messageId: PropTypes.string.isRequired,
       open: PropTypes.bool.isRequired,
       serviceContainer: PropTypes.object.isRequired,
+      payload: PropTypes.object,
       tableData: PropTypes.object,
       timestampsVisible: PropTypes.bool.isRequired,
       repeat: PropTypes.number,
       badge: PropTypes.number,
       indent: PropTypes.number,
       networkMessageUpdate: PropTypes.object,
       getMessage: PropTypes.func.isRequired,
       isPaused: PropTypes.bool.isRequired,
@@ -48,29 +50,31 @@ class MessageContainer extends Component
     return {
       open: false,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     const repeatChanged = this.props.repeat !== nextProps.repeat;
     const openChanged = this.props.open !== nextProps.open;
+    const payloadChanged = this.props.payload !== nextProps.payload;
     const tableDataChanged = this.props.tableData !== nextProps.tableData;
     const timestampVisibleChanged =
       this.props.timestampsVisible !== nextProps.timestampsVisible;
     const networkMessageUpdateChanged =
       this.props.networkMessageUpdate !== nextProps.networkMessageUpdate;
     const pausedChanged = this.props.isPaused !== nextProps.isPaused;
     const executionPointChanged =
       this.props.pausedExecutionPoint !== nextProps.pausedExecutionPoint;
     const badgeChanged = this.props.badge !== nextProps.badge;
 
     return repeatChanged
       || badgeChanged
       || openChanged
+      || payloadChanged
       || tableDataChanged
       || timestampVisibleChanged
       || networkMessageUpdateChanged
       || pausedChanged
       || executionPointChanged;
   }
 
   render() {
@@ -87,16 +91,17 @@ function getMessageComponent(message) {
   }
 
   switch (message.source) {
     case MESSAGE_SOURCE.CONSOLE_API:
       return componentMap.get("ConsoleApiCall");
     case MESSAGE_SOURCE.NETWORK:
       return componentMap.get("NetworkEventMessage");
     case MESSAGE_SOURCE.CSS:
+      return componentMap.get("CSSWarning");
     case MESSAGE_SOURCE.JAVASCRIPT:
       switch (message.type) {
         case MESSAGE_TYPE.COMMAND:
           return componentMap.get("ConsoleCommand");
         case MESSAGE_TYPE.RESULT:
           return componentMap.get("EvaluationResult");
         // @TODO this is probably not the right behavior, but works for now.
         // Chrome doesn't distinguish between page errors and log messages. We
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/components/message-types/CSSWarning.js
@@ -0,0 +1,163 @@
+/* -*- 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const actions = require("devtools/client/webconsole/actions/index");
+
+const Message = createFactory(require("devtools/client/webconsole/components/Message"));
+
+loader.lazyRequireGetter(this, "GripMessageBody", "devtools/client/webconsole/components/GripMessageBody");
+
+/**
+ * This component is responsible for rendering CSS warnings in the Console panel.
+ *
+ * CSS warnings are expandable when they have associated CSS selectors so the
+ * user can inspect any matching DOM elements. Not all CSS warnings have
+ * associated selectors (those that don't are not expandable) and not all
+ * selectors match elements in the current page (warnings can appear for styles
+ * which don't apply to the current page).
+ *
+ * @extends Component
+ */
+class CSSWarning extends Component {
+  static get propTypes() {
+    return {
+      dispatch: PropTypes.func.isRequired,
+      inWarningGroup: PropTypes.bool.isRequired,
+      message: PropTypes.object.isRequired,
+      open: PropTypes.bool,
+      payload: PropTypes.object,
+      repeat: PropTypes.any,
+      serviceContainer: PropTypes.object,
+      timestampsVisible: PropTypes.bool.isRequired,
+    };
+  }
+
+  static get defaultProps() {
+    return {
+      open: false,
+    };
+  }
+
+  static get displayName() {
+    return "CSSWarning";
+  }
+
+  constructor(props) {
+    super(props);
+    this.onToggle = this.onToggle.bind(this);
+  }
+
+  onToggle(messageId) {
+    const {
+      dispatch,
+      message,
+      payload,
+      open,
+    } = this.props;
+
+    const {
+      cssSelectors,
+    } = message;
+
+    if (open) {
+      dispatch(actions.messageClose(messageId));
+    } else if (payload) {
+      // If the message already has information about the elements matching
+      // the selectors associated with this CSS warning, just open the message.
+      dispatch(actions.messageOpen(messageId));
+    } else {
+      // Query the server for elements matching the CSS selectors associated
+      // with this CSS warning and populate the message's additional data payload with
+      // the result. It's an async operation and potentially expensive, so we only do it
+      // on demand, once, when the component is first expanded.
+      dispatch(actions.messageGetMatchingElements(messageId, cssSelectors));
+      dispatch(actions.messageOpen(messageId));
+    }
+  }
+
+  render() {
+    const {
+      dispatch,
+      message,
+      open,
+      payload,
+      repeat,
+      serviceContainer,
+      timestampsVisible,
+      inWarningGroup,
+    } = this.props;
+
+    const {
+      id: messageId,
+      indent,
+      executionPoint,
+      cssSelectors,
+      source,
+      type,
+      level,
+      messageText,
+      frame,
+      exceptionDocURL,
+      timeStamp,
+      notes,
+    } = message;
+
+    let messageBody;
+    if (typeof messageText === "string") {
+      messageBody = messageText;
+    } else if (typeof messageText === "object" && messageText.type === "longString") {
+      messageBody = `${message.messageText.initial}…`;
+    }
+
+    // Create a message attachment only when the message is open and there is a result
+    // to the query for elements matching the CSS selectors associated with the message.
+    const attachment = (open && payload !== undefined) && dom.div(
+      { className: "devtools-monospace" },
+      dom.div(
+        { className: "elements-label"},
+        l10n.getFormatStr("webconsole.cssWarningElements.label", [cssSelectors])
+      ),
+      GripMessageBody({
+        dispatch,
+        escapeWhitespace: false,
+        grip: payload,
+        serviceContainer,
+      })
+    );
+
+    return Message({
+      attachment,
+      collapsible: !!cssSelectors.length,
+      dispatch,
+      executionPoint,
+      exceptionDocURL,
+      frame,
+      indent,
+      inWarningGroup,
+      level,
+      messageBody,
+      messageId,
+      notes,
+      open,
+      onToggle: this.onToggle,
+      repeat,
+      serviceContainer,
+      source,
+      timeStamp,
+      timestampsVisible,
+      topLevelClasses: [],
+      type,
+    });
+  }
+}
+
+module.exports = createFactory(CSSWarning);
--- a/devtools/client/webconsole/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/components/message-types/NetworkEventMessage.js
@@ -92,37 +92,37 @@ function NetworkEventMessage({
       },
     }, status);
     statusInfo = dom.span(
       {className: "status-info"},
       `[${httpVersion} `, statusCode, ` ${statusText} ${totalTime}ms]`
     );
   }
 
-  const toggle = (e) => {
+  const onToggle = (messageId, e) => {
     const shouldOpenLink = (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey);
     if (shouldOpenLink) {
       serviceContainer.openLink(request.url, e);
       e.stopPropagation();
     } else if (open) {
-      dispatch(actions.messageClose(id));
+      dispatch(actions.messageClose(messageId));
     } else {
-      dispatch(actions.messageOpen(id));
+      dispatch(actions.messageOpen(messageId));
     }
   };
 
   // Message body components.
   const method = dom.span({className: "method" }, request.method);
   const xhr = isXHR
     ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
     : null;
-  const requestUrl = dom.span({ className: "url", title: request.url, onClick: toggle },
+  const requestUrl = dom.span({ className: "url", title: request.url },
     request.url);
   const statusBody = statusInfo
-    ? dom.a({ className: "status", onClick: toggle }, statusInfo)
+    ? dom.a({ className: "status" }, statusInfo)
     : null;
 
   const messageBody = [xhr, method, requestUrl, statusBody];
 
   // API consumed by Net monitor UI components. Most of the method
   // are not needed in context of the Console panel (atm) and thus
   // let's just provide empty implementation.
   // Individual methods might be implemented step by step as needed.
@@ -164,16 +164,17 @@ function NetworkEventMessage({
     dispatch,
     messageId: id,
     source,
     type,
     level,
     indent,
     collapsible: true,
     open,
+    onToggle,
     attachment,
     topLevelClasses,
     timeStamp,
     messageBody,
     serviceContainer,
     request,
     timestampsVisible,
   });
--- a/devtools/client/webconsole/components/message-types/moz.build
+++ b/devtools/client/webconsole/components/message-types/moz.build
@@ -1,14 +1,15 @@
 # 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(
     'ConsoleApiCall.js',
     'ConsoleCommand.js',
+    'CSSWarning.js',
     'DefaultRenderer.js',
     'EvaluationResult.js',
     'NetworkEventMessage.js',
     'PageError.js',
     'WarningGroup.js',
 )
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -18,16 +18,17 @@ const actionTypes = {
   FILTER_TEXT_SET: "FILTER_TEXT_SET",
   FILTER_TOGGLE: "FILTER_TOGGLE",
   FILTERS_CLEAR: "FILTERS_CLEAR",
   HISTORY_LOADED: "HISTORY_LOADED",
   INITIALIZE: "INITIALIZE",
   MESSAGE_CLOSE: "MESSAGE_CLOSE",
   MESSAGE_OPEN: "MESSAGE_OPEN",
   MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
+  MESSAGE_UPDATE_PAYLOAD: "MESSAGE_UPDATE_PAYLOAD",
   MESSAGES_ADD: "MESSAGES_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
   MESSAGES_CLEAR_LOGPOINT: "MESSAGES_CLEAR_LOGPOINT",
   NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
   NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
   PERSIST_TOGGLE: "PERSIST_TOGGLE",
   PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR",
   REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
--- a/devtools/client/webconsole/reducers/messages.js
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -32,16 +32,19 @@ const {
 
 const {
   processNetworkUpdates,
 } = require("devtools/client/netmonitor/src/utils/request-utils");
 
 const MessageState = overrides => Object.freeze(Object.assign({
   // List of all the messages added to the console.
   messagesById: new Map(),
+  // List of additional data associated with messages (populated async or on-demand at a
+  // later time after the message is received).
+  messagesPayloadById: new Map(),
   // When recording or replaying, all progress values in messagesById.
   replayProgressMessages: new Set(),
   // Array of the visible messages.
   visibleMessages: [],
   // Object for the filtered messages.
   filteredMessagesCount: getDefaultFiltersCounter(),
   // List of the message ids which are opened.
   messagesUiById: [],
@@ -72,16 +75,17 @@ const MessageState = overrides => Object
 
 function cloneState(state) {
   return {
     messagesById: new Map(state.messagesById),
     replayProgressMessages: new Set(state.replayProgressMessages),
     visibleMessages: [...state.visibleMessages],
     filteredMessagesCount: {...state.filteredMessagesCount},
     messagesUiById: [...state.messagesUiById],
+    messagesPayloadById: new Map(state.messagesPayloadById),
     messagesTableDataById: new Map(state.messagesTableDataById),
     groupsById: new Map(state.groupsById),
     currentGroup: state.currentGroup,
     removedActors: [...state.removedActors],
     repeatById: {...state.repeatById},
     networkMessagesUpdateById: {...state.networkMessagesUpdateById},
     removedLogpointIds: new Set(state.removedLogpointIds),
     pausedExecutionPoint: state.pausedExecutionPoint,
@@ -275,16 +279,17 @@ function addMessage(newMessage, state, f
   }
 
   return state;
 }
 
 function messages(state = MessageState(), action, filtersState, prefsState, uiState) {
   const {
     messagesById,
+    messagesPayloadById,
     messagesUiById,
     messagesTableDataById,
     networkMessagesUpdateById,
     groupsById,
     visibleMessages,
   } = state;
 
   const {logLimit} = prefsState;
@@ -461,16 +466,22 @@ function messages(state = MessageState()
     case constants.MESSAGE_TABLE_RECEIVE:
       const {id, data} = action;
 
       return {
         ...state,
         messagesTableDataById: (new Map(messagesTableDataById)).set(id, data),
       };
 
+    case constants.MESSAGE_UPDATE_PAYLOAD:
+      return {
+        ...state,
+        messagesPayloadById: (new Map(messagesPayloadById)).set(action.id, action.data),
+      };
+
     case constants.NETWORK_MESSAGE_UPDATE:
       return {
         ...state,
         networkMessagesUpdateById: {
           ...networkMessagesUpdateById,
           [action.message.id]: action.message,
         },
       };
--- a/devtools/client/webconsole/selectors/messages.js
+++ b/devtools/client/webconsole/selectors/messages.js
@@ -14,16 +14,19 @@ function getAllMessagesById(state) {
 
 function getMessage(state, id) {
   return getAllMessagesById(state).get(id);
 }
 
 function getAllMessagesUiById(state) {
   return state.messages.messagesUiById;
 }
+function getAllMessagesPayloadById(state) {
+  return state.messages.messagesPayloadById;
+}
 
 function getAllMessagesTableDataById(state) {
   return state.messages.messagesTableDataById;
 }
 
 function getAllGroupsById(state) {
   return state.messages.groupsById;
 }
@@ -67,16 +70,17 @@ function isMessageInWarningGroup(state, 
 
   return getVisibleMessages(state).includes(getParentWarningGroupMessageId(message));
 }
 
 module.exports = {
   getAllGroupsById,
   getAllWarningGroupsById,
   getAllMessagesById,
+  getAllMessagesPayloadById,
   getAllMessagesTableDataById,
   getAllMessagesUiById,
   getAllNetworkMessagesUpdateById,
   getAllRepeatById,
   getCurrentGroup,
   getFilteredMessagesCount,
   getGroupsById,
   getMessage,
--- a/devtools/client/webconsole/types.js
+++ b/devtools/client/webconsole/types.js
@@ -41,16 +41,17 @@ exports.ConsoleMessage = function(props)
     parameters: null,
     repeatId: null,
     stacktrace: null,
     frame: null,
     groupId: null,
     errorMessageName: null,
     exceptionDocURL: null,
     executionPoint: undefined,
+    cssSelectors: "",
     userProvidedStyles: null,
     notes: null,
     indent: 0,
     prefix: "",
     private: false,
     logpointId: undefined,
     chromeContext: false,
   }, props);
--- a/devtools/client/webconsole/utils/messages.js
+++ b/devtools/client/webconsole/utils/messages.js
@@ -257,16 +257,17 @@ function transformPageErrorPacket(packet
     frame,
     errorMessageName: pageError.errorMessageName,
     exceptionDocURL: pageError.exceptionDocURL,
     timeStamp: pageError.timeStamp,
     notes: pageError.notes,
     private: pageError.private,
     executionPoint: pageError.executionPoint,
     chromeContext: pageError.chromeContext,
+    cssSelectors: pageError.cssSelectors,
   });
 }
 
 function transformNetworkEventPacket(packet) {
   const { networkEvent } = packet;
 
   return new NetworkEventMessage({
     actor: networkEvent.actor,
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -175,16 +175,20 @@ class WebConsoleWrapper {
         focusInput: () => {
           return webConsoleUI.jsterm && webConsoleUI.jsterm.focus();
         },
 
         evaluateInput: (expression) => {
           return webConsoleUI.jsterm && webConsoleUI.jsterm.execute(expression);
         },
 
+        requestEvaluation: (string, options) => {
+          return webConsoleUI.webConsoleClient.evaluateJSAsync(string, options);
+        },
+
         getInputCursor: () => {
           return webConsoleUI.jsterm && webConsoleUI.jsterm.getSelectionStart();
         },
 
         getSelectedNodeActor: () => {
           const inspectorSelection = this.hud.getInspectorSelection();
           if (inspectorSelection && inspectorSelection.nodeFront) {
             return inspectorSelection.nodeFront.actorID;
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1547,16 +1547,17 @@ WebConsoleActor.prototype =
       exception: !!(pageError.flags & pageError.exceptionFlag),
       strict: !!(pageError.flags & pageError.strictFlag),
       info: !!(pageError.flags & pageError.infoFlag),
       private: pageError.isFromPrivateWindow,
       stacktrace: stack,
       notes: notesArray,
       executionPoint: pageError.executionPoint,
       chromeContext: pageError.isFromChromeContext,
+      cssSelectors: pageError.cssSelectors,
     };
   },
 
   /**
    * Handler for window.console API calls received from the ConsoleAPIListener.
    * This method sends the object to the remote Web Console client.
    *
    * @see ConsoleAPIListener