Bug 863102 - Automatically scroll down upon new network requests, r=vporof
authorDavid Creswick <dcrewi@gyrae.net>
Fri, 10 May 2013 12:01:08 +0300
changeset 142508 869b002272b3a18c9e69555c20de49d30632e616
parent 142507 9a6679593bcecc3596c518028c60f5f6f5a10892
child 142509 13c1c11245ab90aebe2560a5fb304c7dd62388d7
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs863102
milestone23.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 863102 - Automatically scroll down upon new network requests, r=vporof
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/test/Makefile.in
browser/devtools/netmonitor/test/browser_net_autoscroll.js
browser/devtools/netmonitor/test/head.js
browser/devtools/netmonitor/test/html_infinite-get-page.html
browser/devtools/shared/widgets/SideMenuWidget.jsm
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -266,16 +266,17 @@ create({ constructor: RequestsMenuView, 
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function NVRM_initialize() {
     dumpn("Initializing the RequestsMenuView");
 
     this.node = new SideMenuWidget($("#requests-menu-contents"), false);
     this.node.maintainSelectionVisible = false;
+    this.node.autoscrollWithAppendedItems = true;
 
     this.node.addEventListener("mousedown", this._onMouseDown, false);
     this.node.addEventListener("select", this._onSelect, false);
     window.addEventListener("resize", this._onResize, false);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
--- a/browser/devtools/netmonitor/test/Makefile.in
+++ b/browser/devtools/netmonitor/test/Makefile.in
@@ -7,16 +7,17 @@ topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
 	browser_net_aaa_leaktest.js \
+	browser_net_autoscroll.js \
 	browser_net_simple-init.js \
 	browser_net_page-nav.js \
 	browser_net_prefs-and-l10n.js \
 	browser_net_prefs-reload.js \
 	browser_net_pane-collapse.js \
 	browser_net_simple-request.js \
 	browser_net_simple-request-data.js \
 	browser_net_simple-request-details.js \
@@ -39,16 +40,17 @@ MOCHITEST_BROWSER_PAGES = \
 	html_navigate-test-page.html \
 	html_content-type-test-page.html \
 	html_cyrillic-test-page.html \
 	html_status-codes-test-page.html \
 	html_post-data-test-page.html \
 	html_jsonp-test-page.html \
 	html_json-long-test-page.html \
 	html_sorting-test-page.html \
+	html_infinite-get-page.html \
 	sjs_simple-test-server.sjs \
 	sjs_content-type-test-server.sjs \
 	sjs_status-codes-test-server.sjs \
 	sjs_sorting-test-server.sjs \
 	$(NULL)
 
 MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_autoscroll.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 863102 - Automatically scroll down upon new network requests.
+ */
+
+function test() {
+  let monitor, debuggee, requestsContainer, scrollTop;
+
+  initNetMonitor(INFINITE_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    monitor = aMonitor;
+    debuggee = aDebuggee;
+    let win = monitor.panelWin;
+    let topNode = win.document.getElementById("requests-menu-contents");
+    requestsContainer = topNode.getElementsByTagName("scrollbox")[0];
+    ok(!!requestsContainer, "Container element exists as expected.");
+  })
+
+  // (1) Check that the scroll position is maintained at the bottom
+  // when the requests overflow the vertical size of the container.
+  .then(() => {
+    debuggee.performRequests();
+    return waitForRequestsToOverflowContainer(monitor, requestsContainer);
+  }).then(() => {
+    ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
+  })
+
+  // (2) Now set the scroll position somewhere in the middle and check
+  // that additional requests do not change the scroll position.
+  .then(() => {
+    let children = requestsContainer.childNodes;
+    let middleNode = children.item(children.length / 2);
+    middleNode.scrollIntoView();
+    ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
+    scrollTop = requestsContainer.scrollTop; // save for comparison later
+    return waitForNetworkEvents(monitor, 8);
+  }).then(() => {
+    is(requestsContainer.scrollTop, scrollTop, "Did not scroll.");
+  })
+
+  // (3) Now set the scroll position back at the bottom and check that
+  // additional requests *do* cause the container to scroll down.
+  .then(() => {
+    requestsContainer.scrollTop = requestsContainer.scrollHeight;
+    ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
+    return waitForNetworkEvents(monitor, 8);
+  }).then(() => {
+    ok(scrolledToBottom(requestsContainer), "Still scrolled to bottom.");
+  })
+
+  // Done; clean up.
+  .then(() => {
+    return teardown(monitor).then(finish);
+  })
+
+  // Handle exceptions in the chain of promises.
+  .then(null, (err) => {
+    ok(false, err);
+    finish();
+  });
+
+  function waitForRequestsToOverflowContainer (aMonitor, aContainer) {
+    return waitForNetworkEvents(aMonitor, 1).then(() => {
+      if (aContainer.scrollHeight > aContainer.clientHeight) {
+        // Wait for some more just for good measure.
+        return waitForNetworkEvents(aMonitor, 8);
+      } else {
+        return waitForRequestsToOverflowContainer(aMonitor, aContainer);
+      }
+    });
+  }
+
+  function scrolledToBottom(aElement) {
+    return aElement.scrollTop + aElement.clientHeight >= aElement.scrollHeight;
+  }
+}
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -16,16 +16,17 @@ const SIMPLE_URL = EXAMPLE_URL + "html_s
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
+const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_infinite-get-page.html
@@ -0,0 +1,36 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Infinite GETs</p>
+
+    <script type="text/javascript">
+      function get(aAddress, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", aAddress, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(null);
+      }
+
+      // Use a count parameter to defeat caching.
+      var count = 0;
+
+      function performRequests() {
+        get("request_" + (count++), function() {
+          setTimeout(performRequests, 0);
+        });
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -58,25 +58,33 @@ this.SideMenuWidget = function SideMenuW
 
   // Delegate some of the associated node's methods to satisfy the interface
   // required by MenuContainer instances.
   ViewHelpers.delegateWidgetEventMethods(this, aNode);
 };
 
 SideMenuWidget.prototype = {
   /**
+   * Specifies if groups in this container should be sorted alphabetically.
+   */
+  sortedGroups: true,
+
+  /**
    * Specifies if this container should try to keep the selected item visible.
    * (For example, when new items are added the selection is brought into view).
    */
   maintainSelectionVisible: true,
 
   /**
-   * Specifies if groups in this container should be sorted alphabetically.
+   * Specifies that the container viewport should be "stuck" to the
+   * bottom. That is, the container is automatically scrolled down to
+   * keep appended items visible, but only when the scroll position is
+   * already at the bottom.
    */
-  sortedGroups: true,
+  autoscrollWithAppendedItems: false,
 
   /**
    * Inserts an item in this container at the specified index, optionally
    * grouping by name.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @param string | nsIDOMNode aContents
@@ -87,23 +95,33 @@ SideMenuWidget.prototype = {
    *        The group to place the displayed item into.
    * @return nsIDOMNode
    *         The element associated with the displayed item.
    */
   insertItemAt: function SMW_insertItemAt(aIndex, aContents, aTooltip = "", aGroup = "") {
     // Invalidate any notices set on this widget.
     this.removeAttribute("notice");
 
+    let maintainScrollAtBottom =
+      this.autoscrollWithAppendedItems &&
+      (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) &&
+      (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight);
+
+    let group = this._getGroupForName(aGroup);
+    let item = this._getItemForGroup(group, aContents, aTooltip);
+    let element = item.insertSelfAt(aIndex);
+
     if (this.maintainSelectionVisible) {
       this.ensureSelectionIsVisible({ withGroup: true, delayed: true });
     }
+    if (maintainScrollAtBottom) {
+      this._list.scrollTop = this._list.scrollHeight;
+    }
 
-    let group = this._getGroupForName(aGroup);
-    let item = this._getItemForGroup(group, aContents, aTooltip);
-    return item.insertSelfAt(aIndex);
+    return element;
   },
 
   /**
    * Returns the child node in this container situated at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @return nsIDOMNode