Bug 469443 - Form Manager Storage should be a JavaScript-based component. r=gavin
authorJustin Dolske <dolske@mozilla.com>
Fri, 19 Jun 2009 13:19:18 -0700
changeset 29403 d18fc8d0863fb56142931abf79f0a6a9e49b2890
parent 29402 52d96051f22405be777733c06e3e48aa72f9de19
child 29404 097598383614f52a91c10f4609ddd82cab959164
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin
bugs469443
milestone1.9.2a1pre
Bug 469443 - Form Manager Storage should be a JavaScript-based component. r=gavin
browser/app/profile/firefox.js
browser/base/content/test/test_contextmenu.html
browser/installer/unix/packages-static
browser/installer/windows/packages-static
modules/libpref/src/init/all.js
toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
toolkit/components/satchel/public/Makefile.in
toolkit/components/satchel/public/nsIFormAutoComplete.idl
toolkit/components/satchel/src/Makefile.in
toolkit/components/satchel/src/nsFormAutoComplete.js
toolkit/components/satchel/src/nsFormFillController.cpp
toolkit/components/satchel/src/nsStorageFormHistory.cpp
toolkit/components/satchel/src/nsStorageFormHistory.h
toolkit/components/satchel/test/Makefile.in
toolkit/components/satchel/test/satchel_common.js
toolkit/components/satchel/test/test_form_autocomplete.html
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -192,17 +192,16 @@ pref("browser.shell.checkDefaultBrowser"
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
 pref("browser.startup.homepage",            "resource:/browserconfig.properties");
 
 pref("browser.enable_automatic_image_resizing", true);
 pref("browser.chrome.site_icons", true);
 pref("browser.chrome.favicons", true);
-pref("browser.formfill.enable", true);
 pref("browser.warnOnQuit", true);
 pref("browser.warnOnRestart", true);
 pref("browser.fullscreen.autohide", true);
 pref("browser.fullscreen.animateUp", 1);
 
 #ifdef UNIX_BUT_NOT_MAC
 pref("browser.urlbar.clickSelectsAll", false);
 #else
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -290,17 +290,17 @@ function runTest(testNum) {
      * Other things that would be nice to test:
      *  - selected text
      *  - spelling / misspelled word (in text input?)
      *  - check state of disabled items
      *  - test execution of menu items (maybe as a separate test?)
      */
 
     default:
-        ok(false, "Unexpected invocataion of test #" + testNum);
+        ok(false, "Unexpected invocation of test #" + testNum);
         subwindow.close();
         SimpleTest.finish();
         return;
   }
 
 }
 
 
--- a/browser/installer/unix/packages-static
+++ b/browser/installer/unix/packages-static
@@ -243,16 +243,17 @@ bin/components/nsContentPrefService.js
 bin/components/nsContentDispatchChooser.js
 bin/components/nsHandlerService.js
 bin/components/nsWebHandlerApp.js
 bin/components/libdbusservice.so
 bin/components/aboutRights.js
 bin/components/aboutRobots.js
 bin/components/aboutCertError.js
 bin/components/nsBadCertHandler.js
+bin/components/nsFormAutoComplete.js
 
 ; Modules
 bin/modules/*
 
 ; Safe Browsing
 bin/components/nsSafebrowsingApplication.js
 bin/components/nsUrlClassifierListManager.js
 bin/components/nsUrlClassifierLib.js
--- a/browser/installer/windows/packages-static
+++ b/browser/installer/windows/packages-static
@@ -249,16 +249,17 @@ bin\components\nsDefaultCLH.js
 bin\components\nsContentPrefService.js
 bin\components\nsContentDispatchChooser.js
 bin\components\nsHandlerService.js
 bin\components\nsWebHandlerApp.js
 bin\components\aboutRights.js
 bin\components\aboutRobots.js
 bin\components\aboutCertError.js
 bin\components\nsBadCertHandler.js
+bin\components\nsFormAutoComplete.js
 
 ; Modules
 bin\modules\*
 
 ; Safe Browsing
 bin\components\nsSafebrowsingApplication.js
 bin\components\nsUrlClassifierListManager.js
 bin\components\nsUrlClassifierLib.js
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -2707,16 +2707,21 @@ pref("print.print_command", "lp -c -s ${
 pref("signon.rememberSignons",              true);
 pref("signon.expireMasterPassword",         false);
 pref("signon.SignonFileName",               "signons.txt"); // obsolete 
 pref("signon.SignonFileName2",              "signons2.txt"); // obsolete
 pref("signon.SignonFileName3",              "signons3.txt"); // obsolete
 pref("signon.autofillForms",                true); 
 pref("signon.debug",                        false); // logs to Error Console
 
+// Satchel (Form Manager) prefs
+pref("browser.formfill.enable",     true);
+pref("browser.formfill.debug",      false);
+
+
 // Zoom prefs
 pref("browser.zoom.full", false);
 pref("zoom.minPercent", 30);
 pref("zoom.maxPercent", 300);
 pref("toolkit.zoomManager.zoomValues", ".3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3");
 
 // Image cache prefs
 // The maximum size, in bytes, of the decoded images we cache
--- a/toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
@@ -612,17 +612,17 @@ function runTest(testNum) {
     case 603:
         checkACForm("", "");
         pwmgr.removeLogin(login7);
 
         SimpleTest.finish();
         return;
 
     default:
-        ok(false, "Unexpected invocataion of test #" + testNum);
+        ok(false, "Unexpected invocation of test #" + testNum);
         SimpleTest.finish();
         return;
   }
 
   setTimeout(runTest, 50, testNum + 1); // XXX 40ms was too slow, why?
 }
 
 
--- a/toolkit/components/satchel/public/Makefile.in
+++ b/toolkit/components/satchel/public/Makefile.in
@@ -42,12 +42,13 @@ srcdir=@srcdir@
 VPATH=@srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = satchel
 XPIDL_MODULE = satchel
 
 XPIDLSRCS	= nsIFormFillController.idl \
+            nsIFormAutoComplete.idl \
             nsIFormHistory.idl \
             $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/public/nsIFormAutoComplete.idl
@@ -0,0 +1,52 @@
+/* ***** 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 Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Justin Dolske <dolske@mozilla.com> (original author)
+ *
+ * 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 ***** */
+
+
+#include "nsISupports.idl"
+
+interface nsIAutoCompleteResult;
+
+[scriptable, uuid(2f5bb765-428e-4e33-b2d8-441eb7ddf730)]
+
+interface nsIFormAutoComplete: nsISupports {
+    /**
+     * Generate results for a form input autocomplete menu.
+     */
+    nsIAutoCompleteResult autoCompleteSearch(
+                                    in AString aInputName,
+                                    in AString aSearchString,
+                                    in nsIAutoCompleteResult aPreviousResult);
+};
--- a/toolkit/components/satchel/src/Makefile.in
+++ b/toolkit/components/satchel/src/Makefile.in
@@ -45,16 +45,20 @@ include $(DEPTH)/config/autoconf.mk
 
 MODULE = satchel
 LIBRARY_NAME = satchel
 MODULE_NAME = satchel
 IS_COMPONENT = 1
 LIBXUL_LIBRARY = 1
 EXPORT_LIBRARY = 1
 
