diff options
Diffstat (limited to 'devtools/client/webconsole/test/node/components/page-error.test.js')
-rw-r--r-- | devtools/client/webconsole/test/node/components/page-error.test.js | 661 |
1 files changed, 661 insertions, 0 deletions
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"); + }); +}); |