/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { getAllMessagesUiById, getAllCssMessagesMatchingElements, getAllNetworkMessagesUpdateById, getAllRepeatById, getAllDisabledMessagesById, getCurrentGroup, getGroupsById, getMutableMessagesById, getVisibleMessages, } = require("resource://devtools/client/webconsole/selectors/messages.js"); const { clonePacket, getFirstMessage, getLastMessage, getMessageAt, setupActions, setupStore, } = require("resource://devtools/client/webconsole/test/node/helpers.js"); const { stubPackets, stubPreparedMessages, } = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js"); const { MESSAGE_TYPE, CSS_MESSAGE_ADD_MATCHING_ELEMENTS, } = require("resource://devtools/client/webconsole/constants.js"); const { createWarningGroupMessage, } = require("resource://devtools/client/webconsole/utils/messages.js"); const expect = require("expect"); describe("Message reducer:", () => { let actions; beforeAll(() => { actions = setupActions(); }); describe("mutableMessagesById", () => { it("adds a message to an empty store", () => { const { dispatch, getState } = setupStore(); const packet = stubPackets.get("console.log('foobar', 'test')"); dispatch(actions.messagesAdd([packet])); const message = stubPreparedMessages.get("console.log('foobar', 'test')"); 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], { actions }); const packet = clonePacket(stubPackets.get(key1)); const packet2 = clonePacket(packet); // Repeat ID must be the same even if the timestamp is different. packet.message.timeStamp = 1; packet2.message.timeStamp = 2; dispatch(actions.messagesAdd([packet, packet2])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(1); const repeat = getAllRepeatById(getState()); expect(repeat[getFirstMessage(getState()).id]).toBe(4); }); it("doesn't increment repeat on same log message with different locations", () => { const key1 = "console.log('foobar', 'test')"; const { dispatch, getState } = setupStore(); const packet = clonePacket(stubPackets.get(key1)); // Dispatch original packet. dispatch(actions.messagesAdd([packet])); // Dispatch same packet with modified column number. packet.message.columnNumber = packet.message.columnNumber + 1; dispatch(actions.messagesAdd([packet])); // Dispatch same packet with modified line number. packet.message.lineNumber = packet.message.lineNumber + 1; dispatch(actions.messagesAdd([packet])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(3); const repeat = getAllRepeatById(getState()); expect(Object.keys(repeat).length).toBe(0); }); 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.messagesAdd([packet])); packet.pageError.timeStamp = 2; dispatch(actions.messagesAdd([packet])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(1); const repeat = getAllRepeatById(getState()); expect(repeat[getFirstMessage(getState()).id]).toBe(4); }); it("doesn't increment repeat on same css message with different locations", () => { const key1 = "Unknown property ‘such-unknown-property’. Declaration dropped."; const { dispatch, getState } = setupStore(); const packet = clonePacket(stubPackets.get(key1)); // Dispatch original packet. dispatch(actions.messagesAdd([packet])); // Dispatch same packet with modified column number. packet.pageError.columnNumber = packet.pageError.columnNumber + 1; dispatch(actions.messagesAdd([packet])); // Dispatch same packet with modified line number. packet.pageError.lineNumber = packet.pageError.lineNumber + 1; dispatch(actions.messagesAdd([packet])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(3); const repeat = getAllRepeatById(getState()); expect(Object.keys(repeat).length).toBe(0); }); 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.messagesAdd([packet])); packet.pageError.timeStamp = 2; dispatch(actions.messagesAdd([packet])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(1); const repeat = getAllRepeatById(getState()); 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 = getMutableMessagesById(getState()); expect(messages.size).toBe(4); const repeat = getAllRepeatById(getState()); expect(repeat[getFirstMessage(getState()).id]).toBe(2); expect(repeat[getMessageAt(getState(), 2).id]).toBe(3); expect(repeat[getLastMessage(getState()).id]).toBe(undefined); }); it("doesn't increment undefined messages coming from different places", () => { const { getState } = setupStore(["console.log(undefined)", "undefined"]); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); const repeat = getAllRepeatById(getState()); expect(Object.keys(repeat).length).toBe(0); }); it("doesn't increment successive falsy but different messages", () => { const { getState } = setupStore( ["console.log(NaN)", "console.log(undefined)", "console.log(null)"], { actions } ); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(3); const repeat = getAllRepeatById(getState()); expect(Object.keys(repeat).length).toBe(0); }); it("increment falsy messages when expected", () => { const { dispatch, getState } = setupStore(); const nanPacket = stubPackets.get("console.log(NaN)"); dispatch(actions.messagesAdd([nanPacket, nanPacket])); let messages = getMutableMessagesById(getState()); expect(messages.size).toBe(1); let repeat = getAllRepeatById(getState()); expect(repeat[getLastMessage(getState()).id]).toBe(2); const undefinedPacket = stubPackets.get("console.log(undefined)"); dispatch(actions.messagesAdd([undefinedPacket, undefinedPacket])); messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); repeat = getAllRepeatById(getState()); expect(repeat[getLastMessage(getState()).id]).toBe(2); const nullPacket = stubPackets.get("console.log(null)"); dispatch(actions.messagesAdd([nullPacket, nullPacket])); messages = getMutableMessagesById(getState()); expect(messages.size).toBe(3); repeat = getAllRepeatById(getState()); expect(repeat[getLastMessage(getState()).id]).toBe(2); }); 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.messagesAdd([packet])); const packet2 = stubPackets.get("console.log(undefined)"); dispatch(actions.messagesAdd([packet2])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); const repeat = getAllRepeatById(getState()); expect(repeat[getFirstMessage(getState()).id]).toBe(3); expect(repeat[getLastMessage(getState()).id]).toBe(undefined); }); it("does not increment repeat after adding similar warning group", () => { const { dispatch, getState } = setupStore(); // Mocking a warning message that would create a warning group const warningMessage = stubPreparedMessages.get( "ReferenceError: asdf is not defined" ); warningMessage.messageText = "The resource at “https://evil.com” was blocked."; warningMessage.category = "cookieBlockedPermission"; const type = MESSAGE_TYPE.CONTENT_BLOCKING_GROUP; const firstMessageId = `${warningMessage.type}-${warningMessage.innerWindowID}`; const message1 = createWarningGroupMessage( firstMessageId, type, warningMessage ); const secondMessageId = `${warningMessage.type}-${ warningMessage.innerWindowID + 10 }`; const message2 = createWarningGroupMessage( secondMessageId, type, warningMessage ); dispatch(actions.messagesAdd([message1, message2])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); const repeat = getAllRepeatById(getState()); expect(Object.keys(repeat).length).toBe(0); }); it("adds a message in response to console.clear()", () => { const { dispatch, getState } = setupStore([]); dispatch(actions.messagesAdd([stubPackets.get("console.clear()")])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(1); 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(getMutableMessagesById(state).size).toBe(0); expect(getVisibleMessages(state).length).toBe(0); expect(getAllMessagesUiById(state).length).toBe(0); expect(getGroupsById(state).size).toBe(0); expect(getAllCssMessagesMatchingElements(state).size).toBe(0); expect(getCurrentGroup(state)).toBe(null); expect(getAllRepeatById(state)).toEqual({}); expect(state.messages.mutableMessagesOrder).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)", ], { actions, storeOptions: { logLimit: 2, }, } ); // Check that we have the expected data. let repeats = getAllRepeatById(getState()); expect(Object.keys(repeats).length).toBe(2); 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.messagesAdd([packet])); 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.messagesAdd([packet])); // repeatById should now be empty. expect(getAllRepeatById(getState())).toEqual({}); }); it("properly limits number of messages", () => { const logLimit = 1000; const { dispatch, getState } = setupStore([], { storeOptions: { logLimit, }, }); const packet = clonePacket(stubPackets.get("console.log(undefined)")); for (let i = 1; i <= logLimit + 2; i++) { packet.message.arguments = [`message num ${i}`]; dispatch(actions.messagesAdd([packet])); } 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}` ); const { mutableMessagesOrder } = getState().messages; expect(mutableMessagesOrder.length).toBe(logLimit); }); it("properly limits number of messages when there are nested groups", () => { const logLimit = 1000; const { dispatch, getState } = setupStore([], { storeOptions: { logLimit, }, }); 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.messagesAdd([packetGroup])); packetGroup.message.arguments = [`group-1-1`]; dispatch(actions.messagesAdd([packetGroup])); packetGroup.message.arguments = [`group-1-1-1`]; dispatch(actions.messagesAdd([packetGroup])); packet.message.arguments = [`message-in-group-1`]; dispatch(actions.messagesAdd([packet])); packet.message.arguments = [`message-in-group-2`]; dispatch(actions.messagesAdd([packet])); // Closing group-1-1-1 dispatch(actions.messagesAdd([packetGroupEnd])); // Closing group-1-1 dispatch(actions.messagesAdd([packetGroupEnd])); // Closing group-1 dispatch(actions.messagesAdd([packetGroupEnd])); for (let i = 0; i < logLimit; i++) { packet.message.arguments = [`message-${i}`]; dispatch(actions.messagesAdd([packet])); } const visibleMessages = getVisibleMessages(getState()); const messages = getMutableMessagesById(getState()); const { mutableMessagesOrder } = getState().messages; expect(messages.size).toBe(logLimit); expect(visibleMessages.length).toBe(logLimit); expect(mutableMessagesOrder.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 = getGroupsById(getState()); expect(groups.size).toBe(0); }); it("properly limits number of groups", () => { const logLimit = 100; const { dispatch, getState } = setupStore([], { storeOptions: { 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.messagesAdd([packetGroup])); packet.message.arguments = [`message-${i}-a`]; dispatch(actions.messagesAdd([packet])); packet.message.arguments = [`message-${i}-b`]; dispatch(actions.messagesAdd([packet])); dispatch(actions.messagesAdd([packetGroupEnd])); } const visibleMessages = getVisibleMessages(getState()); const messages = getMutableMessagesById(getState()); // We should have three times the logLimit since each group has one message inside. expect(messages.size).toBe(logLimit * 3); // We should have logLimit number of groups const groups = getGroupsById(getState()); expect(groups.size).toBe(logLimit); expect(messages.get(visibleMessages[1]).parameters[0]).toBe( `message-2-a` ); expect(getLastMessage(getState()).parameters[0]).toBe( `message-${logLimit + 1}-b` ); }); it("properly limits number of collapsed groups", () => { const logLimit = 100; const { dispatch, getState } = setupStore([], { storeOptions: { 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.messagesAdd([packetGroupCollapsed])); packet.message.arguments = [`message-${i}-a`]; dispatch(actions.messagesAdd([packet])); packet.message.arguments = [`message-${i}-b`]; dispatch(actions.messagesAdd([packet])); dispatch(actions.messagesAdd([packetGroupEnd])); } 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 = getGroupsById(getState()); expect(groups.size).toBe(logLimit); 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.messagesAdd([message])); 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.messagesAdd([packet])); 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.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 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.messagesAdd([ stubPackets.get("console.group('bar')"), stubPackets.get("console.log('foobar', 'test')"), ]) ); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); 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.messagesAdd([message])); const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(0); }); it("filters out message added after a console.groupCollapsed message", () => { const { dispatch, getState } = setupStore(); 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.messagesAdd([packet])); const dirxmlMessage = getLastMessage(getState()); expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG); }); it("does not throw when adding incomplete console.count packet", () => { const { dispatch, getState } = setupStore(); const packet = clonePacket(stubPackets.get(`console.count('bar')`)); // Remove counter information to mimick packet we receive in the browser console. delete packet.message.counter; dispatch(actions.messagesAdd([packet])); // The message should not be added to the state. expect(getMutableMessagesById(getState()).size).toBe(0); }); }); describe("mutableMessagesOrder", () => { it("adds a message to an empty store", () => { const { dispatch, getState } = setupStore(); const packet = stubPackets.get("console.log('foobar', 'test')"); dispatch(actions.messagesAdd([packet])); const { mutableMessagesOrder } = getState().messages; expect(mutableMessagesOrder.length).toBe(1); expect(mutableMessagesOrder[0]).toBe( // Don't get getMessageIndexAt/getFirstMessage since it relies on mutableMessagesOrder [...getMutableMessagesById(getState()).keys()][0] ); }); it("reorder messages", () => { const { dispatch, getState } = setupStore(); const naNpacket = stubPackets.get("console.log(NaN)"); dispatch(actions.messagesAdd([naNpacket])); // Add a message that has a shorter timestamp than the previous one, and thus, should // be displayed before const nullPacket = clonePacket(stubPackets.get("console.log(null)")); nullPacket.message.timeStamp = naNpacket.message.timeStamp - 10; dispatch(actions.messagesAdd([nullPacket])); // Add a message that should be display between the 2 previous messages const undefinedPacket = clonePacket( stubPackets.get("console.log(undefined)") ); undefinedPacket.message.timeStamp = naNpacket.message.timeStamp - 5; dispatch(actions.messagesAdd([undefinedPacket])); const { mutableMessagesOrder } = getState().messages; const [nanMessage, nullMessage, undefinedMessage] = [ ...getMutableMessagesById(getState()).values(), ]; const visibleMessages = getVisibleMessages(getState()); // Checking that messages in the Map are the expected ones expect(nanMessage.parameters[0].type).toBe("NaN"); expect(nullMessage.parameters[0].type).toBe("null"); expect(undefinedMessage.parameters[0].type).toBe("undefined"); // Check that mutableMessagesOrder has the message ids in the chronological order expect(mutableMessagesOrder).toEqual([ nullMessage.id, undefinedMessage.id, nanMessage.id, ]); // Since we didn't filtered anything, visibleMessages should be similar to mutableMessagesOrder expect(mutableMessagesOrder).toEqual(visibleMessages); // Check that visibleMessages is computed from mutableMessagesOrder when filtering dispatch(actions.filterToggle("log")); expect(getVisibleMessages(getState())).toEqual([]); dispatch(actions.filterToggle("log")); expect(getVisibleMessages(getState())).toEqual([ nullMessage.id, undefinedMessage.id, nanMessage.id, ]); }); }); describe("expandedMessageIds", () => { it("opens console.trace messages when they are added", () => { const { dispatch, getState } = setupStore(); const message = stubPackets.get("console.trace()"); dispatch(actions.messagesAdd([message])); const expanded = getAllMessagesUiById(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.messagesAdd([traceMessage])); dispatch(actions.messagesClear()); const expanded = getAllMessagesUiById(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()"], { storeOptions: { logLimit: 3, }, } ); // Check that we have the expected data. let expanded = getAllMessagesUiById(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.messagesAdd([packet])); expanded = getAllMessagesUiById(getState()); // There should be only the id of the last console.trace message. 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.messagesAdd([packet])); packet = stubPackets.get("console.log(undefined)"); dispatch(actions.messagesAdd([packet])); // expandedMessageIds should now be empty. expect(getAllMessagesUiById(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.messagesAdd([message])); const expanded = getAllMessagesUiById(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.messagesAdd([message])); const expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(0); }); it("reacts to messageClose/messageOpen actions on console.group", () => { const { dispatch, getState } = setupStore(["console.group('bar')"]); const firstMessageId = getFirstMessage(getState()).id; let expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(1); expect(expanded[0]).toBe(firstMessageId); dispatch(actions.messageClose(firstMessageId)); expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(0); dispatch(actions.messageOpen(firstMessageId)); expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(1); expect(expanded[0]).toBe(firstMessageId); }); it("reacts to messageClose/messageOpen actions on exception", () => { const { dispatch, getState } = setupStore([ "ReferenceError: asdf is not defined", ]); const firstMessageId = getFirstMessage(getState()).id; let expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(0); dispatch(actions.messageOpen(firstMessageId)); expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(1); expect(expanded[0]).toBe(firstMessageId); dispatch(actions.messageClose(firstMessageId)); expanded = getAllMessagesUiById(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.messagesAdd([packet])); const currentGroup = getCurrentGroup(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')", "console.group('bar')", "console.groupEnd('bar')", ]); let currentGroup = getCurrentGroup(getState()); expect(currentGroup).toBe(getMessageAt(getState(), 1).id); const endFooPacket = stubPackets.get("console.groupEnd('foo')"); dispatch(actions.messagesAdd([endFooPacket])); currentGroup = getCurrentGroup(getState()); expect(currentGroup).toBe(getFirstMessage(getState()).id); const endBarPacket = stubPackets.get("console.groupEnd('bar')"); dispatch(actions.messagesAdd([endBarPacket])); currentGroup = getCurrentGroup(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()); 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.messagesAdd([barPacket])); let groupsById = getGroupsById(getState()); expect(groupsById.size).toBe(1); expect(groupsById.has(getFirstMessage(getState()).id)).toBe(true); expect(groupsById.get(getFirstMessage(getState()).id)).toEqual([]); const fooPacket = stubPackets.get("console.groupCollapsed('foo')"); dispatch(actions.messagesAdd([fooPacket])); groupsById = getGroupsById(getState()); expect(groupsById.size).toBe(2); 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 = getGroupsById(getState()); expect(groupsById.size).toBe(2); dispatch(actions.messagesClear()); groupsById = getGroupsById(getState()); expect(groupsById.size).toBe(0); }); it("cleans the groupsById property when messages are pruned", () => { const { dispatch, getState } = setupStore( [ "console.group('bar')", "console.group()", "console.groupEnd()", "console.groupEnd('bar')", "console.group('bar')", "console.groupEnd('bar')", "console.log('foobar', 'test')", ], { actions, storeOptions: { logLimit: 3, }, } ); /* * Here is the initial state of the console: * ▶︎ bar * ▶︎ noLabel * ▶︎ bar * foobar test */ // Check that we have the expected data. let groupsById = getGroupsById(getState()); expect(groupsById.size).toBe(3); // This addition will prune the first group (and its child group) out of the store. /* * ▶︎ bar * foobar test * undefined */ let packet = stubPackets.get("console.log(undefined)"); dispatch(actions.messagesAdd([packet])); groupsById = getGroupsById(getState()); // There should be only the id of the last console.group message. expect(groupsById.size).toBe(1); // This additions will prune the last group message out of the store. /* * foobar test * undefined * foobar test */ packet = stubPackets.get("console.log('foobar', 'test')"); dispatch(actions.messagesAdd([packet])); // groupsById should now be empty. expect(getGroupsById(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.actor = "message1"; dispatch(actions.messagesAdd([packet])); dispatch(actions.networkMessageUpdates([updatePacket], null)); 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.actor = "message2"; dispatch(actions.messagesAdd([packet])); dispatch(actions.networkMessageUpdates([updatePacket], null)); networkUpdates = getAllNetworkMessagesUpdateById(getState()); expect(Object.keys(networkUpdates)).toEqual(["message1", "message2"]); }); it("resets networkMessagesUpdateById in response to MESSAGES_CLEAR action", () => { const { dispatch, getState } = setupStore(["XHR GET request"]); const updatePacket = stubPackets.get("XHR GET request update"); dispatch(actions.networkMessageUpdates([updatePacket], null)); let networkUpdates = getAllNetworkMessagesUpdateById(getState()); expect(!!Object.keys(networkUpdates).length).toBe(true); dispatch(actions.messagesClear()); networkUpdates = getAllNetworkMessagesUpdateById(getState()); expect(Object.keys(networkUpdates).length).toBe(0); }); it("cleans the networkMessagesUpdateById property when messages are pruned", () => { const { dispatch, getState } = setupStore([], { storeOptions: { logLimit: 3, }, }); // Add 3 network messages and their updates let packet = clonePacket(stubPackets.get("XHR GET request")); const updatePacket = clonePacket( stubPackets.get("XHR GET request update") ); packet.actor = "message1"; updatePacket.actor = "message1"; dispatch(actions.messagesAdd([packet])); dispatch(actions.networkMessageUpdates([updatePacket], null)); packet.actor = "message2"; updatePacket.actor = "message2"; dispatch(actions.messagesAdd([packet])); dispatch(actions.networkMessageUpdates([updatePacket], null)); packet.actor = "message3"; updatePacket.actor = "message3"; dispatch(actions.messagesAdd([packet])); dispatch(actions.networkMessageUpdates([updatePacket], null)); // Check that we have the expected data. const 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.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.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.messagesAdd([packet])); // networkMessageUpdateById should now be empty. networkUpdates = getAllNetworkMessagesUpdateById(getState()); expect(Object.keys(networkUpdates)).toEqual([]); }); }); describe("cssMessagesMatchingElements", () => { it("resets cssMessagesMatchingElements in response to MESSAGES_CLEAR action", () => { const { dispatch, getState } = setupStore([ `Unknown property ‘such-unknown-property’. Declaration dropped.`, ]); const data = Symbol(); dispatch({ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS, id: getFirstMessage(getState()).id, elements: data, }); const matchingElements = getAllCssMessagesMatchingElements(getState()); expect(matchingElements.size).toBe(1); expect(matchingElements.get(getFirstMessage(getState()).id)).toBe(data); dispatch(actions.messagesClear()); expect(getAllCssMessagesMatchingElements(getState()).size).toBe(0); }); it("cleans the cssMessagesMatchingElements property when messages are pruned", () => { const { dispatch, getState } = setupStore([], { storeOptions: { logLimit: 2, }, }); // Add 2 css warnings message and their associated data. dispatch( actions.messagesAdd([ stubPackets.get( `Unknown property ‘such-unknown-property’. Declaration dropped.` ), ]) ); dispatch( actions.messagesAdd([ stubPackets.get( `Error in parsing value for ‘padding-top’. Declaration dropped.` ), ]) ); const messages = getMutableMessagesById(getState()); const data1 = Symbol(); const data2 = Symbol(); const [id1, id2] = [...messages.keys()]; dispatch({ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS, id: id1, elements: data1, }); dispatch({ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS, id: id2, elements: data2, }); let matchingElements = getAllCssMessagesMatchingElements(getState()); expect(matchingElements.size).toBe(2); // This addition will remove the first css warning. dispatch( actions.messagesAdd([stubPackets.get("console.log(undefined)")]) ); matchingElements = getAllCssMessagesMatchingElements(getState()); expect(matchingElements.size).toBe(1); expect(matchingElements.get(id2)).toBe(data2); // This addition will remove the second css warning. dispatch( actions.messagesAdd([stubPackets.get("console.log('foobar', 'test')")]) ); expect(getAllCssMessagesMatchingElements(getState()).size).toBe(0); }); }); describe("messagesAdd", () => { it("still log repeated message over logLimit, but only repeated ones", () => { // Log two distinct messages const key1 = "console.log('foobar', 'test')"; const key2 = "console.log(undefined)"; const { dispatch, getState } = setupStore([key1, key2], { storeOptions: { logLimit: 2, }, }); // Then repeat the last one two times and log the first one again const packet1 = clonePacket(stubPackets.get(key2)); const packet2 = clonePacket(stubPackets.get(key2)); const packet3 = clonePacket(stubPackets.get(key1)); // Repeat ID must be the same even if the timestamp is different. packet1.message.timeStamp = packet1.message.timeStamp + 1; packet2.message.timeStamp = packet2.message.timeStamp + 2; packet3.message.timeStamp = packet3.message.timeStamp + 3; dispatch(actions.messagesAdd([packet1, packet2, packet3])); // There is still only two messages being logged, const messages = getMutableMessagesById(getState()); expect(messages.size).toBe(2); // the second one being repeated 3 times const repeat = getAllRepeatById(getState()); expect(repeat[getFirstMessage(getState()).id]).toBe(3); expect(repeat[getLastMessage(getState()).id]).toBe(undefined); }); }); describe("messageRemove", () => { it("removes the message from the store", () => { const { dispatch, getState } = setupStore([ "console.trace()", "console.log(undefined)", "console.trace()", "console.log(undefined)", ]); let expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(2); const secondTraceMessage = getMessageAt(getState(), 2); dispatch(actions.messageRemove(secondTraceMessage.id)); const messages = getMutableMessagesById(getState()); const { mutableMessagesOrder } = getState().messages; // The messages was removed expect(messages.size).toBe(3); expect(mutableMessagesOrder.length).toBe(3); // Its id was removed from the messagesUI property as well expanded = getAllMessagesUiById(getState()); expect(expanded.length).toBe(1); expect(expanded.includes(secondTraceMessage.id)).toBeFalsy(); }); }); describe("disabledMessagesById", () => { it("adds messages ids to disabledMessagesById when message disable action is called", () => { const { dispatch, getState } = setupStore(); dispatch(actions.messagesDisable(["message1", "message2"])); const disabledMessages = getAllDisabledMessagesById(getState()); expect(disabledMessages).toEqual(["message1", "message2"]); }); it("clears disabledMessagesById in response to MESSAGES_CLEAR action", () => { const { dispatch, getState } = setupStore(); dispatch(actions.messagesDisable(["message1", "message2"])); let disabledMessages = getAllDisabledMessagesById(getState()); expect(disabledMessages).toEqual(["message1", "message2"]); dispatch(actions.messagesClear()); disabledMessages = getAllDisabledMessagesById(getState()); expect(disabledMessages).toEqual([]); }); it("remove message id from disabledMessagesById when the message is removed", () => { const { dispatch, getState } = setupStore( [ "console.log('foobar', 'test')", "XHR GET request update", "console.log(undefined)", ], { actions, storeOptions: { logLimit: 3, }, } ); // This is `console.log('foobar', 'test'` const firstMessageId = getMessageAt(getState(), 0).id; // This is for `XHR GET request update` const secondMessageId = getMessageAt(getState(), 1).id; dispatch(actions.messagesDisable([firstMessageId, secondMessageId])); let disabledMessages = getAllDisabledMessagesById(getState()); expect(disabledMessages).toEqual([firstMessageId, secondMessageId]); // Adding a new message should prune the first(oldest) message and should // remove its id from the disabled messages list. const packet = stubPackets.get("GET request"); dispatch(actions.messagesAdd([packet])); disabledMessages = getAllDisabledMessagesById(getState()); expect(disabledMessages).toEqual([secondMessageId]); }); }); });