+EXTRA_COMPONENTS = \
+	nsFormAutoComplete.js \
+	$(NULL)
+
 REQUIRES =	\
 		xpcom \
 		string \
 		autocomplete \
 		dom \
 		layout \
 		docshell \
 		gfx \
@@ -87,9 +91,8 @@ EXTRA_DSO_LDOPTS += \
 	$(EXTRA_DSO_LIBS) \
 	$(MOZ_UNICHARUTIL_LIBS) \
 	$(MOZ_COMPONENT_LIBS) \
 	$(NULL)
 
 ifdef MOZ_MORKREADER
 EXTRA_DSO_LDOPTS += $(DEPTH)/db/morkreader/$(LIB_PREFIX)morkreader_s.$(LIB_SUFFIX)
 endif
-
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/src/nsFormAutoComplete.js
@@ -0,0 +1,311 @@
+/* ***** 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 Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Justin Dolske <dolske@mozilla.com> (original author)
+ *
+ * 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function FormAutoComplete() {
+    this.init();
+}
+
+FormAutoComplete.prototype = {
+    classDescription: "FormAutoComplete",
+    contractID: "@mozilla.org/satchel/form-autocomplete;1",
+    classID: Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
+    QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
+
+    __logService : null, // Console logging service, used for debugging.
+    get _logService() {
+        if (!this.__logService)
+            this.__logService = Cc["@mozilla.org/consoleservice;1"].
+                                getService(Ci.nsIConsoleService);
+        return this.__logService;
+    },
+
+    __formHistory : null,
+    get _formHistory() {
+        if (!this.__formHistory)
+            this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"].
+                                 getService(Ci.nsIFormHistory2);
+        return this.__formHistory;
+    },
+
+    __observerService : null, // Observer Service, for notifications
+    get _observerService() {
+        if (!this.__observerService)
+            this.__observerService = Cc["@mozilla.org/observer-service;1"].
+                                     getService(Ci.nsIObserverService);
+        return this.__observerService;
+    },
+
+    _prefBranch  : null,
+    _enabled : true,  // mirrors browser.formfill.enable preference
+    _debug   : false, // mirrors browser.formfill.debug
+
+    init : function() {
+        // Preferences. Add observer so we get notified of changes.
+        this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
+                           getService(Ci.nsIPrefService).getBranch("browser.formfill.");
+        this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
+        this._prefBranch.addObserver("", this.observer, false);
+        this.observer._self = this;
+
+        this._debug   = this._prefBranch.getBoolPref("debug");
+        this._enabled = this._prefBranch.getBoolPref("enable");
+
+        this._dbStmts = [];
+
+        this._observerService.addObserver(this.observer, "xpcom-shutdown", false);
+    },
+
+    observer : {
+        _self : null,
+
+        QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+                                                Ci.nsISupportsWeakReference]),
+
+        observe : function (subject, topic, data) {
+            let self = this._self;
+            if (topic == "nsPref:changed") {
+                let prefName = data;
+                self.log("got change to " + prefName + " preference");
+
+                if (prefName == "debug") {
+                    self._debug = self._prefBranch.getBoolPref("debug");
+                } else if (prefName == "enable") {
+                    self._enabled = self._prefBranch.getBoolPref("enable");
+                } else {
+                    self.log("Oops! Pref not handled, change ignored.");
+                }
+            } else if (topic == "xpcom-shutdown") {
+                self._dbStmts = null;
+            }
+        }
+    },
+
+
+    /*
+     * log
+     *
+     * Internal function for logging debug messages to the Error Console
+     * window
+     */
+    log : function (message) {
+        if (!this._debug)
+            return;
+        dump("FormAutoComplete: " + message + "\n");
+        this._logService.logStringMessage("FormAutoComplete: " + message);
+    },
+
+
+    /*
+     * autoCompleteSearch
+     *
+     * aInputName    -- |name| attribute from the form input being autocompleted.
+     * aSearchString -- current value of the input
+     * aPreviousResult -- previous search result, if any.
+     *
+     * Returns: an nsIAutoCompleteResult
+     */
+    autoCompleteSearch : function (aInputName, aSearchString, aPreviousResult) {
+        if (!this._enabled)
+            return null;
+
+        this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
+
+        let result = null;
+
+        if (aPreviousResult) {
+            this.log("Using previous autocomplete result");
+            result = aPreviousResult;
+
+            // We have a list of results for a shorter search string, so just
+            // filter them further based on the new search string.
+            // Count backwards, because result.matchCount is decremented
+            // when we remove an entry.
+            for (let i = result.matchCount - 1; i >= 0; i--) {
+                let match = result.getValueAt(i);
+
+                // Remove results that are too short, or have different prefix.
+                // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
+                if (aSearchString.length > match.length ||
+                    aSearchString.toLowerCase() !=
+                        match.substr(0, aSearchString.length).toLowerCase())
+                {
+                    this.log("Removing autocomplete entry '" + match + "'");
+                    result.removeValueAt(i, false);
+                }
+            }
+        } else {
+            this.log("Creating new autocomplete search result.");
+            let entries = this.getAutoCompleteValues(aInputName, aSearchString);
+            result = new FormAutoCompleteResult(this._formHistory, entries, aInputName, aSearchString);
+        }
+
+        return result;
+    },
+
+    getAutoCompleteValues : function (fieldName, searchString) {
+        let values = [];
+
+        let query = "SELECT value FROM moz_formhistory " +
+                    "WHERE fieldname=:fieldname AND value LIKE :valuePrefix ESCAPE '/' " +
+                    "ORDER BY UPPER(value) ASC";
+        let params = {
+            fieldname: fieldName,
+            valuePrefix: null // set below...
+        }
+
+        let stmt;
+        try {
+            stmt = this._dbCreateStatement(query, params);
+
+            // Chicken and egg problem: Need the statement to escape the params we
+            // pass to the function that gives us the statement. So, fix it up now.
+            stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
+
+            while (stmt.step())
+                values.push(stmt.row.value);
+        } catch (e) {
+            this.log("getValues failed: " + e.name + " : " + e.message);
+            throw "DB failed getting form autocomplete falues";
+        } finally {
+            stmt.reset();
+        }
+
+        return values;
+    },
+
+
+    _dbStmts      : null,
+
+    _dbCreateStatement : function (query, params) {
+        let stmt = this._dbStmts[query];
+        // Memoize the statements
+        if (!stmt) {
+            this.log("Creating new statement for query: " + query);
+            stmt = this._formHistory.DBConnection.createStatement(query);
+            this._dbStmts[query] = stmt;
+        }
+        // Replace parameters, must be done 1 at a time
+        if (params)
+            for (let i in params)
+                stmt.params[i] = params[i];
+        return stmt;
+    }
+
+}; // end of FormAutoComplete implementation
+
+
+
+
+// nsIAutoCompleteResult implementation
+function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
+    this.formHistory = formHistory;
+    this.entries = entries;
+    this.fieldName = fieldName;
+    this.searchString = searchString;
+
+    if (entries.length > 0) {
+        this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+        this.defaultIndex = 0;
+    }
+}
+
+FormAutoCompleteResult.prototype = {
+    QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
+                                            Ci.nsISupportsWeakReference]),
+
+    // private
+    formHistory : null,
+    entries : null,
+    fieldName : null,
+
+    _checkIndexBounds : function (index) {
+        if (index < 0 || index >= this.entries.length)
+            Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
+    },
+
+    // Interfaces from idl...
+    searchString : null,
+    searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+    defaultIndex : -1,
+    errorDescription : "",
+    get matchCount() {
+        return this.entries.length;
+    },
+
+    getValueAt : function (index) {
+        this._checkIndexBounds(index);
+        return this.entries[index];
+    },
+
+    getCommentAt : function (index) {
+        this._checkIndexBounds(index);
+        return "";
+    },
+
+    getStyleAt : function (index) {
+        this._checkIndexBounds(index);
+        return "";
+    },
+
+    getImageAt : function (index) {
+        this._checkIndexBounds(index);
+        return "";
+    },
+
+    removeValueAt : function (index, removeFromDB) {
+        this._checkIndexBounds(index);
+
+        let [removedEntry] = this.entries.splice(index, 1);
+
+        if (this.defaultIndex > this.entries.length)
+            this.defaultIndex--;
+
+        if (removeFromDB)
+            this.formHistory.removeEntry(this.fieldName, removedEntry);
+    }
+};
+
+let component = [FormAutoComplete];
+function NSGetModule (compMgr, fileSpec) {
+    return XPCOMUtils.generateModule(component);
+}
--- a/toolkit/components/satchel/src/nsFormFillController.cpp
+++ b/toolkit/components/satchel/src/nsFormFillController.cpp
@@ -35,16 +35,17 @@
  * 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 ***** */
 
 #include "nsFormFillController.h"
 
 #include "nsStorageFormHistory.h"
