Bug 1306622 - New console frontend: Factor out common HTML structures from message types. r=bgrins
authorLin Clark <lclark@mozilla.com>
Sun, 02 Oct 2016 15:16:49 -0700
changeset 359154 c4704a0ce400f59397ad3101402f9ad5ece1fdfb
parent 359153 a277201b2d885ef4511d539a32d2a672f261ae6a
child 359155 d94f92d3dc02d8c3094ef55621393f56de56a204
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1306622
milestone52.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 1306622 - New console frontend: Factor out common HTML structures from message types. r=bgrins MozReview-Commit-ID: 3JjyRuJyFAP
devtools/client/webconsole/new-console-output/components/collapse-button.js
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/console-table.js
devtools/client/webconsole/new-console-output/components/filter-bar.js
devtools/client/webconsole/new-console-output/components/filter-button.js
devtools/client/webconsole/new-console-output/components/grip-message-body.js
devtools/client/webconsole/new-console-output/components/message-container.js
devtools/client/webconsole/new-console-output/components/message-icon.js
devtools/client/webconsole/new-console-output/components/message-repeat.js
devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
devtools/client/webconsole/new-console-output/components/message-types/console-command.js
devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
devtools/client/webconsole/new-console-output/components/message-types/page-error.js
devtools/client/webconsole/new-console-output/components/message.js
devtools/client/webconsole/new-console-output/components/moz.build
devtools/client/webconsole/new-console-output/components/variables-view-link.js
devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
devtools/client/webconsole/new-console-output/test/components/message-container.test.js
devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
devtools/client/webconsole/new-console-output/test/components/page-error.test.js
devtools/client/webconsole/new-console-output/test/components/repeat.test.js
--- a/devtools/client/webconsole/new-console-output/components/collapse-button.js
+++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js
@@ -35,9 +35,9 @@ const CollapseButton = createClass({
     return dom.a({
       className: classes.join(" "),
       onClick,
       title: l10n.getStr("messageToggleDetails"),
     });
   }
 });
 
