Commit 74dab27c authored by nchevobbe's avatar nchevobbe
Browse files

Bug 1712284 - [devtools] Display Error cause in Error rep. r=bomsy

Depends on D119055

Differential Revision: https://phabricator.services.mozilla.com/D119056
parent edbf81e7
Loading
Loading
Loading
Loading
+33 −1
Original line number Diff line number Diff line
@@ -8,7 +8,10 @@
define(function(require, exports, module) {
  // ReactJS
  const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
  const { span } = require("devtools/client/shared/vendor/react-dom-factories");
  const {
    div,
    span,
  } = require("devtools/client/shared/vendor/react-dom-factories");

  // Utils
  const {
@@ -113,6 +116,11 @@ define(function(require, exports, module) {
      content.push(stacktrace);
    }

    const renderCause = customFormat && preview.hasOwnProperty("cause");
    if (renderCause) {
      content.push(getCauseElement(props, preview));
    }

    return span(
      {
        "data-link-actor-id": object.actor,
@@ -206,6 +214,30 @@ define(function(require, exports, module) {
    );
  }

  /**
   * Returns a React element representing the cause of the Error i.e. the `cause`
   * property in the second parameter of the Error constructor (`new Error("message", { cause })`)
   *
   * Example:
   * Caused by: Error: original error
   */
  function getCauseElement(props, preview) {
    const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
    return div(
      {
        key: "cause-container",
        className: "error-rep-cause",
      },
      "Caused by: ",
      Rep({
        ...props,
        key: "cause",
        object: preview.cause,
        mode: props.mode || MODE.TINY,
      })
    );
  }

  /**
   * Parse a string that should represent a stack trace and returns an array of
   * the frames. The shape of the frames are extremely important as they can then
+148 −2
Original line number Diff line number Diff line
@@ -10,7 +10,10 @@ const sinon = require("sinon");
// React
const { createFactory } = require("devtools/client/shared/vendor/react");
const Provider = createFactory(require("react-redux").Provider);
const { setupStore } = require("devtools/client/webconsole/test/node/helpers");
const {
  formatErrorTextWithCausedBy,
  setupStore,
} = require("devtools/client/webconsole/test/node/helpers");

// Components under test.
const EvaluationResult = createFactory(
@@ -147,7 +150,7 @@ describe("EvaluationResult component:", () => {
    expect(wrapper.hasClass("error")).toBe(true);
  });

  it("render thrown ErrorObject with custom name", () => {
  it("render thrown Error object with custom name", () => {
    const message = stubPreparedMessages.get(
      `eval throw Error Object with custom name`
    );
@@ -157,6 +160,133 @@ describe("EvaluationResult component:", () => {
    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
@@ -231,6 +361,22 @@ describe("EvaluationResult component:", () => {
    );
  });

  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
+134 −1
Original line number Diff line number Diff line
@@ -10,7 +10,10 @@ const sinon = require("sinon");
// React
const { createFactory } = require("devtools/client/shared/vendor/react");
const Provider = createFactory(require("react-redux").Provider);
const { setupStore } = require("devtools/client/webconsole/test/node/helpers");
const {
  formatErrorTextWithCausedBy,
  setupStore,
} = require("devtools/client/webconsole/test/node/helpers");
const { prepareMessage } = require("devtools/client/webconsole/utils/messages");

// Components under test.
@@ -169,6 +172,118 @@ describe("PageError component:", () => {
    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 }));
@@ -248,6 +363,24 @@ describe("PageError component:", () => {
    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: TypeError: can't access property "c", a.b is undefined`,
      ].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");
+8 −0
Original line number Diff line number Diff line
@@ -162,9 +162,17 @@ function getProxyMock(overrides = {}) {
  };
}

function formatErrorTextWithCausedBy(text) {
  // The component text does not append new line character before
  // the "Caused by" label, so add it here to make the assertions
  // look more legible
  return text.replace(/Caused by/g, "\nCaused by");
}

module.exports = {
  clearPrefs,
  clonePacket,
  formatErrorTextWithCausedBy,
  getFiltersPrefs,
  getFirstMessage,
  getLastMessage,