Bug 626076 - Add spellcheck UI in the context menu, à la Firefox, for all HTML Textareas r=sid0 ui-r=clarkbw
authorJonathan Protzenko <jonathan.protzenko@gmail.com>
Tue, 08 Mar 2011 17:12:38 +0100
changeset 7283 d02ad4f0a6dbc01bd187d32dd0ac991ed25bca35
parent 7282 ef7477439a051877edc96613a184757ab43cd8d3
child 7284 19ab09dcea5566cd86e192e1676a176ae30c1ae3
push idunknown
push userunknown
push dateunknown
reviewerssid0, clarkbw
bugs626076
Bug 626076 - Add spellcheck UI in the context menu, à la Firefox, for all HTML Textareas r=sid0 ui-r=clarkbw This bug add entries in the context menu to choose a different language when spellchecking textareas. This works for content tabs, but also for in-3-pane HTML textareas, the main use case being Thunderbird Conversation's quick reply faeture, of course.
mail/base/content/mailWindowOverlay.xul
mail/base/content/messenger.xul
mail/base/content/nsContextMenu.js
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/test/mozmill/content-tabs/html/whatsnew.html
mail/test/mozmill/content-tabs/test-about-support.js
mail/test/mozmill/content-tabs/test-content-tab.js
mail/test/mozmill/shared-modules/test-content-tab-helpers.js
mail/test/mozmill/shared-modules/test-dom-helpers.js
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -56,16 +56,18 @@
 
 <!DOCTYPE overlay [
   <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
   %messengerDTD;
   <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd" >
   %msgViewPickerDTD;
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
   %brandDTD;
+  <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
+  %textcontextDTD;
 ]>
 
 <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
 <script type="application/javascript" src="chrome://messenger/content/mailCommands.js"/>
 <script type="application/javascript" src="chrome://messenger/content/junkCommands.js"/>
 <script type="application/javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
 <script type="application/javascript" src="chrome://messenger/content/mailTabs.js"/>
@@ -792,16 +794,48 @@
               label="&saveImageAsCmd.label;"
               accesskey="&saveImageAsCmd.accesskey;"
               oncommand="gContextMenu.saveImage();"/>
     <menuseparator id="mailContext-sep-reportPhishing"/>
     <menuitem id="mailContext-reportPhishingURL"
               label="&reportPhishingURL.label;"
               accesskey="&reportPhishingURL.accesskey;"
               oncommand="gPhishingDetector.reportPhishingURL(gContextMenu.linkURL);"/>
+
+    <!-- Spellchecking menu items -->
+    <menuseparator id="mailContext-spell-suggestions-separator"/>
+    <menuitem id="mailContext-spell-no-suggestions"
+              disabled="true"
+              label="&spellNoSuggestions.label;"/>
+    <menuitem id="mailContext-spell-add-to-dictionary"
+              label="&spellAddToDictionary.label;"
+              accesskey="&spellAddToDictionary.accesskey;"
+              oncommand="gSpellChecker.addToDictionary();"/>
+    <menuseparator id="mailContext-spell-separator"/>
+    <menuitem id="mailContext-spell-check-enabled"
+              label="&spellCheckEnable.label;"
+              type="checkbox"
+              accesskey="&spellCheckEnable.accesskey;"
+              oncommand="gSpellChecker.toggleEnabled();"/>
+    <menuitem id="mailContext-spell-add-dictionaries-main"
+              label="&spellAddDictionaries.label;"
+              accesskey="&spellAddDictionaries.accesskey;"
+              oncommand="gContextMenu.addDictionaries();"/>
+    <menu id="mailContext-spell-dictionaries"
+          label="&spellDictionaries.label;"
+          accesskey="&spellDictionaries.accesskey;">
+        <menupopup id="mailContext-spell-dictionaries-menu">
+            <menuseparator id="mailContext-spell-language-separator"/>
+            <menuitem id="mailContext-spell-add-dictionaries"
+                      label="&spellAddDictionaries.label;"
+                      accesskey="&spellAddDictionaries.accesskey;"
+                      oncommand="gContextMenu.addDictionaries();"/>
+        </menupopup>
+    </menu>
+
   </menupopup>
 
   <menupopup id="folderPaneContext"
              onpopupshowing="return fillFolderPaneContextMenu();"
              onpopuphiding="if (event.target == this) folderPaneOnPopupHiding();">
     <menuitem id="folderPaneContext-getMessages"
               label="&folderContextGetMessages.label;"
               accesskey="&folderContextGetMessages.accesskey;"