-module.exports.CollapseButton = CollapseButton;
+module.exports = CollapseButton;
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -12,28 +12,28 @@ const {
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const { getAllMessages, getAllMessagesUiById, getAllMessagesTableDataById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
+  displayName: "ConsoleOutput",
+
   propTypes: {
     hudProxyClient: PropTypes.object.isRequired,
     messages: PropTypes.object.isRequired,
     messagesUi: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     openNetworkPanel: PropTypes.func.isRequired,
     openLink: PropTypes.func.isRequired,
   },
 
-  displayName: "ConsoleOutput",
-
   componentWillUpdate() {
     let node = ReactDOM.findDOMNode(this);
     if (node.lastChild) {
       this.shouldScrollBottom = isScrolledToBottom(node.lastChild, node);
     }
   },
 
   componentDidUpdate() {
--- a/devtools/client/webconsole/new-console-output/components/console-table.js
+++ b/devtools/client/webconsole/new-console-output/components/console-table.js
@@ -7,17 +7,17 @@ const {
   createClass,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { ObjectClient } = require("devtools/shared/client/main");
 const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
 const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
-const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body").GripMessageBody);
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
 
 const TABLE_ROW_MAX_ITEMS = 1000;
 const TABLE_COLUMN_MAX_ITEMS = 10;
 
 const ConsoleTable = createClass({
 
   displayName: "ConsoleTable",
 
@@ -192,9 +192,9 @@ function getTableItems(data = {}, type, 
   }
 
   return {
     columns,
     items
   };
 }
 
-exports.ConsoleTable = ConsoleTable;
+module.exports = ConsoleTable;
--- a/devtools/client/webconsole/new-console-output/components/filter-bar.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -13,17 +13,17 @@ const { connect } = require("devtools/cl
 const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index");
 const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index");
 const uiActions = require("devtools/client/webconsole/new-console-output/actions/index");
 const {
   MESSAGE_LEVEL
 } = require("../constants");
-const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button").FilterButton);
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
 
 const FilterBar = createClass({
 
   displayName: "FilterBar",
 
   propTypes: {
     filter: PropTypes.object.isRequired,
     ui: PropTypes.object.isRequired
--- a/devtools/client/webconsole/new-console-output/components/filter-button.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-button.js
@@ -38,9 +38,9 @@ const FilterButton = createClass({
 
     return dom.button({
       className: classList.join(" "),
       onClick: this.onClick
     }, label);
   }
 });
 
-exports.FilterButton = FilterButton;
+module.exports = FilterButton;
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -15,17 +15,17 @@ if (typeof define === "undefined") {
 // React
 const {
   createFactory,
   PropTypes
 } = 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").VariablesViewLink);
+const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link"));
 const { Grip } = require("devtools/client/shared/components/reps/grip");
 
 GripMessageBody.displayName = "GripMessageBody";
 
 GripMessageBody.propTypes = {
   grip: PropTypes.oneOfType([
     PropTypes.string,
     PropTypes.number,
@@ -48,9 +48,9 @@ function GripMessageBody(props) {
         object: grip,
         objectLink: VariablesViewLink,
         defaultRep: Grip,
         mode: props.mode,
       })
   );
 }
 
-module.exports.GripMessageBody = GripMessageBody;
+module.exports = GripMessageBody;
--- a/devtools/client/webconsole/new-console-output/components/message-container.js
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -14,22 +14,22 @@ const {
 } = require("devtools/client/shared/vendor/react");
 
 const {
   MESSAGE_SOURCE,
   MESSAGE_TYPE
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 const componentMap = new Map([
-  ["ConsoleApiCall", require("./message-types/console-api-call").ConsoleApiCall],
-  ["ConsoleCommand", require("./message-types/console-command").ConsoleCommand],
-  ["DefaultRenderer", require("./message-types/default-renderer").DefaultRenderer],
-  ["EvaluationResult", require("./message-types/evaluation-result").EvaluationResult],
-  ["NetworkEventMessage", require("./message-types/network-event-message").NetworkEventMessage],
-  ["PageError", require("./message-types/page-error").PageError]
+  ["ConsoleApiCall", require("./message-types/console-api-call")],
+  ["ConsoleCommand", require("./message-types/console-command")],
+  ["DefaultRenderer", require("./message-types/default-renderer")],
+  ["EvaluationResult", require("./message-types/evaluation-result")],
+  ["NetworkEventMessage", require("./message-types/network-event-message")],
+  ["PageError", require("./message-types/page-error")]
 ]);
 
 const MessageContainer = createClass({
   displayName: "MessageContainer",
 
   propTypes: {
     message: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
--- a/devtools/client/webconsole/new-console-output/components/message-icon.js
+++ b/devtools/client/webconsole/new-console-output/components/message-icon.js
@@ -24,9 +24,9 @@ function MessageIcon(props) {
 
   const title = l10n.getStr("level." + level);
   return dom.div({
     className: "icon",
     title
   });
 }
 
-module.exports.MessageIcon = MessageIcon;
+module.exports = MessageIcon;
--- a/devtools/client/webconsole/new-console-output/components/message-repeat.js
+++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js
@@ -20,9 +20,9 @@ MessageRepeat.propTypes = {
 };
 
 function MessageRepeat(props) {
   const { repeat } = props;
   const visibility = repeat > 1 ? "visible" : "hidden";
   return dom.span({className: "message-repeats", style: {visibility}}, repeat);
 }
 
-exports.MessageRepeat = MessageRepeat;
+module.exports = MessageRepeat;
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -7,24 +7,20 @@
 "use strict";
 
 // React & Redux
 const {
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const FrameView = createFactory(require("devtools/client/shared/components/frame"));
-const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
-const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body").GripMessageBody);
-const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
-const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
-const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table").ConsoleTable);
-const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table"));
+
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 ConsoleApiCall.displayName = "ConsoleApiCall";
 
 ConsoleApiCall.propTypes = {
   message: PropTypes.object.isRequired,
   sourceMapService: PropTypes.object,
   onViewSourceInDebugger: PropTypes.func.isRequired,
   open: PropTypes.bool,
@@ -40,112 +36,79 @@ function ConsoleApiCall(props) {
     dispatch,
     message,
     sourceMapService,
     onViewSourceInDebugger,
     open,
     hudProxyClient,
     tableData
   } = props;
-  const {source, level, stacktrace, type, frame, parameters } = message;
+  const {
+    id: messageId,
+    source, type,
+    level,
+    repeat,
+    stacktrace,
+    frame,
+    parameters
+  } = message;
 
   let messageBody;
   if (type === "trace") {
     messageBody = dom.span({className: "cm-variable"}, "console.trace()");
   } else if (type === "assert") {
     let reps = formatReps(parameters);
     messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps);
   } else if (type === "table") {
     // TODO: Chrome does not output anything, see if we want to keep this
     messageBody = dom.span({className: "cm-variable"}, "console.table()");
   } else if (parameters) {
     messageBody = formatReps(parameters);
   } else {
     messageBody = message.messageText;
   }
 
-  const icon = MessageIcon({ level });
-  const repeat = MessageRepeat({ repeat: message.repeat });
-  const shouldRenderFrame = frame && frame.source !== "debugger eval code";
-  const location = dom.span({ className: "message-location devtools-monospace" },
-    shouldRenderFrame ? FrameView({
-      frame,
-      onClick: onViewSourceInDebugger,
-      showEmptyPathAsHost: true,
-      sourceMapService
-    }) : null
-  );
-
-  let collapse = "";
-  let attachment = "";
-  if (stacktrace) {
-    if (open) {
-      attachment = dom.div({ className: "stacktrace devtools-monospace" },
-        StackTrace({
-          stacktrace: stacktrace,
-          onViewSourceInDebugger: onViewSourceInDebugger
-        })
-      );
-    }
-
-    collapse = CollapseButton({
-      open,
-      onClick: function () {
-        if (open) {
-          dispatch(actions.messageClose(message.id));
-        } else {
-          dispatch(actions.messageOpen(message.id));
-        }
-      },
-    });
-  } else if (type === "table") {
+  let attachment = null;
+  if (type === "table") {
     attachment = ConsoleTable({
       dispatch,
       id: message.id,
       hudProxyClient,
       parameters: message.parameters,
       tableData
     });
   }
 
-  const classes = ["message", "cm-s-mozilla"];
-
-  classes.push(source);
-  classes.push(type);
-  classes.push(level);
-
-  if (open === true) {
-    classes.push("open");
-  }
+  const topLevelClasses = ["cm-s-mozilla"];
 
-  return dom.div({ className: classes.join(" ") },
-    // @TODO add timestamp
-    // @TODO add indent if necessary
-    icon,
-    collapse,
-    dom.span({ className: "message-body-wrapper" },
-      dom.span({ className: "message-flex-body" },
-        dom.span({ className: "message-body devtools-monospace" },
-          messageBody
-        ),
-        repeat,
-        location
-      ),
-      attachment
-    )
-  );
+  return Message({
+    messageId,
+    open,
+    source,
+    type,
+    level,
+    topLevelClasses,
+    messageBody,
+    repeat,
+    frame,
+    stacktrace,
+    attachment,
+    onViewSourceInDebugger,
+    sourceMapService,
+    dispatch,
+  });
 }
 
 function formatReps(parameters) {
   return (
     parameters
       // Get all the grips.
       .map((grip, key) => GripMessageBody({ grip, key }))
       // Interleave spaces.
       .reduce((arr, v, i) => {
         return i + 1 < parameters.length
           ? arr.concat(v, dom.span({}, " "))
           : arr.concat(v);
       }, [])
   );
 }
 
-module.exports.ConsoleApiCall = ConsoleApiCall;
+module.exports = ConsoleApiCall;
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -4,52 +4,39 @@
  * 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,
-  DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 ConsoleCommand.displayName = "ConsoleCommand";
 
 ConsoleCommand.propTypes = {
   message: PropTypes.object.isRequired,
 };
 
 /**
  * Displays input from the console.
  */
 function ConsoleCommand(props) {
-  const { message } = props;
-  const {source, type, level} = message;
-
-  const icon = MessageIcon({level});
-
-  const classes = ["message"];
-
-  classes.push(source);
-  classes.push(type);
-  classes.push(level);
+  const {
+    source,
+    type,
+    level,
+    messageText: messageBody
+  } = props.message;
 
-  return dom.div({
-    className: classes.join(" "),
-    ariaLive: "off",
-  },
-    // @TODO add timestamp
-    // @TODO add indent if necessary
-    icon,
-    dom.span({ className: "message-body-wrapper" },
-      dom.span({ className: "message-flex-body" },
-        dom.span({ className: "message-body devtools-monospace" },
-          message.messageText
-        )
-      )
-    )
-  );
+  const childProps = {
+    source,
+    type,
+    level,
+    messageBody
+  };
+  return Message(childProps);
 }
 
-module.exports.ConsoleCommand = ConsoleCommand;
+module.exports = ConsoleCommand;
--- a/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
@@ -14,9 +14,9 @@ const {
 DefaultRenderer.displayName = "DefaultRenderer";
 
 function DefaultRenderer(props) {
   return dom.div({},
     "This message type is not supported yet."
   );
 }
 
-module.exports.DefaultRenderer = DefaultRenderer;
+module.exports = DefaultRenderer;
--- 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
@@ -4,56 +4,47 @@
  * 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,
-  DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body").GripMessageBody);
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
 
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   message: PropTypes.object.isRequired,
 };
 
 function EvaluationResult(props) {
   const { message } = props;
-  const {source, type, level} = message;
-  const icon = MessageIcon({level});
+  const {
+    source,
+    type,
+    level,
+  } = message;
 
   let messageBody;
   if (message.messageText) {
     messageBody = message.messageText;
   } else {
     messageBody = GripMessageBody({grip: message.parameters});
   }
 
-
-  const classes = ["message", "cm-s-mozilla"];
-
-  classes.push(source);
-  classes.push(type);
-  classes.push(level);
+  const topLevelClasses = ["cm-s-mozilla"];
 
-  return dom.div({
-    className: classes.join(" ")
-  },
-    // @TODO add timestamp
-    // @TODO add indent if needed with console.group
-    icon,
-    dom.span({ className: "message-body-wrapper" },
-      dom.span({ className: "message-flex-body" },
-        dom.span({ className: "message-body devtools-monospace" },
-          messageBody
-        )
-      )
-    )
-  );
+  const childProps = {
+    source,
+    type,
+    level,
+    topLevelClasses,
+    messageBody,
+  };
+  return Message(childProps);
 }
 
-module.exports.EvaluationResult = EvaluationResult;
+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
@@ -7,69 +7,48 @@
 "use strict";
 
 // React & Redux
 const {
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
-const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
-const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 NetworkEventMessage.displayName = "NetworkEventMessage";
 
 NetworkEventMessage.propTypes = {
   message: PropTypes.object.isRequired,
   openNetworkPanel: PropTypes.func.isRequired,
-  // @TODO: openLink will be used for mixed-content handling
-  openLink: PropTypes.func.isRequired,
-  open: PropTypes.bool.isRequired,
 };
 
 function NetworkEventMessage(props) {
-  const { dispatch, message, openNetworkPanel, open } = props;
-  const { actor, source, type, level, request, response, isXHR, totalTime } = message;
-  let { method, url } = request;
-  let { httpVersion, status, statusText } = response;
-
-  let classes = ["message", "cm-s-mozilla"];
-
-  classes.push(source);
-  classes.push(type);
-  classes.push(level);
+  const { message, openNetworkPanel } = props;
+  const { actor, source, type, level, request, isXHR } = message;
 
-  if (open) {
-    classes.push("open");
-  }
-
-  let statusInfo = "[]";
-
-  // @TODO: Status will be enabled after NetworkUpdateEvent packet arrives
-  if (httpVersion && status && statusText && totalTime) {
-    statusInfo = `[${httpVersion} ${status} ${statusText} ${totalTime}ms]`;
-  }
-
-  let xhr = l10n.getStr("webConsoleXhrIndicator");
+  const topLevelClasses = [ "cm-s-mozilla" ];
 
   function onUrlClick() {
     openNetworkPanel(actor);
   }
 
-  return dom.div({ className: classes.join(" ") },
-    // @TODO add timestamp
-    // @TODO add indent if necessary
-    MessageIcon({ level }),
-    dom.span({
-      className: "message-body-wrapper message-body devtools-monospace",
-      "aria-haspopup": "true"
-    },
-      dom.span({ className: "method" }, method),
-      isXHR ? dom.span({ className: "xhr" }, xhr) : null,
-      dom.a({ className: "url", title: url, onClick: onUrlClick },
-        url.replace(/\?.+/, ""))
-    )
-  );
+  const method = dom.span({className: "method" }, request.method);
+  const xhr = isXHR
+    ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+    : null;
+  const url = dom.a({ className: "url", title: request.url, onClick: onUrlClick },
+        request.url.replace(/\?.+/, ""));
+
+  const messageBody = dom.span({}, method, xhr, url);
+
+  const childProps = {
+    source,
+    type,
+    level,
+    topLevelClasses,
+    messageBody,
+  };
+  return Message(childProps);
 }
 
-module.exports.NetworkEventMessage = NetworkEventMessage;
+module.exports = NetworkEventMessage;
--- a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -4,97 +4,57 @@
  * 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,
-  DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const FrameView = createFactory(require("devtools/client/shared/components/frame"));
-const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
-const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
-const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
-
-const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
 };
 
 PageError.defaultProps = {
   open: false
 };
 
 function PageError(props) {
-  const { dispatch, message, open, sourceMapService, onViewSourceInDebugger } = props;
-  const { source, type, level, stacktrace, frame } = message;
-
-  const repeat = MessageRepeat({repeat: message.repeat});
-  const icon = MessageIcon({level});
-  const shouldRenderFrame = frame && frame.source !== "debugger eval code";
-  const location = dom.span({ className: "message-location devtools-monospace" },
-    shouldRenderFrame ? FrameView({
-      frame,
-      onClick: onViewSourceInDebugger,
-      showEmptyPathAsHost: true,
-      sourceMapService
-    }) : null
-  );
-
-  let collapse = "";
-  let attachment = "";
-  if (stacktrace) {
-    if (open) {
-      attachment = dom.div({ className: "stacktrace devtools-monospace" },
-        StackTrace({
-          stacktrace: stacktrace,
-          onViewSourceInDebugger: onViewSourceInDebugger
-        })
-      );
-    }
+  const {
+    message,
+    open,
+    sourceMapService,
+    onViewSourceInDebugger
+  } = props;
+  const {
+    id: messageId,
+    source, type,
+    level,
+    messageText: messageBody,
+    repeat,
+    stacktrace,
+    frame
+  } = message;
 
-    collapse = CollapseButton({
-      open,
-      onClick: function () {
-        if (open) {
-          dispatch(actions.messageClose(message.id));
-        } else {
-          dispatch(actions.messageOpen(message.id));
-        }
-      },
-    });
-  }
-
-  const classes = ["message"];
-  classes.push(source);
-  classes.push(type);
-  classes.push(level);
-  if (open === true) {
-    classes.push("open");
-  }
-
-  return dom.div({
-    className: classes.join(" ")
-  },
-    icon,
-    collapse,
-    dom.span({ className: "message-body-wrapper" },
-      dom.span({ className: "message-flex-body" },
-        dom.span({ className: "message-body devtools-monospace" },
-          message.messageText
-        ),
-        repeat,
-        location
-      ),
-      attachment
-    )
-  );
+  const childProps = {
+    messageId,
+    open,
+    source,
+    type,
+    level,
+    messageBody,
+    repeat,
+    frame,
+    stacktrace,
+    onViewSourceInDebugger,
+    sourceMapService,
+  };
+  return Message(childProps);
 }
 
-module.exports.PageError = PageError;
+module.exports = PageError;
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -0,0 +1,128 @@
+/* -*- 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,
+  DOM: dom,
+  PropTypes
+} = require("devtools/client/shared/vendor/react");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button"));
+const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon"));
+const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat"));
+const FrameView = createFactory(require("devtools/client/shared/components/frame"));
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+Message.displayName = "Message";
+
+Message.propTypes = {
+  open: PropTypes.bool,
+  source: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  level: PropTypes.string.isRequired,
+  topLevelClasses: PropTypes.array,
+  messageBody: PropTypes.any.isRequired,
+  repeat: PropTypes.any,
+  frame: PropTypes.any,
+  attachment: PropTypes.any,
+  stacktrace: PropTypes.any,
+  messageId: PropTypes.string,
+  onViewSourceInDebugger: PropTypes.func,
+  sourceMapService: PropTypes.any,
+};
+
+Message.defaultProps = {
+  topLevelClasses: [],
+};
+
+function Message(props) {
+  const {
+    messageId,
+    open,
+    source,
+    type,
+    level,
+    topLevelClasses,
+    messageBody,
+    frame,
+    stacktrace,
+    onViewSourceInDebugger,
+    sourceMapService,
+    dispatch,
+  } = props;
+
+  topLevelClasses.push("message", source, type, level);
+  if (open) {
+    topLevelClasses.push("open");
+  }
+
+  const icon = MessageIcon({level});
+
+  // Figure out if there is an expandable part to the message.
+  let attachment = null;
+  if (props.attachment) {
+    attachment = props.attachment;
+  } else if (stacktrace) {
+    if (open) {
+      attachment = dom.div({ className: "stacktrace devtools-monospace" },
+        StackTrace({
+          stacktrace: stacktrace,
+          onViewSourceInDebugger: onViewSourceInDebugger
+        })
+      );
+    }
+  }
+
+  // If there is an expandable part, make it collapsible.
+  let collapse = null;
+  if (attachment) {
+    collapse = CollapseButton({
+      open,
+      onClick: function () {
+        if (open) {
+          dispatch(actions.messageClose(messageId));
+        } else {
+          dispatch(actions.messageOpen(messageId));
+        }
+      },
+    });
+  }
+
+  const repeat = props.repeat ? MessageRepeat({repeat: props.repeat}) : null;
+
+  // Configure the location.
+  const shouldRenderFrame = frame && frame.source !== "debugger eval code";
+  const location = dom.span({ className: "message-location devtools-monospace" },
+    shouldRenderFrame ? FrameView({
+      frame,
+      onClick: onViewSourceInDebugger,
+      showEmptyPathAsHost: true,
+      sourceMapService
+    }) : null
+  );
+
+  return dom.div({ className: topLevelClasses.join(" ") },
+    // @TODO add timestamp
+    // @TODO add indent if necessary
+    icon,
+    collapse,
+    dom.span({ className: "message-body-wrapper" },
+      dom.span({ className: "message-flex-body" },
+        dom.span({ className: "message-body devtools-monospace" },
+          messageBody
+        ),
+        repeat,
+        location
+      ),
+      attachment
+    )
+  );
+}
+
+module.exports = Message;
--- a/devtools/client/webconsole/new-console-output/components/moz.build
+++ b/devtools/client/webconsole/new-console-output/components/moz.build
@@ -12,10 +12,11 @@ DevToolsModules(
     'console-output.js',
     'console-table.js',
     'filter-bar.js',
     'filter-button.js',
     'grip-message-body.js',
     'message-container.js',
     'message-icon.js',
     'message-repeat.js',
+    'message.js',
     'variables-view-link.js'
 )
--- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -26,9 +26,9 @@ function VariablesViewLink(props) {
     dom.a({
       onClick: openVariablesView.bind(null, object),
       className: "cm-variable",
       draggable: false,
     }, children)
   );
 }
 
-module.exports.VariablesViewLink = VariablesViewLink;
+module.exports = VariablesViewLink;
--- a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
@@ -5,17 +5,17 @@
 // Test utils.
 const expect = require("expect");
 const { render } = require("enzyme");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
 
 // Components under test.
-const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call").ConsoleApiCall);
+const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call"));
 
 // Test fakes.
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const onViewSourceInDebugger = () => {};
 
 const tempfilePath = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js";
 
 describe("ConsoleAPICall component:", () => {
--- a/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
@@ -5,17 +5,17 @@
 // Test utils.
 const expect = require("expect");
 const { render } = require("enzyme");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
 
 // Components under test.
-const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result").EvaluationResult);
+const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result"));
 
 // Test fakes.
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
 describe("EvaluationResult component:", () => {
   it("renders a grip result", () => {
     const message = stubPreparedMessages.get("new Date(0)");
     const wrapper = render(EvaluationResult({ message }));
--- a/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
@@ -4,17 +4,17 @@
 
 const expect = require("expect");
 const sinon = require("sinon");
 const { render, mount } = require("enzyme");
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const Provider = createFactory(require("react-redux").Provider);
 
-const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button").FilterButton);
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
 const FilterBar = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const {
   MESSAGES_CLEAR,
   MESSAGE_LEVEL
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
--- a/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const expect = require("expect");
 const { render } = require("enzyme");
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 
-const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button").FilterButton);
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
 const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
 
 describe("FilterButton component:", () => {
   const props = {
     active: true,
     label: "Error",
     filterKey: MESSAGE_LEVEL.ERROR,
   };
--- a/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
@@ -6,19 +6,19 @@
 const expect = require("expect");
 const {
   renderComponent,
   shallowRenderComponent
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 
 // Components under test.
 const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container");
-const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
-const { EvaluationResult } = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
-const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+const ConsoleApiCall = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
+const EvaluationResult = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
 
 // Test fakes.
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const onViewSourceInDebugger = () => {};
 
 describe("MessageContainer component:", () => {
   it("pipes data to children as expected", () => {
     const message = stubPreparedMessages.get("console.log('foobar', 'test')");
--- a/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const {
   MESSAGE_LEVEL,
 } = require("devtools/client/webconsole/new-console-output/constants");
-const { MessageIcon } = require("devtools/client/webconsole/new-console-output/components/message-icon");
+const MessageIcon = require("devtools/client/webconsole/new-console-output/components/message-icon");
 
 const expect = require("expect");
 
 const {
   renderComponent
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 
 describe("MessageIcon component:", () => {
rename from devtools/client/webconsole/new-console-output/test/components/repeat.test.js
rename to devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
--- a/devtools/client/webconsole/new-console-output/test/components/repeat.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-const { MessageRepeat } = require("devtools/client/webconsole/new-console-output/components/message-repeat");
+const MessageRepeat = require("devtools/client/webconsole/new-console-output/components/message-repeat");
 
 const expect = require("expect");
 
 const {
   renderComponent
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 
 describe("MessageRepeat component:", () => {
--- a/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
@@ -5,17 +5,17 @@
 // Test utils.
 const expect = require("expect");
 const { render } = require("enzyme");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
 
 // Components under test.
-const NetworkEventMessage = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/network-event-message").NetworkEventMessage);
+const NetworkEventMessage = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/network-event-message"));
 
 // Test fakes.
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const onViewSourceInDebugger = () => {};
 const openNetworkPanel = () => {};
 const openLink = () => {};
 const EXPECTED_URL = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html";
 
--- a/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test utils.
 const expect = require("expect");
 const { render } = require("enzyme");
 
 // Components under test.
-const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
 
 // Test fakes.
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
 describe("PageError component:", () => {
   it("renders", () => {
     const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
     const wrapper = render(PageError({ message }));