diff options
Diffstat (limited to '')
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 }]); + }); +}); |