Bug 601667 - Web Console toolbar styling, part 1; f=mihai.sucan,ddahl r=l10n,sdwilsh,dao,kdangoor, a=blocking2.0
authorPatrick Walton <pwalton@mozilla.com>
Thu, 02 Dec 2010 06:50:29 -0400
changeset 58464 d48937dcda74b37ace70e85309a7c6d9f57ab0ec
parent 58463 b92216d0aea951e89dc0f92bebb649fe0e8a5e75
child 58465 841893070e47c0078dc1d46cf9d07b2d8190c403
push idunknown
push userunknown
push dateunknown
reviewersl10n, sdwilsh, dao, kdangoor, blocking2.0
bugs601667
milestone2.0b8pre
Bug 601667 - Web Console toolbar styling, part 1; f=mihai.sucan,ddahl r=l10n,sdwilsh,dao,kdangoor, a=blocking2.0
toolkit/components/console/hudservice/HUDService.jsm
toolkit/components/console/hudservice/tests/browser/Makefile.in
toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_581231_close_button.js
toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_601667_filter_buttons.js
--- a/toolkit/components/console/hudservice/HUDService.jsm
+++ b/toolkit/components/console/hudservice/HUDService.jsm
@@ -3188,53 +3188,72 @@ HeadsUpDisplay.prototype = {
 
   /**
    * Make the filter toolbar where we can toggle logging filters
    *
    * @returns nsIDOMNode
    */
   makeFilterToolbar: function HUD_makeFilterToolbar()
   {
-    let buttons = ["Network", "CSSParser", "Exception", "Error",
-                   "Info", "Warn", "Log",];
-
-    const pageButtons = [
-      { prefKey: "network", name: "PageNet" },
-      { prefKey: "cssparser", name: "PageCSS" },
-      { prefKey: "exception", name: "PageJS" }
-    ];
-    const consoleButtons = [
-      { prefKey: "error", name: "ConsoleErrors" },
-      { prefKey: "warn", name: "ConsoleWarnings" },
-      { prefKey: "info", name: "ConsoleInfo" },
-      { prefKey: "log", name: "ConsoleLog" }
+    const BUTTONS = [
+      {
+        name: "PageNet",
+        category: "net",
+        severities: [
+          { name: "ConsoleErrors", prefKey: "network" },
+          { name: "ConsoleLog", prefKey: "networkinfo" }
+        ]
+      },
+      {
+        name: "PageCSS",
+        category: "css",
+        severities: [
+          { name: "ConsoleErrors", prefKey: "csserror" },
+          { name: "ConsoleWarnings", prefKey: "cssparser" }
+        ]
+      },
+      {
+        name: "PageJS",
+        category: "js",
+        severities: [
+          { name: "ConsoleErrors", prefKey: "exception" },
+          { name: "ConsoleWarnings", prefKey: "jswarn" }
+        ]
+      },
+      {
+        name: "PageWebDeveloper",
+        category: "webdev",
+        severities: [
+          { name: "ConsoleErrors", prefKey: "error" },
+          { name: "ConsoleWarnings", prefKey: "warn" },
+          { name: "ConsoleInfo", prefKey: "info" },
+          { name: "ConsoleLog", prefKey: "log" }
+        ]
+      }
     ];
 
     let toolbar = this.makeXULNode("toolbar");
     toolbar.setAttribute("class", "hud-console-filter-toolbar");
-    toolbar.setAttribute("mode", "text");
-
-    let pageCategoryTitle = this.getStr("categoryPage");
-    this.addButtonCategory(toolbar, pageCategoryTitle, pageButtons);
-
-    let separator = this.makeXULNode("separator");
-    separator.setAttribute("orient", "vertical");
-    toolbar.appendChild(separator);
-
-    let consoleCategoryTitle = this.getStr("categoryConsole");
-    this.addButtonCategory(toolbar, consoleCategoryTitle, consoleButtons);
+    toolbar.setAttribute("mode", "full");
+
+    this.makeCloseButton(toolbar);
+
+    for (let i = 0; i < BUTTONS.length; i++) {
+      this.makeFilterButton(toolbar, BUTTONS[i]);
+    }
 
     toolbar.appendChild(this.filterSpacer);
     toolbar.appendChild(this.filterBox);
+    this.makeClearConsoleButton(toolbar);
+
     return toolbar;
   },
 
   /**
-   * Creates the context menu on the console, which contains the "clear
-   * console" functionality.
+   * Creates the context menu on the console.
    *
    * @param nsIDOMNode aOutputNode
    *        The console output DOM node.
    * @returns void
    */
   createConsoleMenu: function HUD_createConsoleMenu(aConsoleWrapper) {
     let menuPopup = this.makeXULNode("menupopup");
     let id = this.hudId + "-output-contextmenu";
@@ -3262,89 +3281,119 @@ HeadsUpDisplay.prototype = {
     selectAllItem.setAttribute("label", this.getStr("selectAllCmd.label"));
     selectAllItem.setAttribute("accesskey",
                                this.getStr("selectAllCmd.accesskey"));
     selectAllItem.setAttribute("hudId", this.hudId);
     selectAllItem.setAttribute("buttonType", "selectAll");
     selectAllItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
     menuPopup.appendChild(selectAllItem);
 
-    menuPopup.appendChild(this.makeXULNode("menuseparator"));
-
-    let clearItem = this.makeXULNode("menuitem");
-    clearItem.setAttribute("label", this.getStr("clearConsoleCmd.label"));
-    clearItem.setAttribute("accesskey",
-                           this.getStr("clearConsoleCmd.accesskey"));
-    clearItem.setAttribute("hudId", this.hudId);
-    clearItem.setAttribute("buttonType", "clear");
-    clearItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
-    menuPopup.appendChild(clearItem);
-
     aConsoleWrapper.appendChild(menuPopup);
     aConsoleWrapper.setAttribute("context", id);
   },
 
-  makeButton: function HUD_makeButton(aName, aPrefKey, aType)
+  /**
+   * Creates one of the filter buttons on the toolbar.
+   *
+   * @param nsIDOMNode aParent
+   *        The node to which the filter button should be appended.
+   * @param object aDescriptor
+   *        A descriptor that contains info about the button. Contains "name",
+   *        "category", and "prefKey" properties, and optionally a "severities"
+   *        property.
+   * @return void
+   */
+  makeFilterButton: function HUD_makeFilterButton(aParent, aDescriptor)
   {
-    var self = this;
-    let prefKey = aPrefKey;
-
-    let btn;
-    if (aType == "checkbox") {
-      btn = this.makeXULNode("checkbox");
-      btn.setAttribute("type", aType);
-    } else {
-      btn = this.makeXULNode("toolbarbutton");
+    let toolbarButton = this.makeXULNode("toolbarbutton");
+    aParent.appendChild(toolbarButton);
+
+    let toggleFilter = HeadsUpDisplayUICommands.toggleFilter;
+    toolbarButton.addEventListener("click", toggleFilter, false);
+
+    let name = aDescriptor.name;
+    toolbarButton.setAttribute("type", "menu-button");
+    toolbarButton.setAttribute("label", this.getStr("btn" + name));
+    toolbarButton.setAttribute("tooltip", this.getStr("tip" + name));
+    toolbarButton.setAttribute("category", aDescriptor.category);
+    toolbarButton.setAttribute("hudId", this.hudId);
+    toolbarButton.classList.add("webconsole-filter-button");
+
+    let menuPopup = this.makeXULNode("menupopup");
+    toolbarButton.appendChild(menuPopup);
+
+    let allChecked = true;
+    for (let i = 0; i < aDescriptor.severities.length; i++) {
+      let severity = aDescriptor.severities[i];
+      let menuItem = this.makeXULNode("menuitem");
+      menuItem.setAttribute("label", this.getStr("btn" + severity.name));
+      menuItem.setAttribute("type", "checkbox");
+      menuItem.setAttribute("autocheck", "false");
+      menuItem.setAttribute("hudId", this.hudId);
+
+      let prefKey = severity.prefKey;
+      menuItem.setAttribute("prefKey", prefKey);
+
+      let checked = this.filterPrefs[prefKey];
+      menuItem.setAttribute("checked", checked);
+      if (!checked) {
+        allChecked = false;
+      }
+
+      menuItem.addEventListener("command", toggleFilter, false);
+
+      menuPopup.appendChild(menuItem);
     }
 
-    btn.setAttribute("hudId", this.hudId);
-    btn.setAttribute("buttonType", prefKey);
-    btn.setAttribute("class", "hud-filter-btn");
-    let key = "btn" + aName;
-    btn.setAttribute("label", this.getStr(key));
-    key = "tip" + aName;
-    btn.setAttribute("tooltip", this.getStr(key));
-
-    if (aType == "checkbox") {
-      btn.setAttribute("checked", this.filterPrefs[prefKey]);
-      function toggle(btn) {
-        self.consoleFilterCommands.toggle(btn);
-      };
-
-      btn.setAttribute("oncommand", "HUDConsoleUI.toggleFilter(this);");
-    }
-    else {
-      var command = "HUDConsoleUI.command(this)";
-      btn.setAttribute("oncommand", command);
-    }
-    return btn;
+    toolbarButton.setAttribute("checked", allChecked);
   },
 
   /**
-   * Appends a category title and a series of buttons to the filter bar.
+   * Creates the close button on the toolbar.
    *
-   * @param nsIDOMNode aToolbar
-   *        The DOM node to which to add the category.
-   * @param string aTitle
-   *        The title for the category.
-   * @param Array aButtons
-   *        The buttons, specified as objects with "name" and "prefKey"
-   *        properties.
-   * @returns nsIDOMNode
+   * @param nsIDOMNode aParent
+   *        The toolbar to attach the close button to.
+   * @return void
    */
-  addButtonCategory: function(aToolbar, aTitle, aButtons) {
-    let lbl = this.makeXULNode("label");
-    lbl.setAttribute("class", "hud-filter-cat");
-    lbl.setAttribute("value", aTitle);
-    aToolbar.appendChild(lbl);
-
-    for (let i = 0; i < aButtons.length; i++) {
-      let btn = aButtons[i];
-      aToolbar.appendChild(this.makeButton(btn.name, btn.prefKey, "checkbox"));
+  makeCloseButton: function HUD_makeCloseButton(aToolbar)
+  {
+    function HUD_closeButton_onCommand() {
+      let tab = this.ownerDocument.defaultView.gBrowser.selectedTab;
+      HUDService.deactivateHUDForContext(tab);
     }
+
+    let closeButton = this.makeXULNode("toolbarbutton");
+    closeButton.classList.add("webconsole-close-button");
+    closeButton.addEventListener("command", HUD_closeButton_onCommand, false);
+
+    aToolbar.appendChild(closeButton);
+  },
+
+  /**
+   * Creates the "Clear Console" button.
+   *
+   * @param nsIDOMNode aParent
+   *        The toolbar to attach the "Clear Console" button to.
+   * @param string aHUDId
+   *        The ID of the console.
+   * @return void
+   */
+  makeClearConsoleButton: function HUD_makeClearConsoleButton(aToolbar)
+  {
+    let hudId = this.hudId;
+    function HUD_clearButton_onCommand() {
+      HUDService.clearDisplay(hudId);
+    }
+
+    let clearButton = this.makeXULNode("toolbarbutton");
+    clearButton.setAttribute("label", this.getStr("btnClear"));
+    clearButton.classList.add("webconsole-clear-console-button");
+    clearButton.addEventListener("command", HUD_clearButton_onCommand, false);
+
+    aToolbar.appendChild(clearButton);
   },
 
   createHUD: function HUD_createHUD()
   {
     let self = this;
     if (this.HUDBox) {
       return this.HUDBox;
     }
@@ -4568,22 +4617,16 @@ JSTermFirefoxMixin.prototype = {
 
     let inputNode = this.xulElementFactory("textbox");
     inputNode.setAttribute("class", "jsterm-input-node");
     inputNode.setAttribute("flex", "1");
     inputNode.setAttribute("multiline", "true");
     inputNode.setAttribute("rows", "1");
     inputContainer.appendChild(inputNode);
 
-    let closeButton = this.xulElementFactory("button");
-    closeButton.setAttribute("class", "jsterm-close-button");
-    inputContainer.appendChild(closeButton);
-    closeButton.addEventListener("command", HeadsUpDisplayUICommands.toggleHUD,
-                                 false);
-
     if (this.existingConsoleNode == undefined) {
       // create elements
       let term = this.xulElementFactory("vbox");
       term.setAttribute("class", "jsterm-wrapper-node");
       term.setAttribute("flex", "1");
 
       let outputNode = this.xulElementFactory("vbox");
       outputNode.setAttribute("class", "jsterm-output-node");
@@ -4899,37 +4942,94 @@ HeadsUpDisplayUICommands = {
       });
     }
     else {
       HUDService.activateHUDForContext(gBrowser.selectedTab, true);
       HUDService.animate(hudId, ANIMATE_IN);
     }
   },
 
-  toggleFilter: function UIC_toggleFilter(aButton) {
-    var filter = aButton.getAttribute("buttonType");
-    var hudId = aButton.getAttribute("hudId");
-    var state = HUDService.getFilterState(hudId, filter);
-    if (state) {
-      HUDService.setFilterState(hudId, filter, false);
-      aButton.setAttribute("checked", false);
+  /**
+   * The event handler that is called whenever a user switches a filter on or
+   * off.
+   *
+   * @param nsIDOMEvent aEvent
+   *        The event that triggered the filter change.
+   * @return boolean
+   */
+  toggleFilter: function UIC_toggleFilter(aEvent) {
+    let hudId = this.getAttribute("hudId");
+    switch (this.tagName) {
+      case "toolbarbutton": {
+        let originalTarget = aEvent.originalTarget;
+        let classes = originalTarget.classList;
+
+        if (originalTarget.localName !== "toolbarbutton") {
+          // Oddly enough, the click event is sent to the menu button when
+          // selecting a menu item with the mouse. Detect this case and bail
+          // out.
+          break;
+        }
+
+        if (!classes.contains("toolbarbutton-menubutton-button") &&
+            originalTarget.getAttribute("type") === "menu-button") {
+          // This is a filter button with a drop-down. The user clicked the
+          // drop-down, so do nothing. (The menu will automatically appear
+          // without our intervention.)
+          break;
+        }
+
+        let state = this.getAttribute("checked") !== "true";
+        this.setAttribute("checked", state);
+
+        // This is a filter button with a drop-down, and the user clicked the
+        // main part of the button. Go through all the severities and toggle
+        // their associated filters.
+        let menuItems = this.querySelectorAll("menuitem");
+        for (let i = 0; i < menuItems.length; i++) {
+          menuItems[i].setAttribute("checked", state);
+          let prefKey = menuItems[i].getAttribute("prefKey");
+          HUDService.setFilterState(hudId, prefKey, state);
+        }
+        break;
+      }
+
+      case "menuitem": {
+        let state = this.getAttribute("checked") !== "true";
+        this.setAttribute("checked", state);
+
+        let prefKey = this.getAttribute("prefKey");
+        HUDService.setFilterState(hudId, prefKey, state);
+
+        // Adjust the state of the button appropriately.
+        let menuPopup = this.parentNode;
+
+        let allChecked = true;
+        let menuItem = menuPopup.firstChild;
+        while (menuItem) {
+          if (menuItem.getAttribute("checked") !== "true") {
+            allChecked = false;
+            break;
+          }
+          menuItem = menuItem.nextSibling;
+        }
+
+        let toolbarButton = menuPopup.parentNode;
+        toolbarButton.setAttribute("checked", allChecked);
+        break;
+      }
     }
-    else {
-      HUDService.setFilterState(hudId, filter, true);
-      aButton.setAttribute("checked", true);
-    }
+
+    return true;
   },
 
   command: function UIC_command(aButton) {
     var filter = aButton.getAttribute("buttonType");
     var hudId = aButton.getAttribute("hudId");
     switch (filter) {
-      case "clear":
-        HUDService.clearDisplay(hudId);
-        break;
       case "selectAll":
         let outputNode = HUDService.getOutputNodeById(hudId);
         let chromeWindow = outputNode.ownerDocument.defaultView;
         let commandController = chromeWindow.commandController;
         commandController.selectAll(outputNode);
         break;
       case "saveBodies": {
         let checked = aButton.getAttribute("checked") === "true";
@@ -4946,18 +5046,21 @@ HeadsUpDisplayUICommands = {
 //////////////////////////////////////////////////////////////////////////
 
 var prefs = Services.prefs;
 
 const GLOBAL_STORAGE_INDEX_ID = "GLOBAL_CONSOLE";
 const PREFS_BRANCH_PREF = "devtools.hud.display.filter";
 const PREFS_PREFIX = "devtools.hud.display.filter.";
 const PREFS = { network: PREFS_PREFIX + "network",
+                networkinfo: PREFS_PREFIX + "networkinfo",
+                csserror: PREFS_PREFIX + "csserror",
                 cssparser: PREFS_PREFIX + "cssparser",
                 exception: PREFS_PREFIX + "exception",
+                jswarn: PREFS_PREFIX + "jswarn",
                 error: PREFS_PREFIX + "error",
                 info: PREFS_PREFIX + "info",
                 warn: PREFS_PREFIX + "warn",
                 log: PREFS_PREFIX + "log",
                 global: PREFS_PREFIX + "global",
               };
 
 function ConsoleStorage()
@@ -4988,58 +5091,71 @@ function ConsoleStorage()
 
   // TODO: for FINAL release,
   // use the sitePreferencesService to save specific site prefs
   // see bug 570545
 
   if (filterPrefs) {
     defaultDisplayPrefs = {
       network: (prefs.getBoolPref(PREFS.network) ? true: false),
+      networkinfo: (prefs.getBoolPref(PREFS.networkinfo) ? true: false),
+      csserror: (prefs.getBoolPref(PREFS.csserror) ? true: false),
       cssparser: (prefs.getBoolPref(PREFS.cssparser) ? true: false),
       exception: (prefs.getBoolPref(PREFS.exception) ? true: false),
+      jswarn: (prefs.getBoolPref(PREFS.jswarn) ? true: false),
       error: (prefs.getBoolPref(PREFS.error) ? true: false),
       info: (prefs.getBoolPref(PREFS.info) ? true: false),
       warn: (prefs.getBoolPref(PREFS.warn) ? true: false),
       log: (prefs.getBoolPref(PREFS.log) ? true: false),
       global: (prefs.getBoolPref(PREFS.global) ? true: false),
     };
   }
   else {
     prefs.setBoolPref(PREFS_BRANCH_PREF, false);
     // default prefs for each HeadsUpDisplay
     prefs.setBoolPref(PREFS.network, true);
+    prefs.setBoolPref(PREFS.networkinfo, true);
+    prefs.setBoolPref(PREFS.csserror, true);
     prefs.setBoolPref(PREFS.cssparser, true);
     prefs.setBoolPref(PREFS.exception, true);
+    prefs.setBoolPref(PREFS.jswarn, true);
     prefs.setBoolPref(PREFS.error, true);
     prefs.setBoolPref(PREFS.info, true);
     prefs.setBoolPref(PREFS.warn, true);
     prefs.setBoolPref(PREFS.log, true);
     prefs.setBoolPref(PREFS.global, false);
 
     defaultDisplayPrefs = {
       network: prefs.getBoolPref(PREFS.network),
+      networkinfo: prefs.getBoolPref(PREFS.networkinfo),
+      csserror: prefs.getBoolPref(PREFS.csserror),
       cssparser: prefs.getBoolPref(PREFS.cssparser),
       exception: prefs.getBoolPref(PREFS.exception),
+      jswarn: prefs.getBoolPref(PREFS.jswarn),
       error: prefs.getBoolPref(PREFS.error),
       info: prefs.getBoolPref(PREFS.info),
       warn: prefs.getBoolPref(PREFS.warn),
       log: prefs.getBoolPref(PREFS.log),
       global: prefs.getBoolPref(PREFS.global),
     };
   }
   this.defaultDisplayPrefs = defaultDisplayPrefs;
 }
 
 ConsoleStorage.prototype = {
 
   updateDefaultDisplayPrefs:
   function CS_updateDefaultDisplayPrefs(aPrefsObject) {
     prefs.setBoolPref(PREFS.network, (aPrefsObject.network ? true : false));
+    prefs.setBoolPref(PREFS.networkinfo,
+                      (aPrefsObject.networkinfo ? true : false));
+    prefs.setBoolPref(PREFS.csserror, (aPrefsObject.csserror ? true : false));
     prefs.setBoolPref(PREFS.cssparser, (aPrefsObject.cssparser ? true : false));
     prefs.setBoolPref(PREFS.exception, (aPrefsObject.exception ? true : false));
+    prefs.setBoolPref(PREFS.jswarn, (aPrefsObject.jswarn ? true : false));
     prefs.setBoolPref(PREFS.error, (aPrefsObject.error ? true : false));
     prefs.setBoolPref(PREFS.info, (aPrefsObject.info ? true : false));
     prefs.setBoolPref(PREFS.warn, (aPrefsObject.warn ? true : false));
     prefs.setBoolPref(PREFS.log, (aPrefsObject.log ? true : false));
     prefs.setBoolPref(PREFS.global, (aPrefsObject.global ? true : false));
   },
 
   sequenceId: function CS_sequencerId()
--- a/toolkit/components/console/hudservice/tests/browser/Makefile.in
+++ b/toolkit/components/console/hudservice/tests/browser/Makefile.in
@@ -47,16 +47,17 @@ include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
 	browser_webconsole_bug_580030_errors_after_page_reload.js \
 	browser_webconsole_basic_net_logging.js \
 	browser_webconsole_bug_579412_input_focus.js \
 	browser_webconsole_bug_580001_closing_after_completion.js \
 	browser_webconsole_bug_580400_groups.js \
 	browser_webconsole_bug_588730_text_node_insertion.js \
+	browser_webconsole_bug_601667_filter_buttons.js \
 	browser_webconsole_bug_597136_external_script_errors.js \
 	browser_webconsole_bug_597136_network_requests_from_chrome.js \
 	browser_webconsole_completion.js \
 	browser_webconsole_console_logging_api.js \
 	browser_webconsole_consoleonpage.js \
 	browser_webconsole_chrome.js \
 	browser_webconsole_display_accessors.js \
 	browser_webconsole_execution_scope.js \
--- a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_581231_close_button.js
+++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_581231_close_button.js
@@ -22,17 +22,17 @@ function testCloseButton() {
 
   openConsole();
 
   hudId = HUDService.displaysIndex()[0];
   hudBox = HUDService.getHeadsUpDisplay(hudId);
 
   HUDService.disableAnimation(hudId);
   executeSoon(function() {
-    let closeButton = hudBox.querySelector(".jsterm-close-button");
+    let closeButton = hudBox.querySelector(".webconsole-close-button");
     ok(closeButton != null, "we have the close button");
 
     // XXX: ASSERTION: ###!!! ASSERTION: XPConnect is being called on a scope without a 'Components' property!: 'Error', file /home/ddahl/code/moz/mozilla-central/mozilla-central/js/src/xpconnect/src/xpcwrappednativescope.cpp, line 795
 
     EventUtils.synthesizeMouse(closeButton, 0, 0, {});
 
     executeSoon(function (){
       ok(!(hudId in HUDService.windowRegistry), "the console is closed when " +
new file mode 100644
--- /dev/null
+++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_bug_601667_filter_buttons.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the filter button UI logic works correctly.
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", testFilterButtons, true);
+}
+
+function testFilterButtons() {
+  browser.removeEventListener("load", testFilterButtons, true);
+  openConsole();
+
+  hudId = HUDService.displaysIndex()[0];
+  hudBox = HUDService.hudReferences[hudId].HUDBox;
+
+  testMenuFilterButton("net");
+  testMenuFilterButton("css");
+  testMenuFilterButton("js");
+  testMenuFilterButton("webdev");
+
+  finishTest();
+}
+
+function testMenuFilterButton(aCategory) {
+  let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]";
+  let button = hudBox.querySelector(selector);
+  ok(button, "we have the \"" + aCategory + "\" button");
+
+  let firstMenuItem = button.querySelector("menuitem");
+  ok(firstMenuItem, "we have the first menu item for the \"" + aCategory +
+     "\" button");
+
+  // Turn all the filters off, if they were on.
+  let menuItem = firstMenuItem;
+  while (menuItem != null) {
+    if (isChecked(menuItem)) {
+      chooseMenuItem(menuItem);
+    }
+    menuItem = menuItem.nextSibling;
+  }
+
+  // Turn all the filters on; make sure the button gets checked.
+  menuItem = firstMenuItem;
+  let prefKey;
+  while (menuItem) {
+    prefKey = menuItem.getAttribute("prefKey");
+    chooseMenuItem(menuItem);
+    ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+       aCategory + " is checked after clicking it");
+    ok(HUDService.filterPrefs[hudId][prefKey], prefKey + " messages are " +
+       "on after clicking the appropriate menu item");
+    menuItem = menuItem.nextSibling;
+  }
+  ok(isChecked(button), "the button for category " + aCategory + " is " +
+     "checked after turning on all its menu items");
+
+  // Turn one filter off; make sure the button is no longer checked.
+  prefKey = firstMenuItem.getAttribute("prefKey");
+  chooseMenuItem(firstMenuItem);
+  ok(!isChecked(firstMenuItem), "the first menu item for category " +
+     aCategory + " is no longer checked after clicking it");
+  ok(!HUDService.filterPrefs[hudId][prefKey], prefKey + " messages are " +
+     "turned off after clicking the appropriate menu item");
+  ok(!isChecked(button), "the button for category " + aCategory + " is no " +
+     "longer checked after turning off its first menu item");
+
+  // Turn all the filters on by clicking the main part of the button.
+  let anonymousNodes = document.getAnonymousNodes(button);
+  let subbutton;
+  for (let i = 0; i < anonymousNodes.length; i++) {
+    let node = anonymousNodes[i];
+    if (node.classList.contains("toolbarbutton-menubutton-button")) {
+      subbutton = node;
+      break;
+    }
+  }
+  ok(subbutton, "we have the subbutton for category " + aCategory);
+
+  clickButton(subbutton);
+  ok(isChecked(button), "the button for category " + aCategory + " is " +
+     "checked after clicking its main part");
+
+  menuItem = firstMenuItem;
+  while (menuItem) {
+    let prefKey = menuItem.getAttribute("prefKey");
+    ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+       aCategory + " is checked after clicking the button");
+    ok(HUDService.filterPrefs[hudId][prefKey], prefKey + " messages are " +
+       "on after clicking the button");
+    menuItem = menuItem.nextSibling; i++;
+  }
+
+  // Turn all the filters off by clicking the main part of the button.
+  clickButton(subbutton);
+  ok(!isChecked(subbutton), "the button for category " + aCategory + " is " +
+     "no longer checked after clicking it");
+
+  menuItem = firstMenuItem;
+  while (menuItem) {
+    let prefKey = menuItem.getAttribute("prefKey");
+    ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+       aCategory + " is no longer checked after clicking the button");
+    ok(!HUDService.filterPrefs[hudId][prefKey], prefKey + " messages are " +
+       "off after clicking the button");
+    menuItem = menuItem.nextSibling;
+  }
+
+  // Turn all the filters on again by clicking the button.
+  clickButton(subbutton);
+}
+
+function clickButton(aNode) {
+  EventUtils.sendMouseEvent({ type: "click" }, aNode);
+}
+
+function chooseMenuItem(aNode) {
+  let event = document.createEvent("XULCommandEvent");
+  event.initCommandEvent("command", true, true, window, 0, false, false, false,
+                         false, null);
+  aNode.dispatchEvent(event);
+}
+
+function isChecked(aNode) {
+  return aNode.getAttribute("checked") === "true";
+}
+