summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/test/node/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/test/node/components')
-rw-r--r--devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js60
-rw-r--r--devtools/client/webconsole/test/node/components/console-api-call.test.js695
-rw-r--r--devtools/client/webconsole/test/node/components/console-output.test.js52
-rw-r--r--devtools/client/webconsole/test/node/components/css-warning.test.js125
-rw-r--r--devtools/client/webconsole/test/node/components/eager-evaluation.test.js154
-rw-r--r--devtools/client/webconsole/test/node/components/evaluation-result.test.js505
-rw-r--r--devtools/client/webconsole/test/node/components/filter-bar.test.js226
-rw-r--r--devtools/client/webconsole/test/node/components/filter-button.test.js43
-rw-r--r--devtools/client/webconsole/test/node/components/filter-checkbox.test.js39
-rw-r--r--devtools/client/webconsole/test/node/components/message-container.test.js70
-rw-r--r--devtools/client/webconsole/test/node/components/message-icon.test.js47
-rw-r--r--devtools/client/webconsole/test/node/components/message-location.test.js72
-rw-r--r--devtools/client/webconsole/test/node/components/message-repeat.test.js20
-rw-r--r--devtools/client/webconsole/test/node/components/message-types-aria.test.js59
-rw-r--r--devtools/client/webconsole/test/node/components/network-event-message.test.js154
-rw-r--r--devtools/client/webconsole/test/node/components/page-error.test.js661
-rw-r--r--devtools/client/webconsole/test/node/components/warning-group.test.js107
-rw-r--r--devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js143
18 files changed, 3232 insertions, 0 deletions
diff --git a/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js b/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js
new file mode 100644
index 0000000000..afc55c211c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("ConsoleAPICall component for platform message", () => {
+ describe("Services.console.logStringMessage", () => {
+ it("renders logMessage grips", () => {
+ const message = stubPreparedMessages.get("platform-simple-message");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("foobar test");
+
+ // There should not be the location
+ expect(wrapper.find(".message-location").text()).toBe("");
+ });
+
+ it("renders longString logMessage grips", () => {
+ const message = stubPreparedMessages.get("platform-longString-message");
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toInclude(
+ `a\n${"a".repeat(100)}`
+ );
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/console-api-call.test.js b/devtools/client/webconsole/test/node/components/console-api-call.test.js
new file mode 100644
index 0000000000..7e10002f5c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-api-call.test.js
@@ -0,0 +1,695 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+const {
+ prepareMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("ConsoleAPICall component:", () => {
+ describe("console.log", () => {
+ it("renders string grips", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("foobar test");
+ expect(wrapper.find(".objectBox-string").length).toBe(2);
+ const selector =
+ "div.message.cm-s-mozilla span span.message-flex-body " +
+ "span.message-body.devtools-monospace";
+ expect(wrapper.find(selector).length).toBe(1);
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+ });
+
+ it("renders string grips with custom style", () => {
+ const message = stubPreparedMessages.get("console.log(%cfoobar)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("foobar");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ expect(secondElementStyle["line-height"]).toBe("1.5");
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("renders string grips with data-url background", () => {
+ const message = stubPreparedMessages.get("console.log(%cfoobar)");
+
+ const dataURL =
+ "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAAXNSR0IArs4c6QAAAaZJREFUKBV9UjFLQlEUPueaaUUEDZESNTYI9RfK5oimIGxJcy4IodWxpeZKiHBwc6ghIiIaWqNB/ANiBSU6vHfvU+89nSPpINKB996993zfPef7zkP4CyLCzl02Y7VNg4aE8y2QoYrTVJg+ublCROpjURb0ko11mt2i05BkEDjt+CGgvzUYehrvqtTUefFD8KpXoemK1ich1MDALppIPITROARqlwzWJKdbtihYIWH7dv/AenRBAdYmOriKmUJDEv1oHaVnOy39bn27wJjsfLl0qawHaWkFNOWGCUKcOSs0uM0c6wPyWC+H4k2Ce+b+w89yMDKC0LPzWadgORTJxh8YM5ITIdUmw4b5jt9MssaJrdD9DtZGMvjQ+zEbvUoBVgWj2K2CWGsDeyqih4n1zeyk1S4vlcDCteRRbGwe7z2yO0lOiOU5YA3SklTgYee5/WVw1IVoZGnxrVTv+e4dpmIyB74xSayPBQ8GS5qvZgIRjPFfUQlHQ+s9kpSUil9bOxl2U0aQIO0Mf6tA6hoi4Xsw7QfGsHv4OiAJ8b/4XNmeC9pYRgTvF+HgISP3T9PvAAAAAElFTkSuQmCC)";
+
+ message.userProvidedStyles[0] = `background-image: ${dataURL}`;
+
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ const elements = wrapper.find(".objectBox-string");
+ const firstElementStyle = elements.eq(0).prop("style");
+
+ // data-url background applied
+ expect(firstElementStyle["background-image"]).toBe(dataURL);
+ });
+
+ it("renders custom styled logs with empty style as expected", () => {
+ const message = stubPreparedMessages.get(
+ 'console.log("%cHello%c|%cWorld")'
+ );
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("Hello|World");
+ expect(elements.length).toBe(3);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe("red");
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ expect(secondElementStyle.color).toBe(undefined);
+
+ const thirdElementStyle = elements.eq(2).prop("style");
+ // Allowed styles are applied accordingly on the third element.
+ expect(thirdElementStyle.color).toBe("blue");
+ });
+
+ it("renders prefixed messages", () => {
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ const stub = {
+ ...packet,
+ message: {
+ ...packet.message,
+ prefix: "MyNicePrefix",
+ },
+ };
+
+ const wrapper = render(
+ ConsoleApiCall({
+ message: prepareMessage(stub, { getNextId: () => "p" }),
+ serviceContainer,
+ })
+ );
+ const prefix = wrapper.find(".console-message-prefix");
+ expect(prefix.text()).toBe("MyNicePrefix: ");
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "MyNicePrefix: foobar test"
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+ });
+
+ it("renders repeat node", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ repeat: 107,
+ })
+ );
+
+ expect(wrapper.find(".message-repeats").text()).toBe("107");
+ expect(wrapper.find(".message-repeats").prop("title")).toBe(
+ "107 repeats"
+ );
+
+ const selector =
+ "span > span.message-flex-body > " +
+ "span.message-body.devtools-monospace + span.message-repeats";
+ expect(wrapper.find(selector).length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const indent = 10;
+ let wrapper = render(
+ ConsoleApiCall({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("renders a timestamp when passed a truthy timestampsVisible prop", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ });
+
+ it("does not render a timestamp when not asked to", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+ });
+
+ describe("console.count", () => {
+ it("renders", () => {
+ const messages = [
+ {
+ key: "console.count('bar')",
+ expectedBodyText: "bar: 1",
+ },
+ {
+ key: "console.count | default: 1",
+ expectedBodyText: "default: 1",
+ },
+ {
+ key: "console.count | default: 2",
+ expectedBodyText: "default: 2",
+ },
+ {
+ key: "console.count | test counter: 1",
+ expectedBodyText: "test counter: 1",
+ },
+ {
+ key: "console.count | test counter: 2",
+ expectedBodyText: "test counter: 2",
+ },
+ {
+ key: "console.count | default: 3",
+ expectedBodyText: "default: 3",
+ },
+ {
+ key: "console.count | default: 4",
+ expectedBodyText: "default: 4",
+ },
+ {
+ key: "console.count | test counter: 3",
+ expectedBodyText: "test counter: 3",
+ },
+ {
+ key: "console.countReset | test counter: 0",
+ expectedBodyText: "test counter: 0",
+ },
+ {
+ key: "console.countReset | counterDoesntExist",
+ expectedBodyText: "Counter “test counter” doesn’t exist.",
+ },
+ ];
+
+ for (const { key, expectedBodyText } of messages) {
+ const message = stubPreparedMessages.get(key);
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(expectedBodyText);
+ }
+ });
+ });
+
+ describe("console.assert", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "console.assert(false, {message: 'foobar'})"
+ );
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ 'Assertion failed: Object { message: "foobar" }'
+ );
+ });
+ });
+
+ describe("console.time", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.time('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ it("shows an error if called again", () => {
+ const message = stubPreparedMessages.get("timerAlreadyExists");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” already exists."
+ );
+ });
+ });
+
+ describe("console.timeLog", () => {
+ it("renders as expected", () => {
+ let message = stubPreparedMessages.get("console.timeLog('bar') - 1");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ let wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(message.parameters[0]);
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms$/
+ );
+
+ message = stubPreparedMessages.get("console.timeLog('bar') - 2");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms second call Object \{ state\: 1 \}$/
+ );
+ });
+ it("shows an error if the timer doesn't exist", () => {
+ const message = stubPreparedMessages.get("timeLog.timerDoesntExist");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” doesn’t exist."
+ );
+ });
+ });
+
+ describe("console.timeEnd", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("console.timeEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms - timer ended$/
+ );
+ });
+ it("shows an error if the timer doesn't exist", () => {
+ const message = stubPreparedMessages.get("timeEnd.timerDoesntExist");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” doesn’t exist."
+ );
+ });
+ });
+
+ // Unskip will happen in Bug 1529548.
+ describe.skip("console.trace", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.trace()");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ );
+ const filepath =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/fixtures/stub-generators/" +
+ "test-console-api.html";
+
+ expect(wrapper.find(".message-body").text()).toBe("console.trace()");
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url]`);
+ expect(frameLinks.length).toBe(3);
+
+ expect(
+ frameLinks.eq(0).find(".frame-link-function-display-name").text()
+ ).toBe("testStacktraceFiltering");
+ expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(1).find(".frame-link-function-display-name").text()
+ ).toBe("foo");
+ expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(2).find(".frame-link-function-display-name").text()
+ ).toBe("triggerPacket");
+ expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ // it should not be collapsible.
+ expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+ });
+ it("render with arguments", () => {
+ const message = stubPreparedMessages.get(
+ "console.trace('bar', {'foo': 'bar'}, [1,2,3])"
+ );
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ )
+ );
+
+ const filepath =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/fixtures/stub-generators/test-console-api.html";
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ 'console.trace() bar Object { foo: "bar" } Array(3) [ 1, 2, 3 ]'
+ );
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url]`);
+ expect(frameLinks.length).toBe(3);
+
+ expect(
+ frameLinks.eq(0).find(".frame-link-function-display-name").text()
+ ).toBe("testStacktraceWithLog");
+ expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(1).find(".frame-link-function-display-name").text()
+ ).toBe("foo");
+ expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(2).find(".frame-link-function-display-name").text()
+ ).toBe("triggerPacket");
+ expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ // it should not be collapsible.
+ expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+ });
+ });
+
+ describe("console.group", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.group('bar')");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("bar");
+ expect(wrapper.find(".collapse-button[aria-expanded=true]").length).toBe(
+ 1
+ );
+ });
+
+ it("renders group with custom style", () => {
+ const message = stubPreparedMessages.get("console.group(%cfoo%cbar)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.find(".message-body").text()).toBe("foobar");
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("toggle the group when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("toggle the group when the group name is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".message-flex-body").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".message-flex-body").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("doesn't toggle the group when the location link is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".frame-link-source").simulate("click");
+ const call = store.dispatch.getCall(0);
+ expect(call).toNotExist();
+ });
+ });
+
+ describe("console.groupEnd", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.groupEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ });
+
+ describe("console.groupCollapsed", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.groupCollapsed('foo')");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: false })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("foo");
+ expect(wrapper.find(".collapse-button:not(.expanded)").length).toBe(1);
+ });
+
+ it("renders group with custom style", () => {
+ const message = stubPreparedMessages.get(
+ "console.groupCollapsed(%cfoo%cbaz)"
+ );
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("foobaz");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+ });
+
+ describe("console.dirxml", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dirxml(window)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Window https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html"
+ );
+ });
+ });
+
+ describe("console.dir", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dir({C, M, Y, K})");
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ `Object { cyan: "C", magenta: "M", yellow: "Y", black: "K" }`
+ );
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/console-output.test.js b/devtools/client/webconsole/test/node/components/console-output.test.js
new file mode 100644
index 0000000000..a12c2fbae2
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-output.test.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const ConsoleOutput = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
+);
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+const MESSAGES_NUMBER = 100;
+function getDefaultProps() {
+ const store = setupStore(
+ Array.from({ length: MESSAGES_NUMBER })
+ // Alternate message so we don't trigger the repeat mechanism.
+ .map((_, i) => (i % 2 ? "console.log(null)" : "console.log(NaN)"))
+ );
+
+ return {
+ store,
+ serviceContainer,
+ };
+}
+
+describe("ConsoleOutput component:", () => {
+ it("Render every message", () => {
+ const Services = require("resource://devtools/client/shared/test-helpers/jest-fixtures/Services.js");
+ Services.prefs.setBoolPref("devtools.testing", true);
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const rendered = render(
+ Provider({ store: setupStore() }, ConsoleOutput(getDefaultProps()))
+ );
+
+ Services.prefs.setBoolPref("devtools.testing", false);
+ const visibleMessages = JSON.parse(rendered.prop("data-visible-messages"));
+ expect(visibleMessages.length).toBe(MESSAGES_NUMBER);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/css-warning.test.js b/devtools/client/webconsole/test/node/components/css-warning.test.js
new file mode 100644
index 0000000000..ce15b360e6
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/css-warning.test.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("CSSWarning component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+ const wrapper = render(
+ CSSWarning({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ // There shouldn't be a matched elements label rendered by default.
+ const elementLabel = wrapper.find(`.elements-label`);
+ expect(elementLabel.length).toBe(0);
+
+ // There should be a location.
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-css-message.html:3:27");
+ });
+
+ it("closes an open message when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ CSSWarning({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+ });
+
+ it("opens a closed message when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ CSSWarning({
+ message,
+ open: false,
+ // fake the existence of cssMatchingElements to test just MESSAGE_OPEN action
+ cssMatchingElements: {},
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='false']").simulate("click");
+
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/eager-evaluation.test.js b/devtools/client/webconsole/test/node/components/eager-evaluation.test.js
new file mode 100644
index 0000000000..df4bc0dd09
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/eager-evaluation.test.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const EagerEvaluation = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js")
+);
+
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ SET_TERMINAL_EAGER_RESULT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+function getEagerEvaluation(overrides = {}) {
+ return EagerEvaluation({
+ highlightDomElement: () => {},
+ unHighlightDomElement: () => {},
+ ...overrides,
+ });
+}
+
+describe("EagerEvaluation component:", () => {
+ it("render Date result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: stubPackets.get("new Date(0)").result,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe(
+ "Date Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)"
+ );
+ });
+
+ it("render falsy integer (0) result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: 0,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("0");
+ });
+
+ it("render false result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: false,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("false");
+ });
+
+ it("render empty string result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: "",
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe(`""`);
+ });
+
+ it("render null grip result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: { type: "null" },
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("null");
+ });
+
+ it("render undefined grip result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: { type: "undefined" },
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("undefined");
+ });
+
+ it("do not render null result", () => {
+ // This is not to be confused with a grip describing `null` (which is {type: "null"})
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: null,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(0);
+ expect(wrapper.text()).toBe("");
+ });
+
+ it("do not render undefined result", () => {
+ // This is not to be confused with a grip describing `undefined` (which is {type: "undefined"})
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: undefined,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(0);
+ expect(wrapper.text()).toBe("");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/evaluation-result.test.js b/devtools/client/webconsole/test/node/components/evaluation-result.test.js
new file mode 100644
index 0000000000..9cbca548ed
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/evaluation-result.test.js
@@ -0,0 +1,505 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ formatErrorTextWithCausedBy,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const EvaluationResult = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js")
+);
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("EvaluationResult component:", () => {
+ it.skip("renders a grip result", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Date 1970-01-01T00:00:00.000Z"
+ );
+
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("log")).toBe(true);
+ });
+
+ it("renders an error", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Uncaught ReferenceError: asdf is not defined[Learn More]"
+ );
+
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders an error with a longString exception message", () => {
+ const message = stubPreparedMessages.get("longString message Error");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text.startsWith("Uncaught Error: Long error Long error")).toBe(true);
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown empty string", () => {
+ const message = stubPreparedMessages.get(`eval throw ""`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught <empty string>");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown string", () => {
+ const message = stubPreparedMessages.get(`eval throw "tomato"`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught tomato");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Boolean", () => {
+ const message = stubPreparedMessages.get(`eval throw false`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught false");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Number", () => {
+ const message = stubPreparedMessages.get(`eval throw 0`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown null", () => {
+ const message = stubPreparedMessages.get(`eval throw null`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown undefined", () => {
+ const message = stubPreparedMessages.get(`eval throw undefined`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught undefined");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Symbol", () => {
+ const message = stubPreparedMessages.get(`eval throw Symbol`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe('Uncaught Symbol("potato")');
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Object", () => {
+ const message = stubPreparedMessages.get(`eval throw Object`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Object { vegetable: "cucumber" }`);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error Object", () => {
+ const message = stubPreparedMessages.get(`eval throw Error Object`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught Error: pumpkin");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with custom name`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught JuicyError: pineapple");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an error cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with error cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ "Uncaught Error: something went wrong\nCaused by: SyntaxError: original error"
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with cause chain`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-d",
+ "Caused by: Error: err-c",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a cyclical error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with cyclical cause chain`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-y",
+ "Caused by: Error: err-x",
+ // TODO: it shouldn't be displayed like this. This will
+ // be fixed in Bug 1719605
+ "Caused by: undefined",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a falsy cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with falsy cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: false cause\nCaused by: false");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a null cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with null cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: null cause\nCaused by: null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an undefined cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with undefined cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: undefined cause\nCaused by: undefined");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a number cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with number cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: number cause\nCaused by: 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a string cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with string cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ `Uncaught Error: string cause\nCaused by: "cause message"`
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with object cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with object cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(`Uncaught Error: object cause\nCaused by: Object { … }`);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render pending Promise", () => {
+ const message = stubPreparedMessages.get(`eval pending promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "pending" }`);
+ });
+
+ it("render Promise.resolve result", () => {
+ const message = stubPreparedMessages.get(`eval Promise.resolve`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "fulfilled", <value>: 123 }`);
+ });
+
+ it("render Promise.reject result", () => {
+ const message = stubPreparedMessages.get(`eval Promise.reject`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "rejected", <reason>: "ouch" }`);
+ });
+
+ it("render promise fulfilled in microtask", () => {
+ // See Bug 1439963
+ const message = stubPreparedMessages.get(`eval resolved promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "fulfilled", <value>: 246 }`);
+ });
+
+ it("render promise rejected in microtask", () => {
+ // See Bug 1439963
+ const message = stubPreparedMessages.get(`eval rejected promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Promise { <state>: "rejected", <reason>: ReferenceError }`
+ );
+ });
+
+ it("render rejected promise with Error with cause", () => {
+ const message = stubPreparedMessages.get(`eval rejected promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Promise { <state>: "rejected", <reason>: ReferenceError }`
+ );
+ });
+
+ it("renders an inspect command result", () => {
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("Object { a: 1 }");
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore();
+
+ const message = stubPreparedMessages.get("asdf()");
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(
+ Provider(
+ { store },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ dispatch: () => {},
+ })
+ )
+ );
+
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ const call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+
+ const indent = 10;
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ let wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ )
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("has location information", () => {
+ const message = stubPreparedMessages.get("1 + @");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("debugger eval code:1:4");
+ });
+
+ it("has a timestamp when passed a truthy timestampsVisible prop", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ )
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ )
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-bar.test.js b/devtools/client/webconsole/test/node/components/filter-bar.test.js
new file mode 100644
index 0000000000..15fb026d9f
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-bar.test.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const sinon = require("sinon");
+const { render, mount, shallow } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const FilterButton = require("resource://devtools/client/webconsole/components/FilterBar/FilterButton.js");
+const FilterBar = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js")
+);
+const {
+ FILTERBAR_DISPLAY_MODES,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ MESSAGES_CLEAR,
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ setupStore,
+ clearPrefs,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+function getFilterBar(overrides = {}) {
+ return FilterBar({
+ serviceContainer,
+ attachRefToWebConsoleUI: () => {},
+ webConsoleUI: {
+ document,
+ wrapper: {},
+ },
+ ...overrides,
+ });
+}
+
+describe("FilterBar component:", () => {
+ afterEach(() => {
+ clearPrefs();
+ });
+
+ it("initial render", () => {
+ const store = setupStore();
+
+ const wrapper = render(Provider({ store }, getFilterBar()));
+ const toolbar = wrapper.find(
+ ".devtools-toolbar.webconsole-filterbar-primary"
+ );
+
+ // Clear button
+ const clearButton = toolbar.children().eq(0);
+ expect(clearButton.attr("class")).toBe(
+ "devtools-button devtools-clear-icon"
+ );
+ expect(clearButton.attr("title")).toBe("Clear the Web Console output");
+
+ // Separator
+ expect(toolbar.children().eq(1).attr("class")).toBe("devtools-separator");
+
+ // Text filter
+ const textInput = toolbar.children().eq(2);
+ expect(textInput.attr("class")).toBe("devtools-searchbox");
+
+ // Text filter input
+ const textFilter = textInput.children().eq(0);
+ expect(textFilter.attr("class")).toBe("devtools-filterinput");
+ expect(textFilter.attr("placeholder")).toBe("Filter Output");
+ expect(textFilter.attr("type")).toBe("search");
+ expect(textFilter.attr("value")).toBe("");
+
+ // Text filter input clear button
+ const textFilterClearButton = textInput.children().eq(1);
+ expect(textFilterClearButton.attr("class")).toBe(
+ "devtools-searchinput-clear"
+ );
+
+ // Settings menu icon
+ expect(
+ wrapper.find(".webconsole-console-settings-menu-button").length
+ ).toBe(1);
+ });
+
+ it("displays the number of hidden messages when a search hide messages", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ "console.warn('danger, will robinson!')",
+ "console.debug('debug message');",
+ "console.error('error message');",
+ ]);
+ store.dispatch(actions.filterTextSet("qwerty"));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("5 hidden");
+ expect(message.prop("title")).toBe("5 items hidden by text filter");
+ });
+
+ it("displays the number of hidden messages when a search hide 1 message", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ ]);
+ store.dispatch(actions.filterTextSet("foobar"));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("1 hidden");
+ expect(message.prop("title")).toBe("1 item hidden by text filter");
+ });
+
+ it("displays the expected number of hidden messages when multiple filters", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ "console.warn('danger, will robinson!')",
+ "console.debug('debug message');",
+ "console.error('error message');",
+ ]);
+ store.dispatch(actions.filterTextSet("qwerty"));
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("3 hidden");
+ expect(message.prop("title")).toBe("3 items hidden by text filter");
+ });
+
+ it("does not display the number of hidden messages when there are no messages", () => {
+ const store = setupStore();
+ store.dispatch(actions.filterTextSet("qwerty"));
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const toolbar = wrapper.find(".devtools-searchinput-summary");
+ expect(toolbar.exists()).toBeFalsy();
+ });
+
+ it("Displays a filter buttons bar on its own element in narrow displayMode", () => {
+ const store = setupStore();
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ getFilterBar({
+ displayMode: FILTERBAR_DISPLAY_MODES.NARROW,
+ })
+ )
+ );
+
+ const secondaryBar = wrapper.find(".webconsole-filterbar-secondary");
+ expect(secondaryBar.length).toBe(1);
+
+ // Buttons are displayed
+ const filterBtn = props =>
+ FilterButton(
+ Object.assign(
+ {},
+ {
+ active: true,
+ dispatch: store.dispatch,
+ },
+ props
+ )
+ );
+
+ const buttons = [
+ filterBtn({ label: "Errors", filterKey: FILTERS.ERROR }),
+ filterBtn({ label: "Warnings", filterKey: FILTERS.WARN }),
+ filterBtn({ label: "Logs", filterKey: FILTERS.LOG }),
+ filterBtn({ label: "Info", filterKey: FILTERS.INFO }),
+ filterBtn({ label: "Debug", filterKey: FILTERS.DEBUG }),
+ dom.div({
+ className: "devtools-separator",
+ }),
+ filterBtn({
+ label: "CSS",
+ filterKey: "css",
+ active: false,
+ title:
+ "Stylesheets will be reparsed to check for errors. Refresh the page to also see errors from stylesheets modified from Javascript.",
+ }),
+ filterBtn({ label: "XHR", filterKey: "netxhr", active: false }),
+ filterBtn({ label: "Requests", filterKey: "net", active: false }),
+ ];
+
+ secondaryBar.children().forEach((child, index) => {
+ expect(child.html()).toEqual(shallow(buttons[index]).html());
+ });
+ });
+
+ it("fires MESSAGES_CLEAR action when clear button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+ wrapper.find(".devtools-clear-icon").simulate("click");
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ type: MESSAGES_CLEAR,
+ });
+ });
+
+ it("sets filter text when text is typed", () => {
+ const store = setupStore();
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+ const input = wrapper.find(".devtools-filterinput");
+ input.simulate("change", { target: { value: "a" } });
+ expect(store.getState().filters.text).toBe("a");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-button.test.js b/devtools/client/webconsole/test/node/components/filter-button.test.js
new file mode 100644
index 0000000000..1600f43312
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-button.test.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const FilterButton = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterButton.js")
+);
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+describe("FilterButton component:", () => {
+ const props = {
+ active: true,
+ label: "Error",
+ filterKey: MESSAGE_LEVEL.ERROR,
+ };
+
+ it("displays as active when turned on", () => {
+ const wrapper = render(FilterButton(props));
+ expect(wrapper.is("button")).toBe(true);
+ expect(wrapper.hasClass("devtools-togglebutton")).toBe(true);
+ expect(wrapper.attr("data-category")).toBe("error");
+ expect(wrapper.attr("aria-pressed")).toBe("true");
+ expect(wrapper.text()).toBe("Error");
+ });
+
+ it("displays as inactive when turned off", () => {
+ const wrapper = render(FilterButton({ ...props, active: false }));
+ expect(wrapper.is("button")).toBe(true);
+ expect(wrapper.hasClass("devtools-togglebutton")).toBe(true);
+ expect(wrapper.attr("data-category")).toBe("error");
+ expect(wrapper.attr("aria-pressed")).toBe("false");
+ expect(wrapper.text()).toBe("Error");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-checkbox.test.js b/devtools/client/webconsole/test/node/components/filter-checkbox.test.js
new file mode 100644
index 0000000000..41fc65b712
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-checkbox.test.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const FilterCheckbox = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterCheckbox.js")
+);
+
+describe("FilterCheckbox component:", () => {
+ const props = {
+ label: "test label",
+ title: "test title",
+ checked: true,
+ onChange: () => {},
+ };
+
+ it("displays as checked", () => {
+ const wrapper = render(FilterCheckbox(props));
+ expect(wrapper.is("label")).toBe(true);
+ expect(wrapper.attr("title")).toBe("test title");
+ expect(wrapper.hasClass("filter-checkbox")).toBe(true);
+ expect(wrapper.html()).toBe('<input type="checkbox" checked>test label');
+ });
+
+ it("displays as unchecked", () => {
+ const wrapper = render(FilterCheckbox({ ...props, checked: false }));
+ expect(wrapper.is("label")).toBe(true);
+ expect(wrapper.attr("title")).toBe("test title");
+ expect(wrapper.hasClass("filter-checkbox")).toBe(true);
+ expect(wrapper.html()).toBe('<input type="checkbox">test label');
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-container.test.js b/devtools/client/webconsole/test/node/components/message-container.test.js
new file mode 100644
index 0000000000..303de1692e
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-container.test.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+let {
+ MessageContainer,
+ getMessageComponent,
+} = require("resource://devtools/client/webconsole/components/Output/MessageContainer.js");
+MessageContainer = createFactory(MessageContainer);
+const ConsoleApiCall = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js");
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const EvaluationResult = require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js");
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("MessageContainer component:", () => {
+ it("pipes data to children as expected", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const rendered = render(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer,
+ })
+ );
+
+ expect(rendered.text().includes("foobar")).toBe(true);
+ });
+ it("picks correct child component", () => {
+ const messageTypes = [
+ {
+ component: ConsoleApiCall,
+ message: stubPreparedMessages.get("console.log('foobar', 'test')"),
+ },
+ {
+ component: EvaluationResult,
+ message: stubPreparedMessages.get("new Date(0)"),
+ },
+ {
+ component: PageError,
+ message: stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ ),
+ },
+ {
+ component: CSSWarning,
+ message: stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ ),
+ },
+ ];
+
+ messageTypes.forEach(info => {
+ const { component, message } = info;
+ expect(getMessageComponent(message)).toBe(component);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-icon.test.js b/devtools/client/webconsole/test/node/components/message-icon.test.js
new file mode 100644
index 0000000000..7d4a8a3b23
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-icon.test.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const MessageIcon = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/MessageIcon.js")
+);
+
+describe("MessageIcon component:", () => {
+ it("renders icon based on level", () => {
+ const rendered = render(MessageIcon({ level: MESSAGE_LEVEL.ERROR }));
+ expect(rendered.hasClass("icon")).toBe(true);
+ expect(rendered.attr("title")).toBe("Error");
+ expect(rendered.attr("aria-live")).toBe("off");
+ });
+
+ it("renders logpoint items", () => {
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.LOG,
+ type: "logPoint",
+ })
+ );
+ expect(rendered.hasClass("logpoint")).toBe(true);
+ });
+
+ it("renders icon with custom title", () => {
+ const expectedTitle = "Rendered with custom title";
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.INFO,
+ type: "info",
+ title: expectedTitle,
+ })
+ );
+ expect(rendered.attr("title")).toBe(expectedTitle);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-location.test.js b/devtools/client/webconsole/test/node/components/message-location.test.js
new file mode 100644
index 0000000000..a5bc41c143
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-location.test.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { mount } = require("enzyme");
+const sinon = require("sinon");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+let {
+ MessageContainer,
+} = require("resource://devtools/client/webconsole/components/Output/MessageContainer.js");
+MessageContainer = createFactory(MessageContainer);
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+describe("Message - location element", () => {
+ it("Calls onViewSourceInDebugger when clicked", () => {
+ const onViewSourceInDebugger = sinon.spy();
+ const onViewSource = sinon.spy();
+
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = mount(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer: Object.assign({}, serviceContainer, {
+ onViewSourceInDebugger,
+ onViewSource,
+ }),
+ })
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location a`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+
+ locationLink.simulate("click");
+
+ expect(onViewSourceInDebugger.calledOnce).toBe(true);
+ expect(onViewSource.notCalled).toBe(true);
+ });
+
+ it("Calls onViewSource when clicked and onViewSourceInDebugger undefined", () => {
+ const onViewSource = sinon.spy();
+
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const wrapper = mount(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer: Object.assign({}, serviceContainer, {
+ onViewSource,
+ onViewSourceInDebugger: undefined,
+ }),
+ })
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location a`);
+
+ locationLink.simulate("click");
+ expect(onViewSource.calledOnce).toBe(true);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-repeat.test.js b/devtools/client/webconsole/test/node/components/message-repeat.test.js
new file mode 100644
index 0000000000..bfedbc4ef6
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-repeat.test.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const MessageRepeat = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/MessageRepeat.js")
+);
+
+describe("MessageRepeat component:", () => {
+ it("renders repeated value correctly", () => {
+ const rendered = render(MessageRepeat({ repeat: 99 }));
+ expect(rendered.hasClass("message-repeats")).toBe(true);
+ expect(rendered.text()).toBe("99");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-types-aria.test.js b/devtools/client/webconsole/test/node/components/message-types-aria.test.js
new file mode 100644
index 0000000000..dcabd7a572
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-types-aria.test.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+const ConsoleCmd = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js")
+);
+const EvaluationResult = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js")
+);
+
+const {
+ ConsoleCommand,
+} = require("resource://devtools/client/webconsole/types.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("message types component ARIA:", () => {
+ describe("ConsoleAPICall", () => {
+ it("sets aria-live to polite", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("polite");
+ });
+ });
+
+ describe("EvaluationResult", () => {
+ it("sets aria-live to polite", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("polite");
+ });
+ });
+
+ describe("ConsoleCommand", () => {
+ it("sets aria-live to off", () => {
+ const message = new ConsoleCommand({
+ messageText: `"simple"`,
+ });
+ const wrapper = render(ConsoleCmd({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("off");
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/network-event-message.test.js b/devtools/client/webconsole/test/node/components/network-event-message.test.js
new file mode 100644
index 0000000000..6bdc395544
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/network-event-message.test.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+const NetworkEventMessage = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js")
+);
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+const EXPECTED_URL = "https://example.com/inexistent.html";
+const EXPECTED_STATUS = /\[HTTP\/\d\.\d \d+ [A-Za-z ]+ \d+ms\]/;
+
+describe("NetworkEventMessage component:", () => {
+ describe("GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("GET request");
+ const update = stubPreparedMessages.get("GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ networkMessageUpdate: update,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(0);
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").length).toBe(1);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get("GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("GET request");
+
+ const indent = 10;
+ let wrapper = render(
+ NetworkEventMessage({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+ });
+
+ describe("XHR GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR GET request");
+ const update = stubPreparedMessages.get("XHR GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ networkMessageUpdate: update,
+ })
+ );
+
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+ });
+
+ describe("XHR POST request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const update = stubPreparedMessages.get("XHR POST request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ networkMessageUpdate: update,
+ })
+ );
+
+ expect(wrapper.find(".message-body .method").text()).toBe("POST");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").length).toBe(1);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+ });
+
+ describe("is expandable", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ })
+ );
+
+ expect(wrapper.find(".message .theme-twisty")).toExist();
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/page-error.test.js b/devtools/client/webconsole/test/node/components/page-error.test.js
new file mode 100644
index 0000000000..ade7be678a
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/page-error.test.js
@@ -0,0 +1,661 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ formatErrorTextWithCausedBy,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ prepareMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Components under test.
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPackets,
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("PageError component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Uncaught ReferenceError: asdf is not defined[Learn More]"
+ );
+
+ // The stacktrace should be closed by default.
+ const frameLinks = wrapper.find(`.stack-trace`);
+ expect(frameLinks.length).toBe(0);
+
+ // There should be the location.
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ // @TODO Will likely change. See bug 1307952
+ expect(locationLink.text()).toBe("test-console-api.html:3:5");
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("renders an error with a longString exception message", () => {
+ const message = stubPreparedMessages.get("TypeError longString message");
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text.startsWith("Uncaught Error: Long error Long error")).toBe(true);
+ });
+
+ it("renders thrown empty string", () => {
+ const message = stubPreparedMessages.get(`throw ""`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught <empty string>");
+ });
+
+ it("renders thrown string", () => {
+ const message = stubPreparedMessages.get(`throw "tomato"`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught tomato`);
+ });
+
+ it("renders thrown boolean", () => {
+ const message = stubPreparedMessages.get(`throw false`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught false`);
+ });
+
+ it("renders thrown number ", () => {
+ const message = stubPreparedMessages.get(`throw 0`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught 0`);
+ });
+
+ it("renders thrown null", () => {
+ const message = stubPreparedMessages.get(`throw null`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught null`);
+ });
+
+ it("renders thrown undefined", () => {
+ const message = stubPreparedMessages.get(`throw undefined`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught undefined`);
+ });
+
+ it("renders thrown Symbol", () => {
+ const message = stubPreparedMessages.get(`throw Symbol`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Symbol("potato")`);
+ });
+
+ it("renders thrown object", () => {
+ const message = stubPreparedMessages.get(`throw Object`);
+
+ // We need to wrap the PageError in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ PageError({ message, serviceContainer })
+ )
+ );
+
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Object { vegetable: "cucumber" }`);
+ });
+
+ it("renders thrown error", () => {
+ const message = stubPreparedMessages.get(`throw Error Object`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Error: pumpkin`);
+ });
+
+ it("renders thrown Error with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with custom name`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught JuicyError: pineapple`);
+ });
+
+ it("renders thrown Error with error cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with error cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ "Uncaught Error: something went wrong\nCaused by: SyntaxError: original error"
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with cause chain`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-d",
+ "Caused by: Error: err-c",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with cyclical cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with cyclical cause chain`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ // TODO: This is not how we should display cyclical cause chain, but we have it here
+ // to ensure it's displaying something that makes _some_ sense.
+ // This should be properly handled in Bug 1719605.
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-b",
+ "Caused by: Error: err-a",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with null cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with falsy cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: null cause\nCaused by: null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with number cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with number cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: number cause\nCaused by: 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with string cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with string cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ `Uncaught Error: string cause\nCaused by: "cause message"`
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with object cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with object cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: object cause\nCaused by: Object { … }");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders uncaught rejected Promise with empty string", () => {
+ const message = stubPreparedMessages.get(`Promise reject ""`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught (in promise) <empty string>");
+ });
+
+ it("renders uncaught rejected Promise with string", () => {
+ const message = stubPreparedMessages.get(`Promise reject "tomato"`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) tomato`);
+ });
+
+ it("renders uncaught rejected Promise with boolean", () => {
+ const message = stubPreparedMessages.get(`Promise reject false`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) false`);
+ });
+
+ it("renders uncaught rejected Promise with number ", () => {
+ const message = stubPreparedMessages.get(`Promise reject 0`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) 0`);
+ });
+
+ it("renders uncaught rejected Promise with null", () => {
+ const message = stubPreparedMessages.get(`Promise reject null`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) null`);
+ });
+
+ it("renders uncaught rejected Promise with undefined", () => {
+ const message = stubPreparedMessages.get(`Promise reject undefined`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) undefined`);
+ });
+
+ it("renders uncaught rejected Promise with Symbol", () => {
+ const message = stubPreparedMessages.get(`Promise reject Symbol`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Symbol("potato")`);
+ });
+
+ it("renders uncaught rejected Promise with object", () => {
+ const message = stubPreparedMessages.get(`Promise reject Object`);
+ // We need to wrap the PageError in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ PageError({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Object { vegetable: "cucumber" }`);
+ });
+
+ it("renders uncaught rejected Promise with error", () => {
+ const message = stubPreparedMessages.get(`Promise reject Error Object`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Error: pumpkin`);
+ });
+
+ it("renders uncaught rejected Promise with Error with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `Promise reject Error Object with custom name`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) JuicyError: pineapple`);
+ });
+
+ it("renders uncaught rejected Promise with Error with cause", () => {
+ const message = stubPreparedMessages.get(
+ `Promise reject Error Object with error cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ `Uncaught (in promise) Error: something went wrong`,
+ `Caused by: ReferenceError: unknownFunc is not defined`,
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders URLs in message as actual, cropped, links", () => {
+ // Let's replace the packet data in order to mimick a pageError.
+ const packet = stubPackets.get("throw string with URL");
+
+ const evilDomain = `https://evil.com/?`;
+ const badDomain = `https://not-so-evil.com/?`;
+ const paramLength = 200;
+ const longParam = "a".repeat(paramLength);
+
+ const evilURL = `${evilDomain}${longParam}`;
+ const badURL = `${badDomain}${longParam}`;
+
+ // We remove the exceptionDocURL to not have the "learn more" link.
+ packet.pageError.exceptionDocURL = null;
+
+ const message = prepareMessage(packet, { getNextId: () => "1" });
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Uncaught “${evilURL}“ is evil and “${badURL}“ is not good either`
+ );
+
+ // There should be 2 cropped links.
+ const links = wrapper.find(".message-body a.cropped-url");
+ expect(links.length).toBe(2);
+
+ expect(links.eq(0).attr("href")).toBe(evilURL);
+ expect(links.eq(0).attr("title")).toBe(evilURL);
+
+ expect(links.eq(1).attr("href")).toBe(badURL);
+ expect(links.eq(1).attr("title")).toBe(badURL);
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore();
+
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ serviceContainer,
+ dispatch: () => {},
+ })
+ )
+ );
+
+ // There should be a [Learn more] link.
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ const call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ // Unskip will happen in Bug 1529548.
+ it.skip("has a stacktrace which can be opened", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({ message, serviceContainer, open: true })
+ );
+
+ // There should be a collapse button.
+ expect(wrapper.find(".collapse-button[aria-expanded=true]").length).toBe(1);
+
+ // There should be five stacktrace items.
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link`);
+ expect(frameLinks.length).toBe(5);
+ });
+
+ // Unskip will happen in Bug 1529548.
+ it.skip("toggle the stacktrace when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button[aria-expanded='false']").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const indent = 10;
+ let wrapper = render(
+ PageError({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(PageError({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("has empty error notes", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+
+ expect(notes.length).toBe(0);
+ });
+
+ it("can show an error note", () => {
+ const origMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const message = Object.assign({}, origMessage, {
+ notes: [
+ {
+ messageBody: "test note",
+ frame: {
+ source: "https://example.com/test.js",
+ line: 2,
+ column: 6,
+ },
+ },
+ ],
+ });
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(1);
+
+ const note = notes.eq(0);
+ expect(note.find(".message-body").text()).toBe("note: test note");
+
+ // There should be the location.
+ const locationLink = note.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test.js:2:6");
+ });
+
+ it("can show multiple error notes", () => {
+ const origMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const message = Object.assign({}, origMessage, {
+ notes: [
+ {
+ messageBody: "test note 1",
+ frame: {
+ source: "https://example.com/test1.js",
+ line: 2,
+ column: 6,
+ },
+ },
+ {
+ messageBody: "test note 2",
+ frame: {
+ source: "https://example.com/test2.js",
+ line: 10,
+ column: 18,
+ },
+ },
+ {
+ messageBody: "test note 3",
+ frame: {
+ source: "https://example.com/test3.js",
+ line: 9,
+ column: 4,
+ },
+ },
+ ],
+ });
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(3);
+
+ const note1 = notes.eq(0);
+ expect(note1.find(".message-body").text()).toBe("note: test note 1");
+
+ const locationLink1 = note1.find(`.message-location`);
+ expect(locationLink1.length).toBe(1);
+ expect(locationLink1.text()).toBe("test1.js:2:6");
+
+ const note2 = notes.eq(1);
+ expect(note2.find(".message-body").text()).toBe("note: test note 2");
+
+ const locationLink2 = note2.find(`.message-location`);
+ expect(locationLink2.length).toBe(1);
+ expect(locationLink2.text()).toBe("test2.js:10:18");
+
+ const note3 = notes.eq(2);
+ expect(note3.find(".message-body").text()).toBe("note: test note 3");
+
+ const locationLink3 = note3.find(`.message-location`);
+ expect(locationLink3.length).toBe(1);
+ expect(locationLink3.text()).toBe("test3.js:9:4");
+ });
+
+ it("displays error notes", () => {
+ const message = stubPreparedMessages.get(
+ "SyntaxError: redeclaration of let a"
+ );
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(1);
+
+ const note = notes.eq(0);
+ expect(note.find(".message-body").text()).toBe(
+ "note: Previously declared at line 2, column 6"
+ );
+
+ // There should be the location.
+ const locationLink = note.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:2:6");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/warning-group.test.js b/devtools/client/webconsole/test/node/components/warning-group.test.js
new file mode 100644
index 0000000000..12d1615d6c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/warning-group.test.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// Components under test.
+const WarningGroup = require("resource://devtools/client/webconsole/components/Output/message-types/WarningGroup.js");
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ ConsoleMessage,
+} = require("resource://devtools/client/webconsole/types.js");
+const {
+ createWarningGroupMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+const mockMessage = ConsoleMessage({
+ messageText: "this is a warning group",
+ source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+ timeStamp: Date.now(),
+});
+
+describe("WarningGroup component:", () => {
+ it("renders", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ timestampsVisible: true,
+ badge: 42,
+ })
+ );
+
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(mockMessage.timeStamp)
+ );
+ expect(wrapper.find(".message-body").text()).toBe(
+ "this is a warning group 42"
+ );
+ expect(wrapper.find(".arrow[aria-expanded=false]")).toExist();
+ });
+
+ it("does have an expanded arrow when `open` prop is true", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ open: true,
+ })
+ );
+
+ expect(wrapper.find(".arrow[aria-expanded=true]")).toExist();
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("renders Content Blocking Group message", () => {
+ const firstMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ firstMessage.messageText =
+ "The resource at “https://evil.com” was blocked.";
+ firstMessage.category = "cookieBlockedPermission";
+ const type = MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
+ const message = createWarningGroupMessage(
+ `${firstMessage.type}-${firstMessage.innerWindowID}`,
+ type,
+ firstMessage
+ );
+
+ const wrapper = render(
+ WarningGroup({
+ message,
+ serviceContainer,
+ badge: 24,
+ })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "The resource at “<URL>” was blocked. 24"
+ );
+ expect(wrapper.find(".arrow[aria-expanded=false]")).toExist();
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js b/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js
new file mode 100644
index 0000000000..12b4f2a2eb
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ clonePacket,
+ getMessageAt,
+ getPrivatePacket,
+ getWebConsoleUiMock,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+const WebConsoleWrapper = require("resource://devtools/client/webconsole/webconsole-wrapper.js");
+const {
+ messagesAdd,
+} = require("resource://devtools/client/webconsole/actions/messages.js");
+
+async function getWebConsoleWrapper() {
+ const hud = {
+ currentTarget: { client: {}, getFront: () => {} },
+ getMappedExpression: () => {},
+ };
+ const webConsoleUi = getWebConsoleUiMock(hud);
+
+ const wcow = new WebConsoleWrapper(null, webConsoleUi, null, null);
+ await wcow.init();
+ return wcow;
+}
+
+describe("WebConsoleWrapper", () => {
+ it("clears queues when dispatchMessagesClear is called", async () => {
+ const ncow = await getWebConsoleWrapper();
+ ncow.queuedMessageAdds.push({ fakePacket: "message", data: {} });
+ ncow.queuedMessageUpdates.push({ fakePacket: "message-update", data: {} });
+ ncow.queuedRequestUpdates.push({ fakePacket: "request-update", data: {} });
+
+ ncow.dispatchMessagesClear();
+
+ expect(ncow.queuedMessageAdds.length).toBe(0);
+ expect(ncow.queuedMessageUpdates.length).toBe(0);
+ expect(ncow.queuedRequestUpdates.length).toBe(0);
+ });
+
+ it("removes private packets from message queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+
+ const publicLog = stubPackets.get("console.log('mymap')");
+ ncow.queuedMessageAdds.push(
+ getPrivatePacket("console.trace()"),
+ publicLog,
+ getPrivatePacket("XHR POST request")
+ );
+
+ ncow.dispatchPrivateMessagesClear();
+ expect(ncow.queuedMessageAdds).toEqual([publicLog]);
+ });
+
+ it("removes private packets from network update queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+ const publicLog = stubPackets.get("console.log('mymap')");
+ ncow.queuedMessageAdds.push(
+ getPrivatePacket("console.trace()"),
+ publicLog,
+ getPrivatePacket("XHR POST request")
+ );
+
+ const postId = "pid1";
+ const getId = "gid1";
+
+ // Add messages in the store to make sure that update to private requests are
+ // removed from the queue.
+ ncow
+ .getStore()
+ .dispatch(
+ messagesAdd([
+ stubPackets.get("GET request"),
+ { ...getPrivatePacket("XHR GET request"), actor: getId },
+ ])
+ );
+
+ // Add packet to the message queue to make sure that update to private requests are
+ // removed from the queue.
+ ncow.queuedMessageAdds.push({
+ ...getPrivatePacket("XHR POST request"),
+ actor: postId,
+ });
+
+ const publicNetworkUpdate = stubPackets.get("GET request update");
+ ncow.queuedMessageUpdates.push(
+ publicNetworkUpdate,
+ {
+ ...getPrivatePacket("XHR GET request update"),
+ actor: getId,
+ },
+ {
+ ...getPrivatePacket("XHR POST request update"),
+ actor: postId,
+ }
+ );
+
+ ncow.dispatchPrivateMessagesClear();
+
+ expect(ncow.queuedMessageUpdates.length).toBe(1);
+ expect(ncow.queuedMessageUpdates).toEqual([publicNetworkUpdate]);
+ });
+
+ it("removes private packets from network request queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+
+ const packet1 = clonePacket(stubPackets.get("GET request"));
+ const packet2 = clonePacket(getPrivatePacket("XHR GET request"));
+ const packet3 = clonePacket(getPrivatePacket("XHR POST request"));
+
+ // We need to reassign the timeStamp of the packet to guarantee the order.
+ packet1.timeStamp = packet1.timeStamp + 1;
+ packet2.timeStamp = packet2.timeStamp + 2;
+ packet3.timeStamp = packet3.timeStamp + 3;
+
+ ncow.getStore().dispatch(messagesAdd([packet1, packet2, packet3]));
+
+ const state = ncow.getStore().getState();
+ const publicId = getMessageAt(state, 0).id;
+ const privateXhrGetId = getMessageAt(state, 1).id;
+ const privateXhrPostId = getMessageAt(state, 2).id;
+ ncow.queuedRequestUpdates.push(
+ { id: publicId },
+ { id: privateXhrGetId },
+ { id: privateXhrPostId }
+ );
+ // ncow.queuedRequestUpdates.push({fakePacket: "request-update"});
+
+ ncow.dispatchPrivateMessagesClear();
+
+ expect(ncow.queuedRequestUpdates.length).toBe(1);
+ expect(ncow.queuedRequestUpdates).toEqual([{ id: publicId }]);
+ });
+});