Bug 876913: Network Monitor: Allow selection of multiple filters, r=vporof
authorSami Jaktholm <sjakthol@outlook.com>
Mon, 10 Feb 2014 13:18:45 -0500
changeset 167838 f5726434eedc1897fabddcbc066544c4d9de6278
parent 167837 aded6366c9fb531d5345e087c1c9d762090a87af
child 167875 07739c5c874f303b8c0b747c62c9e109e9c8d071
child 167908 3b51568473f2ec5f465bf448b30a82da6dd5b054
push id26189
push userryanvm@gmail.com
push dateMon, 10 Feb 2014 20:32:33 +0000
treeherdermozilla-central@f5726434eedc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs876913
milestone30.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 876913: Network Monitor: Allow selection of multiple filters, r=vporof
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/test/browser_net_filter-01.js
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -555,66 +555,118 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Filters all network requests in this container by a specified type.
    *
    * @param string aType
    *        Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
    *        "flash" or "other".
    */
   filterOn: function(aType = "all") {
-    let target = $("#requests-menu-filter-" + aType + "-button");
-    let buttons = document.querySelectorAll(".requests-menu-footer-button");
+    if (aType === "all") {
+      // The filter "all" is special as it doesn't toggle.
+      // - If some filters are selected and 'all' is clicked, the previously
+      //   selected filters will be disabled and 'all' is the only active one.
+      // - If 'all' is already selected, do nothing.
+      if (this._activeFilters.indexOf("all") !== -1) {
+        return;
+      }
 
-    for (let button of buttons) {
-      if (button != target) {
-        button.removeAttribute("checked");
-      } else {
-        button.setAttribute("checked", "true");
-      }
+      // Uncheck all other filters and select 'all'. Must create a copy as
+      // _disableFilter removes the filters from the list while it's being
+      // iterated. 'all' will be enabled automatically by _disableFilter once
+      // the last filter is disabled.
+      this._activeFilters.slice().forEach(this._disableFilter, this);
+    }
+    else if (this._activeFilters.indexOf(aType) === -1) {
+      this._enableFilter(aType);
+    }
+    else {
+      this._disableFilter(aType);
     }
 
-    // Filter on whatever was requested.
-    switch (aType) {
-      case "all":
-        this.filterContents(() => true);
-        break;
-      case "html":
-        this.filterContents(e => this.isHtml(e));
-        break;
-      case "css":
-        this.filterContents(e => this.isCss(e));
-        break;
-      case "js":
-        this.filterContents(e => this.isJs(e));
-        break;
-      case "xhr":
-        this.filterContents(e => this.isXHR(e));
-        break;
-      case "fonts":
-        this.filterContents(e => this.isFont(e));
-        break;
-      case "images":
-        this.filterContents(e => this.isImage(e));
-        break;
-      case "media":
-        this.filterContents(e => this.isMedia(e));
-        break;
-      case "flash":
-        this.filterContents(e => this.isFlash(e));
-        break;
-      case "other":
-        this.filterContents(e => this.isOther(e));
-        break;
-    }
-
+    this.filterContents(this._filterPredicate);
     this.refreshSummary();
     this.refreshZebra();
   },
 
   /**
+   * Disables the given filter, its button and toggles 'all' on if the filter to
+   * be disabled is the last one active.
+   *
+   * @param string aType
+   *        Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
+   *        "flash" or "other".
+   */
+  _disableFilter: function (aType) {
+    // Remove the filter from list of active filters.
+    this._activeFilters.splice(this._activeFilters.indexOf(aType), 1);
+
+    // Remove the checked status from the filter.
+    let target = $("#requests-menu-filter-" + aType + "-button");
+    target.removeAttribute("checked");
+
+    // Check if the filter disabled was the last one. If so, toggle all on.
+    if (this._activeFilters.length === 0)
+      this._enableFilter("all");
+  },
+
+  /**
+   * Enables the given filter, its button and toggles 'all' off if the filter to
+   * be enabled is the first one active.
+   *
+   * @param string aType
+   *        Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
+   *        "flash" or "other".
+   */
+  _enableFilter: function (aType) {
+    // Add the filter to the list of active filters.
+    this._activeFilters.push(aType);
+
+    // Add the checked status to the filter button.
+    let target = $("#requests-menu-filter-" + aType + "-button");
+    target.setAttribute("checked", true);
+
+    // Check if 'all' was selected before. If so, disable it.
+    if (aType !== "all" && this._activeFilters.indexOf("all") !== -1) {
+      this._disableFilter("all");
+    }
+  },
+
+  /**
+   * Returns a predicate that can be used to test if a request matches any of
+   * the active filters.
+   */
+  get _filterPredicate() {
+    let filterPredicates = {
+      "all": () => true,
+      "html": this.isHtml,
+      "css": this.isCss,
+      "js": this.isJs,
+      "xhr": this.isXHR,
+      "fonts": this.isFont,
+      "images": this.isImage,
+      "media": this.isMedia,
+      "flash": this.isFlash,
+      "other": this.isOther
+    };
+
+     if (this._activeFilters.length === 1) {
+       // The simplest case: only one filter active.
+       return filterPredicates[this._activeFilters[0]].bind(this);
+     } else {
+       // Multiple filters active.
+       return requestItem => {
+         return this._activeFilters.some(filterName => {
+           return filterPredicates[filterName].call(this, requestItem);
+         });
+       };
+     }
+  },
+
+  /**
    * Sorts all network requests in this container by a specified detail.
    *
    * @param string aType
    *        Either "status", "method", "file", "domain", "type", "size" or
    *        "waterfall".
    */
   sortBy: function(aType = "waterfall") {
     let target = $("#requests-menu-" + aType + "-button");
@@ -1521,17 +1573,18 @@ RequestsMenuView.prototype = Heritage.ex
   _canvas: null,
   _ctx: null,
   _cachedWaterfallWidth: 0,
   _cachedWaterfallBackground: "",
   _firstRequestStartedMillis: -1,
   _lastRequestEndedMillis: -1,
   _updateQueue: [],
   _updateTimeout: null,
-  _resizeTimeout: null
+  _resizeTimeout: null,
+  _activeFilters: ["all"]
 });
 
 /**
  * Functions handling the sidebar details view.
  */
 function SidebarView() {
   dumpn("SidebarView was instantiated");
 }