+#include "nsIFormAutoComplete.h"
 #include "nsIAutoCompleteSimpleResult.h"
 #include "nsString.h"
 #include "nsReadableUtils.h"
 #include "nsIServiceManager.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsPIDOMWindow.h"
@@ -491,40 +492,40 @@ nsFormFillController::GetConsumeRollupEv
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIAutoCompleteSearch
 
 NS_IMETHODIMP
 nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAString &aSearchParam,
                                   nsIAutoCompleteResult *aPreviousResult, nsIAutoCompleteObserver *aListener)
 {
+  nsresult rv;
   nsCOMPtr<nsIAutoCompleteResult> result;
 
   // If the login manager has indicated it's responsible for this field, let it
   // handle the autocomplete. Otherwise, handle with form history.
   PRInt32 dummy;
   if (mPwmgrInputs.Get(mFocusedInput, &dummy)) {
     // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting
     // satchel manage the field?
-    mLoginManager->AutoCompleteSearch(aSearchString,
+    rv = mLoginManager->AutoCompleteSearch(aSearchString,
                                          aPreviousResult,
                                          mFocusedInput,
                                          getter_AddRefs(result));
   } else {
-    nsCOMPtr<nsIAutoCompleteSimpleResult> historyResult;
-    historyResult = do_QueryInterface(aPreviousResult);
+    nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
+      do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
 
-    nsFormHistory *history = nsFormHistory::GetInstance();
-    if (history) {
-      history->AutoCompleteSearch(aSearchParam,
-                                  aSearchString,
-                                  historyResult,
-                                  getter_AddRefs(result));
-    }
+    rv = formAutoComplete->AutoCompleteSearch(aSearchParam,
+                                              aSearchString,
+                                              aPreviousResult,
+                                              getter_AddRefs(result));
   }
+  NS_ENSURE_SUCCESS(rv, rv);
 
   aListener->OnSearchResult(this, result);  
   
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsFormFillController::StopSearch()
--- a/toolkit/components/satchel/src/nsStorageFormHistory.cpp
+++ b/toolkit/components/satchel/src/nsStorageFormHistory.cpp
@@ -56,102 +56,32 @@
 #include "nsIDOMHTMLInputElement.h"
 #include "nsIDOMHTMLCollection.h"
 #include "nsIPrefService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefBranch2.h"
 #include "nsCOMArray.h"
 #include "mozStorageHelper.h"
 #include "mozStorageCID.h"
-#include "nsIAutoCompleteSimpleResult.h"
 #include "nsTArray.h"
 #include "nsIPrivateBrowsingService.h"
 #include "nsNetCID.h"
 
 #define DB_SCHEMA_VERSION   2
 #define DB_FILENAME         NS_LITERAL_STRING("formhistory.sqlite")
 #define DB_CORRUPT_FILENAME NS_LITERAL_STRING("formhistory.sqlite.corrupt")
 
 #define PR_HOURS ((PRInt64)60 * 60 * 1000000)
 
 // Limit the length of names and values stored in form history
 #define MAX_HISTORY_NAME_LEN    200
 #define MAX_HISTORY_VALUE_LEN   200
 // Limit the number of fields saved in a form
 #define MAX_FIELDS_SAVED        100
 
-// nsFormHistoryResult is a specialized autocomplete result class that knows
-// how to remove entries from the form history table.
-class nsFormHistoryResult : public nsIAutoCompleteSimpleResult
-{
-public:
-  nsFormHistoryResult(const nsAString &aFieldName)
-    : mFieldName(aFieldName) {}
-
-  nsresult Init();
-
-  NS_DECL_ISUPPORTS
-
-  // Forward everything except RemoveValueAt to the internal result
-  NS_IMETHOD GetSearchString(nsAString &_result)
-  { return mResult->GetSearchString(_result); }
-  NS_IMETHOD GetSearchResult(PRUint16 *_result)
-  { return mResult->GetSearchResult(_result); }
-  NS_IMETHOD GetDefaultIndex(PRInt32 *_result)
-  { return mResult->GetDefaultIndex(_result); }
-  NS_IMETHOD GetErrorDescription(nsAString &_result)
-  { return mResult->GetErrorDescription(_result); }
-  NS_IMETHOD GetMatchCount(PRUint32 *_result)
-  { return mResult->GetMatchCount(_result); }
-  NS_IMETHOD GetValueAt(PRInt32 aIndex, nsAString &_result)
-  { return mResult->GetValueAt(aIndex, _result); }
-  NS_IMETHOD GetCommentAt(PRInt32 aIndex, nsAString &_result)
-  { return mResult->GetCommentAt(aIndex, _result); }
-  NS_IMETHOD GetStyleAt(PRInt32 aIndex, nsAString &_result)
-  { return mResult->GetStyleAt(aIndex, _result); }
-  NS_IMETHOD GetImageAt(PRInt32 aIndex, nsAString &_result)
-  { return mResult->GetImageAt(aIndex, _result); }
-  NS_IMETHOD RemoveValueAt(PRInt32 aRowIndex, PRBool aRemoveFromDB);
-  NS_FORWARD_NSIAUTOCOMPLETESIMPLERESULT(mResult->)
-
-protected:
-  nsCOMPtr<nsIAutoCompleteSimpleResult> mResult;
-  nsString mFieldName;
-};
-
-NS_IMPL_ISUPPORTS2(nsFormHistoryResult,
-                   nsIAutoCompleteResult, nsIAutoCompleteSimpleResult)
-
-nsresult
-nsFormHistoryResult::Init()
-{
-  nsresult rv;
-  mResult = do_CreateInstance(NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID, &rv);
-  return rv;
-}
-
-NS_IMETHODIMP
-nsFormHistoryResult::RemoveValueAt(PRInt32 aRowIndex, PRBool aRemoveFromDB)
-{
-  if (!aRemoveFromDB) {
-    return mResult->RemoveValueAt(aRowIndex, aRemoveFromDB);
-  }
-
-  nsAutoString value;
-  nsresult rv = mResult->GetValueAt(aRowIndex, value);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = mResult->RemoveValueAt(aRowIndex, aRemoveFromDB);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  nsFormHistory* fh = nsFormHistory::GetInstance();
-  NS_ENSURE_TRUE(fh, NS_ERROR_OUT_OF_MEMORY);
-  return fh->RemoveEntry(mFieldName, value);
-}
-
 #define PREF_FORMFILL_BRANCH "browser.formfill."
 #define PREF_FORMFILL_ENABLE "enable"
 
 NS_INTERFACE_MAP_BEGIN(nsFormHistory)
   NS_INTERFACE_MAP_ENTRY(nsIFormHistory2)
   NS_INTERFACE_MAP_ENTRY(nsIFormHistoryPrivate)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIFormSubmitObserver)
@@ -638,21 +568,16 @@ nsFormHistory::CreateStatements()
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
          "SELECT * FROM moz_formhistory WHERE fieldname=?1"),
          getter_AddRefs(mDBFindEntryByName));
   NS_ENSURE_SUCCESS(rv,rv);
 
   rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
