Bug 1201475 - Tests for DOM panel; r=linclark
authorJan Odvarko <odvarko@gmail.com>
Tue, 12 Apr 2016 19:56:52 +0200
changeset 332346 60df1f524d7f3303c20a289f25d546dc18bbac8e
parent 332345 6f83467f1a10cdee1f9b596e7b368b71cc601f2c
child 332347 bfda97a555e0d003b1b6040614709d979db72c03
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslinclark
bugs1201475
milestone48.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 1201475 - Tests for DOM panel; r=linclark MozReview-Commit-ID: AmFZNR3Tp7q
devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
devtools/client/dom/content/components/main-frame.js
devtools/client/dom/content/components/main-toolbar.js
devtools/client/dom/content/dom-view.js
devtools/client/dom/content/reducers/grips.js
devtools/client/dom/dom-panel.js
devtools/client/dom/moz.build
devtools/client/dom/test/.eslintrc
devtools/client/dom/test/browser.ini
devtools/client/dom/test/browser_dom_basic.js
devtools/client/dom/test/browser_dom_refresh.js
devtools/client/dom/test/head.js
devtools/client/dom/test/page_basic.html
devtools/client/framework/test/browser_toolbox_window_reload_target.js
--- a/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
@@ -21,17 +21,17 @@ function test() {
     // Store and enable all optional dev tools panels
     yield pushPrefs(...PREFS);
 
     let addon = yield addAddon(ADDON_URL);
     let addonDebugger = yield initAddonDebugger(ADDON_URL);
 
     // Check only valid tabs are shown
     let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
-    let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"];
+    let expectedTabs = ["webconsole", "jsdebugger", "scratchpad", "dom"];
 
     is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
     Array.forEach(tabs, (tab, i) => {
       let toolName = expectedTabs[i];
       is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
     });
 
     // Check no toolbox buttons are shown
--- a/devtools/client/dom/content/components/main-frame.js
+++ b/devtools/client/dom/content/components/main-frame.js
@@ -32,17 +32,18 @@ var MainFrame = React.createClass({
 
   /**
    * Render DOM panel content
    */
   render: function() {
     return (
       div({className: "mainFrame"},
         MainToolbar({
-          dispatch: this.props.dispatch
+          dispatch: this.props.dispatch,
+          object: this.props.object
         }),
         DomTree({
           object: this.props.object,
           filter: this.props.filter,
         })
       )
     );
   }
--- a/devtools/client/dom/content/components/main-toolbar.js
+++ b/devtools/client/dom/content/components/main-toolbar.js
@@ -24,17 +24,17 @@ const { setVisibilityFilter } = require(
 const PropTypes = React.PropTypes;
 
 /**
  * This template is responsible for rendering a toolbar
  * within the 'Headers' panel.
  */
 var MainToolbar = React.createClass({
   propTypes: {
-    object: PropTypes.any,
+    object: PropTypes.any.isRequired,
     dispatch: PropTypes.func.isRequired,
   },
 
   displayName: "MainToolbar",
 
   onRefresh: function() {
     this.props.dispatch(fetchProperties(this.props.object));
   },
@@ -42,17 +42,17 @@ var MainToolbar = React.createClass({
   onSearch: function(value) {
     this.props.dispatch(setVisibilityFilter(value));
   },
 
   render: function() {
     return (
       Toolbar({},
         ToolbarButton({
-          className: "btn copy",
+          className: "btn refresh",
           onClick: this.onRefresh},
           l10n.getStr("dom.refresh")
         ),
         SearchBox({
           onSearch: this.onSearch
         })
       )
     );
--- a/devtools/client/dom/content/dom-view.js
+++ b/devtools/client/dom/content/dom-view.js
@@ -22,37 +22,44 @@ const createStore = require("devtools/cl
 const { reducers } = require("./reducers/index");
 const store = createStore(combineReducers(reducers));
 
 /**
  * This object represents view of the DOM panel and is responsible
  * for rendering the content. It renders the top level ReactJS
  * component: the MainFrame.
  */
-function DomView() {
+function DomView(localStore) {
   addEventListener("devtools/chrome/message",
     this.onMessage.bind(this), true);
+
+  // Make it local so, tests can access it.
+  this.store = localStore;
 }
 
 DomView.prototype = {
   initialize: function(rootGrip) {
     let content = document.querySelector("#content");
     let mainFrame = MainFrame({
       object: rootGrip,
     });
 
     // Render top level component
-    let provider = React.createElement(Provider, {store: store}, mainFrame);
+    let provider = React.createElement(Provider, {
+      store: this.store
+    }, mainFrame);
+
     this.mainFrame = ReactDOM.render(provider, content);
   },
 
   onMessage: function(event) {
     let data = event.data;
     let method = data.type;
 
     if (typeof this[method] == "function") {
       this[method](data.args);
     }
   },
 };
 
-// Construct DOM panel view object.
-new DomView();
+// Construct DOM panel view object and expose it to tests.
+// Tests can access it throught: |panel.panelWin.view|
+window.view = new DomView(store);
--- a/devtools/client/dom/content/reducers/grips.js
+++ b/devtools/client/dom/content/reducers/grips.js
@@ -48,17 +48,18 @@ function onRequestProperties(state, acti
 function onReceiveProperties(cache, action) {
   let response = action.response;
   let from = response.from;
 
   // Properly deal with getters.
   mergeProperties(response);
 
   // Compute list of requested children.
-  let ownProps = response.ownProperties || response.preview.ownProperties || [];
+  let previewProps = response.preview ? response.preview.ownProperties : null;
+  let ownProps = response.ownProperties || previewProps || [];
   let props = Object.keys(ownProps).map(key => {
     return new Property(key, ownProps[key], key);
   });
 
   props.sort(sortName);
 
   // Return new state/map.
   let newCache = new Map(cache);
--- a/devtools/client/dom/dom-panel.js
+++ b/devtools/client/dom/dom-panel.js
@@ -18,16 +18,18 @@ const EventEmitter = require("devtools/s
  */
 function DomPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
 
   this.onTabNavigated = this.onTabNavigated.bind(this);
   this.onContentMessage = this.onContentMessage.bind(this);
 
+  this.pendingRequests = new Map();
+
   EventEmitter.decorate(this);
 }
 
 DomPanel.prototype = {
   /**
    * Open is effectively an asynchronous constructor.
    *
    * @return object
@@ -43,20 +45,22 @@ DomPanel.prototype = {
 
     // Local monitoring needs to make the target remote.
     if (!this.target.isRemote) {
       yield this.target.makeRemote();
     }
 
     this.initialize();
 
-    this.isReady = true;
-    this.emit("ready");
+    this.once("no-pending-requests", () => {
+      this.isReady = true;
+      this.emit("ready");
+      deferred.resolve(this);
+    });
 
-    deferred.resolve(this);
     return this._opening;
   }),
 
   // Initialization
 
   initialize: function() {
     this.panelWin.addEventListener("devtools/content/message",
       this.onContentMessage, true);
@@ -104,24 +108,40 @@ DomPanel.prototype = {
     let deferred = defer();
 
     if (!grip.actor) {
       console.error("No actor!", grip);
       deferred.reject(new Error("Failed to get actor from grip."));
       return deferred.promise;
     }
 
+    // Bail out if target doesn't exist (toolbox maybe closed already).
     if (!this.target) {
-      console.error("No target!", grip);
-      deferred.reject(new Error("Failed to get debugger target."));
       return deferred.promise;
     }
 
+    // If a request for the grips is already in progress
+    // use the same promise.
+    let request = this.pendingRequests.get(grip.actor);
+    if (request) {
+      return request;
+    }
+
     let client = new ObjectClient(this.target.client, grip);
-    client.getPrototypeAndProperties(deferred.resolve);
+    client.getPrototypeAndProperties(response => {
+      this.pendingRequests.delete(grip.actor, deferred.promise);
+      deferred.resolve(response);
+
+      // Fire an event about not having any pending requests.
+      if (!this.pendingRequests.size) {
+        this.emit("no-pending-requests");
+      }
+    });
+
+    this.pendingRequests.set(grip.actor, deferred.promise);
 
     return deferred.promise;
   },
 
   // Refresh
 
   refresh: function() {
     let deferred = defer();
--- a/devtools/client/dom/moz.build
+++ b/devtools/client/dom/moz.build
@@ -1,13 +1,15 @@
 # vim: set filetype=python:
 # 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/.
 
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
 DIRS += [
     'content',
 ]
 
 DevToolsModules(
     'dom-panel.js',
     'dom.html',
     'main.js',
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../.eslintrc.mochitests",
+}
--- a/devtools/client/dom/test/browser.ini
+++ b/devtools/client/dom/test/browser.ini
@@ -1,5 +1,10 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
+  page_basic.html
+  !/devtools/client/framework/test/shared-head.js
+
+[browser_dom_basic.js]
+[browser_dom_refresh.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_basic.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+  info("Test DOM panel basic started");
+
+  let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+  // Expand specified row and wait till children are displayed.
+  yield expandRow(panel, "_a");
+
+  // Verify that child is displayed now.
+  let childRow = getRowByLabel(panel, "_data");
+  ok(childRow, "Child row must exist");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_refresh.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks the Refresh action in DOM panel.
+ */
+add_task(function* () {
+  info("Test DOM panel basic started");
+
+  let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+  // Create a new variable in the page scope and refresh the panel.
+  yield evaluateJSAsync(panel, "var _b = 10");
+  yield refreshPanel(panel);
+
+  // Verify that the variable is displayed now.
+  let row = getRowByLabel(panel, "_b");
+  ok(row, "New variable must be displayed");
+});
--- a/devtools/client/dom/test/head.js
+++ b/devtools/client/dom/test/head.js
@@ -1,4 +1,164 @@
+/* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
 "use strict";
 
+const FRAME_SCRIPT_UTILS_URL =
+  "chrome://devtools/content/shared/frame-script-utils.js";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+// DOM panel actions.
+const constants = require("devtools/client/dom/content/constants");
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+registerCleanupFunction(() => {
+  info("finish() was called, cleaning up...");
+  Services.prefs.clearUserPref("devtools.dump.emit");
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url
+ *        The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when
+ *        the url is loaded
+ */
+function addTestTab(url) {
+  info("Adding a new test tab with URL: '" + url + "'");
+
+  return new Promise(resolve => {
+    addTab(url).then(tab => {
+      // Load devtools/shared/frame-script-utils.js
+      getFrameScript();
+
+      // Select the DOM panel and wait till it's initialized.
+      initDOMPanel(tab).then(panel => {
+        resolve({
+          tab: tab,
+          browser: tab.linkedBrowser,
+          panel: panel
+        });
+      });
+    });
+  });
+}
+
+/**
+ * Open the DOM panel for the given tab.
+ *
+ * @param {nsIDOMElement} tab
+ *        Optional tab element for which you want open the DOM panel.
+ *        The default tab is taken from the global variable |tab|.
+ * @return a promise that is resolved once the web console is open.
+ */
+function initDOMPanel(tab) {
+  return new Promise(resolve => {
+    let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "dom").then(toolbox => {
+      let panel = toolbox.getCurrentPanel();
+      resolve(panel);
+    });
+  });
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(panel, element) {
+  return new Promise(resolve => {
+    executeSoon(() => {
+      EventUtils.synthesizeMouse(element, 2, 2, {}, panel.panelWin);
+      resolve();
+    });
+  });
+}
+
+/**
+ * Returns tree row with specified label.
+ */
+function getRowByLabel(panel, text) {
+  let doc = panel.panelWin.document;
+  let labels = [...doc.querySelectorAll(".treeLabel")];
+  let label = labels.find(node => node.textContent == text);
+  return label ? label.closest(".treeRow") : null;
+}
+
+/**
+ * Expands elements with given label and waits till
+ * children are received from the backend.
+ */
+function expandRow(panel, labelText) {
+  let row = getRowByLabel(panel, labelText);
+  return synthesizeMouseClickSoon(panel, row).then(() => {
+    // Wait till children (properties) are fetched
+    // from the backend.
+    return waitForDispatch(panel, "FETCH_PROPERTIES");
+  });
+}
+
+function evaluateJSAsync(panel, expression) {
+  return new Promise(resolve => {
+    panel.target.activeConsole.evaluateJSAsync(expression, res => {
+      resolve(res);
+    });
+  });
+}
+
+function refreshPanel(panel) {
+  let doc = panel.panelWin.document;
+  let button = doc.querySelector(".btn.refresh");
+  return synthesizeMouseClickSoon(panel, button).then(() => {
+    // Wait till children (properties) are fetched
+    // from the backend.
+    return waitForDispatch(panel, "FETCH_PROPERTIES");
+  });
+}
+
+// Redux related API, use from shared location
+// as soon as bug 1261076 is fixed.
+
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+  return new Promise(resolve => {
+    store.dispatch({
+      // Normally we would use `services.WAIT_UNTIL`, but use the
+      // internal name here so tests aren't forced to always pass it
+      // in
+      type: "@@service/waitUntil",
+      predicate: action => {
+        if (action.type === type) {
+          return action.status ?
+            (action.status === "end" || action.status === "error") :
+            true;
+        }
+      },
+      run: (dispatch, getState, action) => {
+        resolve(action);
+      }
+    });
+  });
+}
+
+function waitForDispatch(panel, type, eventRepeat = 1) {
+  const store = panel.panelWin.view.mainFrame.store;
+  const actionType = constants[type];
+  let count = 0;
+
+  return Task.spawn(function*() {
+    info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+    while (count < eventRepeat) {
+      yield _afterDispatchDone(store, actionType);
+      count++;
+      info(type + " dispatched " + count + " time(s)");
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/page_basic.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>DOM test page</title>
+  </head>
+  <body>
+  <script type="text/javascript">
+    window._a = {_data: 'test'};
+  </script>
+  </body>
+</html>
--- a/devtools/client/framework/test/browser_toolbox_window_reload_target.js
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -1,14 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-requestLongerTimeout(2);
+requestLongerTimeout(10);
 
 const TEST_URL = "data:text/html;charset=utf-8,"+
                  "<html><head><title>Test reload</title></head>"+
                  "<body><h1>Testing reload from devtools</h1></body></html>";
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
 
 var target, toolbox, description, reloadsSent, toolIDs;