Bug 1524276 - Add WarningGroup message component. r=bgrins.
☠☠ backed out by e365c225e26a ☠ ☠
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 29 Mar 2019 08:03:59 +0000
changeset 466718 1fee0c35777292ad11916d48c2a6d9cad17ee5e9
parent 466717 8bbe3e4f5a54c87b2b466dff3956dc288fd059d7
child 466719 47363a80ef6d1e7a43a2325d32fe024c205cd001
push id35780
push useropoprus@mozilla.com
push dateFri, 29 Mar 2019 21:53:01 +0000
treeherdermozilla-central@414f37afbe07 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1524276
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 1524276 - Add WarningGroup message component. r=bgrins. This component will be used to render warning groups messages. We also add a `inWarningGroup` prop to the `Message` component so warnings that will be displayed in such warningGroup can be styled differently (no warning icon, a different color for the indent). Add some utils functions and constants to check if a message should be a warning group. Differential Revision: https://phabricator.services.mozilla.com/D23551
devtools/client/themes/webconsole.css
devtools/client/webconsole/components/Message.js
devtools/client/webconsole/components/MessageContainer.js
devtools/client/webconsole/components/MessageIndent.js
devtools/client/webconsole/components/message-types/PageError.js
devtools/client/webconsole/components/message-types/WarningGroup.js
devtools/client/webconsole/components/message-types/moz.build
devtools/client/webconsole/constants.js
devtools/client/webconsole/test/components/warning-group.test.js
devtools/client/webconsole/utils/messages.js
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -155,16 +155,20 @@ a {
 
 .message > .indent {
   flex: none;
   display: inline-block;
   margin-inline-start: 12px;
   border-inline-end: solid 1px var(--console-output-indent-border-color);
 }
 
+.message > .indent.warning-indent {
+  border-inline-end-color: var(--warning-color);
+}
+
 .message > .indent[data-indent="0"] {
   display: none;
 }
 
 /* Center first level indent within the left gutter */
 .message:not(.startGroup):not(.startGroupCollapsed) > .indent[data-indent="1"] {
   margin-inline-start: calc(1px + var(--console-icon-horizontal-offset));
   margin-inline-end: calc(11px - var(--console-icon-horizontal-offset));
@@ -244,33 +248,44 @@ a {
   margin: var(--console-output-vertical-padding) 0;
 }
 
 .message-body-wrapper .table-widget-body {
   overflow: visible;
 }
 
 /* The bubble that shows the number of times a message is repeated */
-.message-repeats {
+.message-repeats,
+.warning-group-badge {
   flex-shrink: 0;
   margin: 2px 5px 0 5px;
   padding: 0 6px;
   height: 1.25em;
-  color: white;
-  background-color: var(--repeat-bubble-background-color);
   border-radius: 40px;
   font: message-box;
   font-size: 0.8em;
   font-weight: normal;
 }
 
+.message-repeats {
+  display: inline-block;
+  color: white;
+  background-color: var(--repeat-bubble-background-color);
+}
+
 .message-repeats[value="1"] {
   display: none;
 }
 
+.warning-group-badge {
+  display: inline-block;
+  color: var(--warning-background-color);
+  background-color: var(--warning-color);
+}
+
 .message-location {
   max-width: 40vw;
   flex-shrink: 0;
   color: var(--frame-link-source);
   margin-left: 1ch;
   /* Makes the file name truncated (and ellipsis shown) on the left side */
   direction: rtl;
   white-space: nowrap;
--- a/devtools/client/webconsole/components/Message.js
+++ b/devtools/client/webconsole/components/Message.js
@@ -26,16 +26,17 @@ class Message extends Component {
     return {
       open: PropTypes.bool,
       collapsible: PropTypes.bool,
       collapseTitle: PropTypes.string,
       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,
       frame: PropTypes.any,
       attachment: PropTypes.any,
       stacktrace: PropTypes.any,
       messageId: PropTypes.string,
       executionPoint: PropTypes.shape({
@@ -126,17 +127,27 @@ class Message extends Component {
   onMouseEvent(ev) {
     const {messageId, serviceContainer, executionPoint} = this.props;
     if (serviceContainer.canRewind() && executionPoint) {
       serviceContainer.onMessageHover(ev.type, messageId);
     }
   }
 
   renderIcon() {
-    const { level, messageId, executionPoint, serviceContainer } = this.props;
+    const {
+      level,
+      messageId,
+      executionPoint,
+      serviceContainer,
+      inWarningGroup,
+    } = this.props;
+
+    if (inWarningGroup) {
+      return undefined;
+    }
 
     return MessageIcon({
       level,
       onRewindClick: (serviceContainer.canRewind() && executionPoint)
         ? () => serviceContainer.jumpToExecutionPoint(executionPoint, messageId)
         : null,
     });
   }
@@ -146,16 +157,17 @@ class Message extends Component {
       open,
       collapsible,
       collapseTitle,
       source,
       type,
       isPaused,
       level,
       indent,
+      inWarningGroup,
       topLevelClasses,
       messageBody,
       frame,
       stacktrace,
       serviceContainer,
       exceptionDocURL,
       timeStamp = Date.now(),
       timestampsVisible,
@@ -296,29 +308,32 @@ class Message extends Component {
       ...mouseEvents,
       ref: node => {
         this.messageNode = node;
       },
       "data-message-id": messageId,
       "aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite",
     },
       timestampEl,
-      MessageIndent({indent}),
+      MessageIndent({
+        indent,
+        inWarningGroup,
+      }),
       icon,
       collapse,
       dom.span({ className: "message-body-wrapper" },
         dom.span({
           className: "message-flex-body",
           onClick: collapsible ? this.toggleMessage : undefined,
         },
           // Add whitespaces for formatting when copying to the clipboard.
           timestampEl ? " " : null,
           dom.span({ className: "message-body devtools-monospace" },
             ...bodyElements,
-            learnMore
+            learnMore,
           ),
           repeat ? " " : null,
           repeat,
           " ", location
         ),
         attachment,
         ...notesNodes
       ),
--- a/devtools/client/webconsole/components/MessageContainer.js
+++ b/devtools/client/webconsole/components/MessageContainer.js
@@ -4,40 +4,44 @@
  * 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";
 
 // React & Redux
 const { Component } = require("devtools/client/shared/vendor/react");
 loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
+loader.lazyRequireGetter(this, "isWarningGroup", "devtools/client/webconsole/utils/messages", true);
 
 const {
   MESSAGE_SOURCE,
   MESSAGE_TYPE,
 } = require("devtools/client/webconsole/constants");
 
 const componentMap = new Map([
   ["ConsoleApiCall", require("./message-types/ConsoleApiCall")],
   ["ConsoleCommand", require("./message-types/ConsoleCommand")],
   ["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,
       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,
       pausedExecutionPoint: PropTypes.any,
     };
   }
 
   static get defaultProps() {
@@ -52,18 +56,20 @@ class MessageContainer extends Component
     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
       || tableDataChanged
       || timestampVisibleChanged
       || networkMessageUpdateChanged
       || pausedChanged
       || executionPointChanged;
   }
 
@@ -96,16 +102,21 @@ function getMessageComponent(message) {
         // Chrome doesn't distinguish between page errors and log messages. We
         // may want to remove the PageError component and just handle errors
         // with ConsoleApiCall.
         case MESSAGE_TYPE.LOG:
           return componentMap.get("PageError");
         default:
           return componentMap.get("DefaultRenderer");
       }
+    case MESSAGE_SOURCE.CONSOLE_FRONTEND:
+      if (isWarningGroup(message)) {
+        return componentMap.get("WarningGroup");
+      }
+      break;
   }
 
   return componentMap.get("DefaultRenderer");
 }
 
 module.exports.MessageContainer = MessageContainer;
 
 // Exported so we can test it with unit tests.
--- a/devtools/client/webconsole/components/MessageIndent.js
+++ b/devtools/client/webconsole/components/MessageIndent.js
@@ -5,31 +5,36 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const INDENT_WIDTH = 12;
 
-// Store common indents so they can be used without recreating the element
-// during render.
+// Store common indents so they can be used without recreating the element during render.
 const CONSTANT_INDENTS = [getIndentElement(0), getIndentElement(1)];
+const IN_WARNING_GROUP_INDENT = getIndentElement(1, "warning-indent");
 
-function getIndentElement(indent) {
+function getIndentElement(indent, className) {
   return dom.span({
     "data-indent": indent,
-    className: "indent",
+    className: `indent${className ? " " + className : ""}`,
     style: {
       "width": indent * INDENT_WIDTH,
     },
   });
 }
 
 function MessageIndent(props) {
-  const { indent } = props;
+  const { indent, inWarningGroup } = props;
+
+  if (inWarningGroup) {
+    return IN_WARNING_GROUP_INDENT;
+  }
+
   return CONSTANT_INDENTS[indent] || getIndentElement(indent);
 }
 
 module.exports.MessageIndent = MessageIndent;
 
 // Exported so we can test it with unit tests.
 module.exports.INDENT_WIDTH = INDENT_WIDTH;
--- a/devtools/client/webconsole/components/message-types/PageError.js
+++ b/devtools/client/webconsole/components/message-types/PageError.js
@@ -14,37 +14,38 @@ const Message = createFactory(require("d
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
   maybeScrollToBottom: PropTypes.func,
+  inWarningGroup: PropTypes.bool.isRequired,
 };
 
 PageError.defaultProps = {
   open: false,
 };
 
 function PageError(props) {
   const {
     dispatch,
     message,
     open,
     repeat,
     serviceContainer,
     timestampsVisible,
     isPaused,
     maybeScrollToBottom,
+    inWarningGroup,
   } = props;
   const {
     id: messageId,
     executionPoint,
-    indent,
     source,
     type,
     level,
     messageText,
     stacktrace,
     frame,
     exceptionDocURL,
     timeStamp,
@@ -64,17 +65,18 @@ function PageError(props) {
     executionPoint,
     isPaused,
     open,
     collapsible: Array.isArray(stacktrace),
     source,
     type,
     level,
     topLevelClasses: [],
-    indent,
+    indent: message.indent,
+    inWarningGroup,
     messageBody,
     repeat,
     frame,
     stacktrace,
     serviceContainer,
     exceptionDocURL,
     timeStamp,
     notes,
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/components/message-types/WarningGroup.js
@@ -0,0 +1,73 @@
+/* -*- 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";
+
+// React & Redux
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Message = createFactory(require("devtools/client/webconsole/components/Message"));
+
+WarningGroup.displayName = "WarningGroup";
+
+WarningGroup.propTypes = {
+  dispatch: PropTypes.func.isRequired,
+  message: PropTypes.object.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
+  serviceContainer: PropTypes.object,
+  badge: PropTypes.number.isRequired,
+};
+
+function WarningGroup(props) {
+  const {
+    dispatch,
+    message,
+    serviceContainer,
+    timestampsVisible,
+    badge,
+    open,
+  } = props;
+
+  const {
+    source,
+    type,
+    level,
+    id: messageId,
+    indent,
+    timeStamp,
+  } = message;
+
+  const messageBody = [
+    message.messageText,
+    " ",
+    dom.span({
+      className: "warning-group-badge",
+      title: `${badge} messages`,
+    }, badge),
+  ];
+  const topLevelClasses = ["cm-s-mozilla"];
+
+  return Message({
+    badge,
+    collapsible: true,
+    dispatch,
+    indent,
+    level,
+    messageBody,
+    messageId,
+    open,
+    serviceContainer,
+    source,
+    timeStamp,
+    timestampsVisible,
+    topLevelClasses,
+    type,
+  });
+}
+
+module.exports = WarningGroup;
--- a/devtools/client/webconsole/components/message-types/moz.build
+++ b/devtools/client/webconsole/components/message-types/moz.build
@@ -5,9 +5,10 @@
 
 DevToolsModules(
     'ConsoleApiCall.js',
     'ConsoleCommand.js',
     'DefaultRenderer.js',
     'EvaluationResult.js',
     'NetworkEventMessage.js',
     'PageError.js',
+    'WarningGroup.js',
 )
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -107,32 +107,38 @@ const DEFAULT_FILTERS = Object.keys(DEFA
 
 const chromeRDPEnums = {
   MESSAGE_SOURCE: {
     XML: "xml",
     CSS: "css",
     JAVASCRIPT: "javascript",
     NETWORK: "network",
     CONSOLE_API: "console-api",
+    // Messages emitted by the console frontend itself (i.e. similar messages grouping
+    // header).
+    CONSOLE_FRONTEND: "console-frontend",
     STORAGE: "storage",
     APPCACHE: "appcache",
     RENDERING: "rendering",
     SECURITY: "security",
     OTHER: "other",
     DEPRECATION: "deprecation",
   },
   MESSAGE_TYPE: {
     LOG: "log",
     DIR: "dir",
     TABLE: "table",
     TRACE: "trace",
     CLEAR: "clear",
     START_GROUP: "startGroup",
     START_GROUP_COLLAPSED: "startGroupCollapsed",
     END_GROUP: "endGroup",
+    CONTENT_BLOCKING_GROUP: "contentBlockingWarningGroup",
+    CORS_GROUP: "CORSWarningGroup",
+    CSP_GROUP: "CSPWarningGroup",
     ASSERT: "assert",
     DEBUG: "debug",
     PROFILE: "profile",
     PROFILE_END: "profileEnd",
     // Undocumented in Chrome RDP, but is used for evaluation results.
     RESULT: "result",
     // Undocumented in Chrome RDP, but is used for input.
     COMMAND: "command",
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/components/warning-group.test.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// Components under test.
+const WarningGroup = require("devtools/client/webconsole/components/message-types/WarningGroup");
+const { MESSAGE_SOURCE } = require("devtools/client/webconsole/constants");
+const { ConsoleMessage } = require("devtools/client/webconsole/types");
+
+// Test fakes.
+const serviceContainer = require("devtools/client/webconsole/test/fixtures/serviceContainer");
+const message = ConsoleMessage({
+  messageText: "this is a warning group",
+  source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+  timeStamp: Date.now(),
+});
+
+describe("WarningGroup component:", () => {
+  it("renders", () => {
+    const wrapper = render(WarningGroup({
+      message,
+      serviceContainer,
+      timestampsVisible: true,
+      badge: 42,
+    }));
+
+    const { timestampString } = require("devtools/client/webconsole/webconsole-l10n");
+    expect(wrapper.find(".timestamp").text()).toBe(timestampString(message.timeStamp));
+    expect(wrapper.find(".message-body").text()).toBe("this is a warning group 42");
+    expect(wrapper.find(".arrow[aria-expanded=false]")).toExist();
+  });
+
+  it("does have an expanded arrow when `open` prop is true", () => {
+    const wrapper = render(WarningGroup({
+      message,
+      serviceContainer,
+      open: true,
+    }));
+
+    expect(wrapper.find(".arrow[aria-expanded=true]")).toExist();
+  });
+
+  it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+    const wrapper = render(WarningGroup({
+      message,
+      serviceContainer,
+      timestampsVisible: false,
+    }));
+
+    expect(wrapper.find(".timestamp").length).toBe(0);
+  });
+});
--- a/devtools/client/webconsole/utils/messages.js
+++ b/devtools/client/webconsole/utils/messages.js
@@ -425,17 +425,29 @@ function isPacketPrivate(packet) {
   return (
     packet.private === true ||
     (packet.message && packet.message.private === true) ||
     (packet.pageError && packet.pageError.private === true) ||
     (packet.networkEvent && packet.networkEvent.private === true)
   );
 }
 
+/**
+ * Returns true if the message is a warningGroup message (i.e. the "Header").
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isWarningGroup(message) {
+  return message.type === MESSAGE_TYPE.TRACKING_PROTECTION_GROUP
+   || message.type === MESSAGE_TYPE.CORS_GROUP
+   || message.type === MESSAGE_TYPE.CSP_GROUP;
+}
+
 module.exports = {
   getInitialMessageCountForViewport,
   isGroupType,
   isPacketPrivate,
+  isWarningGroup,
   l10n,
   prepareMessage,
   // Export for use in testing.
   getRepeatId,
 };