Bug 1368100 - Adapt tests to the new state;r=bgrins. draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Wed, 15 Nov 2017 10:58:10 +0100
changeset 699717 cf7ae5bca38a42e26a036faa0e2e031370dbfa21
parent 699716 44dd9e83493734ed4d3b19bb783a7c86ddc28654
child 699718 01395cebdcec7fbf4df5d9e300449567d36e03f1
push id89639
push userbmo:nchevobbe@mozilla.com
push dateFri, 17 Nov 2017 14:42:48 +0000
reviewersbgrins
bugs1368100
milestone59.0a1
Bug 1368100 - Adapt tests to the new state;r=bgrins. This introduces a few helpers to work with the state, as well uses the new selectors. MozReview-Commit-ID: D9aR7kbapZg
devtools/client/webconsole/new-console-output/test/helpers.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
devtools/client/webconsole/new-console-output/test/store/filters.test.js
devtools/client/webconsole/new-console-output/test/store/hidden-messages.test.js
devtools/client/webconsole/new-console-output/test/store/messages.test.js
devtools/client/webconsole/new-console-output/test/store/network-messages.test.js
devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
--- a/devtools/client/webconsole/new-console-output/test/helpers.js
+++ b/devtools/client/webconsole/new-console-output/test/helpers.js
@@ -9,45 +9,46 @@ const dom = require("devtools/client/sha
 const { createElement } = React;
 var TestUtils = React.addons.TestUtils;
 
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
 const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
 const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const {
-  getAllMessagesById,
+  getMutableMessagesById,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 
 /**
  * Prepare actions for use in testing.
  */
 function setupActions() {
   // Some actions use dependency injection. This helps them avoid using state in
   // a hard-to-test way. We need to inject stubbed versions of these dependencies.
   const wrappedActions = Object.assign({}, actions);
 
   const idGenerator = new IdGenerator();
-  wrappedActions.messageAdd = (packet) => {
-    return actions.messageAdd(packet, idGenerator);
+  wrappedActions.messagesAdd = (packets) => {
+    return actions.messagesAdd(packets, idGenerator);
   };
 
   return wrappedActions;
 }
 
 /**
  * Prepare the store for use in testing.
  */
-function setupStore(input = [], hud, options) {
+function setupStore(input = [], hud, options, wrappedActions) {
   const store = configureStore(hud, options);
 
   // Add the messages from the input commands to the store.
-  input.forEach((cmd) => {
-    store.dispatch(actions.messageAdd(stubPackets.get(cmd)));
-  });
+  const messagesAdd = wrappedActions
+    ? wrappedActions.messagesAdd
+    : actions.messagesAdd;
+  store.dispatch(messagesAdd(input.map(cmd => stubPackets.get(cmd))));
 
   return store;
 }
 
 function renderComponent(component, props) {
   const el = createElement(component, props, {});
   // By default, renderIntoDocument() won't work for stateless components, but
   // it will work if the stateless component is wrapped in a stateful one.
@@ -74,20 +75,43 @@ function clonePacket(packet) {
 /**
  * Return the message in the store at the given index.
  *
  * @param {object} state - The redux state of the console.
  * @param {int} index - The index of the message in the map.
  * @return {Message} - The message, or undefined if the index does not exists in the map.
  */
 function getMessageAt(state, index) {
-  const messages = getAllMessagesById(state);
+  const messages = getMutableMessagesById(state);
   return messages.get([...messages.keys()][index]);
 }
 
+/**
+ * Return the first message in the store.
+ *
+ * @param {object} state - The redux state of the console.
+ * @return {Message} - The last message, or undefined if there are no message in store.
+ */
+function getFirstMessage(state) {
+  return getMessageAt(state, 0);
+}
+
+/**
+ * Return the last message in the store.
+ *
+ * @param {object} state - The redux state of the console.
+ * @return {Message} - The last message, or undefined if there are no message in store.
+ */
+function getLastMessage(state) {
+  const lastIndex = getMutableMessagesById(state).size - 1;
+  return getMessageAt(state, lastIndex);
+}
+
 module.exports = {
   clonePacket,
   getMessageAt,
+  getFirstMessage,
+  getLastMessage,
   renderComponent,
   setupActions,
   setupStore,
   shallowRenderComponent,
 };
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
@@ -6,82 +6,83 @@
 "use strict";
 
 // Check console.group, console.groupCollapsed and console.groupEnd calls
 // behave as expected.
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html";
 const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/MessageIndent");
 
-add_task(function* () {
-  let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
-  let hud = toolbox.getCurrentPanel().hud;
+add_task(async function () {
+  const hud = await openNewTabAndConsole(TEST_URI);
 
   const store = hud.ui.newConsoleOutput.getStore();
   // Adding logging each time the store is modified in order to check
   // the store state in case of failure.
   store.subscribe(() => {
-    const messages = store.getState().messages.messagesById
-      .reduce(function (res, message) {
+    const messages = [...store.getState().messages.messagesById]
+      .reduce(function (res, [id, message]) {
         res.push({
-          id: message.id,
+          id,
           type: message.type,
           parameters: message.parameters,
           messageText: message.messageText
         });
         return res;
       }, []);
     info("messages : " + JSON.stringify(messages));
   });
 
-  yield ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+  ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
     content.wrappedJSObject.doLog();
   });
 
   info("Test a group at root level");
-  let node = yield waitFor(() => findMessage(hud, "group-1"));
+  let node = await waitFor(() => findMessage(hud, "group-1"));
   testClass(node, "startGroup");
   testIndent(node, 0);
 
   info("Test a message in a 1 level deep group");
-  node = yield waitFor(() => findMessage(hud, "log-1"));
+  node = await waitFor(() => findMessage(hud, "log-1"), undefined, 200);
   testClass(node, "log");
   testIndent(node, 1);
 
   info("Test a group in a 1 level deep group");
-  node = yield waitFor(() => findMessage(hud, "group-2"));
+  node = await waitFor(() => findMessage(hud, "group-2"));
   testClass(node, "startGroup");
   testIndent(node, 1);
 
   info("Test a message in a 2 level deep group");
-  node = yield waitFor(() => findMessage(hud, "log-2"));
+  node = await waitFor(() => findMessage(hud, "log-2"));
   testClass(node, "log");
   testIndent(node, 2);
 
   info("Test a message in a 1 level deep group, after closing a 2 level deep group");
-  node = yield waitFor(() => findMessage(hud, "log-3"));
+  node = await waitFor(() => findMessage(hud, "log-3"));
   testClass(node, "log");
   testIndent(node, 1);
 
   info("Test a message at root level, after closing all the groups");
-  node = yield waitFor(() => findMessage(hud, "log-4"));
+  node = await waitFor(() => findMessage(hud, "log-4"));
   testClass(node, "log");
   testIndent(node, 0);
 
   info("Test a collapsed group at root level");
-  node = yield waitFor(() => findMessage(hud, "group-3"));
+  node = await waitFor(() => findMessage(hud, "group-3"));
   testClass(node, "startGroupCollapsed");
   testIndent(node, 0);
   info("Test a message at root level, after closing a collapsed group");
-  node = yield waitFor(() => findMessage(hud, "log-6"));
+  node = await waitFor(() => findMessage(hud, "log-6"));
   testClass(node, "log");
   testIndent(node, 0);
   let nodes = hud.ui.outputNode.querySelectorAll(".message");
   is(nodes.length, 8, "expected number of messages are displayed");
 });
+
 function testClass(node, className) {
   ok(node.classList.contains(className), `message has the expected "${className}" class`);
 }
+
 function testIndent(node, indent) {
   indent = `${indent * INDENT_WIDTH}px`;
   is(node.querySelector(".indent").style.width, indent,
     "message has the expected level of indentation");
 }
--- a/devtools/client/webconsole/new-console-output/test/store/filters.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const expect = require("expect");
 
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
-const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messagesAdd } = require("devtools/client/webconsole/new-console-output/actions/index");
 const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
 const { getVisibleMessages } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
 const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { FILTERS, PREFS } = require("devtools/client/webconsole/new-console-output/constants");
 const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const ServicesMock = require("Services");
@@ -82,41 +82,41 @@ describe("Filtering", () => {
       let messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages + 3);
     });
 
     it("filters css messages", () => {
       let message = stubPreparedMessages.get(
         "Unknown property ‘such-unknown-property’.  Declaration dropped."
       );
-      store.dispatch(messageAdd(message));
+      store.dispatch(messagesAdd([message]));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages);
 
       store.dispatch(actions.filterToggle("css"));
       messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages + 1);
     });
 
     it("filters xhr messages", () => {
       let message = stubPreparedMessages.get("XHR GET request");
-      store.dispatch(messageAdd(message));
+      store.dispatch(messagesAdd([message]));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages);
 
       store.dispatch(actions.filterToggle("netxhr"));
       messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages + 1);
     });
 
     it("filters network messages", () => {
       let message = stubPreparedMessages.get("GET request");
-      store.dispatch(messageAdd(message));
+      store.dispatch(messagesAdd([message]));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages);
 
       store.dispatch(actions.filterToggle("net"));
       messages = getVisibleMessages(store.getState());
       expect(messages.length).toEqual(numUnfilterableMessages + 1);
     });
@@ -147,44 +147,44 @@ describe("Filtering", () => {
     });
 
     it("matches locations", () => {
       // Add a message with a different filename.
       let locationMsg =
         Object.assign({}, stubPackets.get("console.log('foobar', 'test')"));
       locationMsg.message =
         Object.assign({}, locationMsg.message, { filename: "search-location-test.js" });
-      store.dispatch(messageAdd(locationMsg));
+      store.dispatch(messagesAdd([locationMsg]));
 
       store.dispatch(actions.filterTextSet("search-location-test.js"));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length - numUnfilterableMessages).toEqual(1);
     });
 
     it("matches stacktrace functionName", () => {
       let traceMessage = stubPackets.get("console.trace()");
-      store.dispatch(messageAdd(traceMessage));
+      store.dispatch(messagesAdd([traceMessage]));
 
       store.dispatch(actions.filterTextSet("testStacktraceFiltering"));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length - numUnfilterableMessages).toEqual(1);
     });
 
     it("matches stacktrace location", () => {
       let traceMessage = stubPackets.get("console.trace()");
       traceMessage.message =
         Object.assign({}, traceMessage.message, {
           filename: "search-location-test.js",
           lineNumber: 85,
           columnNumber: 13
         });
 
-      store.dispatch(messageAdd(traceMessage));
+      store.dispatch(messagesAdd([traceMessage]));
 
       store.dispatch(actions.filterTextSet("search-location-test.js:85:13"));
 
       let messages = getVisibleMessages(store.getState());
       expect(messages.length - numUnfilterableMessages).toEqual(1);
     });
 
     it("restores all messages once text is cleared", () => {
@@ -350,12 +350,12 @@ function prepareBaseStore() {
     "console.debug('debug message');",
     "console.info('info message');",
     "console.error('error message');",
     "console.table(['red', 'green', 'blue']);",
     "console.assert(false, {message: 'foobar'})",
   ]);
 
   // Console Command - never filtered
-  store.dispatch(messageAdd(new ConsoleCommand({ messageText: `console.warn("x")` })));
+  store.dispatch(messagesAdd([new ConsoleCommand({ messageText: `console.warn("x")` })]));
 
   return store;
 }
