Bug 1514339 - Update Debugger [Frontend v109] Provide initial implementation of opening DOM nodes in inspector (#7455). r=davidwalsh
authorJason Laster <jlaster@mozilla.com>
Fri, 14 Dec 2018 14:48:06 -0500
changeset 451236 96044835fb31b27368f935690c5b064ccd5dd839
parent 451235 03a9e0e9a33a0ef9690e972ffc3c6998909f15ba
child 451237 6a997a291a00b738fe03ebe3637cb5291f4ac03d
push id35230
push userbtara@mozilla.com
push dateWed, 19 Dec 2018 04:55:30 +0000
treeherdermozilla-central@204ab379fb82 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavidwalsh
bugs1514339
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1514339 - Update Debugger [Frontend v109] Provide initial implementation of opening DOM nodes in inspector (#7455). r=davidwalsh
devtools/client/debugger/new/panel.js
devtools/client/debugger/new/src/actions/toolbox.js
devtools/client/debugger/new/src/components/Editor/Preview/Popup.js
devtools/client/debugger/new/src/components/SecondaryPanes/Expressions.js
devtools/client/debugger/new/src/components/SecondaryPanes/FrameworkComponent.js
devtools/client/debugger/new/src/components/SecondaryPanes/Scopes.js
devtools/client/debugger/new/test/mochitest/browser.ini
devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
devtools/client/debugger/new/test/mochitest/browser_dbg-inspector-integration.js
devtools/client/debugger/new/test/mochitest/helpers.js
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -30,17 +30,32 @@ DebuggerPanel.prototype = {
     } = await this.panelWin.Debugger.bootstrap({
       threadClient: this.toolbox.threadClient,
       tabTarget: this.toolbox.target,
       debuggerClient: this.toolbox.target.client,
       sourceMaps: this.toolbox.sourceMapService,
       toolboxActions: {
         // Open a link in a new browser tab.
         openLink: this.openLink.bind(this),
-        openWorkerToolbox: this.openWorkerToolbox.bind(this)
+        openWorkerToolbox: this.openWorkerToolbox.bind(this),
+        openElementInInspector: async function(grip) {
+          await this.toolbox.initInspector();
+          const onSelectInspector = this.toolbox.selectTool("inspector");
+          const onGripNodeToFront = this.toolbox.walker.gripToNodeFront(grip);
+          const [
+            front,
+            inspector,
+          ] = await Promise.all([onGripNodeToFront, onSelectInspector]);
+
+          const onInspectorUpdated = inspector.once("inspector-updated");
+          const onNodeFrontSet = this.toolbox.selection
+            .setNodeFront(front, { reason: "debugger" });
+
+          return Promise.all([onNodeFrontSet, onInspectorUpdated]);
+        }.bind(this)
       }
     });
 
     this._actions = actions;
     this._store = store;
     this._selectors = selectors;
     this._client = client;
     this.isReady = true;
--- a/devtools/client/debugger/new/src/actions/toolbox.js
+++ b/devtools/client/debugger/new/src/actions/toolbox.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 const { isDevelopment } = require("devtools-environment");
 const { getSelectedFrameId } = require("../selectors");
 
 import type { ThunkArgs } from "./types";
-import type { Worker } from "../types";
+import type { Worker, Grip } from "../types";
 
 /**
  * @memberof actions/toolbox
  * @static
  */
 export function openLink(url: string) {
   return async function({ openLink: openLinkCommand }: ThunkArgs) {
     if (isDevelopment()) {
@@ -42,8 +42,18 @@ export function evaluateInConsole(inputS
   return async ({ client, getState }: ThunkArgs) => {
     const frameId = getSelectedFrameId(getState());
     client.evaluate(
       `console.log("${inputString}"); console.log(${inputString})`,
       { frameId }
     );
   };
 }
+
+export function openElementInInspectorCommand(grip: Grip) {
+  return async ({ openElementInInspector }: ThunkArgs) => {
+    if (isDevelopment()) {
+      alert(`Opening node in Inspector: ${grip.class}`);
+    } else {
+      return openElementInInspector(grip);
+    }
+  };
+}
--- a/devtools/client/debugger/new/src/components/Editor/Preview/Popup.js
+++ b/devtools/client/debugger/new/src/components/Editor/Preview/Popup.js
@@ -29,32 +29,34 @@ import { isReactComponent, isImmutablePr
 
 import Svg from "../../shared/Svg";
 import { createObjectClient } from "../../../client/firefox";
 
 import "./Popup.css";
 
 import type { EditorRange } from "../../../utils/editor/types";
 import type { Coords } from "../../shared/Popover";
+import type { Grip } from "../../../types";
 
 type PopupValue = Object | null;
 type Props = {
   setPopupObjectProperties: (Object, Object) => void,
   addExpression: (string, ?Object) => void,
   popupObjectProperties: Object,
   popoverPos: Object,
   value: PopupValue,
   expression: string,
   onClose: () => void,
   range: EditorRange,
   editor: any,
   editorRef: ?HTMLDivElement,
   selectSourceURL: (string, Object) => void,
   openLink: string => void,
-  extra: Object
+  extra: Object,
+  openElementInInspector: (grip: Grip) => void
 };
 
 type State = {
   top: number
 };
 
 function inPreview(event) {
   const relatedTarget: Element = (event.relatedTarget: any);
@@ -263,26 +265,28 @@ export class Popup extends Component<Pro
           mode: MODE.LONG,
           openLink
         })}
       </div>
     );
   }
 
   renderObjectInspector(roots: Array<Object>) {
-    const { openLink } = this.props;
+    const { openLink, openElementInInspector } = this.props;
 
     return (
       <ObjectInspector
         roots={roots}
         autoExpandDepth={0}
         disableWrap={true}
         focusable={false}
         openLink={openLink}
         createObjectClient={grip => createObjectClient(grip)}
+        onDOMNodeClick={grip => openElementInInspector(grip)}
+        onInspectIconClick={grip => openElementInInspector(grip)}
       />
     );
   }
 
   renderPreview() {
     // We don't have to check and
     // return on `false`, `""`, `0`, `undefined` etc,
     // these falsy simple typed value because we want to
@@ -339,17 +343,18 @@ export class Popup extends Component<Pro
       >
         {this.renderPreview()}
       </Popover>
     );
   }
 }
 
 const mapStateToProps = state => ({
-  popupObjectProperties: getAllPopupObjectProperties(state)
+  popupObjectProperties: getAllPopupObjectProperties(state),
+  openElementInInspector: actions.openElementInInspectorCommand
 });
 
 const {
   addExpression,
   selectSourceURL,
   selectLocation,
   setPopupObjectProperties,
   openLink
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Expressions.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Expressions.js
@@ -17,17 +17,17 @@ import {
 } from "../../selectors";
 import { getValue } from "../../utils/expressions";
 import { createObjectClient } from "../../client/firefox";
 
 import { CloseButton } from "../shared/Button";
 import { debounce } from "lodash";
 
 import type { List } from "immutable";
-import type { Expression } from "../../types";
+import type { Expression, Grip } from "../../types";
 
 import "./Expressions.css";
 
 const { ObjectInspector } = objectInspector;
 
 type State = {
   editing: boolean,
   editIndex: number,
@@ -43,17 +43,18 @@ type Props = {
   autocomplete: (input: string, cursor: number) => Promise<any>,
   clearAutocomplete: () => void,
   onExpressionAdded: () => void,
   addExpression: (input: string) => void,
   clearExpressionError: () => void,
   evaluateExpressions: () => void,
   updateExpression: (input: string, expression: Expression) => void,
   deleteExpression: (expression: Expression) => void,
-  openLink: (url: string) => void
+  openLink: (url: string) => void,
+  openElementInInspector: (grip: Grip) => void
 };
 
 class Expressions extends Component<Props, State> {
   _input: ?HTMLInputElement;
   renderExpression: (
     expression: Expression,
     index: number
   ) => React$Element<"li">;
@@ -201,17 +202,17 @@ class Expressions extends Component<Prop
     if (!this.props.expressionError) {
       this.hideInput();
     }
 
     this.props.clearAutocomplete();
   };
 
   renderExpression = (expression: Expression, index: number) => {
-    const { expressionError, openLink } = this.props;
+    const { expressionError, openLink, openElementInInspector } = this.props;
     const { editing, editIndex } = this.state;
     const { input, updating } = expression;
     const isEditingExpr = editing && editIndex === index;
     if (isEditingExpr || (isEditingExpr && expressionError)) {
       return this.renderExpressionEditInput(expression);
     }
 
     if (updating) {
@@ -238,16 +239,18 @@ class Expressions extends Component<Prop
         <div className="expression-content">
           <ObjectInspector
             roots={[root]}
             autoExpandDepth={0}
             disableWrap={true}
             focusable={false}
             openLink={openLink}
             createObjectClient={grip => createObjectClient(grip)}
+            onDOMNodeClick={grip => openElementInInspector(grip)}
+            onInspectIconClick={grip => openElementInInspector(grip)}
           />
           <div className="expression-container__close-btn">
             <CloseButton
               handleClick={e => this.deleteExpression(e, expression)}
               tooltip={L10N.getStr("expressions.remove.tooltip")}
             />
           </div>
         </div>
@@ -369,11 +372,12 @@ export default connect(
     autocomplete: actions.autocomplete,
     clearAutocomplete: actions.clearAutocomplete,
     onExpressionAdded: actions.onExpressionAdded,
     addExpression: actions.addExpression,
     clearExpressionError: actions.clearExpressionError,
     evaluateExpressions: actions.evaluateExpressions,
     updateExpression: actions.updateExpression,
     deleteExpression: actions.deleteExpression,
-    openLink: actions.openLink
+    openLink: actions.openLink,
+    openElementInInspector: actions.openElementInInspectorCommand
   }
 )(Expressions);
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/FrameworkComponent.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/FrameworkComponent.js
@@ -8,48 +8,53 @@ import { connect } from "react-redux";
 import actions from "../../actions";
 
 import { createObjectClient } from "../../client/firefox";
 import { getSelectedFrame, getAllPopupObjectProperties } from "../../selectors";
 
 import { objectInspector } from "devtools-reps";
 import { isReactComponent } from "../../utils/preview";
 
-import type { Frame } from "../../types";
+import type { Frame, Grip } from "../../types";
 
 const {
   component: ObjectInspector,
   utils: {
     createNode,
     getChildren,
     loadProperties: { loadItemProperties }
   }
 } = objectInspector;
 
 type Props = {
   selectedFrame: Frame,
   popupObjectProperties: Object,
-  setPopupObjectProperties: (Object, Object) => void
+  setPopupObjectProperties: (Object, Object) => void,
+  openElementInInspector: (grip: Grip) => void
 };
 
 class FrameworkComponent extends PureComponent<Props> {
   async componentWillMount() {
     const expression = "this;";
     const { selectedFrame, setPopupObjectProperties } = this.props;
     const value = selectedFrame.this;
 
     const root = createNode({ name: expression, contents: { value } });
     const properties = await loadItemProperties(root, createObjectClient);
     if (properties) {
       setPopupObjectProperties(value, properties);
     }
   }
 
   renderReactComponent() {
-    const { selectedFrame, popupObjectProperties } = this.props;
+    const {
+      selectedFrame,
+      popupObjectProperties,
+      openElementInInspector
+    } = this.props;
     const expression = "this;";
     const value = selectedFrame.this;
     const root = {
       name: expression,
       path: expression,
       contents: { value }
     };
 
@@ -70,16 +75,18 @@ class FrameworkComponent extends PureCom
         <ObjectInspector
           roots={roots}
           autoExpandAll={false}
           autoExpandDepth={0}
           disableWrap={true}
           focusable={false}
           dimTopLevelWindow={true}
           createObjectClient={grip => createObjectClient(grip)}
+          onDOMNodeClick={grip => openElementInInspector(grip)}
+          onInspectIconClick={grip => openElementInInspector(grip)}
         />
       </div>
     );
   }
 
   render() {
     const { selectedFrame } = this.props;
     if (selectedFrame && isReactComponent(selectedFrame.this)) {
@@ -87,17 +94,18 @@ class FrameworkComponent extends PureCom
     }
 
     return null;
   }
 }
 
 const mapStateToProps = state => ({
   selectedFrame: getSelectedFrame(state),
-  popupObjectProperties: getAllPopupObjectProperties(state)
+  popupObjectProperties: getAllPopupObjectProperties(state),
+  openElementInInspector: actions.openElementInInspectorCommand
 });
 
 export default connect(
   mapStateToProps,
   {
     setPopupObjectProperties: actions.setPopupObjectProperties
   }
 )(FrameworkComponent);
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Scopes.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Scopes.js
@@ -14,31 +14,32 @@ import {
   getGeneratedFrameScope,
   getOriginalFrameScope,
   isPaused as getIsPaused,
   getPauseReason
 } from "../../selectors";
 import { getScopes } from "../../utils/pause/scopes";
 
 import { objectInspector } from "devtools-reps";
-import type { Pause, Why } from "../../types";
+import type { Pause, Why, Grip } from "../../types";
 import type { NamedValue } from "../../utils/pause/scopes/types";
 
 import "./Scopes.css";
 
 const { ObjectInspector } = objectInspector;
 
 type Props = {
   isPaused: Pause,
   selectedFrame: Object,
   generatedFrameScopes: Object,
   originalFrameScopes: Object | null,
   isLoading: boolean,
   why: Why,
-  openLink: string => void
+  openLink: string => void,
+  openElementInInspector: (grip: Grip) => void
 };
 
 type State = {
   originalScopes: ?(NamedValue[]),
   generatedScopes: ?(NamedValue[]),
   showOriginal: boolean
 };
 
@@ -91,33 +92,40 @@ class Scopes extends PureComponent<Props
           nextProps.selectedFrame,
           nextProps.generatedFrameScopes
         )
       });
     }
   }
 
   render() {
-    const { isPaused, isLoading, openLink } = this.props;
+    const {
+      isPaused,
+      isLoading,
+      openLink,
+      openElementInInspector
+    } = this.props;
     const { originalScopes, generatedScopes, showOriginal } = this.state;
 
     const scopes = (showOriginal && originalScopes) || generatedScopes;
 
     if (scopes && !isLoading) {
       return (
         <div className="pane scopes-list">
           <ObjectInspector
             roots={scopes}
             autoExpandAll={false}
             autoExpandDepth={1}
             disableWrap={true}
             focusable={false}
             dimTopLevelWindow={true}
             openLink={openLink}
             createObjectClient={grip => createObjectClient(grip)}
+            onDOMNodeClick={grip => openElementInInspector(grip)}
+            onInspectIconClick={grip => openElementInInspector(grip)}
           />
           {originalScopes ? (
             <div className="scope-type-toggle">
               <a
                 href=""
                 onClick={e => {
                   e.preventDefault();
                   this.setState({ showOriginal: !showOriginal });
@@ -179,11 +187,12 @@ const mapStateToProps = state => {
     originalFrameScopes,
     generatedFrameScopes
   };
 };
 
 export default connect(
   mapStateToProps,
   {
-    openLink: actions.openLink
+    openLink: actions.openLink,
+    openElementInInspector: actions.openElementInInspectorCommand
   }
 )(Scopes);
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -692,16 +692,17 @@ skip-if = (os == "win" && ccov) # Bug 14
 [browser_dbg-editor-gutter.js]
 [browser_dbg-editor-select.js]
 [browser_dbg-editor-highlight.js]
 [browser_dbg-ember-quickstart.js]
 [browser_dbg-expressions.js]
 [browser_dbg-expressions-error.js]
 [browser_dbg-iframes.js]
 [browser_dbg-inline-cache.js]
+[browser_dbg-inspector-integration.js]
 [browser_dbg-keyboard-navigation.js]
 [browser_dbg-keyboard-shortcuts.js]
 skip-if = os == "linux" # bug 1351952
 [browser_dbg-layout-changes.js]
 [browser_dbg-outline.js]
 skip-if = verify
 [browser_dbg-outline-pretty.js]
 [browser_dbg-outline-filter.js]
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
@@ -4,21 +4,16 @@
 /**
  * tests the watch expressions component
  * 1. add watch expressions
  * 2. edit watch expressions
  * 3. delete watch expressions
  * 4. expanding properties when not paused
  */
 
-const expressionSelectors = {
-  plusIcon: ".watch-expressions-pane button.plus",
-  input: "input.input-expression"
-};
-
 function getLabel(dbg, index) {
   return findElement(dbg, "expressionNode", index).innerText;
 }
 
 function getValue(dbg, index) {
   return findElement(dbg, "expressionValue", index).innerText;
 }
 
@@ -27,41 +22,16 @@ function assertEmptyValue(dbg, index) {
   if (value) {
     is(value.innerText, "");
     return;
   }
 
   is(value, null);
 }
 
-async function addExpression(dbg, input) {
-  info("Adding an expression");
-
-  const plusIcon = findElementWithSelector(dbg, expressionSelectors.plusIcon);
-  if (plusIcon) {
-    plusIcon.click();
-  }
-  findElementWithSelector(dbg, expressionSelectors.input).focus();
-  type(dbg, input);
-  pressKey(dbg, "Enter");
-
-  await waitForDispatch(dbg, "EVALUATE_EXPRESSION");
-}
-
-async function editExpression(dbg, input) {
-  info("Updating the expression");
-  dblClickElement(dbg, "expressionNode", 1);
-  // Position cursor reliably at the end of the text.
-  pressKey(dbg, "End");
-  type(dbg, input);
-  const evaluated = waitForDispatch(dbg, "EVALUATE_EXPRESSIONS");
-  pressKey(dbg, "Enter");
-  await evaluated;
-}
-
 add_task(async function() {
   const dbg = await initDebugger("doc-script-switching.html");
 
   invokeInTab("firstCall");
   await waitForPaused(dbg);
 
   await addExpression(dbg, "f");
   is(getLabel(dbg, 1), "f");
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-inspector-integration.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that clicking the DOM node button in any ObjectInspect
+// opens the Inspector panel
+
+function waitForInspectorPanelChange(dbg) {
+  const { toolbox } = dbg;
+
+  return new Promise(resolve => {
+    toolbox.getPanelWhenReady("inspector").then(() => {
+    ok(toolbox.inspector, "Inspector is shown.");
+    resolve(toolbox.inspector);
+    });
+  });
+}
+
+add_task(async function() {
+  const dbg = await initDebugger("doc-script-switching.html");
+
+  await addExpression(dbg, "window.document.body.firstChild");
+
+  await waitForElementWithSelector(dbg, "button.open-inspector");
+  findElementWithSelector(dbg, "button.open-inspector").click();
+
+  await waitForInspectorPanelChange(dbg);
+});
\ No newline at end of file
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -1027,16 +1027,17 @@ const selectors = {
   expressionNode: i =>
     `.expressions-list .expression-container:nth-child(${i}) .object-label`,
   expressionValue: i =>
     `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`,
   expressionClose: i =>
     `.expressions-list .expression-container:nth-child(${i}) .close`,
   expressionInput: ".expressions-list  input.input-expression",
   expressionNodes: ".expressions-list .tree-node",
+  expressionPlus: ".watch-expressions-pane button.plus",
   scopesHeader: ".scopes-pane ._header",
   breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`,
   breakpointItems: `.breakpoints-list .breakpoint`,
   breakpointContextMenu: {
     disableSelf: "#node-menu-disable-self",
     disableAll: "#node-menu-disable-all",
     disableOthers: "#node-menu-disable-others",
     enableSelf: "#node-menu-enable-self",
@@ -1383,8 +1384,33 @@ async function waitForNodeToGainFocus(db
   }, `waiting for source node ${index} to be focused`);
 }
 
 async function assertNodeIsFocused(dbg, index) {
   await waitForNodeToGainFocus(dbg, index);
   const node = findElement(dbg, "sourceNode", index);
   ok(node.classList.contains("focused"), `node ${index} is focused`);
 }
+
+async function addExpression(dbg, input) {
+  info("Adding an expression");
+
+  const plusIcon = findElementWithSelector(dbg, selectors.expressionPlus);
+  if (plusIcon) {
+    plusIcon.click();
+  }
+  findElementWithSelector(dbg, selectors.expressionInput).focus();
+  type(dbg, input);
+  pressKey(dbg, "Enter");
+
+  await waitForDispatch(dbg, "EVALUATE_EXPRESSION");
+}
+
+async function editExpression(dbg, input) {
+  info("Updating the expression");
+  dblClickElement(dbg, "expressionNode", 1);
+  // Position cursor reliably at the end of the text.
+  pressKey(dbg, "End");
+  type(dbg, input);
+  const evaluated = waitForDispatch(dbg, "EVALUATE_EXPRESSIONS");
+  pressKey(dbg, "Enter");
+  await evaluated;
+}
\ No newline at end of file