-        "SELECT value FROM moz_formhistory WHERE fieldname=?1 ORDER BY UPPER(value) ASC"),
-        getter_AddRefs(mDBGetMatchingField));
-  NS_ENSURE_SUCCESS(rv,rv);
-
-  rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
         "INSERT INTO moz_formhistory (fieldname, value, timesUsed, "
         "firstUsed, lastUsed) VALUES (?1, ?2, ?3, ?4, ?5)"),
         getter_AddRefs(mDBInsertNameValue));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
         "UPDATE moz_formhistory "
         "SET timesUsed=timesUsed + 1, lastUsed=?1 "
@@ -875,83 +800,16 @@ nsFormHistory::dbAreExpectedColumnsPrese
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
                   "SELECT fieldname, value, timesUsed, firstUsed, lastUsed "
                   "FROM moz_formhistory"), getter_AddRefs(stmt));
   return NS_SUCCEEDED(rv) ? PR_TRUE : PR_FALSE;
 }
 
 
-nsresult
-nsFormHistory::AutoCompleteSearch(const nsAString &aInputName,
-                                  const nsAString &aInputValue,
-                                  nsIAutoCompleteSimpleResult *aPrevResult,
-                                  nsIAutoCompleteResult **aResult)
-{
-  if (!FormHistoryEnabled())
-    return NS_OK;
-
-  nsCOMPtr<nsIAutoCompleteSimpleResult> result;
-
-  if (aPrevResult) {
-    result = aPrevResult;
-
-    PRUint32 matchCount;
-    result->GetMatchCount(&matchCount);
-
-    for (PRInt32 i = matchCount - 1; i >= 0; --i) {
-      nsAutoString match;
-      result->GetValueAt(i, match);
-      if (!StringBeginsWith(match, aInputValue,
-                            nsCaseInsensitiveStringComparator())) {
-        result->RemoveValueAt(i, PR_FALSE);
-      }
-    }
-  } else {
-    nsCOMPtr<nsFormHistoryResult> fhResult =
-      new nsFormHistoryResult(aInputName);
-    NS_ENSURE_TRUE(fhResult, NS_ERROR_OUT_OF_MEMORY);
-    nsresult rv = fhResult->Init();
-    NS_ENSURE_SUCCESS(rv, rv);
-    reinterpret_cast<nsCOMPtr<nsIAutoCompleteSimpleResult>*>(&fhResult)->swap(result);
-
-    result->SetSearchString(aInputValue);
-
-    // generates query string		
-    mozStorageStatementScoper scope(mDBGetMatchingField);
-    rv = mDBGetMatchingField->BindStringParameter(0, aInputName);
-    NS_ENSURE_SUCCESS(rv,rv);
-
-    PRBool hasMore = PR_FALSE;
-    PRUint32 count = 0;
-    while (NS_SUCCEEDED(mDBGetMatchingField->ExecuteStep(&hasMore)) &&
-           hasMore) {
-      nsAutoString entryString;
-      mDBGetMatchingField->GetString(0, entryString);
-      // filters out irrelevant results
-      if(StringBeginsWith(entryString, aInputValue,
-                          nsCaseInsensitiveStringComparator())) {
-        result->AppendMatch(entryString, EmptyString(), EmptyString(), EmptyString());
-        ++count;
-      }
-    }
-    if (count > 0) {
-      result->SetSearchResult(nsIAutoCompleteResult::RESULT_SUCCESS);
-      result->SetDefaultIndex(0);
-    } else {
-      result->SetSearchResult(nsIAutoCompleteResult::RESULT_NOMATCH);
-      result->SetDefaultIndex(-1);
-    }
-  }
-
-  *aResult = result;
-  NS_IF_ADDREF(*aResult);
-  return NS_OK;
-}
-
 #ifdef MOZ_MORKREADER
 
 // Columns for form history rows
 enum {
   kNameColumn,
   kValueColumn,
   kColumnCount // keep me last
 };
