Bug 1307941 - Add DOM nodes highlighter in new console frontend; r=bgrins draft
authorNicolas Chevobbe <chevobbe.nicolas@gmail.com>
Fri, 04 Nov 2016 23:53:35 +0100
changeset 438618 445f02a72b1c9859c0b034ca9e941a7f3df9f4e0
parent 438550 71fd23fa0803a548b6e571aa25d0533a06cd0421
child 536970 a95da11420395a5389fbba1aaf5369b711898963
push id35788
push userchevobbe.nicolas@gmail.com
push dateMon, 14 Nov 2016 22:03:59 +0000
reviewersbgrins
bugs1307941
milestone53.0a1
Bug 1307941 - Add DOM nodes highlighter in new console frontend; r=bgrins Add in serviceContainer highlight and unhighlight utils function so they can be accessible in components. Fix EvaluationResult component to pass the serviceContainer to the MessageBody. Modify ElementNodeRep and TextNodeRep to allow passing them function properties `onDOMNodeMouseOver` and `onDOMNodeMouseOut` that will be called respectively on mouseOver and mouseOut. Add a mochitest in the webconsole to make sure we indeed highlight and unhighlight node as expected, and add tests in Reps to make sure the passed functions get called as expected. MozReview-Commit-ID: 8o8WM7vBfMM
devtools/client/shared/components/reps/element-node.js
devtools/client/shared/components/reps/text-node.js
devtools/client/shared/components/test/mochitest/test_reps_element-node.html
devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
devtools/client/shared/components/test/mochitest/test_reps_text-node.html
devtools/client/webconsole/new-console-output/components/grip-message-body.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_nodes_highlight.js
--- a/devtools/client/shared/components/reps/element-node.js
+++ b/devtools/client/shared/components/reps/element-node.js
@@ -82,24 +82,41 @@ define(function (require, exports, modul
         "<",
         nodeNameElement,
         ...attributeElements,
         ">",
       ];
     },
 
     render: function () {
-      let {object, mode} = this.props;
+      let {
+        object,
+        mode,
+        onDOMNodeMouseOver,
+        onDOMNodeMouseOut
+      } = this.props;
       let elements = this.getElements(object, mode);
-      const baseElement = span({className: "objectBox"}, ...elements);
+      let objectLink = this.props.objectLink || span;
 
-      if (this.props.objectLink) {
-        return this.props.objectLink({object}, baseElement);
+      let baseConfig = {className: "objectBox objectBox-node"};
+      if (onDOMNodeMouseOver) {
+        Object.assign(baseConfig, {
+          onMouseOver: _ => onDOMNodeMouseOver(object)
+        });
       }
-      return baseElement;
+
+      if (onDOMNodeMouseOut) {
+        Object.assign(baseConfig, {
+          onMouseOut: onDOMNodeMouseOut
+        });
+      }
+
+      return objectLink({object},
+        span(baseConfig, ...elements)
+      );
     },
   });
 
   // Registration
   function supportsObject(object, type) {
     if (!isGrip(object)) {
       return false;
     }
--- a/devtools/client/shared/components/reps/text-node.js
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -39,30 +39,43 @@ define(function (require, exports, modul
       }
       return "";
     },
 
     render: function () {
       let grip = this.props.object;
       let mode = this.props.mode || "short";
 
+      let baseConfig = {className: "objectBox objectBox-textNode"};
+      if (this.props.onDOMNodeMouseOver) {
+        Object.assign(baseConfig, {
+          onMouseOver: _ => this.props.onDOMNodeMouseOver(grip)
+        });
+      }
+
+      if (this.props.onDOMNodeMouseOut) {
+        Object.assign(baseConfig, {
+          onMouseOut: this.props.onDOMNodeMouseOut
+        });
+      }
+
       if (mode == "short" || mode == "tiny") {
         return (
-          DOM.span({className: "objectBox objectBox-textNode"},
+          DOM.span(baseConfig,
             this.getTitle(grip),
             DOM.span({className: "nodeValue"},
               "\"" + this.getTextContent(grip) + "\""
             )
           )
         );
       }
 
       let objectLink = this.props.objectLink || DOM.span;
       return (
-        DOM.span({className: "objectBox objectBox-textNode"},
+        DOM.span(baseConfig,
           this.getTitle(grip),
           objectLink({
             object: grip
           }, "<"),
           DOM.span({className: "nodeTag"}, "TextNode"),
           " textContent=\"",
           DOM.span({className: "nodeValue"},
             this.getTextContent(grip)
--- a/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
@@ -26,16 +26,19 @@ window.onload = Task.async(function* () 
     yield testBodyNode();
     yield testDocumentElement();
     yield testNode();
     yield testNodeWithLeadingAndTrailingSpacesClassName();
     yield testNodeWithoutAttributes();
     yield testLotsOfAttributes();
     yield testSvgNode();
     yield testSvgNodeInXHTML();
+
+    yield testOnMouseOver();
+    yield testOnMouseOut();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBodyNode() {
     const stub = getGripStub("testBodyNode");
@@ -166,16 +169,49 @@ window.onload = Task.async(function* () 
       "Element node rep has expected text content for XHTML SVG element node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: "tiny" });
     is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
       "Element node rep has expected text content for XHTML SVG element in tiny mode");
   }
 
+  function testOnMouseOver() {
+    const stub = getGripStub("testNode");
+
+    let mouseOverValue;
+    let onDOMNodeMouseOver = (object) => {
+      mouseOverValue = object;
+    };
+    const renderedComponent = renderComponent(
+      ElementNode.rep, {object: stub, onDOMNodeMouseOver});
+
+    const node = renderedComponent.querySelector(".objectBox-node");
+    TestUtils.Simulate.mouseOver(node);
+
+    is(mouseOverValue, stub, "onDOMNodeMouseOver is called with the expected argument " +
+      "when mouseover is fired on the Rep");
+  }
+
+  function testOnMouseOut() {
+    const stub = getGripStub("testNode");
+
+    let called = false;
+    let onDOMNodeMouseOut = (object) => {
+      called = true;
+    };
+    const renderedComponent = renderComponent(
+      ElementNode.rep, {object: stub, onDOMNodeMouseOut});
+
+    const node = renderedComponent.querySelector(".objectBox-node");
+    TestUtils.Simulate.mouseOut(node);
+
+    is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
+  }
+
   function getGripStub(name) {
     switch (name) {
       case "testBodyNode":
         return {
           "type": "object",
           "actor": "server1.conn1.child1/obj30",
           "class": "HTMLBodyElement",
           "ownPropertyLength": 0,
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -33,16 +33,21 @@ window.onload = Task.async(function* () 
     yield testMaxProps();
     yield testMoreThanShortMaxProps();
     yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
     yield testPreviewLimit();
     yield testNamedNodeMap();
     yield testNodeList();
     yield testDocumentFragment();
+
+    yield testOnMouseOver();
+    yield testOnMouseOut();
+
+
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test array: `[]`
@@ -300,16 +305,53 @@ window.onload = Task.async(function* () 
         mode: "long",
         expectedOutput: longOutput,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
+
+
+  function testOnMouseOver() {
+    const stub = getGripStub("testNodeList");
+
+    let mouseOverValue;
+    let onDOMNodeMouseOver = (object) => {
+      mouseOverValue = object;
+    };
+    const renderedComponent = renderComponent(
+      GripArray.rep, {object: stub, onDOMNodeMouseOver});
+
+    const nodes = renderedComponent.querySelectorAll(".objectBox-node");
+    nodes.forEach((node, index) => {
+      TestUtils.Simulate.mouseOver(node);
+
+      is(mouseOverValue, stub.preview.items[index], "onDOMNodeMouseOver is called with " +
+        "the expected argument when mouseover is fired on the Rep");
+    });
+  }
+
+  function testOnMouseOut() {
+    const stub = getGripStub("testNodeList");
+
+    let called = false;
+    let onDOMNodeMouseOut = (object) => {
+      called = true;
+    };
+    const renderedComponent = renderComponent(
+      GripArray.rep, {object: stub, onDOMNodeMouseOut});
+
+    const node = renderedComponent.querySelector(".objectBox-node");
+    TestUtils.Simulate.mouseOut(node);
+
+    is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
+  }
+
   function getGripStub(functionName) {
     switch (functionName) {
       case "testBasic":
         return {
           "type": "object",
           "class": "Array",
           "actor": "server1.conn0.obj35",
           "extensible": true,
--- a/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -44,16 +44,19 @@ window.onload = Task.async(function* () 
       object: gripStubs.get("testRendering")
     });
 
     is(renderedRep.type, TextNode.rep,
       `Rep correctly selects ${TextNode.rep.displayName}`);
 
     yield testRendering();
     yield testRenderingWithEOL();
+
+    yield testOnMouseOver();
+    yield testOnMouseOut();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testRendering() {
     const stub = gripStubs.get("testRendering");
@@ -103,13 +106,42 @@ window.onload = Task.async(function* () 
       {
         mode: "long",
         expectedOutput: defaultLongOutput,
       }
     ];
 
     testRepRenderModes(modeTests, "testRenderingWithEOL", TextNode, stub);
   }
+
+  function testOnMouseOver() {
+    const stub = gripStubs.get("testRendering");
+
+    let mouseOverValue;
+    let onDOMNodeMouseOver = (object) => {
+      mouseOverValue = object;
+    };
+    const renderedComponent = renderComponent(
+      TextNode.rep, {object: stub, onDOMNodeMouseOver});
+
+    TestUtils.Simulate.mouseOver(renderedComponent);
+    is(mouseOverValue, stub, "onDOMNodeMouseOver is called with the expected argument " +
+      "when mouseover is fired on the Rep");
+  }
+
+  function testOnMouseOut() {
+    const stub = gripStubs.get("testRendering");
+
+    let called = false;
+    let onDOMNodeMouseOut = (object) => {
+      called = true;
+    };
+    const renderedComponent = renderComponent(
+      TextNode.rep, {object: stub, onDOMNodeMouseOut});
+
+    TestUtils.Simulate.mouseOut(renderedComponent);
+    is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
+  }
 });
 </script>
 </pre>
 </body>
 </html>
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -40,28 +40,37 @@ GripMessageBody.propTypes = {
 function GripMessageBody(props) {
   const { grip, userProvidedStyle, serviceContainer } = props;
 
   let styleObject;
   if (userProvidedStyle && userProvidedStyle !== "") {
     styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
   }
 
+  let onDOMNodeMouseOver;
+  let onDOMNodeMouseOut;
+  if (serviceContainer) {
+    onDOMNodeMouseOver = (object) => serviceContainer.highlightDomElement(object);
+    onDOMNodeMouseOut = serviceContainer.unHighlightDomElement;
+  }
+
   return (
     // @TODO once there is a longString rep, also turn off quotes for those.
     typeof grip === "string"
       ? StringRep({
         object: grip,
         useQuotes: false,
         mode: props.mode,
         style: styleObject
       })
       : Rep({
         object: grip,
         objectLink: VariablesViewLink,
+        onDOMNodeMouseOver,
+        onDOMNodeMouseOut,
         defaultRep: Grip,
         mode: props.mode,
       })
   );
 }
 
 function cleanupStyle(userProvidedStyle, createElement) {
   // Regular expression that matches the allowed CSS property names.
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -35,17 +35,17 @@ function EvaluationResult(props) {
     exceptionDocURL,
     frame,
   } = message;
 
   let messageBody;
   if (message.messageText) {
     messageBody = message.messageText;
   } else {
-    messageBody = GripMessageBody({grip: message.parameters});
+    messageBody = GripMessageBody({grip: message.parameters, serviceContainer});
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
   const childProps = {
     source,
     type,
     level,
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -53,17 +53,27 @@ NewConsoleOutputWrapper.prototype = {
           return this.toolbox.selectTool("netmonitor").then(panel => {
             return panel.panelWin.NetMonitorController.inspectRequest(requestId);
           });
         },
         sourceMapService: this.toolbox ? this.toolbox._sourceMapService : null,
         openLink: url => this.jsterm.hud.owner.openLink.call(this.jsterm.hud.owner, url),
         createElement: nodename => {
           return this.document.createElementNS("http://www.w3.org/1999/xhtml", nodename);
-        }
+        },
+        highlightDomElement: (grip, options = {}) => {
+          return this.toolbox && this.toolbox.highlighterUtils
+            ? this.toolbox.highlighterUtils.highlightDomValueGrip(grip, options)
+            : null;
+        },
+        unHighlightDomElement: (forceHide = false) => {
+          return this.toolbox && this.toolbox.highlighterUtils
+            ? this.toolbox.highlighterUtils.unhighlight(forceHide)
+            : null;
+        },
       }
     });
     let filterBar = FilterBar({
       serviceContainer: {
         attachRefToHud
       }
     });
     let provider = React.createElement(
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -12,10 +12,11 @@ support-files =
 
 [browser_webconsole_batching.js]
 [browser_webconsole_console_group.js]
 [browser_webconsole_console_table.js]
 [browser_webconsole_filters.js]
 [browser_webconsole_init.js]
 [browser_webconsole_input_focus.js]
 [browser_webconsole_keyboard_accessibility.js]
+[browser_webconsole_nodes_highlight.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_vview_close_on_esc_key.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_nodes_highlight.js
@@ -0,0 +1,51 @@
+/* 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";
+
+// Check hovering logged nodes highlight them in the content page.
+
+const HTML = `
+  <!DOCTYPE html>
+  <html>
+    <body>
+      <h1>Node Highlight  Test</h1>
+    </body>
+    <script>
+      function logNode(selector) {
+        console.log(document.querySelector(selector));
+      }
+    </script>
+  </html>
+`;
+const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML);
+
+add_task(function* () {
+  const hud = yield openNewTabAndConsole(TEST_URI);
+  const toolbox = gDevTools.getToolbox(hud.target);
+
+  yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+    content.wrappedJSObject.logNode("h1");
+  });
+
+  let msg = yield waitFor(() => findMessage(hud, "<h1>"));
+  let node = msg.querySelector(".objectBox-node");
+  ok(node !== null, "Node was logged as expected");
+  const view = node.ownerDocument.defaultView;
+
+  info("Highlight the node by moving the cursor on it");
+  let onNodeHighlight = toolbox.once("node-highlight");
+  EventUtils.synthesizeMouseAtCenter(node, {type: "mousemove"}, view);
+
+  let nodeFront = yield onNodeHighlight;
+  is(nodeFront.displayName, "h1", "The correct node was highlighted");
+
+  info("Unhighlight the node by moving away from the node");
+  let onNodeUnhighlight = toolbox.once("node-unhighlight");
+  let btn = toolbox.doc.querySelector(".toolbox-dock-button");
+  EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, view);
+
+  yield onNodeUnhighlight;
+  ok(true, "node-unhighlight event was fired when moving away from the node");
+});