Bug 550589 - Fix the font chooser to be able to display fonts from the font name list. r=bwinton, asuth
authorSiddharth Agarwal <sid.bugzilla@gmail.com>
Thu, 18 Mar 2010 17:04:31 +0530
changeset 5204 7b60a657c20cd1055afe1b0c3308b1e224cab034
parent 5203 5d97bb8bf0ccf4246db53656f4d9fa3ea841552a
child 5205 25591e6c9e1dd72230b03f7d432ea66748b6c86b
push idunknown
push userunknown
push dateunknown
reviewersbwinton, asuth
bugs550589
Bug 550589 - Fix the font chooser to be able to display fonts from the font name list. r=bwinton, asuth This fixes two bugs: one where the Advanced dialog ignores the font list if it is a default pref, and another where the font chooser in the display pane doesn't work at all. This also adds a hook to Mozmill to allow arbitrary python code to run before the Thunderbird process starts. The hook can be extended to run python code at other times, too.
mail/components/preferences/display.js
mail/components/preferences/display.xul
mail/components/preferences/fonts.js
mail/test/mozmill/mozmilltests.list
mail/test/mozmill/pref-window/linux-prefs.js
mail/test/mozmill/pref-window/mac-prefs.js
mail/test/mozmill/pref-window/test-font-chooser.js
mail/test/mozmill/pref-window/windows-prefs.js
mail/test/mozmill/pref-window/wrapper.py
mail/test/mozmill/runtest.py
mail/test/mozmill/shared-modules/test-pref-window-helpers.js
--- a/mail/components/preferences/display.js
+++ b/mail/components/preferences/display.js
@@ -1,45 +1,46 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# ***** 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 the Thunderbird Preferences System.
-#
-# The Initial Developer of the Original Code is
-# Scott MacGregor.
-# Portions created by the Initial Developer are Copyright (C) 2005
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Scott MacGregor <mscott@mozilla.org>
-#
-# 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 *****
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** 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 the Thunderbird Preferences System.
+ *
+ * The Initial Developer of the Original Code is
+ * Scott MacGregor.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Scott MacGregor <mscott@mozilla.org>
+ *   Siddharth Agarwal <sid.bugzilla@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 gDisplayPane = {
   mInitialized: false,
   mTagListBox:  null,
 
   init: function ()
   {
     var preference = document.getElementById("mail.preferences.display.selectedTabIndex");
@@ -77,24 +78,26 @@ var gDisplayPane = {
   _selectDefaultLanguageGroup: function (aLanguageGroup, aIsSerif)
   {
     const kFontNameFmtSerif         = "font.name.serif.%LANG%";
     const kFontNameFmtSansSerif     = "font.name.sans-serif.%LANG%";
     const kFontNameListFmtSerif     = "font.name-list.serif.%LANG%";
     const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
     const kFontSizeFmtVariable      = "font.size.variable.%LANG%";
 
-    var prefs = [{format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
+    // Make sure font.name-list is created before font.name so that it's
+    // available at the time readFontSelection below is called.
+    var prefs = [{format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
+                  type: "unichar",
+                  element: null,
+                  fonttype: aIsSerif ? "serif" : "sans-serif" },
+                 {format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
                   type: "fontname",
                   element: "defaultFont",
                   fonttype: aIsSerif ? "serif" : "sans-serif" },
-                 {format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
-                  type: "unichar",
-                  element: null,
-                  fonttype: aIsSerif ? "serif" : "sans-serif" },
                  {format: kFontSizeFmtVariable,
                   type: "int",
                   element: "defaultFontSize",
                   fonttype: null }];
 
     var preferences = document.getElementById("displayPreferences");
     for (var i = 0; i < prefs.length; ++i) {
       var preference = document.getElementById(prefs[i].format.replace(/%LANG%/,
@@ -108,21 +111,25 @@ var gDisplayPane = {
         preferences.appendChild(preference);
       }
 
       if (!prefs[i].element)
         continue;
 
       var element = document.getElementById(prefs[i].element);
       if (element) {
-        element.setAttribute("preference", preference.id);
-
+        // Make sure we have the font list ready for readFontSelection below to
+        // work. readFontSelection gets called at onsyncfrompreference, but the
+        // exact semantics of when it is called (whether during setAttribute or
+        // during setElementValue) aren't obvious.
         if (prefs[i].fonttype)
           FontBuilder.buildFontList(aLanguageGroup, prefs[i].fonttype, element);
 
+        element.setAttribute("preference", preference.id);
+
         preference.setElementValue(element);
       }
     }
   },
 
   /**
    * Returns the type of the current default font for the language denoted by
    * aLanguageGroup.
@@ -138,16 +145,53 @@ var gDisplayPane = {
       preference.setAttribute("name", defaultFontTypePref);
       preference.setAttribute("type", "string");
       preference.setAttribute("onchange", "gDisplayPane._rebuildFonts();");
       document.getElementById("displayPreferences").appendChild(preference);
     }
     return preference.value;
   },
 
+  /**
+   * Determine the appropriate value to select for defaultFont, for the
+   * following cases:
+   * - there is no setting
+   * - the font selected by the user is no longer present (e.g. deleted from
+   *   fonts folder)
+   */
+  readFontSelection: function gDisplayPane_readFontSelection()
+  {
+    let element = document.getElementById("defaultFont");
+    let preference = document.getElementById(element.getAttribute("preference"));
+    if (preference.value) {
+      var fontItems = element.getElementsByAttribute("value", preference.value);
+
+      // There is a setting that actually is in the list. Respect it.
+      if (fontItems.length > 0)
+        return undefined;
+    }
+
+    let defaultValue = element.firstChild.firstChild.getAttribute("value");
+    let languagePref = document.getElementById("font.language.group");
+    let defaultType = this._readDefaultFontTypeForLanguage(languagePref.value);
+    let listPref = document.getElementById("font.name-list." + defaultType +
+                                           "." + languagePref.value);
+    if (!listPref)
+      return defaultValue;
+
+    let fontNames = listPref.value.split(",");
+
+    for (let [, fontName] in Iterator(fontNames)) {
+      let fontItems = element.getElementsByAttribute("value", fontName.trim());
+      if (fontItems.length)
+        return fontItems[0].getAttribute("value");
+    }
+    return defaultValue;
+  },
+
   tabSelectionChanged: function ()
   {
     if (this.mInitialized)
       document.getElementById("mail.preferences.display.selectedTabIndex")
               .valueFromPreferences = document.getElementById("displayPrefs").selectedIndex;
   },
 
   /**
--- a/mail/components/preferences/display.xul
+++ b/mail/components/preferences/display.xul
@@ -96,17 +96,18 @@
                 <column flex="1"/>
                 <column/>
               </columns>
               <rows id="fontsRows">
                 <row id="fontRow">
                   <hbox align="center">
                     <label accesskey="&defaultFont.accesskey;"
                            control="defaultFont">&defaultFont.label;</label>
-                    <menulist id="defaultFont" flex="1" sizetopopup="false"/>
+                    <menulist id="defaultFont" flex="1" sizetopopup="false"
+                              onsyncfrompreference="return gDisplayPane.readFontSelection()"/>
                     <label accesskey="&defaultSize.accesskey;"
                            control="defaultFontSize">&defaultSize.label;</label>
                     <menulist id="defaultFontSize">
                       <menupopup>
                         <menuitem value="9" label="9"/>
                         <menuitem value="10" label="10"/>
                         <menuitem value="11" label="11"/>
                         <menuitem value="12" label="12"/>
--- a/mail/components/preferences/fonts.js
+++ b/mail/components/preferences/fonts.js
@@ -1,45 +1,46 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# ***** 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 the Thunderbird Preferences System.
-#
-# The Initial Developer of the Original Code is
-# Ben Goodger.
-# Portions created by the Initial Developer are Copyright (C) 2005
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Scott MacGregor <mscott@mozilla.org>
-#
-# 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 *****
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** 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 the Thunderbird Preferences System.
+ *
+ * The Initial Developer of the Original Code is
+ * Ben Goodger.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Scott MacGregor <mscott@mozilla.org>
+ *   Siddharth Agarwal <sid.bugzilla@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 ***** */
 
 const kDefaultFontType          = "font.default.%LANG%";
 const kFontNameFmtSerif         = "font.name.serif.%LANG%";
 const kFontNameFmtSansSerif     = "font.name.sans-serif.%LANG%";
 const kFontNameFmtMonospace     = "font.name.monospace.%LANG%";
 const kFontNameListFmtSerif     = "font.name-list.serif.%LANG%";
 const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
 const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%";
@@ -115,17 +116,17 @@ var gFontsDialog = {
       // There is a setting that actually is in the list. Respect it.
       if (fontItems.length > 0)
         return undefined;
     }
 
     var defaultValue = aElement.firstChild.firstChild.getAttribute("value");
     var languagePref = document.getElementById("font.language.group");
     preference = document.getElementById("font.name-list." + aElement.id + "." + languagePref.value);
-    if (!preference || !preference.hasUserValue)
+    if (!preference)
       return defaultValue;
 
     var fontNames = preference.value.split(",");
 
     for (var i = 0; i < fontNames.length; ++i) {
       var fontName = fontNames[i].trim();
       fontItems = aElement.getElementsByAttribute("value", fontName);
       if (fontItems.length)
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -1,8 +1,9 @@
 folder-display
 junk-commands
 search-window
 cookies
 content-policy
 content-tabs
 message-header
 folder-pane
+pref-window
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/linux-prefs.js
@@ -0,0 +1,5 @@
+// Default preferences used for test-font-chooser.js on Linux. The UUIDs here
+// should be kept in sync with gFakeFonts in test-font-chooser.js.
+pref("font.name-list.serif.x-western", "bc7e8c62-0634-467f-a029-fe6abcdf1582, serif");
+pref("font.name-list.sans-serif.x-western", "419129aa-43b7-40c4-b554-83d99b504b89, sans-serif");
+pref("font.name-list.monospace.x-western", "348df6e5-e874-4d21-ad4b-359b530a33b7, monospace");
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/mac-prefs.js
@@ -0,0 +1,5 @@
+// Default preferences used for test-font-chooser.js on Mac. The UUIDs here
+// should be kept in sync with gFakeFonts in test-font-chooser.js.
+pref("font.name-list.serif.x-western", "bc7e8c62-0634-467f-a029-fe6abcdf1582, Times");
+pref("font.name-list.sans-serif.x-western", "419129aa-43b7-40c4-b554-83d99b504b89, Helvetica");
+pref("font.name-list.monospace.x-western", "348df6e5-e874-4d21-ad4b-359b530a33b7, Courier");
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/test-font-chooser.js
@@ -0,0 +1,199 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** 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 mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Siddharth Agarwal <sid.bugzilla@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 ***** */
+
+/*
+ * Test various things about the font chooser window, including
+ * - whether if the font defined in font.name.<style>.<language> is not present
+ * on the computer, we fall back to displaying what's in
+ * font.name-list.<style>.<language>.
+ */
+
+var MODULE_NAME = "test-font-chooser";
+
+var RELATIVE_ROOT = "../shared-modules";
+var MODULE_REQUIRES = ["folder-display-helpers", "window-helpers",
+                       "pref-window-helpers"];
+
+// We can't initialize them h because the global scope is read far too
+// early.
+var Cc, Ci;
+
+var gPrefBranch;
+var gFontEnumerator;
+
+// We'll test with Western. Unicode has issues on Windows (bug 550443).
+const kLanguage = "x-western";
+
+// A list of fonts present on the computer for each font type.
+var gRealFontLists = {};
+
+// A list of font types to consider
+const kFontTypes = ["serif", "sans-serif", "monospace"];
+
+function setupModule(module) {
+  let fdh = collector.getModule("folder-display-helpers");
+  fdh.installInto(module);
+  let wh = collector.getModule("window-helpers");
+  wh.installInto(module);
+  let pwh = collector.getModule("pref-window-helpers");
+  pwh.installInto(module);
+
+  Cc = Components.classes;
+  Ci = Components.interfaces;
+  gPrefBranch = Cc["@mozilla.org/preferences-service;1"]
+                  .getService(Ci.nsIPrefBranch);
+
+  gFontEnumerator = Cc["@mozilla.org/gfx/fontenumerator;1"]
+                      .createInstance(Ci.nsIFontEnumerator);
+  for (let [, fontType] in Iterator(kFontTypes)) {
+    gRealFontLists[fontType] =
+      gFontEnumerator.EnumerateFonts(kLanguage, fontType, {});
+    if (gRealFontLists[fontType].length == 0)
+      throw new Error("No fonts found for language " + kLanguage +
+                      " and font type " + fontType + ".");
+  }
+}
+
+function assert_fonts_equal(aDescription, aExpected, aActual) {
+  if (aExpected != aActual)
+    throw new Error("The " + aDescription + " font should be " + aExpected +
+                    ", but " + (aActual.length == 0 ?
+                                "nothing is actually selected." :
+                                "is actually " + aActual + "."));
+}
+
+/**
+ * Verify that the given fonts are displayed in the font chooser. This opens the
+ * pref window to the display pane and checks that, then opens the font chooser
+ * and checks that too.
+ */
+function _verify_fonts_displayed(aSerif, aSansSerif, aMonospace) {
+  function verify_display_pane(prefc) {
+    let isSansDefault = (gPrefBranch.getCharPref("font.default." + kLanguage) ==
+                         "sans-serif");
+    let displayPaneExpected = isSansDefault ? aSansSerif : aSerif;
+    let displayPaneActual = prefc.e("defaultFont").value;
+    assert_fonts_equal("display pane", displayPaneExpected, displayPaneActual);
+  }
+
+  // Bring up the preferences window.
+  open_pref_window("paneDisplay", verify_display_pane);
+
+  // Now verify the advanced dialog.
+  function verify_advanced(fontc) {
+    assert_fonts_equal("serif", aSerif, fontc.e("serif").value);
+    assert_fonts_equal("sans-serif", aSansSerif, fontc.e("sans-serif").value);
+    assert_fonts_equal("monospace", aMonospace, fontc.e("monospace").value);
+  }
+
+  // Now open the advanced dialog.
+  plan_for_modal_dialog("FontsDialog", verify_advanced);
+  // XXX This would have been better done from within the prefs dialog, but
+  // test-window-helper.js's WindowWatcher can only handle one modal window at a
+  // time.
+  mc.window.openDialog("chrome://messenger/content/preferences/fonts.xul",
+                       "Fonts", "chrome,titlebar,toolbar,centerscreen,modal");
+  wait_for_modal_dialog("FontsDialog");
+}
+
+/**
+ * Test that for a particular language, whatever's in
+ * font.name.<type>.<language> is displayed in the font chooser (if it is
+ * present on the cocomputer).
+ */
+function test_font_name_displayed() {
+  gPrefBranch.setCharPref("font.language.group", kLanguage);
+
+  // Pick the first font for each font type and set it.
+  let expected = {};
+  for (let [fontType, fontList] in Iterator(gRealFontLists)) {
+    gPrefBranch.setCharPref("font.name." + fontType + "." + kLanguage,
+                            fontList[0]);
+    expected[fontType] = fontList[0];
+  }
+
+  _verify_fonts_displayed.apply(null, [expected[k]
+                                       for ([, k] in Iterator(kFontTypes))]);
+}
+
+// Fonts definitely not present on a computer -- we simply use UUIDs. These
+// should be kept in sync with the ones in *-prefs.js.
+const kFakeFonts = {
+  "serif": "bc7e8c62-0634-467f-a029-fe6abcdf1582",
+  "sans-serif": "419129aa-43b7-40c4-b554-83d99b504b89",
+  "monospace": "348df6e5-e874-4d21-ad4b-359b530a33b7",
+};
+
+/**
+ * Test that for a particular language, if font.name.<type>.<language> is not
+ * present on the computer, we fall back to displaying what's in
+ * font.name-list.<type>.<language>.
+ */
+function test_font_name_not_present() {
+  gPrefBranch.setCharPref("font.language.group", kLanguage);
+
+  // The fonts we're expecting to see selected in the font chooser for
+  // test_font_name_not_present.
+  let expected = {};
+  for (let [fontType, fakeFont] in Iterator(kFakeFonts)) {
+    // Look at the font.name-list. We need to verify that the first font is the
+    // fake one, and that the second one is present on the user's computer.
+    let listPref = "font.name-list." + fontType + "." + kLanguage;
+    let fontList = gPrefBranch.getCharPref(listPref);
+    let fonts = [s.trim() for ([, s] in Iterator(fontList.split(",")))];
+    if (fonts.length != 2)
+      throw new Error(listPref + " should have exactly two fonts, but it is " +
+                      fontList + ".");
+
+    if (fonts[0] != fakeFont)
+      throw new Error("The first font in " + listPref + " should be " + fakeFont +
+                      ", but is actually " + fonts[0] + ".");
+
+    if (gRealFontLists[fontType].indexOf(fonts[1]) == -1)
+      throw new Error("The second font in " + listPref + " (" + fonts[1] +
+                      ") should be present on this computer, but isn't.");
+    expected[fontType] = fonts[1];
+
+    // Set font.name to be the fake font. font.name-list is handled by
+    // wrapper.py.
+    gPrefBranch.setCharPref("font.name." + fontType + "." + kLanguage, fakeFont);
+  }
+
+  _verify_fonts_displayed.apply(null, [expected[k]
+                                       for ([, k] in Iterator(kFontTypes))]);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/windows-prefs.js
@@ -0,0 +1,5 @@
+// Default preferences used for test-font-chooser.js on Windows. The UUIDs here
+// should be kept in sync with gFakeFonts in test-font-chooser.js.
+pref("font.name-list.serif.x-western", "bc7e8c62-0634-467f-a029-fe6abcdf1582, Times New Roman");
+pref("font.name-list.sans-serif.x-western", "419129aa-43b7-40c4-b554-83d99b504b89, Arial");
+pref("font.name-list.monospace.x-western", "348df6e5-e874-4d21-ad4b-359b530a33b7, Courier New");
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/pref-window/wrapper.py
@@ -0,0 +1,68 @@
+# ***** 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) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Siddharth Agarwal <sid.bugzilla@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 *****
+
+# For test-font-chooser.js we need a few default prefs -- this module does that.
+
+import os
+import shutil
+import sys
+
+_pref_file_names = {
+    "win32": "windows-prefs.js",
+    "darwin": "mac-prefs.js",
+    "linux2": "linux-prefs.js",
+}
+
+def on_profile_created(profiledir):
+    """
+    On profile creation, this copies prefs.js from the current folder to
+    profile_dir/preferences. This is a somewhat undocumented feature -- anything
+    in profile_dir/preferences gets treated as a default pref, which is what we
+    want here.
+    """
+    prefdir = os.path.join(profiledir, "preferences")
+    # This needs to be a directory, so if it's a file, raise an exception
+    if os.path.isfile(prefdir):
+        raise Exception("%s needs to be a directory, but is a file" % prefdir)
+    if not os.path.exists(prefdir):
+        os.mkdir(prefdir)
+    # The pref file is in the same directory this script is in
+    # Fallback to Linux prefs for anything not in the dictionary -- we're
+    # assuming that they're other unixes
+    preffile = os.path.join(os.path.dirname(__file__),
+                            _pref_file_names.get(sys.platform, "linux-prefs.js"))
+    shutil.copy(preffile, prefdir)
--- a/mail/test/mozmill/runtest.py
+++ b/mail/test/mozmill/runtest.py
@@ -57,24 +57,33 @@ sys.path.append(SCRIPT_DIRECTORY)
 # The try case handles trunk. The exception case handles MOZILLA_1_9_2_BRANCH.
 try:
     from automation import Automation
     automation = Automation()
 except ImportError:
     import automation
 from automationutils import checkForCrashes
 from time import sleep
+import imp
 
 PROFILE_DIR = os.path.join(SCRIPT_DIRECTORY, 'mozmillprofile')
 SYMBOLS_PATH = None
 # XXX This breaks any semblance of test runner modularity, and only works
 # because we know that we run MozMill only once per process. This needs to be
 # fixed if that ever changes.
 TEST_NAME = None
 
+# The name of the (optional) module that tests can define as a wrapper (e.g. to
+# run before Thunderbird is started)
+WRAPPER_MODULE_NAME = "wrapper"
+
+# The wrapper module (if any) for the test. Just like TEST_NAME, this breaks any
+# semblance of modularity.
+wrapper = None
+
 # We need this because rmtree-ing read-only files fails on Windows
 def rmtree_onerror(func, path, exc_info):
     """
     Error handler for ``shutil.rmtree``.
 
     If the error is due to an access error (read only file)
     it attempts to add write permission and then retries.
 
@@ -165,16 +174,20 @@ class ThunderTestProfile(mozrunner.Thund
         when we are creating a new profile so that we can go in after the run
         and examine things for debugging or general interest.
         '''
         # create a clean directory
         if os.path.exists(PROFILE_DIR):
             shutil.rmtree(PROFILE_DIR, onerror=rmtree_onerror)
         os.makedirs(PROFILE_DIR)
 
+        # If there's a wrapper, call it
+        if wrapper is not None:
+            wrapper.on_profile_created(PROFILE_DIR)
+
         return PROFILE_DIR
 
     def cleanup(self):
         '''
         Do not cleanup at all.  The next iteration will cleanup for us, but
         until that time it's useful for debugging failures to leave everything
         around.
         '''
@@ -245,22 +258,46 @@ class ThunderTestCLI(mozmill.CLI):
     def __init__(self, *args, **kwargs):
         global SYMBOLS_PATH, TEST_NAME
         # invoke jsbridge.CLI's constructor directly since we are explicitly
         #  trying to replace mozmill's CLI constructor here (which hardcodes
         #  the firefox runner and profile in 1.3 for no clear reason).
         jsbridge.CLI.__init__(self, *args, **kwargs)
         SYMBOLS_PATH = self.options.symbols
         TEST_NAME = os.path.basename(self.options.test)
+
+        self._load_wrapper()
+
         self.mozmill = self.mozmill_class(runner_class=self.runner_class,
                                           profile_class=self.profile_class,
                                           jsbridge_port=int(self.options.port))
 
         self.mozmill.add_global_listener(mozmill.LoggerListener())
 
+    def _load_wrapper(self):
+        global wrapper
+        """
+        Load the wrapper module if it is present in the test directory.
+        """
+        if os.path.isdir(self.options.test):
+            testdir = self.options.test
+        else:
+            testdir = os.path.dirname(self.options.test)
+
+        try:
+            (fd, path, desc) = imp.find_module(WRAPPER_MODULE_NAME, [testdir])
+        except ImportError:
+            # No wrapper module, which is fine.
+            pass
+        else:
+            try:
+                wrapper = imp.load_module(WRAPPER_MODULE_NAME, fd, path, desc)
+            finally:
+                if fd is not None:
+                    fd.close()
 
 TEST_RESULTS = []
 # override mozmill's default logging case, which I hate.
 def logFailure(obj):
     FAILURE_LIST.append(obj)
 def logEndTest(obj):
     TEST_RESULTS.append(obj)
 #mozmill.LoggerListener.cases['mozmill.fail'] = logFailure
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-pref-window-helpers.js
@@ -0,0 +1,105 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** 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 mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Siddharth Agarwal <sid.bugzilla@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 ***** */
+
+/*
+ * Helpers to deal with the preferences window.
+ */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+var controller = {};
+Cu.import("resource://mozmill/modules/controller.js", controller);
+
+const MODULE_NAME = "pref-window-helpers";
+
+const RELATIVE_ROOT = "../shared-modules";
+
+const MODULE_REQUIRES = ["folder-display-helpers", "window-helpers"];
+
+const NORMAL_TIMEOUT = 6000;
+const FAST_INTERVAL = 100;
+
+var fdh;
+var wh;
+
+function setupModule() {
+  fdh = collector.getModule("folder-display-helpers");
+  wh = collector.getModule("window-helpers");
+}
+
+function installInto(module) {
+  setupModule();
+
+  // Now copy helper functions
+  module.open_pref_window = open_pref_window;
+}
+
+/**
+ * Open the preferences window with the given pane displayed. The pane needs to
+ * be one of the prefpane ids in mail/components/preferences/preferences.xul.
+ *
+ * Since the preferences window might be modal (it is currently modal on
+ * platforms without instantApply), it spins its own event loop. This means
+ * that you need to provide a callback to be executed when the window is loaded.
+ *
+ * @param aPaneID The ID of the pref pane to display (see
+ *     mail/components/preferences/preferences.xul for valid IDs.)
+ * @param aCallback A callback to be executed once the window is loaded. It will
+ *     be passed the controller for the pref window as its one and only argument.
+ */
+function open_pref_window(aPaneID, aCallback) {
+  function waitForPaneLoad(prefc) {
+    let pane = prefc.e(aPaneID);
+    function paneLoadedChecker() {
+      return pane.loaded;
+    }
+
+    if (!controller.waitForEval("subject()", NORMAL_TIMEOUT, FAST_INTERVAL,
+                                paneLoadedChecker))
+      throw new Error("Timed out waiting for prefpane " + aPaneID +
+                      " to load.");
+
+    aCallback(prefc);
+  }
+
+  wh.plan_for_modal_dialog("Mail:Preferences", waitForPaneLoad);
+  fdh.mc.window.openOptionsDialog(aPaneID);
+  wh.wait_for_modal_dialog("Mail:Preferences");
+}