--- a/mail/base/content/messenger.xul
+++ b/mail/base/content/messenger.xul
@@ -54,16 +54,18 @@
 
 <!DOCTYPE window [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 %brandDTD;
 <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
 %messengerDTD;
 <!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd">
 %customizeToolbarDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
+%textcontextDTD;
 ]>
 
 <!--
   - The 'what you think of when you think of thunderbird' window;
   -  3-pane view inside of tabs.
   -->
 <window id="messengerWindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
--- a/mail/base/content/nsContextMenu.js
+++ b/mail/base/content/nsContextMenu.js
@@ -35,16 +35,25 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
+Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
+var gSpellChecker = new InlineSpellChecker();
+
+function getDictionaryURL() {
+  let formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+                  .getService(Components.interfaces.nsIURLFormatter);
+  return formatter.formatURLPref("spellchecker.dictionaries.download.url");
+}
+
 function nsContextMenu(aXulMenu) {
   this.target         = null;
   this.menu           = null;
   this.onTextInput    = false;
   this.onImage        = false;
   this.onLoadedImage  = false;
   this.onCanvas       = false;
   this.onVideo        = false;
@@ -90,18 +99,63 @@ nsContextMenu.prototype = {
     this.initItems();
   },
   initItems : function CM_initItems() {
     this.initSaveItems();
     this.initClipboardItems();
     this.initMediaPlayerItems();
     this.initBrowserItems();
     this.initMessageItems();
+    this.initSpellingItems();
     this.initSeparators();
   },
+  addDictionaries: function CM_addDictionaries() {
+    openURL(getDictionaryURL());
+  },
+  initSpellingItems: function CM_initSpellingItems() {
+    let canSpell = gSpellChecker.canSpellCheck;
+    let onMisspelling = gSpellChecker.overMisspelling;
+    this.showItem("mailContext-spell-check-enabled", canSpell);
+    this.showItem("mailContext-spell-separator", canSpell || this.onEditableArea);
+    if (canSpell) {
+      document.getElementById("mailContext-spell-check-enabled")
+              .setAttribute("checked", gSpellChecker.enabled);
+    }
+
+    this.showItem("mailContext-spell-add-to-dictionary", onMisspelling);
+
+    // suggestion list
+    this.showItem("mailContext-spell-suggestions-separator", onMisspelling);
+    if (onMisspelling) {
+      let addMenuItem =
+        document.getElementById("mailContext-spell-add-to-dictionary");
+      let suggestionCount =
+        gSpellChecker.addSuggestionsToMenu(addMenuItem.parentNode,
+                                           addMenuItem, 5);
+      this.showItem("mailContext-spell-no-suggestions", suggestionCount == 0);
+    } else {
+      this.showItem("mailContext-spell-no-suggestions", false);
+    }
+
+    // dictionary list
+    this.showItem("mailContext-spell-dictionaries", gSpellChecker.enabled);
+    if (canSpell) {
+      let dictMenu = document.getElementById("mailContext-spell-dictionaries-menu");
+      let dictSep = document.getElementById("mailContext-spell-language-separator");
+      gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep);
+      this.showItem("mailContext-spell-add-dictionaries-main", false);
+    } else if (this.onEditableArea) {
+      // when there is no spellchecker but we might be able to spellcheck
+      // add the add to dictionaries item. This will ensure that people
+      // with no dictionaries will be able to download them
+      this.showItem("mailContext-spell-add-dictionaries-main", true);
+    } else {
+      this.showItem("mailContext-spell-add-dictionaries-main", false);
+    }
+  },
   initSaveItems : function CM_initSaveItems() {
     this.showItem("mailContext-savelink", this.onSaveableLink);
     this.showItem("mailContext-saveimage", this.onLoadedImage);
   },
   initClipboardItems : function CM_initClipboardItems() {
     // Copy depends on whether there is selected text.
     // Enabling this context menu item is now done through the global
     // command updating system.
@@ -286,28 +340,41 @@ nsContextMenu.prototype = {
   initSeparators: function CM_initSeparators() {
     const mailContextSeparators = [
       "mailContext-sep-open-browser", "mailContext-sep-link",
       "mailContext-sep-open", "mailContext-sep-open2",
       "mailContext-sep-reply", "paneContext-afterMove",
       "mailContext-sep-afterTagAddNew", "mailContext-sep-afterTagRemoveAll",
       "mailContext-sep-afterMarkAllRead", "mailContext-sep-afterMarkFlagged",
       "mailContext-sep-afterMarkMenu", "mailContext-sep-edit",
-      "mailContext-sep-copy", "mailContext-sep-reportPhishing"
+      "mailContext-sep-copy", "mailContext-sep-reportPhishing",
+      "mailContext-spell-suggestions-separator", "mailContext-spell-separator",
     ];
     mailContextSeparators.forEach(this.hideIfAppropriate, this);
 
     this.checkLastSeparator(this.menu);
   },
 
   /**
    * Set the nsContextMenu properties based on the selected node and
    * its ancestors.
    */
   setTarget : function CM_setTarget(aNode) {
+    // Clear any old spellchecking items from the menu, this used to
+    // be in the menu hiding code but wasn't getting called in all
+    // situations. Here, we can ensure it gets cleaned up any time the
+    // menu is shown. Note: must be before uninit because that clears the
+    // internal vars
+    // We also need to do that before we possibly bail because we just clicked
+    // on some XUL node. Otherwise, dictionary choices just accumulate until we
+    // right-click on some HTML element again.
+    gSpellChecker.clearSuggestionsFromMenu();
+    gSpellChecker.clearDictionaryListFromMenu();
+    gSpellChecker.uninit();
+
     const xulNS =
       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     if (aNode.namespaceURI == xulNS) {
       if (aNode.localName == "treecol") {
         // The column header was clicked, show the column picker.
         let treecols = aNode.parentNode;
         let nodeList = document.getAnonymousNodes(treecols);
         let treeColPicker;
@@ -351,16 +418,21 @@ nsContextMenu.prototype = {
         if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
           this.onLoadedImage = true;
 
         this.imageURL = this.target.currentURI.spec;
       } else if (this.target instanceof HTMLInputElement) {
         this.onTextInput = this.isTargetATextBox(this.target);
       } else if (this.target instanceof HTMLTextAreaElement) {
         this.onTextInput = true;
+        if (!this.target.readOnly) {
+          this.onEditableArea = true;
+          gSpellChecker.init(this.target.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor);
+          gSpellChecker.initFromEvent(document.popupRangeParent, document.popupRangeOffset);
+        }
       } else if (this.target instanceof HTMLCanvasElement) {
         this.onCanvas = true;
       } else if (this.target instanceof HTMLVideoElement) {
         this.onVideo = true;
         this.onPlayableMedia = true;
         this.mediaURL = this.target.currentSrc || this.target.src;
       } else if (this.target instanceof HTMLAudioElement) {
         this.onAudio = true;
--- a/mail/locales/en-US/chrome/messenger/messenger.dtd
+++ b/mail/locales/en-US/chrome/messenger/messenger.dtd
@@ -645,16 +645,20 @@ you can use these alternative items. Oth
 <!ENTITY starredColumn.tooltip "Click to sort by star">
 <!ENTITY locationColumn.tooltip "Click to sort by location">
 <!ENTITY idColumn.tooltip "Click to sort by order received">
 <!ENTITY attachmentColumn.tooltip "Click to sort by attachments">
 
 <!-- Thread Pane Context Menu -->
 <!ENTITY contextOpenNewWindow.label "Open Message in New Window">
 <!ENTITY contextOpenNewWindow.accesskey "W">
+<!-- The key potentially conflicts with cutCmd.accessKey which is defined in
+     textcontext.dtd from toolkit. Right now, both menu items can't be visible
+     at the same time, but should someone enable copy/paste of message, this key
+     would probably need to be changed. -->
 <!ENTITY contextOpenNewTab.label "Open Message in New Tab">
 <!ENTITY contextOpenNewTab.accesskey "T">
 <!ENTITY contextOpenConversation.label "Open Message in Conversation">
 <!ENTITY contextOpenConversation.accesskey "n">
 <!ENTITY contextEditAsNew.label "Edit As New…">
 <!ENTITY contextEditAsNew.accesskey "E">
 <!ENTITY contextArchive.label "Archive">
 <!ENTITY contextArchive.accesskey "h">
@@ -807,29 +811,25 @@ you can use these alternative items. Oth
 <!ENTITY CopyNewsgroupName.accesskey "C">
 <!ENTITY CopyNewsgroupURL.label "Copy Newsgroup URL">
 <!ENTITY CopyNewsgroupURL.accesskey "U">
 <!ENTITY CreateFilterFrom.label "Create Filter From…">
 <!ENTITY CreateFilterFrom.accesskey "F">
 <!ENTITY reportPhishingURL.label "Report Email Scam">
 <!ENTITY reportPhishingURL.accesskey "o">
 
+<!-- Spell checker context menu items -->
+<!ENTITY spellAddDictionaries.label "Add Dictionaries…">
+<!ENTITY spellAddDictionaries.accesskey "A">
+
 <!-- Content Pane Context Menu -->
 <!ENTITY saveLinkAsCmd.label "Save Link As…">
 <!ENTITY saveLinkAsCmd.accesskey "k">
 <!ENTITY saveImageAsCmd.label "Save Image As…">
 <!ENTITY saveImageAsCmd.accesskey "v">
-<!ENTITY cutCmd.label "Cut">
-<!ENTITY cutCmd.accesskey "u">
-<!ENTITY copyCmd.label "Copy">
-<!ENTITY copyCmd.accesskey "C">
-<!ENTITY pasteCmd.label "Paste">
-<!ENTITY pasteCmd.accesskey "P">
-<!ENTITY selectAllCmd.label "Select All">
-<!ENTITY selectAllCmd.accesskey "A">
 <!ENTITY copyLinkCmd.label "Copy Link Location">
 <!ENTITY copyLinkCmd.accesskey "C">
 <!ENTITY copyImageAllCmd.label "Copy Image">
 <!ENTITY copyImageAllCmd.accesskey "I">
 <!ENTITY copyEmailCmd.label "Copy Email Address">
 <!ENTITY copyEmailCmd.accesskey "E">
 <!ENTITY stopCmd.label "Stop">
 <!ENTITY stopCmd.accesskey "S">
--- a/mail/test/mozmill/content-tabs/html/whatsnew.html
+++ b/mail/test/mozmill/content-tabs/html/whatsnew.html
@@ -1,9 +1,10 @@
 <html>
   <head>
     <title>What's New Content Test</title> 
     <link rel="icon shortcut" href="whatsnew.png"/>
   </head>
   <body bgcolor="#FFFFFF">
     <h1>What's New Content Test</h1>
+    <textarea>Zombocom</textarea>
   </body>
 </html>
--- a/mail/test/mozmill/content-tabs/test-about-support.js
+++ b/mail/test/mozmill/content-tabs/test-about-support.js
@@ -210,23 +210,23 @@ function test_private_data() {
   let tab = open_about_support();
   let checkbox = content_tab_e(tab, "check-show-private-data");
   // We use the profile button's div as an example of a public-only element, and
   // the profile directory display as an example of a private-only element.
   let privateElem = content_tab_e(tab, "profile-dir-box");
   let publicElem = content_tab_e(tab, "profile-dir-button-box");
   assert_true(!checkbox.checked,
               "Private data checkbox shouldn't be checked by default");
-  assert_element_visible(tab, publicElem);
-  assert_element_hidden(tab, privateElem);
+  assert_content_tab_element_visible(tab, publicElem);
+  assert_content_tab_element_hidden(tab, privateElem);
 
   // Now check the checkbox and see what happens
   checkbox.click();
-  wait_for_element_display_value(tab, publicElem, "none");
-  wait_for_element_display_value(tab, privateElem, "inline");
+  wait_for_content_tab_element_display_value(tab, publicElem, "none");
+  wait_for_content_tab_element_display_value(tab, privateElem, "inline");
   close_tab(tab);
 }
 
 /**
  * Test (well, sort of) the copy to clipboard function with public data.
  */
 function test_copy_to_clipboard_public() {
   let tab = open_about_support();
@@ -264,17 +264,17 @@ function test_copy_to_clipboard_private(
     "chrome://messenger/locale/aboutSupportMail.properties");
   let warningText = bundle.GetStringFromName("warningText");
 
   let tab = open_about_support();
 
   // Display private data.
   let privateElem = content_tab_e(tab, "profile-dir-box");
   content_tab_e(tab, "check-show-private-data").click();
-  wait_for_element_display_value(tab, privateElem, "inline");
+  wait_for_content_tab_element_display_value(tab, privateElem, "inline");
 
   // To avoid destroying the current contents of the clipboard, instead of
   // actually copying to it, we just retrieve what would have been copied to it
   let transferable = tab.browser.contentWindow.getClipboardTransferable();
   for (let [, flavor] in Iterator(["text/html", "text/unicode"])) {
     let data = {};
     transferable.getTransferData(flavor, data, {});
     let text = data.value.data;
@@ -338,17 +338,17 @@ function test_send_via_email_private() {
     "chrome://messenger/locale/aboutSupportMail.properties");
   let warningText = bundle.GetStringFromName("warningText");
 
   let tab = open_about_support();
 
   // Display private data.
   let privateElem = content_tab_e(tab, "profile-dir-box");
   content_tab_e(tab, "check-show-private-data").click();
-  wait_for_element_display_value(tab, privateElem, "inline");
+  wait_for_content_tab_element_display_value(tab, privateElem, "inline");
 
   let cwc = open_send_via_email(tab);
 
   let contentFrame = cwc.e("content-frame");
   let text = contentFrame.contentDocument.body.innerHTML;
 
   for (let [, str] in Iterator(ABOUT_SUPPORT_STRINGS)) {
     if (text.indexOf(str) == -1)
--- a/mail/test/mozmill/content-tabs/test-content-tab.js
+++ b/mail/test/mozmill/content-tabs/test-content-tab.js
@@ -36,18 +36,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 var MODULE_NAME = 'test-content-tab';
 
 var RELATIVE_ROOT = '../shared-modules';
 var MODULE_REQUIRES = ['folder-display-helpers', 'content-tab-helpers'];
 
 var controller = {};
-Components.utils.import('resource://mozmill/modules/controller.js', controller)
-;
+Components.utils.import('resource://mozmill/modules/controller.js', controller);
 var mozmill = {};
 Components.utils.import('resource://mozmill/modules/mozmill.js', mozmill);
 var elementslib = {};
 Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
 Components.utils.import('resource://gre/modules/Services.jsm');
 
 // RELATIVE_ROOT messes with the collector, so we have to bring the path back
 // so we get the right path for the resources.
@@ -56,16 +55,18 @@ Components.utils.import('resource://gre/
 var url = collector.addHttpResource('../content-tabs/html', '');
 var whatsUrl = url + "whatsnew.html";
 
 var setupModule = function (module) {
   let fdh = collector.getModule('folder-display-helpers');
   fdh.installInto(module);
   let cth = collector.getModule('content-tab-helpers');
   cth.installInto(module);
+  let dh = collector.getModule('dom-helpers');
+  dh.installInto(module);
 };
 
 function test_content_tab_open() {
   // Set the pref so that what's new opens a local url
   Services.prefs.setCharPref("mailnews.start_page.override_url", whatsUrl);
 
   let tab = open_content_tab_with_click(mc.menus.helpMenu.whatsNew);
 
@@ -74,16 +75,55 @@ function test_content_tab_open() {
   // Check the location of the what's new image, this is via the link element
   // and therefore should be set and not favicon.png.
   assert_content_tab_has_favicon(tab, url + "whatsnew.png");
 
   // Check that window.content is set up correctly wrt content-primary and
   // content-targetable.
   if (mc.window.content.location != whatsUrl)
     throw new Error("window.content is not set to the url loaded, incorrect type=\"...\"?");
+
+}
+
+/**
+ * Just make sure that the context menu does what we expect in content tabs wrt.
+ * spell checking options.
+ */
+function test_spellcheck_in_content_tabs() {
+  let tabmail = mc.tabmail;
+  let w = tabmail.selectedTab.browser.contentWindow;
+  let textarea = w.document.getElementsByTagName("textarea")[0];
+
+  // Test a few random items
+  mc.rightClick(new elementslib.Elem(textarea));
+  assert_element_visible("mailContext-spell-dictionaries");
+  assert_element_visible("mailContext-spell-check-enabled");
+  assert_element_not_visible("mailContext-replySender"); // we're in a content tab!
+  close_popup();
+
+  // Different test
+  mc.rightClick(new elementslib.Elem(w.document.body.firstElementChild));
+  assert_element_not_visible("mailContext-spell-dictionaries");
+  assert_element_not_visible("mailContext-spell-check-enabled");
+  close_popup();
+
+  // Right-click on "zombocom" and add to dictionary
+  EventUtils.synthesizeMouse(textarea, 5, 5,
+                             {type: "contextmenu", button: 2}, w);
+  let suggestions = mc.window.document.getElementsByClassName("spell-suggestion");
+  assert_true(suggestions.length > 0, "What, is zombocom a registered word now?");
+  mc.click(mc.eid("mailContext-spell-add-to-dictionary"));
+  close_popup();
+
+  // Now check we don't have any suggestionss
+  EventUtils.synthesizeMouse(textarea, 5, 5,
+                             {type: "contextmenu", button: 2}, w);
+  let suggestions = mc.window.document.getElementsByClassName("spell-suggestion");
+  assert_true(suggestions.length == 0, "But I just taught you this word!");
+  close_popup();
 }
 
 function test_content_tab_open_same() {
   let preCount = mc.tabmail.tabContainer.childNodes.length;
 
   mc.click(new elementslib.Elem(mc.menus.helpMenu.whatsNew));
 
   controller.sleep(0);
--- a/mail/test/mozmill/shared-modules/test-content-tab-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-content-tab-helpers.js
@@ -76,20 +76,20 @@ function installInto(module) {
   module.open_content_tab_with_url = open_content_tab_with_url;
   module.open_content_tab_with_click = open_content_tab_with_click;
   module.plan_for_content_tab_load = plan_for_content_tab_load;
   module.wait_for_content_tab_load = wait_for_content_tab_load;
   module.assert_content_tab_has_url = assert_content_tab_has_url;
   module.assert_content_tab_has_favicon = assert_content_tab_has_favicon;
   module.content_tab_e = content_tab_e;
   module.content_tab_eid = content_tab_eid;
-  module.get_element_display = get_element_display;
-  module.assert_element_hidden = assert_element_hidden;
-  module.assert_element_visible = assert_element_visible;
-  module.wait_for_element_display_value = wait_for_element_display_value;
+  module.get_content_tab_element_display = get_content_tab_element_display;
+  module.assert_content_tab_element_hidden = assert_content_tab_element_hidden;
+  module.assert_content_tab_element_visible = assert_content_tab_element_visible;
+  module.wait_for_content_tab_element_display_value = wait_for_content_tab_element_display_value;
   module.assert_content_tab_text_present = assert_content_tab_text_present;
   module.assert_content_tab_text_absent = assert_content_tab_text_absent;
 }
 
 /**
  * Opens a content tab with the given URL.
  *
  * @param aURL The URL to load (string).
@@ -230,49 +230,49 @@ function assert_content_tab_has_favicon(
  */
 function content_tab_eid(aTab, aId) {
   return new elib.Elem(content_tab_e(aTab, aId));
 }
 
 /**
  * Returns the current "display" style property of an element.
  */
-function get_element_display(aTab, aElem) {
+function get_content_tab_element_display(aTab, aElem) {
   let style = aTab.browser.contentWindow.getComputedStyle(aElem);
   return style.getPropertyValue("display");
 }
 
 /**
  * Asserts that the given element is hidden from view on the page.
  */
-function assert_element_hidden(aTab, aElem) {
-  let display = get_element_display(aTab, aElem);
+function assert_content_tab_element_hidden(aTab, aElem) {
+  let display = get_content_tab_element_display(aTab, aElem);
   if (display != "none") {
     mark_failure(["Element", aElem, "should be hidden but has display", display,
                   "instead"]);
   }
 }
 
 /**
  * Asserts that the given element is visible on the page.
  */
-function assert_element_visible(aTab, aElem) {
-  let display = get_element_display(aTab, aElem);
+function assert_content_tab_element_visible(aTab, aElem) {
+  let display = get_content_tab_element_display(aTab, aElem);
   if (display != "inline") {
     mark_failure(["Element", aElem, "should be visible but has display", display,
                   "instead"]);
   }
 }
 
 /**
  * Waits for the element's display property to be the given value.
  */
-function wait_for_element_display_value(aTab, aElem, aValue) {
+function wait_for_content_tab_element_display_value(aTab, aElem, aValue) {
   function isValue() {
-    return get_element_display(aTab, aElem) == aValue;
+    return get_content_tab_element_display(aTab, aElem) == aValue;
   }
   if (!controller.waitForEval("subject()", NORMAL_TIMEOUT, FAST_INTERVAL,
                               isValue)) {
     mark_failure(["Timeout waiting for element", aElem, "to have display value",
                   aValue]);
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-dom-helpers.js
@@ -0,0 +1,113 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jonathan Protzenko <jonathan.protzenko@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+var elib = {};
+Cu.import('resource://mozmill/modules/elementslib.js', elib);
+var mozmill = {};
+Cu.import('resource://mozmill/modules/mozmill.js', mozmill);
+var controller = {};
+Cu.import('resource://mozmill/modules/controller.js', controller);
+
+const MODULE_NAME = 'dom-helpers';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+// we need this for the main controller
+const MODULE_REQUIRES = ['folder-display-helpers'];
+
+const NORMAL_TIMEOUT = 6000;
+const FAST_TIMEOUT = 1000;
+const FAST_INTERVAL = 100;
+
+var folderDisplayHelper;
+var mc;
+
+// logHelper (and therefore folderDisplayHelper) exports
+var mark_failure;
+
+function setupModule() {
+  folderDisplayHelper = collector.getModule('folder-display-helpers');
+  mc = folderDisplayHelper.mc;
+  mark_failure = folderDisplayHelper.mark_failure;
+}
+
+function installInto(module) {
+  setupModule();
+
+  // Now copy helper functions
+  module.assert_element_visible = assert_element_visible;
+  module.assert_element_not_visible = assert_element_not_visible;
+}
+
+/**
+ * This function takes either a string or an elementlibs.Elem, and returns
+ * whether it is hidden or not (simply by poking at its hidden property). It
+ * doesn't try to do anything smart, like is it not into view, or whatever.
+ *
+ * @param aElt The element to query.
+ * @return Whether the element is visible or not.
+ */
+function element_visible(aElt) {
+  let e;
+  if (typeof aElt == "string") {
+    e = mc.eid(aElt);
+  } else {
+    e = aElt;
+  }
+  return !e.getNode().hidden;
+}
+
+/**
+ * Assert that en element's visible.
+ * @param aElt The element, an ID or an elementlibs.Elem
+ * @param aWhy The error message in case of failure
+ */
+function assert_element_visible(aElt, aWhy) {
+  folderDisplayHelper.assert_true(element_visible(aElt), aWhy);
+}
+
+/**
+ * Assert that en element's not visible.
+ * @param aElt The element, an ID or an elementlibs.Elem
+ * @param aWhy The error message in case of failure
+ */
+function assert_element_not_visible(aElt, aWhy) {
+  folderDisplayHelper.assert_true(!element_visible(aElt), aWhy);
+}