Bug 1393609 - Fix line-height custom style for console.log;r=nchevobbe draft
authorewhite7 <ewhite7@myseneca.ca>
Fri, 05 Jan 2018 21:33:53 -0500
changeset 716659 f7b79dff43ee2e4bfa20e4c797b1270cd101ac2f
parent 716638 9099a6ed993f0113c47c0d9e800bf0ff6e1a1dc1
child 745076 2c9b518e9f3932d2eaac8e5b8bbf7a21cfcc4edd
push id94482
push userbmo:ewhite7@myseneca.ca
push dateSat, 06 Jan 2018 02:39:57 +0000
reviewersnchevobbe
bugs1393609
milestone59.0a1
Bug 1393609 - Fix line-height custom style for console.log;r=nchevobbe MozReview-Commit-ID: BD2fjv6u4b2
devtools/client/webconsole/new-console-output/components/GripMessageBody.js
devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
--- a/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
+++ b/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
@@ -1,129 +1,178 @@
-/* -*- 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";
-
-// If this is being run from Mocha, then the browser loader hasn't set up
-// define. We need to do that before loading Rep.
-if (typeof define === "undefined") {
-  require("amd-loader");
-}
-
-// React
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const {
-  MESSAGE_TYPE,
-  JSTERM_COMMANDS,
-} = require("../constants");
-const { getObjectInspector } = require("devtools/client/webconsole/new-console-output/utils/object-inspector");
-
-const reps = require("devtools/client/shared/components/reps/reps");
-const { MODE } = reps;
-
-GripMessageBody.displayName = "GripMessageBody";
-
-GripMessageBody.propTypes = {
-  grip: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-    PropTypes.object,
-  ]).isRequired,
-  serviceContainer: PropTypes.shape({
-    createElement: PropTypes.func.isRequired,
-    hudProxy: PropTypes.object.isRequired,
-    onViewSourceInDebugger: PropTypes.func.isRequired,
-  }),
-  userProvidedStyle: PropTypes.string,
-  useQuotes: PropTypes.bool,
-  escapeWhitespace: PropTypes.bool,
-  type: PropTypes.string,
-  helperType: PropTypes.string,
-};
-
-GripMessageBody.defaultProps = {
-  mode: MODE.LONG,
-};
-
-function GripMessageBody(props) {
-  const {
-    grip,
-    userProvidedStyle,
-    serviceContainer,
-    useQuotes,
-    escapeWhitespace,
-    mode = MODE.LONG,
-  } = props;
-
-  let styleObject;
-  if (userProvidedStyle && userProvidedStyle !== "") {
-    styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
-  }
-
-  let objectInspectorProps = {
-    autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
-    mode,
-  };
-
-  if (typeof grip === "string" || (grip && grip.type === "longString")) {
-    Object.assign(objectInspectorProps, {
-      useQuotes,
-      escapeWhitespace,
-      style: styleObject
-    });
-  }
-
-  return getObjectInspector(grip, serviceContainer, objectInspectorProps);
-}
-
-// Regular expression that matches the allowed CSS property names.
-const allowedStylesRegex = new RegExp(
-  "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
-  "margin|padding|text|transition|outline|white-space|word|writing|" +
-  "(?:min-|max-)?width|(?:min-|max-)?height)"
-);
-
-// Regular expression that matches the forbidden CSS property values.
-const forbiddenValuesRegexs = [
-  // url(), -moz-element()
-  /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
-
-  // various URL protocols
-  /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
-];
-
-function cleanupStyle(userProvidedStyle, createElement) {
-  // Use a dummy element to parse the style string.
-  let dummy = createElement("div");
-  dummy.style = userProvidedStyle;
-
-  // Return a style object as expected by React DOM components, e.g.
-  // {color: "red"}
-  // without forbidden properties and values.
-  return [...dummy.style]
-    .filter(name => {
-      return allowedStylesRegex.test(name)
-        && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name]));
-    })
-    .reduce((object, name) => {
-      return Object.assign({
-        [name]: dummy.style[name]
-      }, object);
-    }, {});
-}
-
-function shouldAutoExpandObjectInspector(props) {
-  const {
-    helperType,
-    type,
-  } = props;
-
-  return (
-    type === MESSAGE_TYPE.DIR
-    || helperType === JSTERM_COMMANDS.INSPECT
-  );
-}
-
-module.exports = GripMessageBody;
+Stud-snippets
+/* -*- 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";
+
+// If this is being run from Mocha, then the browser loader hasn't set up
+// define. We need to do that before loading Rep.
+if (typeof define === "undefined") {
+  require("amd-loader");
+}
+
+// React
+const {
+  createFactory,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const ObjectClient = require("devtools/shared/client/object-client");
+const {
+  MESSAGE_TYPE,
+  JSTERM_COMMANDS,
+} = require("../constants");
+
+const reps = require("devtools/client/shared/components/reps/reps");
+const { REPS, MODE } = reps;
+const ObjectInspector = createFactory(reps.ObjectInspector);
+const { Grip } = REPS;
+
+GripMessageBody.displayName = "GripMessageBody";
+
+GripMessageBody.propTypes = {
+  grip: PropTypes.oneOfType([
+    PropTypes.string,
+    PropTypes.number,
+    PropTypes.object,
+  ]).isRequired,
+  serviceContainer: PropTypes.shape({
+    createElement: PropTypes.func.isRequired,
+    hudProxy: PropTypes.object.isRequired,
+  }),
+  userProvidedStyle: PropTypes.string,
+  useQuotes: PropTypes.bool,
+  escapeWhitespace: PropTypes.bool,
+  type: PropTypes.string,
+  helperType: PropTypes.string,
+};
+
+GripMessageBody.defaultProps = {
+  mode: MODE.LONG,
+};
+
+function GripMessageBody(props) {
+  const {
+    grip,
+    userProvidedStyle,
+    serviceContainer,
+    useQuotes,
+    escapeWhitespace,
+    mode = MODE.LONG,
+  } = props;
+
+  let styleObject;
+  if (userProvidedStyle && userProvidedStyle !== "") {
+    styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
+  }
+
+  let onDOMNodeMouseOver;
+  let onDOMNodeMouseOut;
+  let onInspectIconClick;
+  if (serviceContainer) {
+    onDOMNodeMouseOver = serviceContainer.highlightDomElement
+      ? (object) => serviceContainer.highlightDomElement(object)
+      : null;
+    onDOMNodeMouseOut = serviceContainer.unHighlightDomElement;
+    onInspectIconClick = serviceContainer.openNodeInInspector
+      ? (object, e) => {
+        // Stop the event propagation so we don't trigger ObjectInspector expand/collapse.
+        e.stopPropagation();
+        serviceContainer.openNodeInInspector(object);
+      }
+      : null;
+  }
+
+  const objectInspectorProps = {
+    // Auto-expand the ObjectInspector when the message is a console.dir one.
+    autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
+    mode,
+    // TODO: we disable focus since it's not currently working well in ObjectInspector.
+    // Let's remove the property below when problem are fixed in OI.
+    disabledFocus: true,
+    roots: [{
+      path: (grip && grip.actor) || JSON.stringify(grip),
+      contents: {
+        value: grip
+      }
+    }],
+    createObjectClient: object =>
+      new ObjectClient(serviceContainer.hudProxy.client, object),
+    releaseActor: actor => {
+      if (!actor || !serviceContainer.hudProxy.releaseActor) {
+        return;
+      }
+      serviceContainer.hudProxy.releaseActor(actor);
+    },
+    openLink: serviceContainer.openLink,
+  };
+
+  if (typeof grip === "string" || (grip && grip.type === "longString")) {
+    Object.assign(objectInspectorProps, {
+      useQuotes,
+      escapeWhitespace,
+      style: styleObject
+    });
+  } else {
+    Object.assign(objectInspectorProps, {
+      onDOMNodeMouseOver,
+      onDOMNodeMouseOut,
+      onInspectIconClick,
+      defaultRep: Grip,
+    });
+  }
+
+  return ObjectInspector(objectInspectorProps);
+}
+
+// Regular expression that matches the allowed CSS property names.
+const allowedStylesRegex = new RegExp(
+  "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
+  "margin|padding|text|transition|outline|white-space|word|writing|" +
+  "(?:min-|max-)?width|(?:min-|max-)?height)"
+);
+
+// Regular expression that matches the forbidden CSS property values.
+const forbiddenValuesRegexs = [
+  // url(), -moz-element()
+  /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+  // various URL protocols
+  /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+];
+
+function cleanupStyle(userProvidedStyle, createElement) {
+  // Use a dummy element to parse the style string.
+  let dummy = createElement("div");
+  dummy.style = userProvidedStyle;
+
+  const cssToIdlDictionnary = {
+  "line-height": "lineHeight"
+  };
+
+  return [...dummy.style]
+    .filter(name => {
+      return allowedStylesRegex.test(name)
+        && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name]));
+    })
+    .reduce((object, name) => {
+      return Object.assign({
+        [cssToIdlDictionnary[name] || name]: dummy.style[name]
+      }, object);
+    }, {});
+}
+
+function shouldAutoExpandObjectInspector(props) {
+  const {
+    helperType,
+    type,
+  } = props;
+
+  return (
+    type === MESSAGE_TYPE.DIR
+    || helperType === JSTERM_COMMANDS.INSPECT
+  );
+}
+
+module.exports = GripMessageBody;
--- 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
@@ -1,343 +1,344 @@
-/* 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, mount } = require("enzyme");
-const sinon = require("sinon");
-
-// React
-const { createFactory } = require("devtools/client/shared/vendor/react");
-const Provider = createFactory(require("react-redux").Provider);
-const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
-
-// Components under test.
-const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/ConsoleApiCall"));
-const {
-  MESSAGE_OPEN,
-  MESSAGE_CLOSE,
-} = require("devtools/client/webconsole/new-console-output/constants");
-const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/MessageIndent");
-
-// Test fakes.
-const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
-const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
-
-describe("ConsoleAPICall component:", () => {
-  describe("console.log", () => {
-    it("renders string grips", () => {
-      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("foobar test");
-      expect(wrapper.find(".objectBox-string").length).toBe(2);
-      let selector = "div.message.cm-s-mozilla span span.message-flex-body " +
-        "span.message-body.devtools-monospace";
-      expect(wrapper.find(selector).length).toBe(1);
-
-      // There should be the location
-      const locationLink = wrapper.find(`.message-location`);
-      expect(locationLink.length).toBe(1);
-      expect(locationLink.text()).toBe("test-console-api.html:1:27");
-    });
-
-    it("renders string grips with custom style", () => {
-      const message = stubPreparedMessages.get("console.log(%cfoobar)");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      const elements = wrapper.find(".objectBox-string");
-      expect(elements.text()).toBe("foobar");
-      expect(elements.length).toBe(2);
-
-      const firstElementStyle = elements.eq(0).prop("style");
-      // Allowed styles are applied accordingly on the first element.
-      expect(firstElementStyle.color).toBe(`blue`);
-      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
-      // Forbidden styles are not applied.
-      expect(firstElementStyle["background-image"]).toBe(undefined);
-      expect(firstElementStyle.position).toBe(undefined);
-      expect(firstElementStyle.top).toBe(undefined);
-
-      const secondElementStyle = elements.eq(1).prop("style");
-      // Allowed styles are applied accordingly on the second element.
-      expect(secondElementStyle.color).toBe(`red`);
-      // Forbidden styles are not applied.
-      expect(secondElementStyle.background).toBe(undefined);
-    });
-
-    it("renders repeat node", () => {
-      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-      const wrapper = render(ConsoleApiCall({
-        message,
-        serviceContainer,
-        repeat: 107
-      }));
-
-      expect(wrapper.find(".message-repeats").text()).toBe("107");
-      expect(wrapper.find(".message-repeats").prop("title")).toBe("107 repeats");
-
-      let selector = "span > span.message-flex-body > " +
-        "span.message-body.devtools-monospace + span.message-repeats";
-      expect(wrapper.find(selector).length).toBe(1);
-    });
-
-    it("has the expected indent", () => {
-      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-
-      const indent = 10;
-      let wrapper = render(ConsoleApiCall({
-        message: Object.assign({}, message, {indent}),
-        serviceContainer
-      }));
-      let indentEl = wrapper.find(".indent");
-      expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
-      expect(indentEl.prop("data-indent")).toBe(`${indent}`);
-
-      wrapper = render(ConsoleApiCall({ message, serviceContainer}));
-      indentEl = wrapper.find(".indent");
-      expect(indentEl.prop("style").width).toBe(`0`);
-      expect(indentEl.prop("data-indent")).toBe(`0`);
-    });
-
-    it("renders a timestamp when passed a truthy timestampsVisible prop", () => {
-      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-      const wrapper = render(ConsoleApiCall({
-        message,
-        serviceContainer,
-        timestampsVisible: true,
-      }));
-      const { timestampString } = require("devtools/client/webconsole/webconsole-l10n");
-
-      expect(wrapper.find(".timestamp").text()).toBe(timestampString(message.timeStamp));
-    });
-
-    it("does not render a timestamp when not asked to", () => {
-      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-      const wrapper = render(ConsoleApiCall({
-        message,
-        serviceContainer,
-      }));
-
-      expect(wrapper.find(".timestamp").length).toBe(0);
-    });
-  });
-
-  describe("console.count", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.count('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("bar: 1");
-    });
-  });
-
-  describe("console.assert", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get(
-        "console.assert(false, {message: 'foobar'})");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text())
-        .toBe("Assertion failed: Object { message: \"foobar\" }");
-    });
-  });
-
-  describe("console.time", () => {
-    it("does not show anything", () => {
-      const message = stubPreparedMessages.get("console.time('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("");
-    });
-    it("shows an error if called again", () => {
-      const message = stubPreparedMessages.get("timerAlreadyExists");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("Timer “bar” already exists.");
-    });
-  });
-
-  describe("console.timeEnd", () => {
-    it("renders as expected", () => {
-      const message = stubPreparedMessages.get("console.timeEnd('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe(message.messageText);
-      expect(wrapper.find(".message-body").text()).toMatch(/^bar: \d+(\.\d+)?ms$/);
-    });
-    it("shows an error if the timer doesn't exist", () => {
-      const message = stubPreparedMessages.get("timerDoesntExist");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("Timer “bar” doesn’t exist.");
-    });
-  });
-
-  describe("console.trace", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.trace()");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
-      const filepath = "http://example.com/browser/devtools/client/webconsole/" +
-                       "new-console-output/test/fixtures/stub-generators/" +
-                       "test-console-api.html";
-
-      expect(wrapper.find(".message-body").text()).toBe("console.trace()");
-
-      const frameLinks = wrapper.find(
-        `.stack-trace span.frame-link[data-url]`);
-      expect(frameLinks.length).toBe(3);
-
-      expect(frameLinks.eq(0).find(".frame-link-function-display-name").text())
-        .toBe("testStacktraceFiltering");
-      expect(frameLinks.eq(0).find(".frame-link-filename").text())
-        .toBe(filepath);
-
-      expect(frameLinks.eq(1).find(".frame-link-function-display-name").text())
-        .toBe("foo");
-      expect(frameLinks.eq(1).find(".frame-link-filename").text())
-        .toBe(filepath);
-
-      expect(frameLinks.eq(2).find(".frame-link-function-display-name").text())
-        .toBe("triggerPacket");
-      expect(frameLinks.eq(2).find(".frame-link-filename").text())
-        .toBe(filepath);
-
-      // it should not be collapsible.
-      expect(wrapper.find(`.theme-twisty`).length).toBe(0);
-    });
-  });
-
-  describe("console.group", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.group('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
-
-      expect(wrapper.find(".message-body").text()).toBe("bar");
-      expect(wrapper.find(".theme-twisty.open").length).toBe(1);
-    });
-
-    it("renders group with custom style", () => {
-      const message = stubPreparedMessages.get("console.group(%cfoo%cbar)");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-      expect(wrapper.find(".message-body").text()).toBe("foobar");
-
-      const elements = wrapper.find(".objectBox-string");
-      expect(elements.length).toBe(2);
-
-      const firstElementStyle = elements.eq(0).prop("style");
-      // Allowed styles are applied accordingly on the first element.
-      expect(firstElementStyle.color).toBe(`blue`);
-      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
-      // Forbidden styles are not applied.
-      expect(firstElementStyle["background-image"]).toBe(undefined);
-      expect(firstElementStyle.position).toBe(undefined);
-      expect(firstElementStyle.top).toBe(undefined);
-
-      const secondElementStyle = elements.eq(1).prop("style");
-      // Allowed styles are applied accordingly on the second element.
-      expect(secondElementStyle.color).toBe(`red`);
-      // Forbidden styles are not applied.
-      expect(secondElementStyle.background).toBe(undefined);
-    });
-
-    it("toggle the group when the collapse button is clicked", () => {
-      const store = setupStore([]);
-      store.dispatch = sinon.spy();
-      const message = stubPreparedMessages.get("console.group('bar')");
-
-      let wrapper = mount(Provider({store},
-        ConsoleApiCall({
-          message,
-          open: true,
-          dispatch: store.dispatch,
-          serviceContainer,
-        })
-      ));
-      wrapper.find(".theme-twisty.open").simulate("click");
-      let call = store.dispatch.getCall(0);
-      expect(call.args[0]).toEqual({
-        id: message.id,
-        type: MESSAGE_CLOSE
-      });
-
-      wrapper = mount(Provider({store},
-        ConsoleApiCall({
-          message,
-          open: false,
-          dispatch: store.dispatch,
-          serviceContainer,
-        })
-      ));
-      wrapper.find(".theme-twisty").simulate("click");
-      call = store.dispatch.getCall(1);
-      expect(call.args[0]).toEqual({
-        id: message.id,
-        type: MESSAGE_OPEN
-      });
-    });
-  });
-
-  describe("console.groupEnd", () => {
-    it("does not show anything", () => {
-      const message = stubPreparedMessages.get("console.groupEnd('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text()).toBe("");
-    });
-  });
-
-  describe("console.groupCollapsed", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.groupCollapsed('foo')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: false}));
-
-      expect(wrapper.find(".message-body").text()).toBe("foo");
-      expect(wrapper.find(".theme-twisty:not(.open)").length).toBe(1);
-    });
-
-    it("renders group with custom style", () => {
-      const message = stubPreparedMessages.get("console.groupCollapsed(%cfoo%cbaz)");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      const elements = wrapper.find(".objectBox-string");
-      expect(elements.text()).toBe("foobaz");
-      expect(elements.length).toBe(2);
-
-      const firstElementStyle = elements.eq(0).prop("style");
-      // Allowed styles are applied accordingly on the first element.
-      expect(firstElementStyle.color).toBe(`blue`);
-      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
-      // Forbidden styles are not applied.
-      expect(firstElementStyle["background-image"]).toBe(undefined);
-      expect(firstElementStyle.position).toBe(undefined);
-      expect(firstElementStyle.top).toBe(undefined);
-
-      const secondElementStyle = elements.eq(1).prop("style");
-      // Allowed styles are applied accordingly on the second element.
-      expect(secondElementStyle.color).toBe(`red`);
-      // Forbidden styles are not applied.
-      expect(secondElementStyle.background).toBe(undefined);
-    });
-  });
-
-  describe("console.dirxml", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.dirxml(window)");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text())
-        .toBe("Window http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html");
-    });
-  });
-
-  describe("console.dir", () => {
-    it("renders", () => {
-      const message = stubPreparedMessages.get("console.dir({C, M, Y, K})");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
-
-      expect(wrapper.find(".message-body").text())
-        .toBe(`Object { cyan: "C", magenta: "M", yellow: "Y", black: "K" }`);
-    });
-  });
-});
+/* 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, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/ConsoleApiCall"));
+const {
+  MESSAGE_OPEN,
+  MESSAGE_CLOSE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/MessageIndent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("ConsoleAPICall component:", () => {
+  describe("console.log", () => {
+    it("renders string grips", () => {
+      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("foobar test");
+      expect(wrapper.find(".objectBox-string").length).toBe(2);
+      let selector = "div.message.cm-s-mozilla span span.message-flex-body " +
+        "span.message-body.devtools-monospace";
+      expect(wrapper.find(selector).length).toBe(1);
+
+      // There should be the location
+      const locationLink = wrapper.find(`.message-location`);
+      expect(locationLink.length).toBe(1);
+      expect(locationLink.text()).toBe("test-console-api.html:1:27");
+    });
+
+    it("renders string grips with custom style", () => {
+      const message = stubPreparedMessages.get("console.log(%cfoobar)");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      const elements = wrapper.find(".objectBox-string");
+      expect(elements.text()).toBe("foobar");
+      expect(elements.length).toBe(2);
+
+      const firstElementStyle = elements.eq(0).prop("style");
+      // Allowed styles are applied accordingly on the first element.
+      expect(firstElementStyle.color).toBe(`blue`);
+      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+      // Forbidden styles are not applied.
+      expect(firstElementStyle["background-image"]).toBe(undefined);
+      expect(firstElementStyle.position).toBe(undefined);
+      expect(firstElementStyle.top).toBe(undefined);
+
+      const secondElementStyle = elements.eq(1).prop("style");
+      // Allowed styles are applied accordingly on the second element.
+      expect(secondElementStyle.color).toBe(`red`);
+      expect(secondElementStyle.lineHeight).toBe(1.5);
+      // Forbidden styles are not applied.
+      expect(secondElementStyle.background).toBe(undefined);
+    });
+
+    it("renders repeat node", () => {
+      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+      const wrapper = render(ConsoleApiCall({
+        message,
+        serviceContainer,
+        repeat: 107
+      }));
+
+      expect(wrapper.find(".message-repeats").text()).toBe("107");
+      expect(wrapper.find(".message-repeats").prop("title")).toBe("107 repeats");
+
+      let selector = "span > span.message-flex-body > " +
+        "span.message-body.devtools-monospace + span.message-repeats";
+      expect(wrapper.find(selector).length).toBe(1);
+    });
+
+    it("has the expected indent", () => {
+      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+      const indent = 10;
+      let wrapper = render(ConsoleApiCall({
+        message: Object.assign({}, message, {indent}),
+        serviceContainer
+      }));
+      let indentEl = wrapper.find(".indent");
+      expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+      expect(indentEl.prop("data-indent")).toBe(`${indent}`);
+
+      wrapper = render(ConsoleApiCall({ message, serviceContainer}));
+      indentEl = wrapper.find(".indent");
+      expect(indentEl.prop("style").width).toBe(`0`);
+      expect(indentEl.prop("data-indent")).toBe(`0`);
+    });
+
+    it("renders a timestamp when passed a truthy timestampsVisible prop", () => {
+      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+      const wrapper = render(ConsoleApiCall({
+        message,
+        serviceContainer,
+        timestampsVisible: true,
+      }));
+      const { timestampString } = require("devtools/client/webconsole/webconsole-l10n");
+
+      expect(wrapper.find(".timestamp").text()).toBe(timestampString(message.timeStamp));
+    });
+
+    it("does not render a timestamp when not asked to", () => {
+      const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+      const wrapper = render(ConsoleApiCall({
+        message,
+        serviceContainer,
+      }));
+
+      expect(wrapper.find(".timestamp").length).toBe(0);
+    });
+  });
+
+  describe("console.count", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.count('bar')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("bar: 1");
+    });
+  });
+
+  describe("console.assert", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get(
+        "console.assert(false, {message: 'foobar'})");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text())
+        .toBe("Assertion failed: Object { message: \"foobar\" }");
+    });
+  });
+
+  describe("console.time", () => {
+    it("does not show anything", () => {
+      const message = stubPreparedMessages.get("console.time('bar')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("");
+    });
+    it("shows an error if called again", () => {
+      const message = stubPreparedMessages.get("timerAlreadyExists");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("Timer “bar” already exists.");
+    });
+  });
+
+  describe("console.timeEnd", () => {
+    it("renders as expected", () => {
+      const message = stubPreparedMessages.get("console.timeEnd('bar')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+      expect(wrapper.find(".message-body").text()).toMatch(/^bar: \d+(\.\d+)?ms$/);
+    });
+    it("shows an error if the timer doesn't exist", () => {
+      const message = stubPreparedMessages.get("timerDoesntExist");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("Timer “bar” doesn’t exist.");
+    });
+  });
+
+  describe("console.trace", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.trace()");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
+      const filepath = "http://example.com/browser/devtools/client/webconsole/" +
+                       "new-console-output/test/fixtures/stub-generators/" +
+                       "test-console-api.html";
+
+      expect(wrapper.find(".message-body").text()).toBe("console.trace()");
+
+      const frameLinks = wrapper.find(
+        `.stack-trace span.frame-link[data-url]`);
+      expect(frameLinks.length).toBe(3);
+
+      expect(frameLinks.eq(0).find(".frame-link-function-display-name").text())
+        .toBe("testStacktraceFiltering");
+      expect(frameLinks.eq(0).find(".frame-link-filename").text())
+        .toBe(filepath);
+
+      expect(frameLinks.eq(1).find(".frame-link-function-display-name").text())
+        .toBe("foo");
+      expect(frameLinks.eq(1).find(".frame-link-filename").text())
+        .toBe(filepath);
+
+      expect(frameLinks.eq(2).find(".frame-link-function-display-name").text())
+        .toBe("triggerPacket");
+      expect(frameLinks.eq(2).find(".frame-link-filename").text())
+        .toBe(filepath);
+
+      // it should not be collapsible.
+      expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+    });
+  });
+
+  describe("console.group", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.group('bar')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
+
+      expect(wrapper.find(".message-body").text()).toBe("bar");
+      expect(wrapper.find(".theme-twisty.open").length).toBe(1);
+    });
+
+    it("renders group with custom style", () => {
+      const message = stubPreparedMessages.get("console.group(%cfoo%cbar)");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      const elements = wrapper.find(".objectBox-string");
+      expect(elements.text()).toBe("foobar");
+      expect(elements.length).toBe(2);
+
+      const firstElementStyle = elements.eq(0).prop("style");
+      // Allowed styles are applied accordingly on the first element.
+      expect(firstElementStyle.color).toBe(`blue`);
+      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+      // Forbidden styles are not applied.
+      expect(firstElementStyle["background-image"]).toBe(undefined);
+      expect(firstElementStyle.position).toBe(undefined);
+      expect(firstElementStyle.top).toBe(undefined);
+
+      const secondElementStyle = elements.eq(1).prop("style");
+      // Allowed styles are applied accordingly on the second element.
+      expect(secondElementStyle.color).toBe(`red`);
+      // Forbidden styles are not applied.
+      expect(secondElementStyle.background).toBe(undefined);
+    });
+
+    it("toggle the group when the collapse button is clicked", () => {
+      const store = setupStore([]);
+      store.dispatch = sinon.spy();
+      const message = stubPreparedMessages.get("console.group('bar')");
+
+      let wrapper = mount(Provider({store},
+        ConsoleApiCall({
+          message,
+          open: true,
+          dispatch: store.dispatch,
+          serviceContainer,
+        })
+      ));
+      wrapper.find(".theme-twisty.open").simulate("click");
+      let call = store.dispatch.getCall(0);
+      expect(call.args[0]).toEqual({
+        id: message.id,
+        type: MESSAGE_CLOSE
+      });
+
+      wrapper = mount(Provider({store},
+        ConsoleApiCall({
+          message,
+          open: false,
+          dispatch: store.dispatch,
+          serviceContainer,
+        })
+      ));
+      wrapper.find(".theme-twisty").simulate("click");
+      call = store.dispatch.getCall(1);
+      expect(call.args[0]).toEqual({
+        id: message.id,
+        type: MESSAGE_OPEN
+      });
+    });
+  });
+
+  describe("console.groupEnd", () => {
+    it("does not show anything", () => {
+      const message = stubPreparedMessages.get("console.groupEnd('bar')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text()).toBe("");
+    });
+  });
+
+  describe("console.groupCollapsed", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.groupCollapsed('foo')");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: false}));
+
+      expect(wrapper.find(".message-body").text()).toBe("foo");
+      expect(wrapper.find(".theme-twisty:not(.open)").length).toBe(1);
+    });
+
+    it("renders group with custom style", () => {
+      const message = stubPreparedMessages.get("console.groupCollapsed(%cfoo%cbaz)");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      const elements = wrapper.find(".objectBox-string");
+      expect(elements.text()).toBe("foobaz");
+      expect(elements.length).toBe(2);
+
+      const firstElementStyle = elements.eq(0).prop("style");
+      // Allowed styles are applied accordingly on the first element.
+      expect(firstElementStyle.color).toBe(`blue`);
+      expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+      // Forbidden styles are not applied.
+      expect(firstElementStyle["background-image"]).toBe(undefined);
+      expect(firstElementStyle.position).toBe(undefined);
+      expect(firstElementStyle.top).toBe(undefined);
+
+      const secondElementStyle = elements.eq(1).prop("style");
+      // Allowed styles are applied accordingly on the second element.
+      expect(secondElementStyle.color).toBe(`red`);
+      // Forbidden styles are not applied.
+      expect(secondElementStyle.background).toBe(undefined);
+    });
+  });
+
+  describe("console.dirxml", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.dirxml(window)");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text())
+        .toBe("Window http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html");
+    });
+  });
+
+  describe("console.dir", () => {
+    it("renders", () => {
+      const message = stubPreparedMessages.get("console.dir({C, M, Y, K})");
+      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+      expect(wrapper.find(".message-body").text())
+        .toBe(`Object { cyan: "C", magenta: "M", yellow: "Y", black: "K" }`);
+    });
+  });
+});
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -1,221 +1,226 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-// Console API
-
-const consoleApiCommands = [
-  "console.log('foobar', 'test')",
-  "console.log(undefined)",
-  "console.warn('danger, will robinson!')",
-  "console.log(NaN)",
-  "console.log(null)",
-  "console.log('\u9f2c')",
-  "console.clear()",
-  "console.count('bar')",
-  "console.assert(false, {message: 'foobar'})",
-  "console.log('hello \\nfrom \\rthe \\\"string world!')",
-  "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')",
-  "console.dirxml(window)",
-  "console.log('myarray', ['red', 'green', 'blue'])",
-  "console.log('myregex', /a.b.c/)",
-  "console.table(['red', 'green', 'blue']);",
-  "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});",
-  "console.debug('debug message');",
-  "console.info('info message');",
-  "console.error('error message');",
-];
-
-let consoleApi = new Map(consoleApiCommands.map(
-  cmd => [cmd, {keys: [cmd], code: cmd}]));
-
-consoleApi.set("console.log('mymap')", {
-  keys: ["console.log('mymap')"],
-  code: `
-var map = new Map();
-map.set("key1", "value1");
-map.set("key2", "value2");
-console.log('mymap', map);
-`});
-
-consoleApi.set("console.log('myset')", {
-  keys: ["console.log('myset')"],
-  code: `
-console.log('myset', new Set(["a", "b"]));
-`});
-
-consoleApi.set("console.trace()", {
-  keys: ["console.trace()"],
-  code: `
-function testStacktraceFiltering() {
-  console.trace()
-}
-function foo() {
-  testStacktraceFiltering()
-}
-
-foo()
-`});
-
-consoleApi.set("console.time('bar')", {
-  keys: ["console.time('bar')", "timerAlreadyExists",
-         "console.timeEnd('bar')", "timerDoesntExist"],
-  code: `
-console.time("bar");
-console.time("bar");
-console.timeEnd("bar");
-console.timeEnd("bar");
-`});
-
-consoleApi.set("console.table('bar')", {
-  keys: ["console.table('bar')"],
-  code: `
-console.table('bar');
-`});
-
-consoleApi.set("console.table(['a', 'b', 'c'])", {
-  keys: ["console.table(['a', 'b', 'c'])"],
-  code: `
-console.table(['a', 'b', 'c']);
-`});
-
-consoleApi.set("console.group('bar')", {
-  keys: ["console.group('bar')", "console.groupEnd('bar')"],
-  code: `
-console.group("bar");
-console.groupEnd();
-`});
-
-consoleApi.set("console.groupCollapsed('foo')", {
-  keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"],
-  code: `
-console.groupCollapsed("foo");
-console.groupEnd();
-`});
-
-consoleApi.set("console.group()", {
-  keys: ["console.group()", "console.groupEnd()"],
-  code: `
-console.group();
-console.groupEnd();
-`});
-
-consoleApi.set("console.log(%cfoobar)", {
-  keys: ["console.log(%cfoobar)"],
-  code: `
-console.log(
-  "%cfoo%cbar",
-  "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
-  "color:red;background:\\165rl('http://example.com/test')");
-`});
-
-consoleApi.set("console.group(%cfoo%cbar)", {
-  keys: ["console.group(%cfoo%cbar)", "console.groupEnd(%cfoo%cbar)"],
-  code: `
-console.group(
-  "%cfoo%cbar",
-  "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
-  "color:red;background:\\165rl('http://example.com/test')");
-console.groupEnd();
-`});
-
-consoleApi.set("console.groupCollapsed(%cfoo%cbaz)", {
-  keys: ["console.groupCollapsed(%cfoo%cbaz)", "console.groupEnd(%cfoo%cbaz)"],
-  code: `
-console.groupCollapsed(
-  "%cfoo%cbaz",
-  "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
-  "color:red;background:\\165rl('http://example.com/test')");
-console.groupEnd();
-`});
-
-consoleApi.set("console.dir({C, M, Y, K})", {
-  keys: ["console.dir({C, M, Y, K})"],
-  code: "console.dir({cyan: 'C', magenta: 'M', yellow: 'Y', black: 'K'});"
-});
-
-// CSS messages
-const cssMessage = new Map();
-
-cssMessage.set("Unknown property", `
-p {
-  such-unknown-property: wow;
-}
-`);
-
-cssMessage.set("Invalid property value", `
-p {
-  padding-top: invalid value;
-}
-`);
-
-// Evaluation Result
-const evaluationResultCommands = [
-  "new Date(0)",
-  "asdf()",
-  "1 + @",
-  "inspect({a: 1})",
-  "cd(document)"
-];
-
-let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));
-evaluationResult.set("longString message Error",
-  `throw new Error("Long error ".repeat(10000))`);
-
-// Network Event
-
-let networkEvent = new Map();
-
-networkEvent.set("GET request", {
-  keys: ["GET request"],
-  code: `
-let i = document.createElement("img");
-i.src = "inexistent.html";
-`});
-
-networkEvent.set("XHR GET request", {
-  keys: ["XHR GET request"],
-  code: `
-const xhr = new XMLHttpRequest();
-xhr.open("GET", "inexistent.html");
-xhr.send();
-`});
-
-networkEvent.set("XHR POST request", {
-  keys: ["XHR POST request"],
-  code: `
-const xhr = new XMLHttpRequest();
-xhr.open("POST", "inexistent.html");
-xhr.send();
-`});
-
-// Page Error
-
-let pageError = new Map();
-
-pageError.set("ReferenceError: asdf is not defined", `
-  function bar() {
-    asdf()
-  }
-  function foo() {
-    bar()
-  }
-
-  foo()
-`);
-
-pageError.set("SyntaxError: redeclaration of let a", `
-  let a, a;
-`);
-
-pageError.set("TypeError longString message",
-  `throw new Error("Long error ".repeat(10000))`);
-
-module.exports = {
-  consoleApi,
-  cssMessage,
-  evaluationResult,
-  networkEvent,
-  pageError,
-};
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Console API
+
+const consoleApiCommands = [
+  "console.log('foobar', 'test')",
+  "console.log(undefined)",
+  "console.warn('danger, will robinson!')",
+  "console.log(NaN)",
+  "console.log(null)",
+  "console.log('\u9f2c')",
+  "console.clear()",
+  "console.count('bar')",
+  "console.assert(false, {message: 'foobar'})",
+  "console.log('hello \\nfrom \\rthe \\\"string world!')",
+  "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')",
+  "console.dirxml(window)",
+  "console.log('myarray', ['red', 'green', 'blue'])",
+  "console.log('myregex', /a.b.c/)",
+  "console.table(['red', 'green', 'blue']);",
+  "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});",
+  "console.debug('debug message');",
+  "console.info('info message');",
+  "console.error('error message');",
+];
+
+let consoleApi = new Map(consoleApiCommands.map(
+  cmd => [cmd, {keys: [cmd], code: cmd}]));
+
+consoleApi.set("console.log('mymap')", {
+  keys: ["console.log('mymap')"],
+  code: `
+var map = new Map();
+map.set("key1", "value1");
+map.set("key2", "value2");
+console.log('mymap', map);
+`});
+
+consoleApi.set("console.log('myset')", {
+  keys: ["console.log('myset')"],
+  code: `
+console.log('myset', new Set(["a", "b"]));
+`});
+
+consoleApi.set("console.trace()", {
+  keys: ["console.trace()"],
+  code: `
+function testStacktraceFiltering() {
+  console.trace()
+}
+function foo() {
+  testStacktraceFiltering()
+}
+
+foo()
+`});
+
+consoleApi.set("console.time('bar')", {
+  keys: ["console.time('bar')", "timerAlreadyExists",
+         "console.timeEnd('bar')", "timerDoesntExist"],
+  code: `
+console.time("bar");
+console.time("bar");
+console.timeEnd("bar");
+console.timeEnd("bar");
+`});
+
+consoleApi.set("console.table('bar')", {
+  keys: ["console.table('bar')"],
+  code: `
+console.table('bar');
+`});
+
+consoleApi.set("console.table(['a', 'b', 'c'])", {
+  keys: ["console.table(['a', 'b', 'c'])"],
+  code: `
+console.table(['a', 'b', 'c']);
+`});
+
+consoleApi.set("console.group('bar')", {
+  keys: ["console.group('bar')", "console.groupEnd('bar')"],
+  code: `
+console.group("bar");
+console.groupEnd();
+`});
+
+consoleApi.set("console.groupCollapsed('foo')", {
+  keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"],
+  code: `
+console.groupCollapsed("foo");
+console.groupEnd();
+`});
+
+consoleApi.set("console.group()", {
+  keys: ["console.group()", "console.groupEnd()"],
+  code: `
+console.group();
+console.groupEnd();
+`});
+
+consoleApi.set("console.log(%cfoobar)", {
+  keys: ["console.log(%cfoobar)"],
+  code: `
+console.log(
+  "%cfoo%cbar",
+  "color:blue;
+  font-size:1.3em;
+  background:url('http://example.com/test');
+  position:absolute;
+  top:10px",
+  "color:red;
+  line-height: 1.5;
+  background:\\165rl('http://example.com/test')"),
+`});
+
+consoleApi.set("console.group(%cfoo%cbar)", {
+  keys: ["console.group(%cfoo%cbar)", "console.groupEnd(%cfoo%cbar)"],
+  code: `
+console.group(
+  "%cfoo%cbar",
+  "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+  "color:red;background:\\165rl('http://example.com/test')");
+console.groupEnd();
+`});
+
+consoleApi.set("console.groupCollapsed(%cfoo%cbaz)", {
+  keys: ["console.groupCollapsed(%cfoo%cbaz)", "console.groupEnd(%cfoo%cbaz)"],
+  code: `
+console.groupCollapsed(
+  "%cfoo%cbaz",
+  "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+  "color:red;background:\\165rl('http://example.com/test')");
+console.groupEnd();
+`});
+
+consoleApi.set("console.dir({C, M, Y, K})", {
+  keys: ["console.dir({C, M, Y, K})"],
+  code: "console.dir({cyan: 'C', magenta: 'M', yellow: 'Y', black: 'K'});"
+});
+
+// CSS messages
+const cssMessage = new Map();
+
+cssMessage.set("Unknown property", `
+p {
+  such-unknown-property: wow;
+}
+`);
+
+cssMessage.set("Invalid property value", `
+p {
+  padding-top: invalid value;
+}
+`);
+
+// Evaluation Result
+const evaluationResultCommands = [
+  "new Date(0)",
+  "asdf()",
+  "1 + @",
+  "inspect({a: 1})"
+];
+
+let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));
+evaluationResult.set("longString message Error",
+  `throw new Error("Long error ".repeat(10000))`);
+
+// Network Event
+
+let networkEvent = new Map();
+
+networkEvent.set("GET request", {
+  keys: ["GET request"],
+  code: `
+let i = document.createElement("img");
+i.src = "inexistent.html";
+`});
+
+networkEvent.set("XHR GET request", {
+  keys: ["XHR GET request"],
+  code: `
+const xhr = new XMLHttpRequest();
+xhr.open("GET", "inexistent.html");
+xhr.send();
+`});
+
+networkEvent.set("XHR POST request", {
+  keys: ["XHR POST request"],
+  code: `
+const xhr = new XMLHttpRequest();
+xhr.open("POST", "inexistent.html");
+xhr.send();
+`});
+
+// Page Error
+
+let pageError = new Map();
+
+pageError.set("ReferenceError: asdf is not defined", `
+  function bar() {
+    asdf()
+  }
+  function foo() {
+    bar()
+  }
+
+  foo()
+`);
+
+pageError.set("SyntaxError: redeclaration of let a", `
+  let a, a;
+`);
+
+pageError.set("TypeError longString message",
+  `throw new Error("Long error ".repeat(10000))`);
+
+module.exports = {
+  consoleApi,
+  cssMessage,
+  evaluationResult,
+  networkEvent,
+  pageError,
+};