Bug 1316225 - Add a Rep for Error objects; r=Honza draft
authorNicolas Chevobbe <chevobbe.nicolas@gmail.com>
Wed, 09 Nov 2016 20:35:12 +0100
changeset 437445 76391283e3edee1d2228a0715f0ecf4fa53aa0d3
parent 436738 336759fad4621dfcd0a3293840edbed67018accd
child 536633 813f1918e689303c82d398e97b6d0e09bc7a44ef
push id35406
push userchevobbe.nicolas@gmail.com
push dateThu, 10 Nov 2016 21:29:39 +0000
reviewersHonza
bugs1316225
milestone52.0a1
Bug 1316225 - Add a Rep for Error objects; r=Honza Handle the different kind of error objects. Allow to pass a className to the objectLink component. Add test to make sure we handle those objects as expected. MozReview-Commit-ID: 4eA19hKexRE
devtools/client/shared/components/reps/error.js
devtools/client/shared/components/reps/moz.build
devtools/client/shared/components/reps/rep.js
devtools/client/shared/components/test/mochitest/chrome.ini
devtools/client/shared/components/test/mochitest/test_reps_error.html
devtools/client/webconsole/new-console-output/components/variables-view-link.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/reps/error.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+  // ReactJS
+  const React = require("devtools/client/shared/vendor/react");
+  // Dependencies
+  const { isGrip } = require("./rep-utils");
+  const { LocalizationHelper } = require("devtools/shared/l10n");
+  const l10n = new LocalizationHelper("devtools/client/locales/debugger.properties");
+
+  // Shortcuts
+  const { span } = React.DOM;
+
+  /**
+   * Renders Error objects.
+   */
+  const ErrorRep = React.createClass({
+    displayName: "Error",
+
+    propTypes: {
+      object: React.PropTypes.object.isRequired,
+      mode: React.PropTypes.string
+    },
+
+    render: function () {
+      let object = this.props.object;
+      let preview = object.preview;
+      let name = preview && preview.name
+        ? preview.name
+        : "Error";
+
+      let content = this.props.mode === "tiny"
+        ? name
+        : `${name}: ${preview.message}`;
+
+      if (preview.stack && this.props.mode !== "tiny") {
+        content = content + "\n" +
+                  l10n.getStr("variablesViewErrorStacktrace") + "\n" +
+                  preview.stack;
+      }
+
+      let objectLink = this.props.objectLink || span;
+      return (
+        objectLink({object, className: "objectBox-stackTrace"},
+          span({}, content)
+        )
+      );
+    },
+  });
+
+  // Registration
+  function supportsObject(object, type) {
+    if (!isGrip(object)) {
+      return false;
+    }
+    return (object.preview && type === "Error");
+  }
+
+  // Exports from this module
+  exports.ErrorRep = {
+    rep: ErrorRep,
+    supportsObject: supportsObject
+  };
+});
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -7,16 +7,17 @@
 DevToolsModules(
     'array.js',
     'attribute.js',
     'caption.js',
     'comment-node.js',
     'date-time.js',
     'document.js',
     'element-node.js',
+    'error.js',
     'event.js',
     'function.js',
     'grip-array.js',
     'grip-map.js',
     'grip.js',
     'infinity.js',
     'nan.js',
     'null.js',
--- a/devtools/client/shared/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -31,16 +31,17 @@ define(function (require, exports, modul
   const { Event } = require("./event");
   const { Func } = require("./function");
   const { PromiseRep } = require("./promise");
   const { RegExp } = require("./regexp");
   const { StyleSheet } = require("./stylesheet");
   const { CommentNode } = require("./comment-node");
   const { ElementNode } = require("./element-node");
   const { TextNode } = require("./text-node");
+  const { ErrorRep } = require("./error");
   const { Window } = require("./window");
   const { ObjectWithText } = require("./object-with-text");
   const { ObjectWithURL } = require("./object-with-url");
   const { GripArray } = require("./grip-array");
   const { GripMap } = require("./grip-map");
   const { Grip } = require("./grip");
 
   // List of all registered template.
@@ -57,16 +58,17 @@ define(function (require, exports, modul
     Attribute,
     Func,
     PromiseRep,
     ArrayRep,
     Document,
     Window,
     ObjectWithText,
     ObjectWithURL,
+    ErrorRep,
     GripArray,
     GripMap,
     Grip,
     Undefined,
     Null,
     StringRep,
     Number,
     SymbolRep,
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -8,16 +8,17 @@ support-files =
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_reps_array.html]
 [test_reps_attribute.html]
 [test_reps_comment-node.html]
 [test_reps_date-time.html]
 [test_reps_document.html]
 [test_reps_element-node.html]
+[test_reps_error.html]
 [test_reps_event.html]
 [test_reps_function.html]
 [test_reps_grip.html]
 [test_reps_grip-array.html]
 [test_reps_grip-map.html]
 [test_reps_infinity.html]
 [test_reps_nan.html]
 [test_reps_null.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_error.html
@@ -0,0 +1,421 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Error rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - Error</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+  let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+  let { ErrorRep } = browserRequire("devtools/client/shared/components/reps/error");
+
+  try {
+    // Test errors with different properties
+    yield testSimpleError();
+    yield testMultilineStackError();
+    yield testErrorWithoutStacktrace();
+
+    // Test different kind of errors
+    yield testEvalError();
+    yield testInternalError();
+    yield testRangeError();
+    yield testReferenceError();
+    yield testSyntaxError();
+    yield testTypeError();
+    yield testURIError();
+  } catch (e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+
+  function testSimpleError() {
+    // Test object = `new Error("Error message")`
+    const stub = getGripStub("testSimpleError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "Error: Error message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:1:13\n",
+      "Error Rep has expected text content for a simple error");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "Error",
+      "Error Rep has expected text content for a simple error in tiny mode");
+  }
+
+  function testMultilineStackError() {
+    /*
+     * Test object = `
+     *   function errorFoo() {
+     *     errorBar();
+     *   }
+     *   function errorBar() {
+     *     console.log(new Error("bar"));
+     *   }
+     *   errorFoo();`
+     */
+    const stub = getGripStub("testMultilineStackError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "Error: bar\n" +
+      "Stack trace:\n" +
+      "errorBar@debugger eval code:6:15\n" +
+      "errorFoo@debugger eval code:3:3\n" +
+      "@debugger eval code:8:1\n",
+      "Error Rep has expected text content for an error with a multiple line");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "Error",
+      "Error Rep has expected text content for an error with a multiple line in tiny mode");
+  }
+
+  function testErrorWithoutStacktrace() {
+    const stub = getGripStub("testErrorWithoutStacktrace");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "Error: Error message",
+      "Error Rep has expected text content for an error without stacktrace");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "Error",
+      "Error Rep has expected text content for an error without stacktrace in tiny mode");
+  }
+
+  function testEvalError() {
+    // Test object = `new EvalError("EvalError message")`
+    const stub = getGripStub("testEvalError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for EvalError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "EvalError: EvalError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:10:13\n",
+      "Error Rep has expected text content for an EvalError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "EvalError",
+      "Error Rep has expected text content for an EvalError in tiny mode");
+  }
+
+  function testInternalError() {
+    // Test object = `new InternalError("InternalError message")`
+    const stub = getGripStub("testInternalError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for InternalError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "InternalError: InternalError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:11:13\n",
+      "Error Rep has expected text content for an InternalError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "InternalError",
+      "Error Rep has expected text content for an InternalError in tiny mode");
+  }
+
+  function testRangeError() {
+    // Test object = `new RangeError("RangeError message")`
+    const stub = getGripStub("testRangeError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for RangeError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "RangeError: RangeError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:12:13\n",
+      "Error Rep has expected text content for RangeError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "RangeError",
+      "Error Rep has expected text content for RangeError in tiny mode");
+  }
+
+  function testReferenceError() {
+    // Test object = `new ReferenceError("ReferenceError message"`
+    const stub = getGripStub("testReferenceError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for ReferenceError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "ReferenceError: ReferenceError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:13:13\n",
+      "Error Rep has expected text content for ReferenceError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "ReferenceError",
+      "Error Rep has expected text content for ReferenceError in tiny mode");
+  }
+
+  function testSyntaxError() {
+    // Test object = `new SyntaxError("SyntaxError message"`
+    const stub = getGripStub("testSyntaxError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for SyntaxError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "SyntaxError: SyntaxError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:14:13\n",
+      "Error Rep has expected text content for SyntaxError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "SyntaxError",
+      "SyntaxError Rep has expected text content for SyntaxError in tiny mode");
+  }
+
+  function testTypeError() {
+    // Test object = `new TypeError("TypeError message"`
+    const stub = getGripStub("testTypeError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for TypeError`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "TypeError: TypeError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:15:13\n",
+      "Error Rep has expected text content for TypeError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "TypeError",
+      "Error Rep has expected text content for a TypeError in tiny mode");
+  }
+
+  function testURIError() {
+    // Test object = `new URIError("URIError message")`
+    const stub = getGripStub("testURIError");
+    const renderedRep = shallowRenderComponent(Rep, {object: stub});
+    is(renderedRep.type, ErrorRep.rep,
+      `Rep correctly selects ${ErrorRep.rep.displayName} for URIError object`);
+
+    const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
+    is(renderedComponent.textContent,
+      "URIError: URIError message\n" +
+      "Stack trace:\n" +
+      "@debugger eval code:16:13\n",
+      "Error Rep has expected text content for URIError");
+
+    const tinyRenderedComponent = renderComponent(ErrorRep.rep, {object: stub, mode: "tiny"});
+    is(tinyRenderedComponent.textContent,
+      "URIError",
+      "Error Rep has expected text content for URIError in tiny mode");
+  }
+
+  function getGripStub(name) {
+    switch (name) {
+      case "testSimpleError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1020",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "Error",
+            "message": "Error message",
+            "stack": "@debugger eval code:1:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 1,
+            "columnNumber": 13
+          }
+        };
+      case "testMultilineStackError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1021",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "Error",
+            "message": "bar",
+            "stack": "errorBar@debugger eval code:6:15\n" +
+                     "errorFoo@debugger eval code:3:3\n" +
+                     "@debugger eval code:8:1\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 6,
+            "columnNumber": 15
+          }
+        };
+      case "testErrorWithoutStacktrace":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1020",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "Error",
+            "message": "Error message",
+          }
+        };
+      case "testEvalError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1022",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "EvalError",
+            "message": "EvalError message",
+            "stack": "@debugger eval code:10:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 10,
+            "columnNumber": 13
+          }
+        };
+      case "testInternalError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1023",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "InternalError",
+            "message": "InternalError message",
+            "stack": "@debugger eval code:11:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 11,
+            "columnNumber": 13
+          }
+        };
+      case "testRangeError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1024",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "RangeError",
+            "message": "RangeError message",
+            "stack": "@debugger eval code:12:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 12,
+            "columnNumber": 13
+          }
+        };
+      case "testReferenceError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1025",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "ReferenceError",
+            "message": "ReferenceError message",
+            "stack": "@debugger eval code:13:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 13,
+            "columnNumber": 13
+          }
+        };
+      case "testSyntaxError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1026",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "SyntaxError",
+            "message": "SyntaxError message",
+            "stack": "@debugger eval code:14:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 14,
+            "columnNumber": 13
+          }
+        };
+      case "testTypeError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1027",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "TypeError",
+            "message": "TypeError message",
+            "stack": "@debugger eval code:15:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 15,
+            "columnNumber": 13
+          }
+        };
+      case "testURIError":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj1028",
+          "class": "Error",
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "Error",
+            "name": "URIError",
+            "message": "URIError message",
+            "stack": "@debugger eval code:16:13\n",
+            "fileName": "debugger eval code",
+            "lineNumber": 16,
+            "columnNumber": 13
+          }
+        };
+    }
+
+    return null;
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -15,20 +15,20 @@ const {openVariablesView} = require("dev
 
 VariablesViewLink.displayName = "VariablesViewLink";
 
 VariablesViewLink.propTypes = {
   object: PropTypes.object.isRequired
 };
 
 function VariablesViewLink(props) {
-  const { object, children } = props;
+  const { className, object, children } = props;
 
   return (
     dom.a({
       onClick: openVariablesView.bind(null, object),
-      className: "cm-variable",
+      className: className || "cm-variable",
       draggable: false,
     }, children)
   );
 }
 
 module.exports = VariablesViewLink;