--- a/browser/devtools/netmonitor/test/browser_net_filter-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_filter-01.js
@@ -19,81 +19,151 @@ function test() {
 
       isnot(RequestsMenu.selectedItem, null,
         "There should be a selected item in the requests menu.");
       is(RequestsMenu.selectedIndex, 0,
         "The first item should be selected in the requests menu.");
       is(NetMonitorView.detailsPaneHidden, false,
         "The details pane should not be hidden after toggle button was pressed.");
 
+      // First test with single filters...
       testButtons("all");
       testContents([1, 1, 1, 1, 1, 1, 1, 1])
         .then(() => {
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
           testButtons("html");
           return testContents([1, 0, 0, 0, 0, 0, 0, 0]);
         })
         .then(() => {
+          // Reset filters
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
           testButtons("css");
           return testContents([0, 1, 0, 0, 0, 0, 0, 0]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
           testButtons("js");
           return testContents([0, 0, 1, 0, 0, 0, 0, 0]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
           testButtons("xhr");
           return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
           testButtons("fonts");
           return testContents([0, 0, 0, 1, 0, 0, 0, 0]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
           testButtons("images");
           return testContents([0, 0, 0, 0, 1, 0, 0, 0]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
           testButtons("media");
           return testContents([0, 0, 0, 0, 0, 1, 1, 0]);
         })
         .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
           testButtons("flash");
           return testContents([0, 0, 0, 0, 0, 0, 0, 1]);
         })
         .then(() => {
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           testButtons("all");
           return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
         })
+        // ...then combine multiple filters together.
+        .then(() => {
+          // Enable filtering for html and css; should show request of both type.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+          testButtonsCustom([0, 1, 1, 0, 0, 0, 0, 0, 0, 0]);
+          return testContents([1, 1, 0, 0, 0, 0, 0, 0]);
+        })
+        .then(() => {
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+          testButtonsCustom([0, 1, 1, 0, 0, 0, 0, 0, 1, 0]);
+          return testContents([1, 1, 0, 0, 0, 0, 0, 1]);
+        })
+        .then(() => {
+          // Disable some filters. Only one left active.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+          testButtons("html");
+          return testContents([1, 0, 0, 0, 0, 0, 0, 0]);
+        })
+        .then(() => {
+          // Disable last active filter. Should toggle to all.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+          testButtons("all");
+          return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+        })
+        .then(() => {
+          // Enable few filters and click on all. Only "all" should be checked.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+          testButtonsCustom([0, 1, 1, 0, 0, 0, 0, 0, 0]);
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+          testButtons("all");
+          return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+        })
         .then(() => {
           return teardown(aMonitor);
         })
         .then(finish);
     });
 
+    /**
+     * Tests if a button for a filter of given type is the only one checked.
+     *
+     * @param string aFilterType
+     *        The type of the filter that should be the only one checked.
+     *
+     */
     function testButtons(aFilterType) {
       let doc = aMonitor.panelWin.document;
       let target = doc.querySelector("#requests-menu-filter-" + aFilterType + "-button");
       let buttons = doc.querySelectorAll(".requests-menu-footer-button");
 
-      for (let button of buttons) {
-        if (button != target) {
+      // Only target should be checked.
+      let checkStatus = [(button == target) ? 1 : 0 for (button of buttons)]
+
+      testButtonsCustom(checkStatus);
+    }
+
+    /**
+     * Tests if filter buttons have 'checked' attributes set correctly.
+     *
+     * @param array aIsChecked
+     *        An array specifying if a button at given index should have a 
+     *        'checked' attribute. For example, if the third item of the array
+     *        evaluates to true, the third button should be checked.
+     *
+     */
+    function testButtonsCustom(aIsChecked) {
+      let doc = aMonitor.panelWin.document;
+      let buttons = doc.querySelectorAll(".requests-menu-footer-button");
+      for (let i = 0; i < aIsChecked.length; i++) {
+        let button = buttons[i];
+        if (aIsChecked[i]) {
+          is(button.hasAttribute("checked"), true,
+            "The " + button.id + " button should have a 'checked' attribute.");
+        } else {
           is(button.hasAttribute("checked"), false,
             "The " + button.id + " button should not have a 'checked' attribute.");
-        } else {
-          is(button.hasAttribute("checked"), true,
-            "The " + button.id + " button should have a 'checked' attribute.");
         }
       }
     }
 
     function testContents(aVisibility) {
       isnot(RequestsMenu.selectedItem, null,
         "There should still be a selected item after filtering.");
       is(RequestsMenu.selectedIndex, 0,