Bug 1333014 - Pretty-print HTML elements; r?maja_zf draft
authorAndreas Tolfsen <ato@mozilla.com>
Tue, 07 Feb 2017 16:59:59 +0000
changeset 486090 a5a478359543b47c4f647c178dd9c940aabae9f5
parent 486089 03fbf9dd3061bbc2054326ab1c046f73b7694391
child 486091 e13b45ab9f0dc80cfa85bbcd02538d89da021b8d
push id45904
push userbmo:ato@mozilla.com
push dateFri, 17 Feb 2017 15:35:32 +0000
reviewersmaja_zf
bugs1333014
milestone54.0a1
Bug 1333014 - Pretty-print HTML elements; r?maja_zf When passing a DOM element that is an HTML element to error.pprint, it will get pretty-printed with its ID and class properties. This helps to identify elements when one is obscuring the other when clicking. For example, the error message Element <input id="foo"> is obscured by <input id="bar"> is nicer than the old error message Element [object HTMLElement] {} is obscured by [object HTMLElement] {} MozReview-Commit-ID: 8U2Lo8V4lmv
testing/marionette/element.js
testing/marionette/error.js
testing/marionette/test_error.js
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -175,18 +175,19 @@ element.Store = class {
       wrappedShadowRoot = new XPCNativeWrapper(container.shadowRoot);
     }
 
     let wrappedEl = new XPCNativeWrapper(el);
     if (!el ||
         !(wrappedEl.ownerDocument == wrappedFrame.document) ||
         element.isDisconnected(wrappedEl, wrappedFrame, wrappedShadowRoot)) {
       throw new StaleElementReferenceError(
-          "The element reference is stale. Either the element " +
-          "is no longer attached to the DOM or the page has been refreshed.");
+          error.pprint`The element reference of ${el} stale: ` +
+              "either the element is no longer attached to the DOM " +
+              "or the page has been refreshed");
     }
 
     return el;
   }
 };
 
 /**
  * Find a single element or a collection of elements starting at the
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -120,32 +120,70 @@ error.stringify = function (err) {
   }
 };
 
 /**
  * Pretty-print values passed to template strings.
  *
  * Usage:
  *
- *     let input = {value: true};
- *     error.pprint`Expected boolean, got ${input}`;
- *     => "Expected boolean, got [object Object] {"value": true}"
+ *     let bool = {value: true};
+ *     error.pprint`Expected boolean, got ${bool}`;
+ *     => 'Expected boolean, got [object Object] {"value": true}'
+ *
+ *     let htmlElement = document.querySelector("input#foo");
+ *     error.pprint`Expected element ${htmlElement}`;
+ *     => 'Expected element <input id="foo" class="bar baz">'
  */
-error.pprint = function (strings, ...values) {
+error.pprint = function (ss, ...values) {
+  function prettyObject (obj) {
+    let proto = Object.prototype.toString.call(obj);
+    let s = "";
+    try {
+      s = JSON.stringify(obj);
+    } catch (e if e instanceof TypeError) {
+      s = `<${e.message}>`;
+    }
+    return proto + " " + s;
+  }
+
+  function prettyElement (el) {
+    let ident = [];
+    if (el.id) {
+      ident.push(`id="${el.id}"`);
+    }
+    if (el.classList.length > 0) {
+      ident.push(`class="${el.className}"`);
+    }
+
+    let idents = "";
+    if (ident.length > 0) {
+      idents = " " + ident.join(" ");
+    }
+
+    return `<${el.localName}${idents}>`;
+  }
+
   let res = [];
-  for (let i = 0; i < strings.length; i++) {
-    res.push(strings[i]);
+  for (let i = 0; i < ss.length; i++) {
+    res.push(ss[i]);
     if (i < values.length) {
       let val = values[i];
-      res.push(Object.prototype.toString.call(val));
-      let s = JSON.stringify(val);
-      if (s && s.length > 0) {
-        res.push(" ");
-        res.push(s);
+      let typ = Object.prototype.toString.call(val);
+      let s;
+      try {
+        if (val && val.nodeType === 1) {
+          s = prettyElement(val);
+        } else {
+          s = prettyObject(val);
+        }
+      } catch (e) {
+        s = typeof val;
       }
+      res.push(s);
     }
   }
   return res.join("");
 };
 
 /**
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
--- a/testing/marionette/test_error.js
+++ b/testing/marionette/test_error.js
@@ -84,16 +84,43 @@ add_test(function test_stringify() {
   equal("WebDriverError: foo",
       error.stringify(new WebDriverError("foo")).split("\n")[0]);
   equal("InvalidArgumentError: foo",
       error.stringify(new InvalidArgumentError("foo")).split("\n")[0]);
 
   run_next_test();
 });
 
+add_test(function test_pprint() {
+  equal('[object Object] {"foo":"bar"}', error.pprint`${{foo: "bar"}}`);
+
+  equal("[object Number] 42", error.pprint`${42}`);
+  equal("[object Boolean] true", error.pprint`${true}`);
+  equal("[object Undefined] undefined", error.pprint`${undefined}`);
+  equal("[object Null] null", error.pprint`${null}`);
+
+  let complexObj = {toJSON: () => "foo"};
+  equal('[object Object] "foo"', error.pprint`${complexObj}`);
+
+  let cyclic = {};
+  cyclic.me = cyclic;
+  equal("[object Object] <cyclic object value>", error.pprint`${cyclic}`);
+
+  let el = {
+    nodeType: 1,
+    localName: "input",
+    id: "foo",
+    classList: {length: 1},
+    className: "bar baz",
+  };
+  equal('<input id="foo" class="bar baz">', error.pprint`${el}`);
+
+  run_next_test();
+});
+
 add_test(function test_toJSON() {
   let e0 = new WebDriverError();
   let e0s = e0.toJSON();
   equal(e0s.error, "webdriver error");
   equal(e0s.message, "");
   equal(e0s.stacktrace, e0.stack);
 
   let e1 = new WebDriverError("a");