--- a/toolkit/components/satchel/src/nsStorageFormHistory.h
+++ b/toolkit/components/satchel/src/nsStorageFormHistory.h
@@ -90,29 +90,16 @@ public:
   NS_DECL_NSIOBSERVER
   
   // nsIFormSubmitObserver
   NS_IMETHOD Notify(nsIDOMHTMLFormElement* formElt, nsIDOMWindowInternal* window, nsIURI* actionURL, PRBool* cancelSubmit);
 
   nsFormHistory();
   nsresult Init();
 
-  static nsFormHistory* GetInstance()
-    {
-      if (!gFormHistory) {
-        nsCOMPtr<nsIFormHistory2> fh = do_GetService(NS_FORMHISTORY_CONTRACTID);
-      }
-      return gFormHistory;
-    }
-
-  nsresult AutoCompleteSearch(const nsAString &aInputName,
-			      const nsAString &aInputValue,
-                              nsIAutoCompleteSimpleResult *aPrevResult,
-			      nsIAutoCompleteResult **aNewResult);
-
  private:
   ~nsFormHistory();
 
  protected:
   // Database I/O
   nsresult OpenDatabase(PRBool *aDoImport);
   nsresult CloseDatabase();
   nsresult GetDatabaseFile(nsIFile** aFile);