--- a/devtools/client/webconsole/new-console-output/test/store/hidden-messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/hidden-messages.test.js
@@ -46,18 +46,18 @@ describe("Filtering - Hidden messages", 
 
     // Numbers update if the text search is cleared.
     store.dispatch(actions.filterTextSet(""));
     counter = getFilteredMessagesCount(store.getState());
     expect(counter).toEqual(BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT);
   });
 
   it("updates when messages are added", () => {
-    MESSAGES.forEach(message =>
-      store.dispatch(actions.messageAdd(stubPackets.get(message))));
+    const packets = MESSAGES.map(key => stubPackets.get(key));
+    store.dispatch(actions.messagesAdd(packets));
 
     let counter = getFilteredMessagesCount(store.getState());
     expect(counter).toEqual({
       [FILTERS.ERROR]: 6,
       [FILTERS.WARN]: 2,
       [FILTERS.LOG]: 10,
       [FILTERS.INFO]: 2,
       [FILTERS.DEBUG]: 2,
--- a/devtools/client/webconsole/new-console-output/test/store/messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -1,24 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const {
-  getAllGroupsById,
-  getAllMessagesById,
+  getAllExpandedMessageIds,
   getAllMessagesTableDataById,
-  getAllMessagesUiById,
   getAllNetworkMessagesUpdateById,
   getAllRepeatById,
-  getCurrentGroup,
+  getMutableCurrentGroup,
+  getMutableGroupsById,
+  getMutableMessagesById,
   getVisibleMessages,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const {
   clonePacket,
+  getFirstMessage,
+  getLastMessage,
   getMessageAt,
   setupActions,
   setupStore,
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const {
   MESSAGE_TYPE,
 } = require("devtools/client/webconsole/new-console-output/constants");
@@ -33,572 +35,598 @@ describe("Message reducer:", () => {
   });
 
   describe("messagesById", () => {
     it("adds a message to an empty store", () => {
       const { dispatch, getState } = setupStore([]);
 
       const packet = stubPackets.get("console.log('foobar', 'test')");
       const message = stubPreparedMessages.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
-
-      expect(messages.first()).toEqual(message);
+      expect(getFirstMessage(getState())).toEqual(message);
     });
 
     it("increments repeat on a repeating log message", () => {
       const key1 = "console.log('foobar', 'test')";
       const { dispatch, getState } = setupStore([key1, key1]);
 
       const packet = clonePacket(stubPackets.get(key1));
 
       // Repeat ID must be the same even if the timestamp is different.
       packet.message.timeStamp = 1;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       packet.message.timeStamp = 2;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
       expect(messages.size).toBe(1);
-
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(4);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(4);
     });
 
     it("increments repeat on a repeating css message", () => {
       const key1 = "Unknown property ‘such-unknown-property’.  Declaration dropped.";
       const { dispatch, getState } = setupStore([key1, key1]);
 
       const packet = clonePacket(stubPackets.get(key1));
 
       // Repeat ID must be the same even if the timestamp is different.
       packet.pageError.timeStamp = 1;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       packet.pageError.timeStamp = 2;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
       expect(messages.size).toBe(1);
 
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(4);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(4);
     });
 
     it("increments repeat on a repeating error message", () => {
       const key1 = "ReferenceError: asdf is not defined";
       const { dispatch, getState } = setupStore([key1, key1]);
 
       const packet = clonePacket(stubPackets.get(key1));
 
       // Repeat ID must be the same even if the timestamp is different.
       packet.pageError.timeStamp = 1;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       packet.pageError.timeStamp = 2;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
       expect(messages.size).toBe(1);
 
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(4);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(4);
     });
 
     it("does not increment repeat after closing a group", () => {
       const logKey = "console.log('foobar', 'test')";
       const { getState } = setupStore([
         logKey,
         logKey,
         "console.group('bar')",
         logKey,
         logKey,
         logKey,
         "console.groupEnd()",
         logKey,
       ]);
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
       expect(messages.size).toBe(4);
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(2);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(2);
       expect(repeat[getMessageAt(getState(), 2).id]).toBe(3);
-      expect(repeat[messages.last().id]).toBe(undefined);
+      expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
     });
 
     it("does not clobber a unique message", () => {
       const key1 = "console.log('foobar', 'test')";
       const { dispatch, getState } = setupStore([key1, key1]);
 
       const packet = stubPackets.get(key1);
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       const packet2 = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet2));
+      dispatch(actions.messagesAdd([packet2]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       expect(messages.size).toBe(2);
 
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(3);
-      expect(repeat[messages.last().id]).toBe(undefined);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(3);
+      expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
     });
 
     it("adds a message in response to console.clear()", () => {
       const { dispatch, getState } = setupStore([]);
 
-      dispatch(actions.messageAdd(stubPackets.get("console.clear()")));
+      dispatch(actions.messagesAdd([stubPackets.get("console.clear()")]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
       expect(messages.size).toBe(1);
-      expect(messages.first().parameters[0]).toBe("Console was cleared.");
+      expect(getFirstMessage(getState()).parameters[0]).toBe("Console was cleared.");
     });
 
     it("clears the messages list in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
         "console.log('foobar', 'test')",
         "console.log('foobar', 'test')",
         "console.log(undefined)",
         "console.table(['red', 'green', 'blue']);",
         "console.group('bar')",
       ]);
 
       dispatch(actions.messagesClear());
 
       const state = getState();
-      expect(getAllMessagesById(state).size).toBe(0);
+      expect(getMutableMessagesById(state).size).toBe(0);
       expect(getVisibleMessages(state).length).toBe(0);
-      expect(getAllMessagesUiById(state).size).toBe(0);
-      expect(getAllGroupsById(state).size).toBe(0);
+      expect(getAllExpandedMessageIds(state).length).toBe(0);
+      expect(getMutableGroupsById(state).size).toBe(0);
       expect(getAllMessagesTableDataById(state).size).toBe(0);
-      expect(getCurrentGroup(state)).toBe(null);
+      expect(getMutableCurrentGroup(state)).toBe(null);
       expect(getAllRepeatById(state)).toEqual({});
     });
 
     it("cleans the repeatsById object when messages are pruned", () => {
       const { dispatch, getState } = setupStore(
         [
           "console.log('foobar', 'test')",
           "console.log('foobar', 'test')",
           "console.log(undefined)",
           "console.log(undefined)",
         ],
         null, {
           logLimit: 2
-        }
+        },
+        actions
       );
 
       // Check that we have the expected data.
-      let messages = getAllMessagesById(getState());
       let repeats = getAllRepeatById(getState());
       expect(Object.keys(repeats).length).toBe(2);
-      const lastMessageId = messages.last().id;
+      const lastMessageId = getLastMessage(getState()).id;
 
       // This addition will prune the first message out of the store.
       let packet = stubPackets.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      messages = getAllMessagesById(getState());
       repeats = getAllRepeatById(getState());
 
       // There should be only the data for the "undefined" message.
       expect(Object.keys(repeats)).toEqual([lastMessageId]);
       expect(Object.keys(repeats).length).toBe(1);
       expect(repeats[lastMessageId]).toBe(2);
 
       // This addition will prune the first message out of the store.
       packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       // repeatById should now be empty.
       expect(getAllRepeatById(getState())).toEqual({});
     });
 
     it("properly limits number of messages", () => {
       const { dispatch, getState } = setupStore([]);
 
       const logLimit = 1000;
       const packet = clonePacket(stubPackets.get("console.log(undefined)"));
 
       for (let i = 1; i <= logLimit + 2; i++) {
         packet.message.arguments = [`message num ${i}`];
-        dispatch(actions.messageAdd(packet));
+        dispatch(actions.messagesAdd([packet]));
       }
 
-      const messages = getAllMessagesById(getState());
-      expect(messages.count()).toBe(logLimit);
-      expect(messages.first().parameters[0]).toBe(`message num 3`);
-      expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 2}`);
+      const messages = getMutableMessagesById(getState());
+      expect(messages.size).toBe(logLimit);
+      expect(getFirstMessage(getState()).parameters[0]).toBe(`message num 3`);
+      expect(getLastMessage(getState()).parameters[0])
+        .toBe(`message num ${logLimit + 2}`);
     });
 
     it("properly limits number of messages when there are nested groups", () => {
       const { dispatch, getState } = setupStore([]);
 
       const logLimit = 1000;
 
       const packet = clonePacket(stubPackets.get("console.log(undefined)"));
       const packetGroup = clonePacket(stubPackets.get("console.group('bar')"));
       const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
 
       packetGroup.message.arguments = [`group-1`];
-      dispatch(actions.messageAdd(packetGroup));
+      dispatch(actions.messagesAdd([packetGroup]));
       packetGroup.message.arguments = [`group-1-1`];
-      dispatch(actions.messageAdd(packetGroup));
+      dispatch(actions.messagesAdd([packetGroup]));
       packetGroup.message.arguments = [`group-1-1-1`];
-      dispatch(actions.messageAdd(packetGroup));
+      dispatch(actions.messagesAdd([packetGroup]));
       packet.message.arguments = [`message-in-group-1`];
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       packet.message.arguments = [`message-in-group-2`];
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       // Closing group-1-1-1
-      dispatch(actions.messageAdd(packetGroupEnd));
+      dispatch(actions.messagesAdd([packetGroupEnd]));
       // Closing group-1-1
-      dispatch(actions.messageAdd(packetGroupEnd));
+      dispatch(actions.messagesAdd([packetGroupEnd]));
       // Closing group-1
-      dispatch(actions.messageAdd(packetGroupEnd));
+      dispatch(actions.messagesAdd([packetGroupEnd]));
 
       for (let i = 0; i < logLimit; i++) {
         packet.message.arguments = [`message-${i}`];
-        dispatch(actions.messageAdd(packet));
+        dispatch(actions.messagesAdd([packet]));
       }
 
       const visibleMessages = getVisibleMessages(getState());
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
 
-      expect(messages.count()).toBe(logLimit);
+      expect(messages.size).toBe(logLimit);
       expect(visibleMessages.length).toBe(logLimit);
       expect(messages.get(visibleMessages[0]).parameters[0]).toBe(`message-0`);
       expect(messages.get(visibleMessages[logLimit - 1]).parameters[0])
         .toBe(`message-${logLimit - 1}`);
 
       // The groups were cleaned up.
-      const groups = getAllGroupsById(getState());
-      expect(groups.count()).toBe(0);
+      const groups = getMutableGroupsById(getState());
+      expect(groups.size).toBe(0);
     });
 
     it("properly limits number of groups", () => {
       const logLimit = 100;
       const { dispatch, getState } = setupStore([], null, {logLimit});
 
       const packet = clonePacket(stubPackets.get("console.log(undefined)"));
       const packetGroup = clonePacket(stubPackets.get("console.group('bar')"));
       const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
 
       for (let i = 0; i < logLimit + 2; i++) {
-        dispatch(actions.messageAdd(packetGroup));
+        dispatch(actions.messagesAdd([packetGroup]));
         packet.message.arguments = [`message-${i}-a`];
-        dispatch(actions.messageAdd(packet));
+        dispatch(actions.messagesAdd([packet]));
         packet.message.arguments = [`message-${i}-b`];
-        dispatch(actions.messageAdd(packet));
-        dispatch(actions.messageAdd(packetGroupEnd));
+        dispatch(actions.messagesAdd([packet]));
+        dispatch(actions.messagesAdd([packetGroupEnd]));
       }
 
       const visibleMessages = getVisibleMessages(getState());
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       // We should have three times the logLimit since each group has one message inside.
-      expect(messages.count()).toBe(logLimit * 3);
+      expect(messages.size).toBe(logLimit * 3);
 
       // We should have logLimit number of groups
-      const groups = getAllGroupsById(getState());
-      expect(groups.count()).toBe(logLimit);
+      const groups = getMutableGroupsById(getState());
+      expect(groups.size).toBe(logLimit);
 
       expect(messages.get(visibleMessages[1]).parameters[0]).toBe(`message-2-a`);
-      expect(messages.last().parameters[0]).toBe(`message-${logLimit + 1}-b`);
+      expect(getLastMessage(getState()).parameters[0]).toBe(`message-${logLimit + 1}-b`);
     });
 
     it("properly limits number of collapsed groups", () => {
       const logLimit = 100;
       const { dispatch, getState } = setupStore([], null, {logLimit});
 
       const packet = clonePacket(stubPackets.get("console.log(undefined)"));
       const packetGroupCollapsed = clonePacket(
         stubPackets.get("console.groupCollapsed('foo')"));
       const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
 
       for (let i = 0; i < logLimit + 2; i++) {
         packetGroupCollapsed.message.arguments = [`group-${i}`];
-        dispatch(actions.messageAdd(packetGroupCollapsed));
+        dispatch(actions.messagesAdd([packetGroupCollapsed]));
         packet.message.arguments = [`message-${i}-a`];
-        dispatch(actions.messageAdd(packet));
+        dispatch(actions.messagesAdd([packet]));
         packet.message.arguments = [`message-${i}-b`];
-        dispatch(actions.messageAdd(packet));
-        dispatch(actions.messageAdd(packetGroupEnd));
+        dispatch(actions.messagesAdd([packet]));
+        dispatch(actions.messagesAdd([packetGroupEnd]));
       }
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       // We should have three times the logLimit since each group has two message inside.
       expect(messages.size).toBe(logLimit * 3);
 
       // We should have logLimit number of groups
-      const groups = getAllGroupsById(getState());
-      expect(groups.count()).toBe(logLimit);
+      const groups = getMutableGroupsById(getState());
+      expect(groups.size).toBe(logLimit);
 
-      expect(messages.first().parameters[0]).toBe(`group-2`);
-      expect(messages.last().parameters[0]).toBe(`message-${logLimit + 1}-b`);
+      expect(getFirstMessage(getState()).parameters[0]).toBe(`group-2`);
+      expect(getLastMessage(getState()).parameters[0]).toBe(`message-${logLimit + 1}-b`);
 
       const visibleMessages = getVisibleMessages(getState());
       expect(visibleMessages.length).toBe(logLimit);
       const lastVisibleMessageId = visibleMessages[visibleMessages.length - 1];
       expect(messages.get(lastVisibleMessageId).parameters[0])
         .toBe(`group-${logLimit + 1}`);
     });
 
     it("does not add null messages to the store", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.time('bar')");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       expect(messages.size).toBe(0);
     });
 
     it("adds console.table call with unsupported type as console.log", () => {
       const { dispatch, getState } = setupStore([]);
 
       const packet = stubPackets.get("console.table('bar')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
-      const tableMessage = messages.last();
+      const tableMessage = getLastMessage(getState());
       expect(tableMessage.level).toEqual(MESSAGE_TYPE.LOG);
     });
 
     it("adds console.group messages to the store", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.group('bar')");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
+
+      const messages = getMutableMessagesById(getState());
+      expect(messages.size).toBe(1);
+    });
+
+    it("adds messages in console.group to the store", () => {
+      const { dispatch, getState } = setupStore([]);
+
+      const groupPacket = stubPackets.get("console.group('bar')");
+      const groupEndPacket = stubPackets.get("console.groupEnd('bar')");
+      const logPacket = stubPackets.get("console.log('foobar', 'test')");
 
-      const messages = getAllMessagesById(getState());
-      expect(messages.size).toBe(1);
+      const packets = [
+        groupPacket,
+        logPacket,
+        groupPacket,
+        groupPacket,
+        logPacket,
+        groupEndPacket,
+        logPacket,
+        groupEndPacket,
+        logPacket,
+        groupEndPacket,
+        logPacket,
+      ];
+      dispatch(actions.messagesAdd(packets));
+
+      // Here is what we should have (8 messages)
+      // ▼ bar
+      // |  foobar test
+      // |  ▼ bar
+      // |  |  ▼ bar
+      // |  |  |  foobar test
+      // |  |  foobar test
+      // |  foobar test
+      // foobar test
+
+      const isNotGroupEnd = p => p !== groupEndPacket;
+      const messageCount = packets.filter(isNotGroupEnd).length;
+
+      const messages = getMutableMessagesById(getState());
+      const visibleMessages = getVisibleMessages(getState());
+      expect(messages.size).toBe(messageCount);
+      expect(visibleMessages.length).toBe(messageCount);
     });
 
     it("sets groupId property as expected", () => {
       const { dispatch, getState } = setupStore([]);
 
-      dispatch(actions.messageAdd(
-        stubPackets.get("console.group('bar')")));
+      dispatch(actions.messagesAdd([
+        stubPackets.get("console.group('bar')"),
+        stubPackets.get("console.log('foobar', 'test')")
+      ]));
 
-      const packet = stubPackets.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
-
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       expect(messages.size).toBe(2);
-      expect(messages.last().groupId).toBe(messages.first().id);
+      expect(getLastMessage(getState()).groupId).toBe(getFirstMessage(getState()).id);
     });
 
     it("does not display console.groupEnd messages to the store", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.groupEnd('bar')");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
 
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       expect(messages.size).toBe(0);
     });
 
     it("filters out message added after a console.groupCollapsed message", () => {
       const { dispatch, getState } = setupStore([]);
 
-      const message = stubPackets.get("console.groupCollapsed('foo')");
-      dispatch(actions.messageAdd(message));
-
-      dispatch(actions.messageAdd(
-        stubPackets.get("console.log('foobar', 'test')")));
+      dispatch(actions.messagesAdd([
+        stubPackets.get("console.groupCollapsed('foo')"),
+        stubPackets.get("console.log('foobar', 'test')"),
+      ]));
 
       const messages = getVisibleMessages(getState());
       expect(messages.length).toBe(1);
     });
 
     it("adds console.dirxml call as console.log", () => {
       const { dispatch, getState } = setupStore([]);
 
       const packet = stubPackets.get("console.dirxml(window)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
-      const dirxmlMessage = messages.last();
+      const dirxmlMessage = getLastMessage(getState());
       expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG);
     });
   });
 
-  describe("messagesUiById", () => {
+  describe("expandedMessageIds", () => {
     it("opens console.trace messages when they are added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.trace()");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
 
-      const messages = getAllMessagesById(getState());
-      const messagesUi = getAllMessagesUiById(getState());
-      expect(messagesUi.size).toBe(1);
-      expect(messagesUi.first()).toBe(messages.first().id);
+      const expanded = getAllExpandedMessageIds(getState());
+      expect(expanded.length).toBe(1);
+      expect(expanded[0]).toBe(getFirstMessage(getState()).id);
     });
 
     it("clears the messages UI list in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
         "console.log('foobar', 'test')",
         "console.log(undefined)"
       ]);
 
       const traceMessage = stubPackets.get("console.trace()");
-      dispatch(actions.messageAdd(traceMessage));
+      dispatch(actions.messagesAdd([traceMessage]));
 
       dispatch(actions.messagesClear());
 
-      const messagesUi = getAllMessagesUiById(getState());
-      expect(messagesUi.size).toBe(0);
+      const expanded = getAllExpandedMessageIds(getState());
+      expect(expanded.length).toBe(0);
     });
 
     it("cleans the messages UI list when messages are pruned", () => {
       const { dispatch, getState } = setupStore(
         ["console.trace()", "console.log(undefined)", "console.trace()"],
         null, {
           logLimit: 3
         }
       );
 
       // Check that we have the expected data.
-      let messages = getAllMessagesById(getState());
-      let messagesUi = getAllMessagesUiById(getState());
-      expect(messagesUi.size).toBe(2);
-      expect(messagesUi.first()).toBe(messages.first().id);
-      const lastMessageId = messages.last().id;
-      expect(messagesUi.last()).toBe(lastMessageId);
+      let expanded = getAllExpandedMessageIds(getState());
+      expect(expanded.length).toBe(2);
+      expect(expanded[0]).toBe(getFirstMessage(getState()).id);
+      const lastMessageId = getLastMessage(getState()).id;
+      expect(expanded[expanded.length - 1]).toBe(lastMessageId);
 
       // This addition will prune the first message out of the store.
       let packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      messages = getAllMessagesById(getState());
-      messagesUi = getAllMessagesUiById(getState());
+      expanded = getAllExpandedMessageIds(getState());
 
       // There should be only the id of the last console.trace message.
-      expect(messagesUi.size).toBe(1);
-      expect(messagesUi.first()).toBe(lastMessageId);
+      expect(expanded.length).toBe(1);
+      expect(expanded[0]).toBe(lastMessageId);
 
       // These additions will prune the last console.trace message out of the store.
       packet = stubPackets.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      // messagesUiById should now be empty.
-      expect(getAllMessagesUiById(getState()).size).toBe(0);
+      // expandedMessageIds should now be empty.
+      expect(getAllExpandedMessageIds(getState()).length).toBe(0);
     });
 
     it("opens console.group messages when they are added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.group('bar')");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
 
-      const messages = getAllMessagesById(getState());
-      const messagesUi = getAllMessagesUiById(getState());
-      expect(messagesUi.size).toBe(1);
-      expect(messagesUi.first()).toBe(messages.first().id);
+      const expanded = getAllExpandedMessageIds(getState());
+      expect(expanded.length).toBe(1);
+      expect(expanded[0]).toBe(getFirstMessage(getState()).id);
     });
 
     it("does not open console.groupCollapsed messages when they are added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.groupCollapsed('foo')");
-      dispatch(actions.messageAdd(message));
+      dispatch(actions.messagesAdd([message]));
 
-      const messagesUi = getAllMessagesUiById(getState());
-      expect(messagesUi.size).toBe(0);
+      const expanded = getAllExpandedMessageIds(getState());
+      expect(expanded.length).toBe(0);
     });
   });
 
   describe("currentGroup", () => {
     it("sets the currentGroup when console.group message is added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const packet = stubPackets.get("console.group('bar')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      const messages = getAllMessagesById(getState());
-      const currentGroup = getCurrentGroup(getState());
-      expect(currentGroup).toBe(messages.first().id);
+      const currentGroup = getMutableCurrentGroup(getState());
+      expect(currentGroup).toBe(getFirstMessage(getState()).id);
     });
 
     it("sets currentGroup to expected value when console.groupEnd is added", () => {
       const { dispatch, getState } = setupStore([
         "console.group('bar')",
         "console.groupCollapsed('foo')"
       ]);
 
-      let messages = getAllMessagesById(getState());
-      let currentGroup = getCurrentGroup(getState());
-      expect(currentGroup).toBe(messages.last().id);
+      let currentGroup = getMutableCurrentGroup(getState());
+      expect(currentGroup).toBe(getLastMessage(getState()).id);
 
       const endFooPacket = stubPackets.get("console.groupEnd('foo')");
-      dispatch(actions.messageAdd(endFooPacket));
-      messages = getAllMessagesById(getState());
-      currentGroup = getCurrentGroup(getState());
-      expect(currentGroup).toBe(messages.first().id);
+      dispatch(actions.messagesAdd([endFooPacket]));
+      currentGroup = getMutableCurrentGroup(getState());
+      expect(currentGroup).toBe(getFirstMessage(getState()).id);
 
       const endBarPacket = stubPackets.get("console.groupEnd('bar')");
-      dispatch(actions.messageAdd(endBarPacket));
-      messages = getAllMessagesById(getState());
-      currentGroup = getCurrentGroup(getState());
+      dispatch(actions.messagesAdd([endBarPacket]));
+      currentGroup = getMutableCurrentGroup(getState());
       expect(currentGroup).toBe(null);
     });
 
     it("resets the currentGroup to null in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
         "console.group('bar')"
       ]);
 
       dispatch(actions.messagesClear());
 
-      const currentGroup = getCurrentGroup(getState());
+      const currentGroup = getMutableCurrentGroup(getState());
       expect(currentGroup).toBe(null);
     });
   });
 
   describe("groupsById", () => {
     it("adds the group with expected array when console.group message is added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const barPacket = stubPackets.get("console.group('bar')");
-      dispatch(actions.messageAdd(barPacket));
+      dispatch(actions.messagesAdd([barPacket]));
 
-      let messages = getAllMessagesById(getState());
-      let groupsById = getAllGroupsById(getState());
+      let groupsById = getMutableGroupsById(getState());
       expect(groupsById.size).toBe(1);
-      expect(groupsById.has(messages.first().id)).toBe(true);
-      expect(groupsById.get(messages.first().id)).toEqual([]);
+      expect(groupsById.has(getFirstMessage(getState()).id)).toBe(true);
+      expect(groupsById.get(getFirstMessage(getState()).id)).toEqual([]);
 
       const fooPacket = stubPackets.get("console.groupCollapsed('foo')");
-      dispatch(actions.messageAdd(fooPacket));
-      messages = getAllMessagesById(getState());
-      groupsById = getAllGroupsById(getState());
+      dispatch(actions.messagesAdd([fooPacket]));
+
+      groupsById = getMutableGroupsById(getState());
       expect(groupsById.size).toBe(2);
-      expect(groupsById.has(messages.last().id)).toBe(true);
-      expect(groupsById.get(messages.last().id)).toEqual([messages.first().id]);
+      expect(groupsById.has(getLastMessage(getState()).id)).toBe(true);
+      expect(groupsById.get(getLastMessage(getState()).id))
+        .toEqual([getFirstMessage(getState()).id]);
     });
 
     it("resets groupsById in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
         "console.group('bar')",
         "console.groupCollapsed('foo')",
       ]);
 
-      let groupsById = getAllGroupsById(getState());
+      let groupsById = getMutableGroupsById(getState());
       expect(groupsById.size).toBe(2);
 
       dispatch(actions.messagesClear());
 
-      groupsById = getAllGroupsById(getState());
+      groupsById = getMutableGroupsById(getState());
       expect(groupsById.size).toBe(0);
     });
 
     it("cleans the groupsById property when messages are pruned", () => {
       const { dispatch, getState } = setupStore(
         [
           "console.group('bar')",
           "console.group()",
@@ -609,57 +637,57 @@ describe("Message reducer:", () => {
           "console.log('foobar', 'test')",
         ],
         null, {
           logLimit: 3
         }
       );
 
       // Check that we have the expected data.
-      let groupsById = getAllGroupsById(getState());
+      let groupsById = getMutableGroupsById(getState());
       expect(groupsById.size).toBe(3);
 
       // This addition will prune the first group (and its child group) out of the store.
       let packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
-      groupsById = getAllGroupsById(getState());
+      groupsById = getMutableGroupsById(getState());
 
       // There should be only the id of the last console.trace message.
       expect(groupsById.size).toBe(1);
 
       // This additions will prune the last group message out of the store.
       packet = stubPackets.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       // groupsById should now be empty.
-      expect(getAllGroupsById(getState()).size).toBe(0);
+      expect(getMutableGroupsById(getState()).size).toBe(0);
     });
   });
 
   describe("networkMessagesUpdateById", () => {
     it("adds the network update message when network update action is called", () => {
       const { dispatch, getState } = setupStore([]);
 
       let packet = clonePacket(stubPackets.get("GET request"));
       let updatePacket = clonePacket(stubPackets.get("GET request update"));
 
       packet.actor = "message1";
       updatePacket.networkInfo.actor = "message1";
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       let networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual(["message1"]);
 
       packet = clonePacket(stubPackets.get("GET request"));
       updatePacket = stubPackets.get("XHR GET request update");
       packet.actor = "message2";
       updatePacket.networkInfo.actor = "message2";
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual(["message1", "message2"]);
     });
 
     it("resets networkMessagesUpdateById in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
@@ -683,121 +711,120 @@ describe("Message reducer:", () => {
         logLimit: 3
       });
 
       // Add 3 network messages and their updates
       let packet = clonePacket(stubPackets.get("XHR GET request"));
       let updatePacket = clonePacket(stubPackets.get("XHR GET request update"));
       packet.actor = "message1";
       updatePacket.networkInfo.actor = "message1";
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       packet.actor = "message2";
       updatePacket.networkInfo.actor = "message2";
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       packet.actor = "message3";
       updatePacket.networkInfo.actor = "message3";
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       // Check that we have the expected data.
-      let messages = getAllMessagesById(getState());
+      let messages = getMutableMessagesById(getState());
       const [
         firstNetworkMessageId,
         secondNetworkMessageId,
         thirdNetworkMessageId
       ] = [...messages.keys()];
 
       let networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual([
         firstNetworkMessageId,
         secondNetworkMessageId,
         thirdNetworkMessageId
       ]);
 
       // This addition will remove the first network message.
       packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual([
         secondNetworkMessageId,
         thirdNetworkMessageId
       ]);
 
       // This addition will remove the second network message.
       packet = stubPackets.get("console.log('foobar', 'test')");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual([
         thirdNetworkMessageId
       ]);
 
       // This addition will remove the last network message.
       packet = stubPackets.get("console.log(undefined)");
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       // networkMessageUpdateById should now be empty.
       networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates)).toEqual([]);
     });
   });
 
   describe("messagesTableDataById", () => {
     it("resets messagesTableDataById in response to MESSAGES_CLEAR action", () => {
       const { dispatch, getState } = setupStore([
         "console.table(['a', 'b', 'c'])"
       ]);
 
-      let messages = getAllMessagesById(getState());
       const data = Symbol("tableData");
-      dispatch(actions.messageTableDataReceive(messages.first().id, data));
+      dispatch(actions.messageTableDataReceive(getFirstMessage(getState()).id, data));
       let table = getAllMessagesTableDataById(getState());
       expect(table.size).toBe(1);
-      expect(table.get(messages.first().id)).toBe(data);
+      expect(table.get(getFirstMessage(getState()).id)).toBe(data);
 
       dispatch(actions.messagesClear());
 
       expect(getAllMessagesTableDataById(getState()).size).toBe(0);
     });
 
     it("cleans the messagesTableDataById property when messages are pruned", () => {
       const { dispatch, getState } = setupStore([], null, {
         logLimit: 2
       });
 
       // Add 2 table message and their data.
-      dispatch(actions.messageAdd(stubPackets.get("console.table(['a', 'b', 'c'])")));
-      dispatch(actions.messageAdd(
-        stubPackets.get("console.table(['red', 'green', 'blue']);")));
+      dispatch(actions.messagesAdd([stubPackets.get("console.table(['a', 'b', 'c'])")]));
+      dispatch(actions.messagesAdd(
+        [stubPackets.get("console.table(['red', 'green', 'blue']);")]));
 
-      let messages = getAllMessagesById(getState());
+      let messages = getMutableMessagesById(getState());
 
       const tableData1 = Symbol();
       const tableData2 = Symbol();
       const [id1, id2] = [...messages.keys()];
       dispatch(actions.messageTableDataReceive(id1, tableData1));
       dispatch(actions.messageTableDataReceive(id2, tableData2));
 
       let table = getAllMessagesTableDataById(getState());
       expect(table.size).toBe(2);
 
       // This addition will remove the first table message.
-      dispatch(actions.messageAdd(stubPackets.get("console.log(undefined)")));
+      dispatch(actions.messagesAdd([stubPackets.get("console.log(undefined)")]));
 
       table = getAllMessagesTableDataById(getState());
       expect(table.size).toBe(1);
       expect(table.get(id2)).toBe(tableData2);
 
       // This addition will remove the second table message.
-      dispatch(actions.messageAdd(stubPackets.get("console.log('foobar', 'test')")));
+      dispatch(actions.messagesAdd([stubPackets.get("console.log('foobar', 'test')")]));
 
       expect(getAllMessagesTableDataById(getState()).size).toBe(0);
     });
   });
 
   describe("messagesAdd", () => {
     it("still log repeated message over logLimit, but only repeated ones", () => {
       // Log two distinct messages
@@ -814,18 +841,18 @@ describe("Message reducer:", () => {
 
       // Repeat ID must be the same even if the timestamp is different.
       packet1.message.timeStamp = 1;
       packet2.message.timeStamp = 2;
       packet3.message.timeStamp = 3;
       dispatch(actions.messagesAdd([packet1, packet2, packet3]));
 
       // There is still only two messages being logged,
-      const messages = getAllMessagesById(getState());
+      const messages = getMutableMessagesById(getState());
       expect(messages.size).toBe(2);
 
       // the second one being repeated 3 times
       const repeat = getAllRepeatById(getState());
-      expect(repeat[messages.first().id]).toBe(3);
-      expect(repeat[messages.last().id]).toBe(undefined);
+      expect(repeat[getFirstMessage(getState()).id]).toBe(3);
+      expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
     });
   });
 });
--- a/devtools/client/webconsole/new-console-output/test/store/network-messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/network-messages.test.js
@@ -29,17 +29,17 @@ describe("Network message reducer:", () 
     getState = store.getState;
     dispatch = store.dispatch;
 
     let packet = clonePacket(stubPackets.get("GET request"));
     let updatePacket = clonePacket(stubPackets.get("GET request update"));
 
     packet.actor = "message1";
     updatePacket.networkInfo.actor = "message1";
-    dispatch(actions.messageAdd(packet));
+    dispatch(actions.messagesAdd([packet]));
     dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
   });
 
   describe("networkMessagesUpdateById", () => {
     it("adds fetched HTTP request headers", () => {
       let headers = {
         headers: []
       };
--- a/devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const {
+  clonePacket,
+  getFirstMessage,
   setupActions,
   setupStore,
-  clonePacket
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
-const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
-const {
-  getAllMessagesById,
-} = require("devtools/client/webconsole/new-console-output/selectors/messages");
-
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const expect = require("expect");
 
 describe("Release actor enhancer:", () => {
   let actions;
 
   before(() => {
     actions = setupActions();
   });
@@ -30,36 +27,35 @@ describe("Release actor enhancer:", () =
         proxy: {
           releaseActor: (actor) => {
             releasedActors.push(actor);
           }
         }
       }, { logLimit });
 
       // Add a log message.
-      dispatch(actions.messageAdd(
-        stubPackets.get("console.log('myarray', ['red', 'green', 'blue'])")));
+      dispatch(actions.messagesAdd([
+        stubPackets.get("console.log('myarray', ['red', 'green', 'blue'])")]));
 
-      let messages = getAllMessagesById(getState());
-      const firstMessage = messages.first();
+      const firstMessage = getFirstMessage(getState());
       const firstMessageActor = firstMessage.parameters[1].actor;
 
       // Add an evaluation result message (see Bug 1408321).
       const evaluationResultPacket = stubPackets.get("new Date(0)");
-      dispatch(actions.messageAdd(evaluationResultPacket));
+      dispatch(actions.messagesAdd([evaluationResultPacket]));
       const secondMessageActor = evaluationResultPacket.result.actor;
 
       const logCount = logLimit + 1;
       const packet = clonePacket(stubPackets.get(
         "console.assert(false, {message: 'foobar'})"));
       const thirdMessageActor = packet.message.arguments[0].actor;
 
       for (let i = 1; i <= logCount; i++) {
         packet.message.arguments.push(`message num ${i}`);
-        dispatch(actions.messageAdd(packet));
+        dispatch(actions.messagesAdd([packet]));
       }
 
       expect(releasedActors.length).toBe(3);
       expect(releasedActors).toInclude(firstMessageActor);
       expect(releasedActors).toInclude(secondMessageActor);
       expect(releasedActors).toInclude(thirdMessageActor);
     });
 
@@ -69,36 +65,35 @@ describe("Release actor enhancer:", () =
         proxy: {
           releaseActor: (actor) => {
             releasedActors.push(actor);
           }
         }
       });
 
       // Add a log message.
-      dispatch(actions.messageAdd(
-        stubPackets.get("console.log('myarray', ['red', 'green', 'blue'])")));
+      dispatch(actions.messagesAdd([
+        stubPackets.get("console.log('myarray', ['red', 'green', 'blue'])")]));
 
-      let messages = getAllMessagesById(getState());
-      const firstMessage = messages.first();
+      const firstMessage = getFirstMessage(getState());
       const firstMessageActor = firstMessage.parameters[1].actor;
 
       const packet = clonePacket(stubPackets.get(
         "console.assert(false, {message: 'foobar'})"));
       const secondMessageActor = packet.message.arguments[0].actor;
-      dispatch(actions.messageAdd(packet));
+      dispatch(actions.messagesAdd([packet]));
 
       // Add an evaluation result message (see Bug 1408321).
       const evaluationResultPacket = stubPackets.get("new Date(0)");
-      dispatch(actions.messageAdd(evaluationResultPacket));
+      dispatch(actions.messagesAdd([evaluationResultPacket]));
       const thirdMessageActor = evaluationResultPacket.result.actor;
 
       // Add a message with a long string messageText property.
       const longStringPacket = stubPackets.get("TypeError longString message");
-      dispatch(actions.messageAdd(longStringPacket));
+      dispatch(actions.messagesAdd([longStringPacket]));
       const fourthMessageActor = longStringPacket.pageError.errorMessage.actor;
 
       // Kick-off the actor release.
       dispatch(actions.messagesClear());
 
       expect(releasedActors.length).toBe(4);
       expect(releasedActors).toInclude(firstMessageActor);
       expect(releasedActors).toInclude(secondMessageActor);