@@ -132,17 +119,16 @@ public:
   static PRBool gPrefsInitialized;
 
   nsresult ExpireOldEntries();
   PRInt32 CountAllEntries();
   PRInt64 GetExistingEntryID(const nsAString &aName, const nsAString &aValue);
 
   nsCOMPtr<nsIPrefBranch> mPrefBranch;
   nsCOMPtr<mozIStorageService> mStorageService;
-  nsCOMPtr<mozIStorageStatement> mDBGetMatchingField;
   nsCOMPtr<mozIStorageStatement> mDBFindEntry;
   nsCOMPtr<mozIStorageStatement> mDBFindEntryByName;
   nsCOMPtr<mozIStorageStatement> mDBSelectEntries;
   nsCOMPtr<mozIStorageStatement> mDBInsertNameValue;
   nsCOMPtr<mozIStorageStatement> mDBUpdateEntry;
 };
 
 #ifdef MOZ_MORKREADER
--- a/toolkit/components/satchel/test/Makefile.in
+++ b/toolkit/components/satchel/test/Makefile.in
@@ -46,16 +46,17 @@ include $(DEPTH)/config/autoconf.mk
 
 MODULE = test_satchel
 
 XPCSHELL_TESTS = \
   unit \
   $(NULL)
 
 MOCHI_TESTS = \
+		test_form_autocomplete.html \
 		test_form_submission.html \
 		test_form_submission_cap.html \
 		test_form_submission_cap2.html \
 		test_privbrowsing.html \
 		$(NULL)
 
 MOCHI_CONTENT = \
 		satchel_common.js \
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -60,15 +60,37 @@ function $_(formNum, name) {
   if (element.hasAttribute("name") && element.getAttribute("name") != name) {
     ok(false, "$_ got confused.");
     return null;
   }
 
   return element;
 }
 
+// Mochitest gives us a sendKey(), but it's targeted to a specific element.
+// This basically sends an untargeted key event, to whatever's focused.
+function doKey(aKey, modifier) {
+    // Seems we need to enable this again, or sendKeyEvent() complaints.
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+
+    var keyName = "DOM_VK_" + aKey.toUpperCase();
+    var key = Components.interfaces.nsIDOMKeyEvent[keyName];
+
+    // undefined --> null
+    if (!modifier)
+        modifier = null;
+
+    // Window utils for sending fake sey events.
+    var wutils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                          getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+    wutils.sendKeyEvent("keydown",  key, 0, modifier);
+    wutils.sendKeyEvent("keypress", key, 0, modifier);
+    wutils.sendKeyEvent("keyup",    key, 0, modifier);
+}
+
 function cleanUpFormHist() {
   netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   var formhist = Components.classes["@mozilla.org/satchel/form-history;1"].
                  getService(Components.interfaces.nsIFormHistory2);
   formhist.removeAllEntries();
 }
 cleanUpFormHist();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -0,0 +1,482 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Form History Autocomplete</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="satchel_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form History test: form field autocomplete
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+  <!-- normal, basic form -->
+  <form id="form1" onsubmit="return false;">
+    <input  type="text" name="field1">
+    <button type="submit">Submit</button>
+  </form>
+
+  <!-- normal, basic form (new fieldname) -->
+  <form id="form2" onsubmit="return false;">
+    <input  type="text" name="field2">
+    <button type="submit">Submit</button>
+  </form>
+
+  <!-- form with autocomplete=off on input -->
+  <form id="form3" onsubmit="return false;">
+    <input  type="text" name="field2" autocomplete="off">
+    <button type="submit">Submit</button>
+  </form>
+
+  <!-- form with autocomplete=off on form -->
+  <form id="form4" autocomplete="off" onsubmit="return false;">
+    <input  type="text" name="field2">
+    <button type="submit">Submit</button>
+  </form>
+
+  <!-- normal form for testing filtering -->
+  <form id="form5" onsubmit="return false;">
+    <input  type="text" name="field3">
+    <button type="submit">Submit</button>
+  </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Form History autocomplete **/
+
+netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+
+var input = $_(1, "field1");
+const shiftModifier = Components.interfaces.nsIDOMNSEvent.SHIFT_MASK;
+
+// Get the form history service
+var fh = Components.classes["@mozilla.org/satchel/form-history;1"].
+         getService(Components.interfaces.nsIFormHistory2);
+ok(fh != null, "got form history service");
+
+fh.removeAllEntries();
+fh.addEntry("field1", "value1");
+fh.addEntry("field1", "value2");
+fh.addEntry("field1", "value3");
+fh.addEntry("field1", "value4");
+fh.addEntry("field2", "value1");
+fh.addEntry("field3", "a");
+fh.addEntry("field3", "aa");
+fh.addEntry("field3", "aa\xe6"); // 0xae == latin ae pair (0xc6 == AE)
+fh.addEntry("field3", "az");
+fh.addEntry("field3", "z");
+
+// Restore the form to the default state.
+function restoreForm() {
+    input.value = "";
+    input.focus();
+}
+
+// Check for expected form data.
+function checkForm(expectedValue) {
+  var formID = input.parentNode.id;
+  is(input.value, expectedValue, "Checking " + formID + " input");
+}
+
+
+/*
+ * Main section of test...
+ *
+ * This is a bit hacky, because the events are either being sent or
+ * processes asynchronously, so we need to interrupt our flow with lots of
+ * setTimeout() calls. The case statements are executed in order, one per
+ * timeout.
+ */
+function runTest(testNum) {
+  // Seems we need to enable this again, or sendKeyEvent() complaints.
+  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+  ok(true, "Starting test #" + testNum);
+
+  switch(testNum) {
+    case 1:
+        // Make sure initial form is empty.
+        checkForm("");
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 2:
+        checkMenuEntries(["value1", "value2", "value3", "value4"]);
+        // Check first entry
+        doKey("down");
+        checkForm(""); // value shouldn't update
+        doKey("return"); // not "enter"!
+        checkForm("value1");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 3:
+        // Check second entry
+        doKey("down");
+        doKey("down");
+        doKey("return"); // not "enter"!
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 4:
+        // Check third entry
+        doKey("down");
+        doKey("down");
+        doKey("down");
+        doKey("return");
+        checkForm("value3");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 5:
+        // Check fourth entry
+        doKey("down");
+        doKey("down");
+        doKey("down");
+        doKey("down");
+        doKey("return");
+        checkForm("value4");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 6:
+        // Check first entry (wraparound)
+        doKey("down");
+        doKey("down");
+        doKey("down");
+        doKey("down");
+        doKey("down"); // deselects
+        doKey("down");
+        doKey("return");
+        checkForm("value1");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 7:
+        // Check the last entry via arrow-up
+        doKey("up");
+        doKey("return");
+        checkForm("value4");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 8:
+        // Check the last entry via arrow-up
+        doKey("down"); // select first entry
+        doKey("up");   // selects nothing!
+        doKey("up");   // select last entry
+        doKey("return");
+        checkForm("value4");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 9:
+        // Check the last entry via arrow-up (wraparound)
+        doKey("down");
+        doKey("up"); // deselects
+        doKey("up"); // last entry
+        doKey("up");
+        doKey("up");
+        doKey("up"); // first entry
+        doKey("up"); // deselects
+        doKey("up"); // last entry
+        doKey("return");
+        checkForm("value4");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 10:
+        // Set first entry w/o triggering autocomplete
+        doKey("down");
+        doKey("right");
+        checkForm("value1");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 11:
+        // Set first entry w/o triggering autocomplete
+        doKey("down");
+        doKey("left");
+        checkForm("value1");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 12:
+        // Check first entry (page up)
+        doKey("down");
+        doKey("down");
+        doKey("page_up");
+        doKey("return");
+        checkForm("value1");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 13:
+        // Check last entry (page down)
+        doKey("down");
+        doKey("page_down");
+        doKey("return");
+        checkForm("value4");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        testNum = 49;
+        break;
+
+    /* Test removing entries from the dropdown */
+
+    case 50:
+        checkMenuEntries(["value1", "value2", "value3", "value4"]);
+        // Delete the first entry (of 4)
+        doKey("down");
+
+        // On OS X, shift-backspace and shift-delete work, just delete does not.
+        // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+        doKey("delete", shiftModifier);
+
+        checkForm("");
+        ok(!fh.entryExists("field1", "value1"), "checking that f1/v1 was deleted");
+        doKey("return");
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 51:
+        checkMenuEntries(["value2", "value3", "value4"]);
+        // Check the new first entry (of 3)
+        doKey("down");
+        doKey("return");
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 52:
+        // Delete the second entry (of 3)
+        doKey("down");
+        doKey("down");
+        doKey("delete", shiftModifier);
+        checkForm("");
+        ok(!fh.entryExists("field1", "value3"), "checking that f1/v3 was deleted");
+        doKey("return");
+        checkForm("value4")
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 53:
+        checkMenuEntries(["value2", "value4"]);
+        // Check the new first entry (of 2)
+        doKey("down");
+        doKey("return");
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 54:
+        // Delete the last entry (of 2)
+        doKey("down");
+        doKey("down");
+        doKey("delete", shiftModifier);
+        checkForm("");
+        ok(!fh.entryExists("field1", "value4"), "checking that f1/v4 was deleted");
+        doKey("return");
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 55:
+        checkMenuEntries(["value2"]);
+        // Check the new first entry (of 1)
+        doKey("down");
+        doKey("return");
+        checkForm("value2");
+
+        // Trigger autocomplete popup
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 56:
+        // Delete the only remaining entry
+        doKey("down");
+        doKey("delete", shiftModifier);
+        checkForm("");
+        ok(!fh.entryExists("field1", "value2"), "checking that f1/v2 was deleted");
+
+        // Look at form 2, trigger autocomplete popup
+        input = $_(2, "field2");
+        restoreForm();
+        doKey("down");
+        testNum = 99;
+        break;
+
+    /* Test entries with autocomplete=off */
+
+    case 100:
+        // Select first entry
+        doKey("down");
+        doKey("return");
+        checkForm("value1");
+
+        // Look at form 3, try to trigger autocomplete popup
+        input = $_(3, "field2");
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 101:
+        // Ensure there's no autocomplete dropdown (autocomplete=off is present)
+        doKey("down");
+        doKey("return");
+        checkForm("");
+
+        // Look at form 4, try to trigger autocomplete popup
+        input = $_(4, "field2");
+        restoreForm();
+        doKey("down");
+        break;
+
+    case 102:
+        // Ensure there's no autocomplete dropdown (autocomplete=off is present)
+        doKey("down");
+        doKey("return");
+        checkForm("");
+
+        // Look at form 5, try to trigger autocomplete popup
+        input = $_(5, "field3");
+        restoreForm();
+        testNum = 199;
+        sendChar("a", input);
+        break;
+
+    /* Test filtering as characters are typed. */
+
+    case 200:
+        checkMenuEntries(["a", "aa", "aa\xe6", "az"]);
+        sendChar("a", input);
+        break;
+
+    case 201:
+        checkMenuEntries(["aa", "aa\xe6"]);
+        sendChar("\xc6", input);
+        break;
+
+    case 202:
+        checkMenuEntries(["aa\xe6"]);
+        doKey("escape");
+
+        SimpleTest.finish();
+        return;
+
+    default:
+        ok(false, "Unexpected invocation of test #" + testNum);
+        SimpleTest.finish();
+        return;
+  }
+
+  setTimeout(runTest, 50, testNum + 1); // XXX 40ms was too slow, why?
+}
+
+function checkMenuEntries(expectedValues) {
+    var actualValues = getMenuEntries();
+    is(actualValues.length, expectedValues.length, "Checking length of expected menu");
+    for (var i = 0; i < expectedValues.length; i++)
+        is(actualValues[i], expectedValues[i], "Checking menu entry #"+i);
+}
+
+var autocompleteMenu;
+function getMenuEntries() {
+    var entries = [];
+
+    // Could perhaps pull values directly from the controller, but it seems
+    // more reliable to test the values that are actually in the tree?
+    var column = autocompleteMenu.tree.columns[0];
+    var numRows = autocompleteMenu.tree.view.rowCount;
+    for (var i = 0; i < numRows; i++) {
+        entries.push(autocompleteMenu.tree.view.getValueAt(i, column));
+    }
+    return entries;
+}
+
+function startTest() {
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    var Ci = Components.interfaces;
+    chromeWin = window
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIWebNavigation)
+                    .QueryInterface(Ci.nsIDocShellTreeItem)
+                    .rootTreeItem
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindow)
+                    .QueryInterface(Ci.nsIDOMChromeWindow);
+    autocompleteMenu = chromeWin.document.getElementById("PopupAutoComplete");
+    ok(autocompleteMenu, "Got autocomplete popup");
+    runTest(1);
+}
+
+window.onload = startTest;
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+