Re-land patches from bug 566746 and bug 697377 now that bug 851641 is addressed
authorGavin Sharp <gavin@gavinsharp.com>
Fri, 19 Apr 2013 15:21:30 -0700
changeset 142530 0b21e902f7a0ba74960e757379f18d4c90dcadba
parent 142529 6035591d40f89d74c82f6d8fa8d7e4ba3c3e79ce
child 142531 96fe69d53f356ff1ffe0ad508bb4b7a65c4cbbfb
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs566746, 697377, 851641
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Re-land patches from bug 566746 and bug 697377 now that bug 851641 is addressed
b2g/installer/package-manifest.in
browser/base/content/sanitize.js
browser/base/content/sanitizeDialog.js
browser/base/content/test/browser_bug409624.js
browser/base/content/test/browser_sanitize-timespans.js
browser/base/content/test/browser_sanitizeDialog.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
browser/components/search/content/search.xml
browser/components/search/test/browser_426329.js
browser/installer/package-manifest.in
editor/libeditor/text/tests/test_bug527935.html
mobile/android/chrome/content/browser.js
mobile/android/installer/package-manifest.in
testing/specialpowers/content/specialpowersAPI.js
toolkit/components/satchel/FormHistory.jsm
toolkit/components/satchel/FormHistoryStartup.js
toolkit/components/satchel/Makefile.in
toolkit/components/satchel/nsFormAutoComplete.js
toolkit/components/satchel/nsFormFillController.cpp
toolkit/components/satchel/nsFormFillController.h
toolkit/components/satchel/nsFormHistory.js
toolkit/components/satchel/nsIFormAutoComplete.idl
toolkit/components/satchel/satchel.manifest
toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js
toolkit/components/satchel/test/satchel_common.js
toolkit/components/satchel/test/test_bug_511615.html
toolkit/components/satchel/test/test_bug_787624.html
toolkit/components/satchel/test/test_form_autocomplete.html
toolkit/components/satchel/test/test_form_autocomplete_with_list.html
toolkit/components/satchel/test/test_form_submission.html
toolkit/components/satchel/test/test_form_submission_cap.html
toolkit/components/satchel/test/test_form_submission_cap2.html
toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite
toolkit/components/satchel/test/unit/formhistory_apitest.sqlite
toolkit/components/satchel/test/unit/formhistory_expire.sqlite
toolkit/components/satchel/test/unit/formhistory_v0.sqlite
toolkit/components/satchel/test/unit/formhistory_v0v1.sqlite
toolkit/components/satchel/test/unit/formhistory_v1.sqlite
toolkit/components/satchel/test/unit/formhistory_v1v2.sqlite
toolkit/components/satchel/test/unit/formhistory_v2.sqlite
toolkit/components/satchel/test/unit/formhistory_v2v3.sqlite
toolkit/components/satchel/test/unit/head_satchel.js
toolkit/components/satchel/test/unit/test_async_expire.js
toolkit/components/satchel/test/unit/test_autocomplete.js
toolkit/components/satchel/test/unit/test_db_corrupt.js
toolkit/components/satchel/test/unit/test_db_update_v1.js
toolkit/components/satchel/test/unit/test_db_update_v1b.js
toolkit/components/satchel/test/unit/test_db_update_v2.js
toolkit/components/satchel/test/unit/test_db_update_v2b.js
toolkit/components/satchel/test/unit/test_db_update_v3.js
toolkit/components/satchel/test/unit/test_db_update_v3b.js
toolkit/components/satchel/test/unit/test_db_update_v4.js
toolkit/components/satchel/test/unit/test_db_update_v4b.js
toolkit/components/satchel/test/unit/test_db_update_v999a.js
toolkit/components/satchel/test/unit/test_db_update_v999b.js
toolkit/components/satchel/test/unit/test_expire.js
toolkit/components/satchel/test/unit/test_history_api.js
toolkit/components/satchel/test/unit/test_notify.js
toolkit/components/satchel/test/unit/xpcshell.ini
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -442,16 +442,17 @@
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
 @BINPATH@/components/nsHandlerService.js
 @BINPATH@/components/nsWebHandlerApp.manifest
 @BINPATH@/components/nsWebHandlerApp.js
 @BINPATH@/components/satchel.manifest
 @BINPATH@/components/nsFormAutoComplete.js
 @BINPATH@/components/nsFormHistory.js
+@BINPATH@/components/FormHistoryStartup.js
 @BINPATH@/components/nsInputListAutoComplete.js
 @BINPATH@/components/contentSecurityPolicy.manifest
 @BINPATH@/components/contentSecurityPolicy.js
 @BINPATH@/components/contentAreaDropListener.manifest
 @BINPATH@/components/contentAreaDropListener.js
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
 @BINPATH@/components/SettingsManager.js
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -1,77 +1,99 @@
 # -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
 
 function Sanitizer() {}
 Sanitizer.prototype = {
   // warning to the caller: this one may raise an exception (e.g. bug #265028)
   clearItem: function (aItemName)
   {
     if (this.items[aItemName].canClear)
       this.items[aItemName].clear();
   },
 
-  canClearItem: function (aItemName)
+  canClearItem: function (aItemName, aCallback, aArg)
   {
-    return this.items[aItemName].canClear;
+    let canClear = this.items[aItemName].canClear;
+    if (typeof canClear == "function") {
+      canClear(aCallback, aArg);
+      return false;
+    }
+
+    aCallback(aItemName, canClear, aArg);
+    return canClear;
   },
   
   prefDomain: "",
   
   getNameFromPreference: function (aPreferenceName)
   {
     return aPreferenceName.substr(this.prefDomain.length);
   },
   
   /**
-   * Deletes privacy sensitive data in a batch, according to user preferences
-   *
-   * @returns  null if everything's fine;  an object in the form
-   *           { itemName: error, ... } on (partial) failure
+   * Deletes privacy sensitive data in a batch, according to user preferences. Calls
+   * errorHandler with a list of errors on failure.
    */
-  sanitize: function ()
+  sanitize: function (errorHandler)
   {
     var psvc = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefService);
     var branch = psvc.getBranch(this.prefDomain);
     var errors = null;
 
     // Cache the range of times to clear
     if (this.ignoreTimespan)
       var range = null;  // If we ignore timespan, clear everything
     else
       range = this.range || Sanitizer.getClearRange();
-      
+
+    let itemCount = this.items.length;
     for (var itemName in this.items) {
-      var item = this.items[itemName];
+      let item = this.items[itemName];
       item.range = range;
-      if ("clear" in item && item.canClear && branch.getBoolPref(itemName)) {
-        // Some of these clear() may raise exceptions (see bug #265028)
-        // to sanitize as much as possible, we catch and store them, 
-        // rather than fail fast.
-        // Callers should check returned errors and give user feedback
-        // about items that could not be sanitized
-        try {
-          item.clear();
-        } catch(er) {
-          if (!errors) 
-            errors = {};
-          errors[itemName] = er;
-          dump("Error sanitizing " + itemName + ": " + er + "\n");
-        }
+      if ("clear" in item && branch.getBoolPref(itemName)) {
+        let clearCallback = (itemName, aCanClear) => {
+          // Some of these clear() may raise exceptions (see bug #265028)
+          // to sanitize as much as possible, we catch and store them,
+          // rather than fail fast.
+          // Callers should check returned errors and give user feedback
+          // about items that could not be sanitized
+          let item = this.items[itemName];
+          try {
+            if (aCanClear)
+              item.clear();
+          } catch(er) {
+            if (!errors)
+              errors = {};
+            errors[itemName] = er;
+            dump("Error sanitizing " + itemName + ": " + er + "\n");
+          }
+
+          // If this is the last item that needs to receive the callback, call the error handler
+          if (!--itemCount && errors) {
+            errorHandler(error);
+          }
+        };
+
+        this.canClearItem(itemName, clearCallback);
       }
     }
-    return errors;
+
+    if (errors) {
+      errorHandler(error);
+    }
   },
   
   // Time span only makes sense in certain cases.  Consumers who want
   // to only clear some private data can opt in by setting this to false,
   // and can optionally specify a specific range.  If timespan is not ignored,
   // and range is not set, sanitize() will use the value of the timespan
   // pref to determine a range
   ignoreTimespan : true,
@@ -226,47 +248,56 @@ Sanitizer.prototype = {
           let searchBar = currentDocument.getElementById("searchbar");
           if (searchBar)
             searchBar.textbox.reset();
           let findBar = currentDocument.getElementById("FindToolbar");
           if (findBar)
             findBar.clear();
         }
 
-        let formHistory = Components.classes["@mozilla.org/satchel/form-history;1"]
-                                    .getService(Components.interfaces.nsIFormHistory2);
-        if (this.range)
-          formHistory.removeEntriesByTimeframe(this.range[0], this.range[1]);
-        else
-          formHistory.removeAllEntries();
+        let change = { op: "remove" };
+        if (this.range) {
+          [ change.firstUsedStart, change.firstUsedEnd ] = this.range;
+        }
+        FormHistory.update(change);
       },
 
-      get canClear()
+      canClear : function(aCallback, aArg)
       {
         var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                       .getService(Components.interfaces.nsIWindowMediator);
         var windows = windowManager.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
           let currentDocument = windows.getNext().document;
           let searchBar = currentDocument.getElementById("searchbar");
           if (searchBar) {
             let transactionMgr = searchBar.textbox.editor.transactionManager;
             if (searchBar.value ||
                 transactionMgr.numberOfUndoItems ||
-                transactionMgr.numberOfRedoItems)
-              return true;
+                transactionMgr.numberOfRedoItems) {
+              aCallback("formdata", true, aArg);
+              return false;
+            }
           }
           let findBar = currentDocument.getElementById("FindToolbar");
-          if (findBar && findBar.canClear)
-            return true;
+          if (findBar && findBar.canClear) {
+            aCallback("formdata", true, aArg);
+            return false;
+          }
         }
 
-        let formHistory = Components.classes["@mozilla.org/satchel/form-history;1"]
-                                    .getService(Components.interfaces.nsIFormHistory2);
-        return formHistory.hasEntries;
+        let count = 0;
+        let countDone = {
+          handleResult : function(aResult) count = aResult,
+          handleError : function(aError) Components.utils.reportError(aError),
+          handleCompletion :
+            function(aReason) { aCallback("formdata", aReason == 0 && count > 0, aArg); }
+        };
+        FormHistory.count({}, countDone);
+        return false;
       }
     },
     
     downloads: {
       clear: function ()
       {
         var dlMgr = Components.classes["@mozilla.org/download-manager;1"]
                               .getService(Components.interfaces.nsIDownloadManager);
@@ -481,14 +512,12 @@ Sanitizer.onShutdown = function()
 Sanitizer._checkAndSanitize = function() 
 {
   const prefs = Sanitizer.prefs;
   if (prefs.getBoolPref(Sanitizer.prefShutdown) && 
       !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) {
     // this is a shutdown or a startup after an unclean exit
     var s = new Sanitizer();
     s.prefDomain = "privacy.clearOnShutdown.";
-    s.sanitize() || // sanitize() returns null on full success
-      prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
+    let errorHandler = function() prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
+    s.sanitize(errorHandler);
   }
 };
-
-
--- a/browser/base/content/sanitizeDialog.js
+++ b/browser/base/content/sanitizeDialog.js
@@ -42,21 +42,23 @@ var gSanitizePromptDialog = {
 
     var s = new Sanitizer();
     s.prefDomain = "privacy.cpd.";
 
     let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
     for (let i = 0; i < sanitizeItemList.length; i++) {
       let prefItem = sanitizeItemList[i];
       let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
-      if (!s.canClearItem(name)) {
-        prefItem.preference = null;
-        prefItem.checked = false;
-        prefItem.disabled = true;
-      }
+      s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) {
+        if (!aCanClear) {
+          aPrefItem.preference = null;
+          aPrefItem.checked = false;
+          aPrefItem.disabled = true;
+        }
+      }, prefItem);
     }
 
     document.documentElement.getButton("accept").label =
       this.bundleBrowser.getString("sanitizeButtonOK");
 
     if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
       this.prepareWarning();
       this.warningBox.hidden = false;
@@ -276,21 +278,23 @@ var gSanitizePromptDialog = {
 
     var s = new Sanitizer();
     s.prefDomain = "privacy.cpd.";
 
     let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
     for (let i = 0; i < sanitizeItemList.length; i++) {
       let prefItem = sanitizeItemList[i];
       let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
-      if (!s.canClearItem(name)) {
-        prefItem.preference = null;
-        prefItem.checked = false;
-        prefItem.disabled = true;
-      }
+      s.canClearItem(name, function canClearCallback(aCanClear) {
+        if (!aCanClear) {
+          prefItem.preference = null;
+          prefItem.checked = false;
+          prefItem.disabled = true;
+        }
+      });
     }
 
     document.documentElement.getButton("accept").label =
       this.bundleBrowser.getString("sanitizeButtonOK");
 
     this.selectByTimespan();
   },
 
--- a/browser/base/content/test/browser_bug409624.js
+++ b/browser/base/content/test/browser_bug409624.js
@@ -1,15 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  *  License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+
 function test() {
   waitForExplicitFinish();
 
+  // This test relies on the form history being empty to start with delete
+  // all the items first.
+  FormHistory.update({ op: "remove" },
+                     { handleError: function (error) {
+                         do_throw("Error occurred updating form history: " + error);
+                       },
+                       handleCompletion: function (reason) { if (!reason) test2(); },
+                     });
+}
+
+function test2()
+{
   let prefService = Cc["@mozilla.org/preferences-service;1"]
                     .getService(Components.interfaces.nsIPrefBranch2);
 
   let findBar = gFindBar;
   let textbox = gFindBar.getElement("findbar-textbox");
 
   let tempScope = {};
   Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
@@ -24,18 +39,33 @@ function test() {
   prefBranch.setBoolPref("downloads", false);
   prefBranch.setBoolPref("formdata", true);
   prefBranch.setBoolPref("history", false);
   prefBranch.setBoolPref("offlineApps", false);
   prefBranch.setBoolPref("passwords", false);
   prefBranch.setBoolPref("sessions", false);
   prefBranch.setBoolPref("siteSettings", false);
 
-  // Sanitize now so we can test that canClear is correct
+  // Sanitize now so we can test that canClear is correct. Formdata is cleared asynchronously.
   s.sanitize();
-  ok(!s.canClearItem("formdata"), "pre-test baseline for sanitizer");
-  textbox.value = "m";
-  ok(s.canClearItem("formdata"), "formdata can be cleared after input");
-  s.sanitize();
-  is(textbox.value, "", "findBar textbox should be empty after sanitize");
-  ok(!s.canClearItem("formdata"), "canClear now false after sanitize");
+  s.canClearItem("formdata", clearDone1, s);
+}
+
+function clearDone1(aItemName, aResult, aSanitizer)
+{
+  ok(!aResult, "pre-test baseline for sanitizer");
+  gFindBar.getElement("findbar-textbox").value = "m";
+  aSanitizer.canClearItem("formdata", inputEntered, aSanitizer);
+}
+
+function inputEntered(aItemName, aResult, aSanitizer)
+{
+  ok(aResult, "formdata can be cleared after input");
+  aSanitizer.sanitize();
+  aSanitizer.canClearItem("formdata", clearDone2);
+}
+
+function clearDone2(aItemName, aResult)
+{
+  is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize");
+  ok(!aResult, "canClear now false after sanitize");
   finish();
 }
--- a/browser/base/content/test/browser_sanitize-timespans.js
+++ b/browser/base/content/test/browser_sanitize-timespans.js
@@ -1,29 +1,67 @@
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
 // Bug 453440 - Test the timespan-based logic of the sanitizer code
 var now_uSec = Date.now() * 1000;
 
 const dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
-const formhist = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);
 
 const kUsecPerMin = 60 * 1000000;
 
 let tempScope = {};
 Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
                                            .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
 let Sanitizer = tempScope.Sanitizer;
 
+let FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+
+function promiseFormHistoryRemoved() {
+  let deferred = Promise.defer();
+  Services.obs.addObserver(function onfh() {
+    Services.obs.removeObserver(onfh, "satchel-storage-changed", false);
+    deferred.resolve();
+  }, "satchel-storage-changed", false);
+  return deferred.promise;
+}
+
 function test() {
   waitForExplicitFinish();
 
-  setupDownloads();
-  setupFormHistory();
-  setupHistory(function() {
-    Task.spawn(onHistoryReady).then(finish);
-  });
+  Task.spawn(function() {
+    setupDownloads();
+    yield setupFormHistory();
+    yield setupHistory();
+    yield onHistoryReady();
+  }).then(finish);
+}
+
+function countEntries(name, message, check) {
+  let deferred = Promise.defer();
+
+  var obj = {};
+  if (name !== null)
+    obj.fieldname = name;
+
+  let count;
+  FormHistory.count(obj, { handleResult: function (result) count = result,
+                           handleError: function (error) {
+                             do_throw("Error occurred searching form history: " + error);
+                             deferred.reject(error)
+                           },
+                           handleCompletion: function (reason) {
+                             if (!reason) {
+                               check(count, message);
+                               deferred.resolve();
+                             }
+                           },
+                         });
+
+  return deferred.promise;
 }
 
 function onHistoryReady() {
   var hoursSinceMidnight = new Date().getHours();
   var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes();
 
   // Should test cookies here, but nsICookieManager/nsICookieService
   // doesn't let us fake creation times.  bug 463127
@@ -42,16 +80,18 @@ function onHistoryReady() {
   itemPrefs.setBoolPref("sessions", false);
   itemPrefs.setBoolPref("siteSettings", false);
 
   // Clear 10 minutes ago
   s.range = [now_uSec - 10*60*1000000, now_uSec];
   s.sanitize();
   s.range = null;
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))),
      "Pretend visit to 10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://1hour.com"))),
      "Pretend visit to 1hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should should still exist");
@@ -63,26 +103,29 @@ function onHistoryReady() {
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (minutesSinceMidnight > 10) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("10minutes"), "10minutes form entry should be deleted");
-  ok(formhist.nameExists("1hour"), "1hour form entry should still exist");
-  ok(formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should still exist");
-  ok(formhist.nameExists("2hour"), "2hour form entry should still exist");
-  ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist");
-  ok(formhist.nameExists("4hour"), "4hour form entry should still exist");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  let checkZero = function(num, message) { is(num, 0, message); }
+  let checkOne = function(num, message) { is(num, 1, message); }
+
+  yield countEntries("10minutes", "10minutes form entry should be deleted", checkZero);
+  yield countEntries("1hour", "1hour form entry should still exist", checkOne);
+  yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+  yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+  yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+  yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (minutesSinceMidnight > 10)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
   ok(!downloadExists(5555555), "10 minute download should now be deleted");
   ok(downloadExists(5555551), "<1 hour download should still be present");
   ok(downloadExists(5555556), "1 hour 10 minute download should still be present");
   ok(downloadExists(5555550), "Year old download should still be present");
   ok(downloadExists(5555552), "<2 hour old download should still be present");
   ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
   ok(downloadExists(5555553), "<4 hour old download should still be present");
@@ -90,16 +133,18 @@ function onHistoryReady() {
 
   if (minutesSinceMidnight > 10)
     ok(downloadExists(5555554), "'Today' download should still be present");
 
   // Clear 1 hour
   Sanitizer.prefs.setIntPref("timeSpan", 1);
   s.sanitize();
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))),
      "Pretend visit to 1hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should should still exist");
@@ -109,25 +154,25 @@ function onHistoryReady() {
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (hoursSinceMidnight > 1) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("1hour"), "1hour form entry should be deleted");
-  ok(formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should still exist");
-  ok(formhist.nameExists("2hour"), "2hour form entry should still exist");
-  ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist");
-  ok(formhist.nameExists("4hour"), "4hour form entry should still exist");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  yield countEntries("1hour", "1hour form entry should be deleted", checkZero);
+  yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+  yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+  yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+  yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (hoursSinceMidnight > 1)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
   ok(!downloadExists(5555551), "<1 hour download should now be deleted");
   ok(downloadExists(5555556), "1 hour 10 minute download should still be present");
   ok(downloadExists(5555550), "Year old download should still be present");
   ok(downloadExists(5555552), "<2 hour old download should still be present");
   ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
   ok(downloadExists(5555553), "<4 hour old download should still be present");
   ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
@@ -135,16 +180,18 @@ function onHistoryReady() {
   if (hoursSinceMidnight > 1)
     ok(downloadExists(5555554), "'Today' download should still be present");
   
   // Clear 1 hour 10 minutes
   s.range = [now_uSec - 70*60*1000000, now_uSec];
   s.sanitize();
   s.range = null;
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should should still exist");
@@ -152,188 +199,202 @@ function onHistoryReady() {
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (minutesSinceMidnight > 70) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should be deleted");
-  ok(formhist.nameExists("2hour"), "2hour form entry should still exist");
-  ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist");
-  ok(formhist.nameExists("4hour"), "4hour form entry should still exist");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  yield countEntries("1hour10minutes", "1hour10minutes form entry should be deleted", checkZero);
+  yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+  yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+  yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (minutesSinceMidnight > 70)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
   ok(!downloadExists(5555556), "1 hour 10 minute old download should now be deleted");
   ok(downloadExists(5555550), "Year old download should still be present");
   ok(downloadExists(5555552), "<2 hour old download should still be present");
   ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
   ok(downloadExists(5555553), "<4 hour old download should still be present");
   ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
   if (minutesSinceMidnight > 70)
     ok(downloadExists(5555554), "'Today' download should still be present");
 
   // Clear 2 hours
   Sanitizer.prefs.setIntPref("timeSpan", 2);
   s.sanitize();
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (hoursSinceMidnight > 2) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("2hour"), "2hour form entry should be deleted");
-  ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist");
-  ok(formhist.nameExists("4hour"), "4hour form entry should still exist");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  yield countEntries("2hour", "2hour form entry should be deleted", checkZero);
+  yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+  yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (hoursSinceMidnight > 2)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
   ok(!downloadExists(5555552), "<2 hour old download should now be deleted");
   ok(downloadExists(5555550), "Year old download should still be present");
   ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
   ok(downloadExists(5555553), "<4 hour old download should still be present");
   ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
   if (hoursSinceMidnight > 2)
     ok(downloadExists(5555554), "'Today' download should still be present");
   
   // Clear 2 hours 10 minutes
   s.range = [now_uSec - 130*60*1000000, now_uSec];
   s.sanitize();
   s.range = null;
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (minutesSinceMidnight > 130) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should be deleted");
-  ok(formhist.nameExists("4hour"), "4hour form entry should still exist");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  yield countEntries("2hour10minutes", "2hour10minutes form entry should be deleted", checkZero);
+  yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (minutesSinceMidnight > 130)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
   ok(!downloadExists(5555557), "2 hour 10 minute old download should now be deleted");
   ok(downloadExists(5555553), "<4 hour old download should still be present");
   ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
   ok(downloadExists(5555550), "Year old download should still be present");
   if (minutesSinceMidnight > 130)
     ok(downloadExists(5555554), "'Today' download should still be present");
 
   // Clear 4 hours
   Sanitizer.prefs.setIntPref("timeSpan", 3);
   s.sanitize();
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (hoursSinceMidnight > 4) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
 
-  ok(!formhist.nameExists("4hour"), "4hour form entry should be deleted");
-  ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist");
+  yield countEntries("4hour", "4hour form entry should be deleted", checkZero);
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
   if (hoursSinceMidnight > 4)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
 
   ok(!downloadExists(5555553), "<4 hour old download should now be deleted");
   ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
   ok(downloadExists(5555550), "Year old download should still be present");
   if (hoursSinceMidnight > 4)
     ok(downloadExists(5555554), "'Today' download should still be present");
 
   // Clear 4 hours 10 minutes
   s.range = [now_uSec - 250*60*1000000, now_uSec];
   s.sanitize();
   s.range = null;
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should now be deleted");
   if (minutesSinceMidnight > 250) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
     "Pretend visit to before-today.com should still exist");
-  
-  ok(!formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should be deleted");
+
+  yield countEntries("4hour10minutes", "4hour10minutes form entry should be deleted", checkZero);
   if (minutesSinceMidnight > 250)
-    ok(formhist.nameExists("today"), "today form entry should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
-
+    yield countEntries("today", "today form entry should still exist", checkOne);
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+  
   ok(!downloadExists(5555558), "4 hour 10 minute download should now be deleted");
   ok(downloadExists(5555550), "Year old download should still be present");
   if (minutesSinceMidnight > 250)
     ok(downloadExists(5555554), "'Today' download should still be present");
 
   // Clear Today
   Sanitizer.prefs.setIntPref("timeSpan", 4);
   s.sanitize();
 
+  yield promiseFormHistoryRemoved();
+
   // Be careful.  If we add our objectss just before midnight, and sanitize
   // runs immediately after, they won't be expired.  This is expected, but
   // we should not test in that case.  We cannot just test for opposite
   // condition because we could cross midnight just one moment after we
   // cache our time, then we would have an even worse random failure.
   var today = isToday(new Date(now_uSec/1000));
   if (today) {
     ok(!(yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should now be deleted");
-    ok(!formhist.nameExists("today"), "today form entry should be deleted");
+
+    yield countEntries("today", "today form entry should be deleted", checkZero);
     ok(!downloadExists(5555554), "'Today' download should now be deleted");
   }
 
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
      "Pretend visit to before-today.com should still exist");
-  ok(formhist.nameExists("b4today"), "b4today form entry should still exist");
+  yield countEntries("b4today", "b4today form entry should still exist", checkOne);
   ok(downloadExists(5555550), "Year old download should still be present");
 
   // Choose everything
   Sanitizer.prefs.setIntPref("timeSpan", 0);
   s.sanitize();
 
+  yield promiseFormHistoryRemoved();
+
   ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))),
      "Pretend visit to before-today.com should now be deleted");
 
-  ok(!formhist.nameExists("b4today"), "b4today form entry should be deleted");
+  yield countEntries("b4today", "b4today form entry should be deleted", checkZero);
 
   ok(!downloadExists(5555550), "Year old download should now be deleted");
 }
 
-function setupHistory(aCallback) {
+function setupHistory() {
+  let deferred = Promise.defer();
+
   let places = [];
 
   function addPlace(aURI, aTitle, aVisitDate) {
     places.push({
       uri: aURI,
       title: aTitle,
       visits: [{
         visitDate: aVisitDate,
@@ -358,83 +419,150 @@ function setupHistory(aCallback) {
 
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
   addPlace(makeURI("http://before-today.com/"), "Before Today", lastYear.getTime() * 1000);
 
   PlacesUtils.asyncHistory.updatePlaces(places, {
     handleError: function () ok(false, "Unexpected error in adding visit."),
     handleResult: function () { },
-    handleCompletion: function () aCallback()
+    handleCompletion: function () deferred.resolve()
   });
+
+  return deferred.promise;
 }
 
 function setupFormHistory() {
-  // Make sure we've got a clean DB to start with.
-  formhist.removeAllEntries();
+
+  function searchEntries(terms, params) {
+    let deferred = Promise.defer();
+
+    let results = [];
+    FormHistory.search(terms, params, { handleResult: function (result) results.push(result),
+                                        handleError: function (error) {
+                                          do_throw("Error occurred searching form history: " + error);
+                                          deferred.reject(error);
+                                        },
+                                        handleCompletion: function (reason) { deferred.resolve(results); }
+                                      });
+    return deferred.promise;
+  }
+
+  function update(changes)
+  {
+    let deferred = Promise.defer();
+    FormHistory.update(changes, { handleError: function (error) {
+                                    do_throw("Error occurred searching form history: " + error);
+                                    deferred.reject(error);
+                                  },
+                                  handleCompletion: function (reason) { deferred.resolve(); }
+                                });
+    return deferred.promise;
+  }
 
-  // Add the entries we'll be testing.
-  formhist.addEntry("10minutes", "10m");
-  formhist.addEntry("1hour", "1h");
-  formhist.addEntry("1hour10minutes", "1h10m");
-  formhist.addEntry("2hour", "2h");
-  formhist.addEntry("2hour10minutes", "2h10m");
-  formhist.addEntry("4hour", "4h");
-  formhist.addEntry("4hour10minutes", "4h10m");
-  formhist.addEntry("today", "1d");
-  formhist.addEntry("b4today", "1y");
+  // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+  yield update(
+    [{
+        op: "remove"
+     },
+     {
+        op : "add",
+        fieldname : "10minutes",
+        value : "10m"
+      }, {
+        op : "add",
+        fieldname : "1hour",
+        value : "1h"
+      }, {
+        op : "add",
+        fieldname : "1hour10minutes",
+        value : "1h10m"
+      }, {
+        op : "add",
+        fieldname : "2hour",
+        value : "2h"
+      }, {
+        op : "add",
+        fieldname : "2hour10minutes",
+        value : "2h10m"
+      }, {
+        op : "add",
+        fieldname : "4hour",
+        value : "4h"
+      }, {
+        op : "add",
+        fieldname : "4hour10minutes",
+        value : "4h10m"
+      }, {
+        op : "add",
+        fieldname : "today",
+        value : "1d"
+      }, {
+        op : "add",
+        fieldname : "b4today",
+        value : "1y"
+      }]);
 
   // Artifically age the entries to the proper vintage.
-  let db = formhist.DBConnection;
   let timestamp = now_uSec - 10 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '10minutes'");
+  let results = yield searchEntries(["guid"], { fieldname: "10minutes" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 45 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '1hour'");
+  results = yield searchEntries(["guid"], { fieldname: "1hour" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 70 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '1hour10minutes'");
+  results = yield searchEntries(["guid"], { fieldname: "1hour10minutes" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 90 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '2hour'");
+  results = yield searchEntries(["guid"], { fieldname: "2hour" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 130 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '2hour10minutes'");
+  results = yield searchEntries(["guid"], { fieldname: "2hour10minutes" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 180 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '4hour'");
+  results = yield searchEntries(["guid"], { fieldname: "4hour" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
   timestamp = now_uSec - 250 * kUsecPerMin;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '4hour10minutes'");
+  results = yield searchEntries(["guid"], { fieldname: "4hour10minutes" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
 
   let today = new Date();
   today.setHours(0);
   today.setMinutes(0);
   today.setSeconds(1);
   timestamp = today.getTime() * 1000;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = 'today'");
+  results = yield searchEntries(["guid"], { fieldname: "today" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
 
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
   timestamp = lastYear.getTime() * 1000;
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = 'b4today'");
+  results = yield searchEntries(["guid"], { fieldname: "b4today" });
+  yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+  var checks = 0;
+  let checkOne = function(num, message) { is(num, 1, message); checks++; }
 
   // Sanity check.
-  ok(formhist.nameExists("10minutes"), "Checking for 10minutes form history entry creation");
-  ok(formhist.nameExists("1hour"), "Checking for 1hour form history entry creation");
-  ok(formhist.nameExists("1hour10minutes"), "Checking for 1hour10minutes form history entry creation");
-  ok(formhist.nameExists("2hour"), "Checking for 2hour form history entry creation");
-  ok(formhist.nameExists("2hour10minutes"), "Checking for 2hour10minutes form history entry creation");
-  ok(formhist.nameExists("4hour"), "Checking for 4hour form history entry creation");
-  ok(formhist.nameExists("4hour10minutes"), "Checking for 4hour10minutes form history entry creation");
-  ok(formhist.nameExists("today"), "Checking for today form history entry creation");
-  ok(formhist.nameExists("b4today"), "Checking for b4today form history entry creation");
+  yield countEntries("10minutes", "Checking for 10minutes form history entry creation", checkOne);
+  yield countEntries("1hour", "Checking for 1hour form history entry creation", checkOne);
+  yield countEntries("1hour10minutes", "Checking for 1hour10minutes form history entry creation", checkOne);
+  yield countEntries("2hour", "Checking for 2hour form history entry creation", checkOne);
+  yield countEntries("2hour10minutes", "Checking for 2hour10minutes form history entry creation", checkOne);
+  yield countEntries("4hour", "Checking for 4hour form history entry creation", checkOne);
+  yield countEntries("4hour10minutes", "Checking for 4hour10minutes form history entry creation", checkOne);
+  yield countEntries("today", "Checking for today form history entry creation", checkOne);
+  yield countEntries("b4today", "Checking for b4today form history entry creation", checkOne);
+  is(checks, 9, "9 checks made");
 }
 
 function setupDownloads() {
 
   // Add 10-minutes download to DB
   let data = {
     id:   "5555555",
     name: "fakefile-10-minutes",
--- a/browser/base/content/test/browser_sanitizeDialog.js
+++ b/browser/base/content/test/browser_sanitizeDialog.js
@@ -12,28 +12,33 @@
  * browser/base/content/test/browser_sanitize-timespans.js does that.  This
  * test checks the UI of the dialog and makes sure it's correctly connected to
  * the sanitize timespan code.
  *
  * Some of this code, especially the history creation parts, was taken from
  * browser/base/content/test/browser_sanitize-timespans.js.
  */
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+
 let tempScope = {};
 Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
                                            .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
 let Sanitizer = tempScope.Sanitizer;
 
 const dm = Cc["@mozilla.org/download-manager;1"].
            getService(Ci.nsIDownloadManager);
-const formhist = Cc["@mozilla.org/satchel/form-history;1"].
-                 getService(Ci.nsIFormHistory2);
 
 const kUsecPerMin = 60 * 1000000;
 
+let formEntries;
+
 // Add tests here.  Each is a function that's called by doNextTest().
 var gAllTests = [
 
   /**
    * Initializes the dialog to its default state.
    */
   function () {
     let wh = new WindowHelper();
@@ -75,17 +80,17 @@ var gAllTests = [
 
         // Hide details
         this.toggleDetails();
         this.checkDetails(false);
         this.cancelDialog();
       };
       wh.onunload = function () {
         yield promiseHistoryClearedState(uris, false);
-        blankSlate();
+        yield blankSlate();
         yield promiseHistoryClearedState(uris, true);
       };
       wh.open();
     });
   },
 
   /**
    * Ensures that the combined history-downloads checkbox clears both history
@@ -143,25 +148,42 @@ var gAllTests = [
         yield promiseHistoryClearedState(uris, true);
         ensureDownloadsClearedState(downloadIDs, true);
 
         // Visits and downloads > 1 hour should still exist.
         yield promiseHistoryClearedState(olderURIs, false);
         ensureDownloadsClearedState(olderDownloadIDs, false);
 
         // OK, done, cleanup after ourselves.
-        blankSlate();
+        yield blankSlate();
         yield promiseHistoryClearedState(olderURIs, true);
         ensureDownloadsClearedState(olderDownloadIDs, true);
       };
       wh.open();
     });
   },
 
   /**
+   * Add form history entries for the next test.
+   */
+  function () {
+    formEntries = [];
+
+    let iter = function() {
+      for (let i = 0; i < 5; i++) {
+        formEntries.push(addFormEntryWithMinutesAgo(iter, i));
+        yield;
+      }
+      doNextTest();
+    }();
+
+    iter.next();
+  },
+
+  /**
    * Ensures that the combined history-downloads checkbox removes neither
    * history visits nor downloads when not checked.
    */
   function () {
     // Add history, downloads, form entries (within the past hour).
     let uris = [];
     let places = [];
     let pURI;
@@ -171,20 +193,16 @@ var gAllTests = [
       uris.push(pURI);
     }
 
     addVisits(places, function() {
       let downloadIDs = [];
       for (let i = 0; i < 5; i++) {
         downloadIDs.push(addDownloadWithMinutesAgo(i));
       }
-      let formEntries = [];
-      for (let i = 0; i < 5; i++) {
-        formEntries.push(addFormEntryWithMinutesAgo(i));
-      }
 
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), false,
            "Warning panel should be hidden after previously accepting dialog " +
            "with a predefined timespan");
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
 
@@ -202,20 +220,24 @@ var gAllTests = [
         boolPrefIs("cpd.downloads", false,
                    "downloads pref should be false after accepting dialog with " +
                    "history checkbox unchecked");
       };
       wh.onunload = function () {
         // Of the three only form entries should be cleared.
         yield promiseHistoryClearedState(uris, false);
         ensureDownloadsClearedState(downloadIDs, false);
-        ensureFormEntriesClearedState(formEntries, true);
+
+        formEntries.forEach(function (entry) {
+          let exists = yield formNameExists(entry);
+          is(exists, false, "form entry " + entry + " should no longer exist");
+        });
 
         // OK, done, cleanup after ourselves.
-        blankSlate();
+        yield blankSlate();
         yield promiseHistoryClearedState(uris, true);
         ensureDownloadsClearedState(downloadIDs, true);
       };
       wh.open();
     });
   },
 
   /**
@@ -297,26 +319,38 @@ var gAllTests = [
       wh.onunload = function () {
         yield promiseHistoryClearedState(uris, true);
       };
       wh.open();
     });
   },
 
   /**
+   * Add form history entry for the next test.
+   */
+  function () {
+    let iter = function() {
+      formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ];
+      yield;
+      doNextTest();
+    }();
+
+    iter.next();
+  },
+
+  /**
    * The next three tests checks that when a certain history item cannot be
    * cleared then the checkbox should be both disabled and unchecked.
    * In addition, we ensure that this behavior does not modify the preferences.
    */
   function () {
     // Add history.
     let pURI = makeURI("http://" + 10 + "-minutes-ago.com/");
     addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}, function() {
       let uris = [ pURI ];
-      let formEntries = [ addFormEntryWithMinutesAgo(10) ];
 
       let wh = new WindowHelper();
       wh.onload = function() {
         // Check that the relevant checkboxes are enabled
         var cb = this.win.document.querySelectorAll(
                    "#itemList > [preference='privacy.cpd.formdata']");
         ok(cb.length == 1 && !cb[0].disabled, "There is formdata, checkbox to " +
            "clear formdata should be enabled.");
@@ -326,17 +360,19 @@ var gAllTests = [
         ok(cb.length == 1 && !cb[0].disabled, "There is history, checkbox to " +
            "clear history should be enabled.");
 
         this.checkAllCheckboxes();
         this.acceptDialog();
       };
       wh.onunload = function () {
         yield promiseHistoryClearedState(uris, true);
-        ensureFormEntriesClearedState(formEntries, true);
+
+        let exists = yield formNameExists(formEntries[0]);
+        is(exists, false, "form entry " + formEntries[0] + " should no longer exist");
       };
       wh.open();
     });
   },
   function () {
     let wh = new WindowHelper();
     wh.onload = function() {
       boolPrefIs("cpd.history", true,
@@ -360,35 +396,48 @@ var gAllTests = [
       ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
          "There is no history, but history checkbox should always be enabled " +
          "and will be checked from previous preference.");
 
       this.acceptDialog();
     }
     wh.open();
   },
+
+  /**
+   * Add form history entry for the next test.
+   */
   function () {
-    let formEntries = [ addFormEntryWithMinutesAgo(10) ];
+    let iter = function() {
+      formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ];
+      yield;
+      doNextTest();
+    }();
 
+    iter.next();
+  },
+
+  function () {
     let wh = new WindowHelper();
     wh.onload = function() {
       boolPrefIs("cpd.formdata", true,
                  "formdata pref should persist previous value after accepting " +
                  "dialog where you could not clear formdata.");
 
       var cb = this.win.document.querySelectorAll(
                  "#itemList > [preference='privacy.cpd.formdata']");
       ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
          "There exists formEntries so the checkbox should be in sync with " +
          "the pref.");
 
       this.acceptDialog();
     };
     wh.onunload = function () {
-      ensureFormEntriesClearedState(formEntries, true);
+      let exists = yield formNameExists(formEntries[0]);
+      is(exists, false, "form entry " + formEntries[0] + " should no longer exist");
     };
     wh.open();
   },
 
 
   /**
    * These next six tests together ensure that toggling details persists
    * across dialog openings.
@@ -734,17 +783,19 @@ WindowHelper.prototype = {
   isWarningPanelVisible: function () {
     return !this.getWarningPanel().hidden;
   },
 
   /**
    * Opens the clear recent history dialog.  Before calling this, set
    * this.onload to a function to execute onload.  It should close the dialog
    * when done so that the tests may continue.  Set this.onunload to a function
-   * to execute onunload.  this.onunload is optional.
+   * to execute onunload.  this.onunload is optional. If it returns true, the
+   * caller is expected to call waitForAsyncUpdates at some point; if false is
+   * returned, waitForAsyncUpdates is called automatically.
    */
   open: function () {
     let wh = this;
 
     function windowObserver(aSubject, aTopic, aData) {
       if (aTopic != "domwindowopened")
         return;
 
@@ -886,38 +937,69 @@ function addDownloadWithMinutesAgo(aMinu
 }
 
 /**
  * Adds a form entry to history.
  *
  * @param aMinutesAgo
  *        The entry will be added this many minutes ago
  */
-function addFormEntryWithMinutesAgo(aMinutesAgo) {
+function addFormEntryWithMinutesAgo(then, aMinutesAgo) {
   let name = aMinutesAgo + "-minutes-ago";
-  formhist.addEntry(name, "dummy");
 
   // Artifically age the entry to the proper vintage.
-  let db = formhist.DBConnection;
   let timestamp = now_uSec - (aMinutesAgo * kUsecPerMin);
-  db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
-                      timestamp +  " WHERE fieldname = '" + name + "'");
+
+  FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp },
+                     { handleError: function (error) {
+                         do_throw("Error occurred updating form history: " + error);
+                       },
+                       handleCompletion: function (reason) { then.next(); }
+                     });
+  return name;
+}
 
-  is(formhist.nameExists(name), true,
-     "Sanity check: form entry " + name + " should exist after creating it");
-  return name;
+/**
+ * Checks if a form entry exists.
+ */
+function formNameExists(name)
+{
+  let deferred = Promise.defer();
+
+  let count = 0;
+  FormHistory.count({ fieldname: name },
+                    { handleResult: function (result) count = result,
+                      handleError: function (error) {
+                        do_throw("Error occurred searching form history: " + error);
+                        deferred.reject(error);
+                      },
+                      handleCompletion: function (reason) {
+                          if (!reason) deferred.resolve(count);
+                      }
+                    });
+
+  return deferred.promise;
 }
 
 /**
  * Removes all history visits, downloads, and form entries.
  */
 function blankSlate() {
   PlacesUtils.bhistory.removeAllPages();
   dm.cleanUp();
-  formhist.removeAllEntries();
+
+  let deferred = Promise.defer();
+  FormHistory.update({ op: "remove" },
+                     { handleError: function (error) {
+                         do_throw("Error occurred updating form history: " + error);
+                         deferred.reject(error);
+                       },
+                       handleCompletion: function (reason) { if (!reason) deferred.resolve(); }
+                     });
+  return deferred.promise;
 }
 
 /**
  * Ensures that the given pref is the expected value.
  *
  * @param aPrefName
  *        The pref's sub-branch under the privacy branch
  * @param aExpectedVal
@@ -978,32 +1060,16 @@ function ensureDownloadsClearedState(aDo
   let niceStr = aShouldBeCleared ? "no longer" : "still";
   aDownloadIDs.forEach(function (id) {
     is(downloadExists(id), !aShouldBeCleared,
        "download " + id + " should " + niceStr + " exist");
   });
 }
 
 /**
- * Ensures that the specified form entries are either cleared or not.
- *
- * @param aFormEntries
- *        Array of form entry names
- * @param aShouldBeCleared
- *        True if each form entry should be cleared, false otherwise
- */
-function ensureFormEntriesClearedState(aFormEntries, aShouldBeCleared) {
-  let niceStr = aShouldBeCleared ? "no longer" : "still";
-  aFormEntries.forEach(function (entry) {
-    is(formhist.nameExists(entry), !aShouldBeCleared,
-       "form entry " + entry + " should " + niceStr + " exist");
-  });
-}
-
-/**
  * Ensures that the given pref is the expected value.
  *
  * @param aPrefName
  *        The pref's sub-branch under the privacy branch
  * @param aExpectedVal
  *        The pref's expected value
  * @param aMsg
  *        Passed to is()
@@ -1021,13 +1087,13 @@ function intPrefIs(aPrefName, aExpectedV
 function visitTimeForMinutesAgo(aMinutesAgo) {
   return now_uSec - aMinutesAgo * kUsecPerMin;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 
 function test() {
   requestLongerTimeout(2);
+  waitForExplicitFinish();
   blankSlate();
-  waitForExplicitFinish();
   // Kick off all the tests in the gAllTests array.
   waitForAsyncUpdates(doNextTest);
 }
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -25,16 +25,22 @@ let EXPECTED_NOTIFICATIONS = [
 ];
 
 const UNEXPECTED_NOTIFICATIONS = [
   "xpcom-shutdown"
 ];
 
 const URL = "ftp://localhost/clearHistoryOnShutdown/";
 
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+                         getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
 let notificationIndex = 0;
 
 let notificationsObserver = {
   observe: function observe(aSubject, aTopic, aData) {
     print("Received notification: " + aTopic);
 
     // Note that some of these notifications could arrive multiple times, for
     // example in case of sync, we allow that.
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -104,16 +104,19 @@
       <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-stringbundle");</field>
       <field name="_textbox">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-textbox");</field>
       <field name="_popup">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-popup");</field>
       <field name="_ss">null</field>
       <field name="_engines">null</field>
+      <field name="FormHistory" readonly="true">
+        (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+      </field>
 
       <property name="engines" readonly="true">
         <getter><![CDATA[
           if (!this._engines)
             this._engines = this.searchService.getVisibleEngines();
           return this._engines;
         ]]></getter>
       </property>
@@ -450,22 +453,23 @@
           else {
             var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab");
             if ((aEvent && aEvent.altKey) ^ newTabPref)
               where = "tab";
           }
 
           // Save the current value in the form history
           if (textValue && !PrivateBrowsingUtils.isWindowPrivate(window)) {
-            try {
-              textBox._formHistSvc.addEntry(textBox.getAttribute("autocompletesearchparam"),
-                                            textValue);
-            } catch (ex) {
-              Components.utils.reportError("Saving search to form history failed: " + ex);
-            }
+            this.FormHistory.update(
+              { op : "bump",
+                fieldname : textBox.getAttribute("autocompletesearchparam"),
+                value : textValue },
+              { handleError : function(aError) {
+                  Components.utils.reportError("Saving search to form history failed: " + aError.message);
+              }});
           }
 
           this.doSearch(textValue, where);
         ]]></body>
       </method>
 
       <method name="doSearch">
         <parameter name="aData"/>
@@ -549,30 +553,26 @@
         // Because XBL and the customize toolbar code interacts poorly,
         // there may not be anything to remove here
         try {
           this.controllers.removeController(this.searchbarController);
         } catch (ex) { }
       ]]></destructor>
 
       <field name="_stringBundle"/>
-      <field name="_formHistSvc"/>
       <field name="_prefBranch"/>
       <field name="_suggestMenuItem"/>
       <field name="_suggestEnabled"/>
 
       <method name="initialize">
         <body><![CDATA[
           const kXULNS =
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
           // Initialize fields
           this._stringBundle = document.getBindingParent(this)._stringBundle;
-          this._formHistSvc =
-                   Components.classes["@mozilla.org/satchel/form-history;1"]
-                             .getService(Components.interfaces.nsIFormHistory2);
           this._prefBranch =
                     Components.classes["@mozilla.org/preferences-service;1"]
                               .getService(Components.interfaces.nsIPrefBranch);
           this._suggestEnabled =
               this._prefBranch.getBoolPref("browser.search.suggest.enabled");
 
           if (this._prefBranch.getBoolPref("browser.urlbar.clickSelectsAll"))
             this.setAttribute("clickSelectsAll", true);
@@ -743,28 +743,27 @@
       <field name="searchbarController" readonly="true"><![CDATA[({
         _self: this,
         supportsCommand: function(aCommand) {
           return aCommand == "cmd_clearhistory" ||
                  aCommand == "cmd_togglesuggest";
         },
 
         isCommandEnabled: function(aCommand) {
-          if (aCommand == "cmd_clearhistory") {
-            var param = this._self.getAttribute("autocompletesearchparam");
-            return this._self._formHistSvc.nameExists(param);
-          }
           return true;
         },
 
         doCommand: function (aCommand) {
           switch (aCommand) {
             case "cmd_clearhistory":
               var param = this._self.getAttribute("autocompletesearchparam");
-              this._self._formHistSvc.removeEntriesForName(param);
+
+              let searchBar = this._self.parentNode;
+
+              BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null);
               this._self.value = "";
               break;
             case "cmd_togglesuggest":
               // The pref observer will update _suggestEnabled and the menu
               // checkmark.
               this._self._prefBranch.setBoolPref("browser.search.suggest.enabled",
                                                  !this._self._suggestEnabled);
               break;
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser_426329.js
@@ -1,30 +1,35 @@
 // Instead of loading ChromeUtils.js into the test scope in browser-test.js for all tests,
 // we only need ChromeUtils.js for a few files which is why we are using loadSubScript.
 var ChromeUtils = {};
 this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
                      getService(Ci.mozIJSSubScriptLoader);
 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils);
 
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+  "resource://gre/modules/FormHistory.jsm");
+
 function test() {
   waitForExplicitFinish();
 
   const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/test.html";
 
   var searchEntries = ["test", "More Text", "Some Text"];
   var searchBar = BrowserSearch.searchBar;
   var searchButton = document.getAnonymousElementByAttribute(searchBar,
                      "anonid", "search-go-button");
   ok(searchButton, "got search-go-button");
 
   searchBar.value = "test";
 
   var ss = Services.search;
 
+  let testIterator;
+
   function observer(aSub, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         var engine = ss.getEngineByName("Bug 426329");
         ok(engine, "Engine was added.");
         //XXX Bug 493051
         //ss.currentEngine = engine;
         break;
@@ -177,43 +182,74 @@ function test() {
   }
 
   function testRightClick() {
     init();
     searchBar.removeEventListener("popupshowing", stopPopup, true);
     content.location.href = "about:blank";
     simulateClick({ button: 2 }, searchButton);
     setTimeout(function() {
-
       is(gBrowser.tabs.length, preTabNo, "RightClick did not open new tab");
       is(gBrowser.currentURI.spec, "about:blank", "RightClick did nothing");
 
-      testSearchHistory();
+      testIterator = testSearchHistory();
+      testIterator.next();
     }, 5000);
   }
 
+  function countEntries(name, value, message) {
+    let count = 0;
+    FormHistory.count({ fieldname: name, value: value },
+                      { handleResult: function(result) { count = result; },
+                        handleError: function(error) { throw error; },
+                        handleCompletion: function(reason) {
+                          if (!reason) {
+                            ok(count > 0, message);
+                            testIterator.next();
+                          }
+                        }
+                      });
+  }
+
   function testSearchHistory() {
     var textbox = searchBar._textbox;
     for (var i = 0; i < searchEntries.length; i++) {
-      let exists = textbox._formHistSvc.entryExists(textbox.getAttribute("autocompletesearchparam"), searchEntries[i]);
-      ok(exists, "form history entry '" + searchEntries[i] + "' should exist");
+      yield countEntries(textbox.getAttribute("autocompletesearchparam"), searchEntries[i],
+                         "form history entry '" + searchEntries[i] + "' should exist");
     }
     testAutocomplete();
   }
 
   function testAutocomplete() {
     var popup = searchBar.textbox.popup;
-    popup.addEventListener("popupshowing", function testACPopupShowing() {
-      popup.removeEventListener("popupshowing", testACPopupShowing);
+    popup.addEventListener("popupshown", function testACPopupShowing() {
+      popup.removeEventListener("popupshown", testACPopupShowing);
       checkMenuEntries(searchEntries);
-      SimpleTest.executeSoon(finalize);
+      testClearHistory();
     });
     searchBar.textbox.showHistoryPopup();
   }
 
+  function testClearHistory() {
+    let controller = searchBar.textbox.controllers.getControllerForCommand("cmd_clearhistory")
+    ok(controller.isCommandEnabled("cmd_clearhistory"), "Clear history command enabled");
+    controller.doCommand("cmd_clearhistory");
+    let count = 0;
+    FormHistory.count({ },
+                      { handleResult: function(result) { count = result; },
+                        handleError: function(error) { throw error; },
+                        handleCompletion: function(reason) {
+                          if (!reason) {
+                            ok(count == 0, "History cleared");
+                            finalize();
+                          }
+                        }
+                      });
+  }
+
   function finalize() {
     searchBar.value = "";
     while (gBrowser.tabs.length != 1) {
       gBrowser.removeTab(gBrowser.tabs[0]);
     }
     content.location.href = "about:blank";
     var engine = ss.getEngineByName("Bug 426329");
     ss.removeEngine(engine);
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -427,16 +427,17 @@
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
 @BINPATH@/components/nsHandlerService.js
 @BINPATH@/components/nsWebHandlerApp.manifest
 @BINPATH@/components/nsWebHandlerApp.js
 @BINPATH@/components/satchel.manifest
 @BINPATH@/components/nsFormAutoComplete.js
 @BINPATH@/components/nsFormHistory.js
+@BINPATH@/components/FormHistoryStartup.js
 @BINPATH@/components/nsInputListAutoComplete.js
 @BINPATH@/components/contentSecurityPolicy.manifest
 @BINPATH@/components/contentSecurityPolicy.js
 @BINPATH@/components/contentAreaDropListener.manifest
 @BINPATH@/components/contentAreaDropListener.js
 #ifdef MOZ_B2G_RIL
 @BINPATH@/components/RadioInterfaceLayer.manifest
 @BINPATH@/components/RadioInterfaceLayer.js
--- a/editor/libeditor/text/tests/test_bug527935.html
+++ b/editor/libeditor/text/tests/test_bug527935.html
@@ -32,29 +32,29 @@ SimpleTest.waitForFocus(function() {
     newInput.setAttribute("name", "test");
     document.body.appendChild(newInput);
 
     setTimeout(function() {
       var popupShown = false;
       function listener() {
         popupShown = true;
       }
-      SpecialPowers.addAutoCompletePopupEventListener(window, listener);
+      SpecialPowers.addAutoCompletePopupEventListener(window, "popupshowing", listener);
 
       var event = document.createEvent("KeyboardEvent");
 
       event.initKeyEvent("keypress", true, true, null, false, false,
                          false, false, 0, "f".charCodeAt(0));
       newInput.value = "";
       newInput.focus();
       newInput.dispatchEvent(event);
 
       hitEventLoop(function() {
         ok(!popupShown, "Popup must not be opened");
-        SpecialPowers.removeAutoCompletePopupEventListener(window, listener);
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshowing", listener);
         SimpleTest.finish();
       }, 100);
     }, 0);
   }, false);
 
   initValue.focus();
   initValue.value = "foo";
   synthesizeKey("VK_ENTER", {});
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4662,17 +4662,17 @@ var FormAssistant = {
         if (aData == "NOTHING") {
           // only look for input elements, not contentEditable or multiline text areas
           let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
           if (!focused)
             break;
 
           if (this._showValidationMessage(focused))
             break;
-          this._showAutoCompleteSuggestions(focused);
+          this._showAutoCompleteSuggestions(focused, function () {});
         } else {
           // temporarily hide the form assist popup while we're panning or zooming the page
           this._hideFormAssistPopup();
         }
         break;
       case "FormAssist:AutoComplete":
         if (!this._currentInputElement)
           break;
@@ -4732,32 +4732,43 @@ var FormAssistant = {
       case "click":
         currentElement = aEvent.target;
 
         // Prioritize a form validation message over autocomplete suggestions
         // when the element is first focused (a form validation message will
         // only be available if an invalid form was submitted)
         if (this._showValidationMessage(currentElement))
           break;
-        if (!this._showAutoCompleteSuggestions(currentElement))
-          this._hideFormAssistPopup();
+
+        let checkResultsClick = hasResults => {
+          if (!hasResults) {
+            this._hideFormAssistPopup();
+          }
+        };
+
+        this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
         break;
 
       case "input":
         currentElement = aEvent.target;
 
         // Since we can only show one popup at a time, prioritze autocomplete
         // suggestions over a form validation message
-        if (this._showAutoCompleteSuggestions(currentElement))
-          break;
-        if (this._showValidationMessage(currentElement))
-          break;
-
-        // If we're not showing autocomplete suggestions, hide the form assist popup
-        this._hideFormAssistPopup();
+        let checkResultsInput = hasResults => {
+          if (hasResults)
+            return;
+
+          if (!this._showValidationMessage(currentElement))
+            return;
+
+          // If we're not showing autocomplete suggestions, hide the form assist popup
+          this._hideFormAssistPopup();
+        };
+
+        this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
         break;
 
       // Reset invalid submit state on each pageshow
       case "pageshow":
         let target = aEvent.originalTarget;
         let selectedDocument = BrowserApp.selectedBrowser.contentDocument;
         if (target == selectedDocument || target.ownerDocument == selectedDocument)
           this._invalidSubmit = false;
@@ -4771,37 +4782,41 @@ var FormAssistant = {
         (aElement.hasAttribute("autocomplete") &&
          aElement.getAttribute("autocomplete").toLowerCase() == "off"))
       return false;
 
     return true;
   },
 
   // Retrieves autocomplete suggestions for an element from the form autocomplete service.
-  _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement) {
+  // aCallback(array_of_suggestions) is called when results are available.
+  _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
     // Cache the form autocomplete service for future use
     if (!this._formAutoCompleteService)
       this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].
                                       getService(Ci.nsIFormAutoComplete);
 
-    let results = this._formAutoCompleteService.autoCompleteSearch(aElement.name || aElement.id,
-                                                                   aSearchString, aElement, null);
-    let suggestions = [];
-    for (let i = 0; i < results.matchCount; i++) {
-      let value = results.getValueAt(i);
-
-      // Do not show the value if it is the current one in the input field
-      if (value == aSearchString)
-        continue;
-
-      // Supply a label and value, since they can differ for datalist suggestions
-      suggestions.push({ label: value, value: value });
-    }
-
-    return suggestions;
+    let resultsAvailable = function (results) {
+      let suggestions = [];
+      for (let i = 0; i < results.matchCount; i++) {
+        let value = results.getValueAt(i);
+
+        // Do not show the value if it is the current one in the input field
+        if (value == aSearchString)
+          continue;
+
+        // Supply a label and value, since they can differ for datalist suggestions
+        suggestions.push({ label: value, value: value });
+        aCallback(suggestions);
+      }
+    };
+
+    this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
+                                                          aSearchString, aElement, null,
+                                                          resultsAvailable);
   },
 
   /**
    * (Copied from mobile/xul/chrome/content/forms.js)
    * This function is similar to getListSuggestions from
    * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
    * used by the autocomplete.xml binding which is not in used in fennec
    */
@@ -4828,50 +4843,57 @@ var FormAssistant = {
         continue;
       suggestions.push({ label: label, value: item.value });
     }
 
     return suggestions;
   },
 
   // Retrieves autocomplete suggestions for an element from the form autocomplete service
-  // and sends the suggestions to the Java UI, along with element position data.
-  // Returns true if there are suggestions to show, false otherwise.
-  _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement) {
-    if (!this._isAutoComplete(aElement))
-      return false;
+  // and sends the suggestions to the Java UI, along with element position data. As
+  // autocomplete queries are asynchronous, calls aCallback when done with a true
+  // argument if results were found and false if no results were found.
+  _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
+    if (!this._isAutoComplete(aElement)) {
+      aCallback(false);
+      return;
+    }
 
     // Don't display the form auto-complete popup after the user starts typing
     // to avoid confusing somes IME. See bug 758820 and bug 632744.
     if (this._isBlocklisted && aElement.value.length > 0) {
-      return false;
-    }
-
-    let autoCompleteSuggestions = this._getAutoCompleteSuggestions(aElement.value, aElement);
-    let listSuggestions = this._getListSuggestions(aElement);
-
-    // On desktop, we show datalist suggestions below autocomplete suggestions,
-    // without duplicates removed.
-    let suggestions = autoCompleteSuggestions.concat(listSuggestions);
-
-    // Return false if there are no suggestions to show
-    if (!suggestions.length)
-      return false;
-
-    sendMessageToJava({
-      type:  "FormAssist:AutoComplete",
-      suggestions: suggestions,
-      rect: ElementTouchHelper.getBoundingContentRect(aElement)
-    });
-
-    // Keep track of input element so we can fill it in if the user
-    // selects an autocomplete suggestion
-    this._currentInputElement = aElement;
-
-    return true;
+      aCallback(false);
+      return;
+    }
+
+    let resultsAvailable = autoCompleteSuggestions => {
+      // On desktop, we show datalist suggestions below autocomplete suggestions,
+      // without duplicates removed.
+      let listSuggestions = this._getListSuggestions(aElement);
+      let suggestions = autoCompleteSuggestions.concat(listSuggestions);
+
+      // Return false if there are no suggestions to show
+      if (!suggestions.length) {
+        aCallback(false);
+        return;
+      }
+
+      sendMessageToJava({
+        type:  "FormAssist:AutoComplete",
+        suggestions: suggestions,
+        rect: ElementTouchHelper.getBoundingContentRect(aElement)
+      });
+
+      // Keep track of input element so we can fill it in if the user
+      // selects an autocomplete suggestion
+      this._currentInputElement = aElement;
+      aCallback(true);
+    };
+
+    this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
   },
 
   // Only show a validation message if the user submitted an invalid form,
   // there's a non-empty message string, and the element is the correct type
   _isValidateable: function _isValidateable(aElement) {
     if (!this._invalidSubmit ||
         !aElement.validationMessage ||
         !(aElement instanceof HTMLInputElement ||
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -340,16 +340,17 @@
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
 @BINPATH@/components/nsHandlerService.js
 @BINPATH@/components/nsWebHandlerApp.manifest
 @BINPATH@/components/nsWebHandlerApp.js
 @BINPATH@/components/satchel.manifest
 @BINPATH@/components/nsFormAutoComplete.js
 @BINPATH@/components/nsFormHistory.js
+@BINPATH@/components/FormHistoryStartup.js
 @BINPATH@/components/nsInputListAutoComplete.js
 @BINPATH@/components/contentSecurityPolicy.manifest
 @BINPATH@/components/contentSecurityPolicy.js
 @BINPATH@/components/contentAreaDropListener.manifest
 @BINPATH@/components/contentAreaDropListener.js
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
 @BINPATH@/components/nsFilePicker.js
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -957,26 +957,31 @@ SpecialPowersAPI.prototype = {
                  .QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindow)
                  .QueryInterface(Ci.nsIDOMChromeWindow);
   },
   _getAutoCompletePopup: function(window) {
     return this._getTopChromeWindow(window).document
                                            .getElementById("PopupAutoComplete");
   },
-  addAutoCompletePopupEventListener: function(window, listener) {
-    this._getAutoCompletePopup(window).addEventListener("popupshowing",
+  addAutoCompletePopupEventListener: function(window, eventname, listener) {
+    this._getAutoCompletePopup(window).addEventListener(eventname,
                                                         listener,
                                                         false);
   },
-  removeAutoCompletePopupEventListener: function(window, listener) {
-    this._getAutoCompletePopup(window).removeEventListener("popupshowing",
+  removeAutoCompletePopupEventListener: function(window, eventname, listener) {
+    this._getAutoCompletePopup(window).removeEventListener(eventname,
                                                            listener,
                                                            false);
   },
+  get formHistory() {
+    let tmp = {};
+    Cu.import("resource://gre/modules/FormHistory.jsm", tmp);
+    return wrapPrivileged(tmp.FormHistory);
+  },
   getFormFillController: function(window) {
     return Components.classes["@mozilla.org/satchel/form-fill-controller;1"]
                      .getService(Components.interfaces.nsIFormFillController);
   },
   attachFormFillControllerTo: function(window) {
     this.getFormFillController()
         .attachToBrowser(this._getDocShell(window),
                          this._getAutoCompletePopup(window));
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/FormHistory.jsm
@@ -0,0 +1,1089 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * FormHistory
+ *
+ * Used to store values that have been entered into forms which may later
+ * be used to automatically fill in the values when the form is visited again.
+ *
+ * search(terms, queryData, callback)
+ *   Look up values that have been previously stored.
+ *     terms - array of terms to return data for
+ *     queryData - object that contains the query terms
+ *       The query object contains properties for each search criteria to match, where the value
+ *       of the property specifies the value that term must have. For example,
+ *       { term1: value1, term2: value2 }
+ *     callback - callback that is called when results are available or an error occurs.
+ *       The callback is passed a result array containing each found entry. Each element in
+ *       the array is an object containing a property for each search term specified by 'terms'.
+ * count(terms, queryData, callback)
+ *   Find the number of stored entries that match the given criteria.
+ *     queryData - array of objects that indicate the query. See the search method for details.
+ *     callback - callback that is called when results are available or an error occurs.
+ *       The callback is passed the number of found entries.
+ * update(changes, callback)
+ *    Write data to form history storage.
+ *      changes - an array of changes to be made. If only one change is to be made, it
+ *                may be passed as an object rather than a one-element array.
+ *        Each change object is of the form:
+ *          { op: operation, term1: value1, term2: value2, ... }
+ *        Valid operations are:
+ *          add - add a new entry
+ *          update - update an existing entry
+ *          remove - remove an entry
+ *          bump - update the last accessed time on an entry
+ *        The terms specified allow matching of one or more specific entries. If no terms
+ *        are specified then all entries are matched. This means that { op: "remove" } is
+ *        used to remove all entries and clear the form history.
+ *      callback - callback that is called when results have been stored.
+ * getAutoCompeteResults(searchString, params, callback)
+ *   Retrieve an array of form history values suitable for display in an autocomplete list.
+ *   Returns an mozIStoragePendingStatement that can be used to cancel the operation if
+ *   needed.
+ *     searchString - the string to search for, typically the entered value of a textbox
+ *     params - zero or more filter arguments:
+ *       fieldname - form field name
+ *       agedWeight
+ *       bucketSize
+ *       expiryDate
+ *       maxTimeGroundings
+ *       timeGroupingSize
+ *       prefixWeight
+ *       boundaryWeight
+ *     callback - callback that is called with the array of results. Each result in the array
+ *                is an object with four arguments:
+ *                  text, textLowerCase, frecency, totalScore
+ * schemaVersion
+ *   This property holds the version of the database schema
+ *
+ * Terms:
+ *  guid - entry identifier. For 'add', a guid will be generated.
+ *  fieldname - form field name
+ *  value - form value
+ *  timesUsed - the number of times the entry has been accessed
+ *  firstUsed - the time the the entry was first created
+ *  lastUsed - the time the entry was last accessed
+ *  firstUsedStart - search for entries created after or at this time
+ *  firstUsedEnd - search for entries created before or at this time
+ *  lastUsedStart - search for entries last accessed after or at this time
+ *  lastUsedEnd - search for entries last accessed before or at this time
+ *
+ * In all of the above methods, the callback argument should be an object with
+ * handleResult(result), handleFailure(error) and handleCompletion(reason) functions.
+ * For search and getAutoCompeteResults, result is an object containing the desired
+ * properties. For count, result is the integer count. For, update, handleResult is
+ * not called. For handleCompletion, reason is either 0 if successful or 1 if
+ * an error occurred.
+ */
+
+EXPORTED_SYMBOLS = [ "FormHistory" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidService",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
+const DB_SCHEMA_VERSION = 4;
+const DAY_IN_MS  = 86400000; // 1 day in milliseconds
+const NOOP = function noop() {};
+
+let supportsDeletedTable =
+#ifdef ANDROID
+  true;
+#else
+  false;
+#endif
+
+let Prefs = {
+  initialized: false,
+
+  get debug() { this.ensureInitialized(); return this._debug; },
+  get enabled() { this.ensureInitialized(); return this._enabled; },
+  get expireDays() { this.ensureInitialized(); return this._expireDays; },
+
+  ensureInitialized: function() {
+    if (this.initialized)
+      return;
+
+    this.initialized = true;
+
+    this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
+    this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
+    this._expireDays = Services.prefs.getIntPref("browser.formfill.expire_days");
+  }
+};
+
+function log(aMessage) {
+  if (Prefs.debug) {
+    Services.console.logStringMessage("FormHistory: " + aMessage);
+  }
+}
+
+function sendNotification(aType, aData) {
+  if (typeof aData == "string") {
+    let strWrapper = Cc["@mozilla.org/supports-string;1"].
+                     createInstance(Ci.nsISupportsString);
+    strWrapper.data = aData;
+    aData = strWrapper;
+  }
+  else if (typeof aData == "number") {
+    let intWrapper = Cc["@mozilla.org/supports-PRInt64;1"].
+                     createInstance(Ci.nsISupportsPRInt64);
+    intWrapper.data = aData;
+    aData = intWrapper;
+  }
+  else if (aData) {
+    throw Components.Exception("Invalid type " + (typeof aType) + " passed to sendNotification",
+                               Cr.NS_ERROR_ILLEGAL_VALUE);
+  }
+
+  Services.obs.notifyObservers(aData, "satchel-storage-changed", aType);
+}
+
+/**
+ * Current database schema
+ */
+
+const dbSchema = {
+  tables : {
+    moz_formhistory : {
+      "id"        : "INTEGER PRIMARY KEY",
+      "fieldname" : "TEXT NOT NULL",
+      "value"     : "TEXT NOT NULL",
+      "timesUsed" : "INTEGER",
+      "firstUsed" : "INTEGER",
+      "lastUsed"  : "INTEGER",
+      "guid"      : "TEXT",
+    },
+    moz_deleted_formhistory: {
+        "id"          : "INTEGER PRIMARY KEY",
+        "timeDeleted" : "INTEGER",
+        "guid"        : "TEXT"
+    }
+  },
+  indices : {
+    moz_formhistory_index : {
+      table   : "moz_formhistory",
+      columns : [ "fieldname" ]
+    },
+    moz_formhistory_lastused_index : {
+      table   : "moz_formhistory",
+      columns : [ "lastUsed" ]
+    },
+    moz_formhistory_guid_index : {
+      table   : "moz_formhistory",
+      columns : [ "guid" ]
+    },
+  }
+};
+
+/**
+ * Validating and processing API querying data
+ */
+
+const validFields = [
+  "fieldname",
+  "value",
+  "timesUsed",
+  "firstUsed",
+  "lastUsed",
+  "guid",
+];
+
+const searchFilters = [
+  "firstUsedStart",
+  "firstUsedEnd",
+  "lastUsedStart",
+  "lastUsedEnd",
+];
+
+function validateOpData(aData, aDataType) {
+  for (let field in aData) {
+    if (field != "op" && validFields.indexOf(field) == -1) {
+      throw Components.Exception(
+        aDataType + " query contains an unrecognized field: " + field,
+        Cr.NS_ERROR_ILLEGAL_VALUE);
+    }
+  }
+  return aData;
+}
+
+function validateSearchData(aData, aDataType) {
+  for (let field in aData) {
+    if (field != "op" && validFields.indexOf(field) == -1 && searchFilters.indexOf(field) == -1) {
+      throw Components.Exception(
+        aDataType + " query contains an unrecognized field: " + field,
+        Cr.NS_ERROR_ILLEGAL_VALUE);
+    }
+  }
+}
+
+function makeQueryPredicates(aQueryData, delimiter = ' AND ') {
+  return Object.keys(aQueryData).map(function(field) {
+    if (field == "firstUsedStart") {
+      return "firstUsed >= :" + field;
+    } else if (field == "firstUsedEnd") {
+      return "firstUsed <= :" + field;
+    } else if (field == "lastUsedStart") {
+      return "lastUsed >= :" + field;
+    } else if (field == "lastUsedEnd") {
+      return "lastUsed <= :" + field;
+    }
+    return field + " = :" + field;
+  }).join(delimiter);
+}
+
+/**
+ * Storage statement creation and parameter binding
+ */
+
+function makeCountStatement(aSearchData) {
+  let query = "SELECT COUNT(*) AS numEntries FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+  if (queryTerms) {
+    query += " WHERE " + queryTerms;
+  }
+  return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeSearchStatement(aSearchData, aSelectTerms) {
+  let query = "SELECT " + aSelectTerms.join(", ") + " FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+  if (queryTerms) {
+    query += " WHERE " + queryTerms;
+  }
+
+  return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeAddStatement(aNewData, aNow, aBindingArrays) {
+  let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
+              "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
+
+  aNewData.timesUsed = aNewData.timesUsed || 1;
+  aNewData.firstUsed = aNewData.firstUsed || aNow;
+  aNewData.lastUsed = aNewData.lastUsed || aNow;
+  return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeBumpStatement(aGuid, aNow, aBindingArrays) {
+  let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
+  let queryParams = {
+    lastUsed : aNow,
+    guid : aGuid,
+  };
+
+  return dbCreateAsyncStatement(query, queryParams, aBindingArrays);
+}
+
+function makeRemoveStatement(aSearchData, aBindingArrays) {
+  let query = "DELETE FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+
+  if (queryTerms) {
+    log("removeEntries");
+    query += " WHERE " + queryTerms;
+  } else {
+    log("removeAllEntries");
+    // Not specifying any fields means we should remove all entries. We
+    // won't need to modify the query in this case.
+  }
+
+  return dbCreateAsyncStatement(query, aSearchData, aBindingArrays);
+}
+
+function makeUpdateStatement(aGuid, aNewData, aBindingArrays) {
+  let query = "UPDATE moz_formhistory SET ";
+  let queryTerms = makeQueryPredicates(aNewData, ', ');
+
+  if (!queryTerms) {
+    throw Components.Exception("Update query must define fields to modify.",
+                               Cr.NS_ERROR_ILLEGAL_VALUE);
+  }
+
+  query += queryTerms + " WHERE guid = :guid";
+  aNewData["guid"] = aGuid;
+
+  return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeMoveToDeletedStatement(aGuid, aNow, aData, aBindingArrays) {
+  if (supportsDeletedTable) {
+    let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
+    let queryTerms = makeQueryPredicates(aData);
+
+    if (aGuid) {
+      query += " VALUES (:guid, :timeDeleted)";
+    } else {
+      // TODO: Add these items to the deleted items table once we've sorted
+      //       out the issues from bug 756701
+      if (!queryTerms)
+        return;
+
+      query += " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
+    }
+
+    aData.timeDeleted = aNow;
+
+    return dbCreateAsyncStatement(query, aData, aBindingArrays);
+  }
+
+  return null;
+}
+
+function generateGUID() {
+  // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
+  let uuid = uuidService.generateUUID().toString();
+  let raw = ""; // A string with the low bytes set to random values
+  let bytes = 0;
+  for (let i = 1; bytes < 12 ; i+= 2) {
+    // Skip dashes
+    if (uuid[i] == "-")
+      i++;
+    let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
+    raw += String.fromCharCode(hexVal);
+    bytes++;
+  }
+  return btoa(raw);
+}
+
+/**
+ * Database creation and access
+ */
+
+let _dbConnection = null;
+XPCOMUtils.defineLazyGetter(this, "dbConnection", function() {
+  let dbFile;
+
+  try {
+    dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+    dbFile.append("formhistory.sqlite");
+    log("Opening database at " + dbFile.path);
+
+    _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+    dbInit();
+  } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+    dbCleanup(dbFile);
+    _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+    dbInit();
+  }
+
+  return _dbConnection;
+});
+
+
+let dbStmts = new Map();
+
+/*
+ * dbCreateAsyncStatement
+ *
+ * Creates a statement, wraps it, and then does parameter replacement
+ */
+function dbCreateAsyncStatement(aQuery, aParams, aBindingArrays) {
+  if (!aQuery)
+    return null;
+
+  let stmt = dbStmts.get(aQuery);
+  if (!stmt) {
+    log("Creating new statement for query: " + aQuery);
+    stmt = dbConnection.createAsyncStatement(aQuery);
+    dbStmts.set(aQuery, stmt);
+  }
+
+  if (aBindingArrays) {
+    let bindingArray = aBindingArrays.get(stmt);
+    if (!bindingArray) {
+      // first time using a particular statement in update
+      bindingArray = stmt.newBindingParamsArray();
+      aBindingArrays.set(stmt, bindingArray);
+    }
+
+    if (aParams) {
+      let bindingParams = bindingArray.newBindingParams();
+      for (let field in aParams) {
+        bindingParams.bindByName(field, aParams[field]);
+      }
+      bindingArray.addParams(bindingParams);
+    }
+  } else {
+    if (aParams) {
+      for (let field in aParams) {
+        stmt.params[field] = aParams[field];
+      }
+    }
+  }
+
+  return stmt;
+}
+
+/**
+ * dbInit
+ *
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc.
+ */
+function dbInit() {
+  log("Initializing Database");
+
+  if (!_dbConnection.tableExists("moz_formhistory")) {
+    dbCreate();
+    return;
+  }
+
+  // When FormHistory is released, we will no longer support the various schema versions prior to
+  // this release that nsIFormHistory2 once did.
+  let version = _dbConnection.schemaVersion;
+  if (version < 3) {
+    throw Components.Exception("DB version is unsupported.",
+                               Cr.NS_ERROR_FILE_CORRUPTED);
+  } else if (version != DB_SCHEMA_VERSION) {
+    dbMigrate(version);
+  }
+}
+
+function dbCreate() {
+  log("Creating DB -- tables");
+  for (let name in dbSchema.tables) {
+    let table = dbSchema.tables[name];
+    let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", ");
+    log("Creating table " + name + " with " + tSQL);
+    _dbConnection.createTable(name, tSQL);
+  }
+
+  log("Creating DB -- indices");
+  for (let name in dbSchema.indices) {
+    let index = dbSchema.indices[name];
+    let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+                    "(" + index.columns.join(", ") + ")";
+    _dbConnection.executeSimpleSQL(statement);
+  }
+
+  _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+}
+
+function dbMigrate(oldVersion) {
+  log("Attempting to migrate from version " + oldVersion);
+
+  if (oldVersion > DB_SCHEMA_VERSION) {
+    log("Downgrading to version " + DB_SCHEMA_VERSION);
+    // User's DB is newer. Sanity check that our expected columns are
+    // present, and if so mark the lower version and merrily continue
+    // on. If the columns are borked, something is wrong so blow away
+    // the DB and start from scratch. [Future incompatible upgrades
+    // should switch to a different table or file.]
+
+    if (!dbAreExpectedColumnsPresent()) {
+      throw Components.Exception("DB is missing expected columns",
+                                 Cr.NS_ERROR_FILE_CORRUPTED);
+    }
+
+    // Change the stored version to the current version. If the user
+    // runs the newer code again, it will see the lower version number
+    // and re-upgrade (to fixup any entries the old code added).
+    _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+    return;
+  }
+
+  // Note that migration is currently performed synchronously.
+  _dbConnection.beginTransaction();
+
+  try {
+    for (let v = oldVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
+      this.log("Upgrading to version " + v + "...");
+      Migrators["dbMigrateToVersion" + v]();
+    }
+  } catch (e) {
+    this.log("Migration failed: "  + e);
+    this.dbConnection.rollbackTransaction();
+    throw e;
+  }
+
+  _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+  _dbConnection.commitTransaction();
+
+  log("DB migration completed.");
+}
+
+var Migrators = {
+  /*
+   * Updates the DB schema to v3 (bug 506402).
+   * Adds deleted form history table.
+   */
+  dbMigrateToVersion4: function dbMigrateToVersion4() {
+    if (!_dbConnection.tableExists("moz_deleted_formhistory")) {
+      let table = dbSchema.tables["moz_deleted_formhistory"];
+      let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", ");
+      _dbConnection.createTable("moz_deleted_formhistory", tSQL);
+    }
+  }
+};
+
+/**
+ * dbAreExpectedColumnsPresent
+ *
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ */
+function dbAreExpectedColumnsPresent() {
+  for (let name in dbSchema.tables) {
+    let table = dbSchema.tables[name];
+    let query = "SELECT " +
+                [col for (col in table)].join(", ") +
+                " FROM " + name;
+    try {
+      let stmt = _dbConnection.createStatement(query);
+      // (no need to execute statement, if it compiled we're good)
+      stmt.finalize();
+    } catch (e) {
+      return false;
+    }
+  }
+
+  log("verified that expected columns are present in DB.");
+  return true;
+}
+
+/**
+ * dbCleanup
+ *
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ */
+function dbCleanup(dbFile) {
+  log("Cleaning up DB file - close & remove & backup");
+
+  // Create backup file
+  let backupFile = dbFile.leafName + ".corrupt";
+  Services.storage.backupDatabaseFile(dbFile, backupFile);
+
+  dbClose(false);
+  dbFile.remove(false);
+}
+
+function dbClose(aShutdown) {
+  log("dbClose(" + aShutdown + ")");
+
+  if (aShutdown) {
+    sendNotification("formhistory-shutdown", null);
+  }
+
+  // Connection may never have been created if say open failed but we still
+  // end up calling dbClose as part of the rest of dbCleanup.
+  if (!_dbConnection) {
+    return;
+  }
+
+  log("dbClose finalize statements");
+  for (let stmt of dbStmts.values()) {
+    stmt.finalize();
+  }
+
+  dbStmts = new Map();
+
+  let closed = false;
+  _dbConnection.asyncClose(function () closed = true);
+
+  if (!aShutdown) {
+    let thread = Services.tm.currentThread;
+    while (!closed) {
+      thread.processNextEvent(true);
+    }
+  }
+}
+
+/**
+ * updateFormHistoryWrite
+ *
+ * Constructs and executes database statements from a pre-processed list of
+ * inputted changes.
+ */
+function updateFormHistoryWrite(aChanges, aCallbacks) {
+  log("updateFormHistoryWrite  " + aChanges.length);
+
+  // pass 'now' down so that every entry in the batch has the same timestamp
+  let now = Date.now() * 1000;
+
+  // for each change, we either create and append a new storage statement to
+  // stmts or bind a new set of parameters to an existing storage statement.
+  // stmts and bindingArrays are updated when makeXXXStatement eventually
+  // calls dbCreateAsyncStatement.
+  let stmts = [];
+  let notifications = [];
+  let bindingArrays = new Map();
+
+  for each (let change in aChanges) {
+    let operation = change.op;
+    delete change.op;
+    let stmt;
+    switch (operation) {
+      case "remove":
+        log("Remove from form history  " + change);
+        let delStmt = makeMoveToDeletedStatement(change.guid, now, change, bindingArrays);
+        if (delStmt && stmts.indexOf(delStmt) == -1)
+          stmts.push(delStmt);
+        if ("timeDeleted" in change)
+          delete change.timeDeleted;
+        stmt = makeRemoveStatement(change, bindingArrays);
+        notifications.push([ "formhistory-remove", null ]);
+        break;
+      case "update":
+        log("Update form history " + change);
+        let guid = change.guid;
+        delete change.guid;
+        stmt = makeUpdateStatement(guid, change, bindingArrays);
+        notifications.push([ "formhistory-update", guid ]);
+        break;
+      case "bump":
+        log("Bump form history " + change);
+        if (change.guid) {
+          stmt = makeBumpStatement(change.guid, now, bindingArrays);
+          notifications.push([ "formhistory-update", change.guid ]);
+        } else {
+          change.guid = generateGUID();
+          stmt = makeAddStatement(change, now, bindingArrays);
+          notifications.push([ "formhistory-add", change.guid ]);
+        }
+        break;
+      case "add":
+        log("Add to form history " + change);
+        change.guid = generateGUID();
+        stmt = makeAddStatement(change, now, bindingArrays);
+        notifications.push([ "formhistory-add", change.guid ]);
+        break;
+      default:
+        // We should've already guaranteed that change.op is one of the above
+        throw Components.Exception("Invalid operation " + operation,
+                                   Cr.NS_ERROR_ILLEGAL_VALUE);
+    }
+
+    // As identical statements are reused, only add statements if they aren't already present.
+    if (stmt && stmts.indexOf(stmt) == -1) {
+      stmts.push(stmt);
+    }
+  }
+
+  for (let stmt of stmts) {
+    stmt.bindParameters(bindingArrays.get(stmt));
+  }
+
+  let handlers = {
+    handleCompletion : function(aReason) {
+      if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+        for (let [notification, param] of notifications) {
+          // We're either sending a GUID or nothing at all.
+          sendNotification(notification, param);
+        }
+      }
+
+      if (aCallbacks && aCallbacks.handleCompletion) {
+        aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+      }
+    },
+    handleError : function(aError) {
+      if (aCallbacks && aCallbacks.handleError) {
+        aCallbacks.handleError(aError);
+      }
+    },
+    handleResult : NOOP
+  };
+
+  dbConnection.executeAsync(stmts, stmts.length, handlers);
+}
+
+/**
+ * Functions that expire entries in form history and shrinks database
+ * afterwards as necessary initiated by expireOldEntries.
+ */
+
+/**
+ * expireOldEntriesDeletion
+ *
+ * Removes entries from database.
+ */
+function expireOldEntriesDeletion(aExpireTime, aBeginningCount) {
+  log("expireOldEntriesDeletion(" + aExpireTime + "," + aBeginningCount + ")");
+
+  FormHistory.update([
+    {
+      op: "remove",
+      lastUsedEnd : aExpireTime,
+    }], {
+      handleCompletion: function() {
+        expireOldEntriesVacuum(aExpireTime, aBeginningCount);
+      },
+      handleError: function(aError) {
+        log("expireOldEntriesDeletionFailure");
+      }
+  });
+}
+
+/**
+ * expireOldEntriesVacuum
+ *
+ * Counts number of entries removed and shrinks database as necessary.
+ */
+function expireOldEntriesVacuum(aExpireTime, aBeginningCount) {
+  FormHistory.count({}, {
+    handleResult: function(aEndingCount) {
+      if (aBeginningCount - aEndingCount > 500) {
+        log("expireOldEntriesVacuum");
+
+        let stmt = dbCreateAsyncStatement("VACUUM");
+        stmt.executeAsync({
+          handleResult : NOOP,
+          handleError : function(aError) {
+            log("expireVacuumError");
+          },
+          handleCompletion : NOOP
+        });
+      }
+
+      sendNotification("formhistory-expireoldentries", aExpireTime);
+    },
+    handleError: function(aError) {
+      log("expireEndCountFailure");
+    }
+  });
+}
+
+const FormHistory = {
+  search : function formHistorySearch(aSelectTerms, aSearchData, aCallbacks) {
+    // if no terms selected, select everything
+    aSelectTerms = (aSelectTerms) ?  aSelectTerms : validFields;
+    validateSearchData(aSearchData, "Search");
+
+    let stmt = makeSearchStatement(aSearchData, aSelectTerms);
+
+    let handlers = {
+      handleResult : function(aResultSet) {
+        let formHistoryFields = dbSchema.tables.moz_formhistory;
+        for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+          let result = {};
+          for each (let field in aSelectTerms) {
+            result[field] = row.getResultByName(field);
+          }
+
+          if (aCallbacks && aCallbacks.handleResult) {
+            aCallbacks.handleResult(result);
+          }
+        }
+      },
+
+      handleError : function(aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
+        }
+      },
+
+      handleCompletion : function searchCompletionHandler(aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+        }
+      }
+    };
+
+    stmt.executeAsync(handlers);
+  },
+
+  count : function formHistoryCount(aSearchData, aCallbacks) {
+    validateSearchData(aSearchData, "Count");
+    let stmt = makeCountStatement(aSearchData);
+    let handlers = {
+      handleResult : function countResultHandler(aResultSet) {
+        let row = aResultSet.getNextRow();
+        let count = row.getResultByName("numEntries");
+        if (aCallbacks && aCallbacks.handleResult) {
+          aCallbacks.handleResult(count);
+        }
+      },
+
+      handleError : function(aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
+        }
+      },
+
+      handleCompletion : function searchCompletionHandler(aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+        }
+      }
+    };
+
+    stmt.executeAsync(handlers);
+  },
+
+  update : function formHistoryUpdate(aChanges, aCallbacks) {
+    if (!Prefs.enabled) {
+      return;
+    }
+
+    // Used to keep track of how many searches have been started. When that number
+    // are finished, updateFormHistoryWrite can be called.
+    let numSearches = 0;
+    let completedSearches = 0;
+    let searchFailed = false;
+
+    function validIdentifier(change) {
+      // The identifier is only valid if one of either the guid or the (fieldname/value) are set
+      return Boolean(change.guid) != Boolean(change.fieldname && change.value);
+    }
+
+    if (!("length" in aChanges))
+      aChanges = [aChanges];
+
+    for each (let change in aChanges) {
+      switch (change.op) {
+        case "remove":
+          validateSearchData(change, "Remove");
+          continue;
+        case "update":
+          if (validIdentifier(change)) {
+            validateOpData(change, "Update");
+            if (change.guid) {
+              continue;
+            }
+          } else {
+            throw Components.Exception(
+              "update op='update' does not correctly reference a entry.",
+              Cr.NS_ERROR_ILLEGAL_VALUE);
+          }
+          break;
+        case "bump":
+          if (validIdentifier(change)) {
+            validateOpData(change, "Bump");
+            if (change.guid) {
+              continue;
+            }
+          } else {
+            throw Components.Exception(
+              "update op='bump' does not correctly reference a entry.",
+              Cr.NS_ERROR_ILLEGAL_VALUE);
+          }
+          break;
+        case "add":
+          if (change.guid) {
+            throw Components.Exception(
+              "op='add' cannot contain field 'guid'. Either use op='update' " +
+                "explicitly or make 'guid' undefined.",
+              Cr.NS_ERROR_ILLEGAL_VALUE);
+          } else if (change.fieldname && change.value) {
+            validateOpData(change, "Add");
+          }
+          break;
+        default:
+          throw Components.Exception(
+            "update does not recognize op='" + change.op + "'",
+            Cr.NS_ERROR_ILLEGAL_VALUE);
+      }
+
+      numSearches++;
+      FormHistory.search(
+        [ "guid" ],
+        {
+          fieldname : change.fieldname,
+          value : change.value
+        }, {
+          foundResult : false,
+          handleResult : function(aResult) {
+            if (this.foundResult) {
+              log("Database contains multiple entries with the same fieldname/value pair.");
+              if (aCallbacks && aCallbacks.handleError) {
+                aCallbacks.handleError({
+                  message :
+                    "Database contains multiple entries with the same fieldname/value pair.",
+                  result : 19 // Constraint violation
+                });
+              }
+
+              searchFailed = true;
+              return;
+            }
+
+            this.foundResult = true;
+            change.guid = aResult["guid"];
+          },
+
+          handleError : function(aError) {
+            if (aCallbacks && aCallbacks.handleError) {
+              aCallbacks.handleError(aError);
+            }
+          },
+
+          handleCompletion : function(aReason) {
+            completedSearches++;
+            if (completedSearches == numSearches) {
+              if (!aReason && !searchFailed) {
+                updateFormHistoryWrite(aChanges, aCallbacks);
+              }
+              else if (aCallbacks && aCallbacks.handleCompletion) {
+                aCallbacks.handleCompletion(1);
+              }
+            }
+          }
+        });
+    }
+
+    if (numSearches == 0) {
+      // We don't have to wait for any statements to return.
+      updateFormHistoryWrite(aChanges, aCallbacks);
+    }
+  },
+
+  getAutoCompleteResults: function getAutoCompleteResults(searchString, params, aCallbacks) {
+    // only do substring matching when the search string contains more than one character
+    let searchTokens;
+    let where = ""
+    let boundaryCalc = "";
+    if (searchString.length > 1) {
+        searchTokens = searchString.split(/\s+/);
+
+        // build up the word boundary and prefix match bonus calculation
+        boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
+        // for each word, calculate word boundary weights for the SELECT clause and
+        // add word to the WHERE clause of the query
+        let tokenCalc = [];
+        for (let i = 0; i < searchTokens.length; i++) {
+            tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
+                            "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
+            where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
+        }
+        // add more weight if we have a traditional prefix match and
+        // multiply boundary bonuses by boundary weight
+        boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
+    } else if (searchString.length == 1) {
+        where = "AND (value LIKE :valuePrefix ESCAPE '/') ";
+        boundaryCalc = "1";
+        delete params.prefixWeight;
+        delete params.boundaryWeight;
+    } else {
+        where = "";
+        boundaryCalc = "1";
+        delete params.prefixWeight;
+        delete params.boundaryWeight;
+    }
+
+    params.now = Date.now() * 1000; // convert from ms to microseconds
+
+    /* Three factors in the frecency calculation for an entry (in order of use in calculation):
+     * 1) average number of times used - items used more are ranked higher
+     * 2) how recently it was last used - items used recently are ranked higher
+     * 3) additional weight for aged entries surviving expiry - these entries are relevant
+     *    since they have been used multiple times over a large time span so rank them higher
+     * The score is then divided by the bucket size and we round the result so that entries
+     * with a very similar frecency are bucketed together with an alphabetical sort. This is
+     * to reduce the amount of moving around by entries while typing.
+     */
+
+    let query = "/* do not warn (bug 496471): can't use an index */ " +
+                "SELECT value, " +
+                "ROUND( " +
+                    "timesUsed / MAX(1.0, (lastUsed - firstUsed) / :timeGroupingSize) * " +
+                    "MAX(1.0, :maxTimeGroupings - (:now - lastUsed) / :timeGroupingSize) * "+
+                    "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
+                    ":bucketSize "+
+                ", 3) AS frecency, " +
+                boundaryCalc + " AS boundaryBonuses " +
+                "FROM moz_formhistory " +
+                "WHERE fieldname=:fieldname " + where +
+                "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
+
+    let stmt = dbCreateAsyncStatement(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.
+    if (searchString.length >= 1)
+      stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
+    if (searchString.length > 1) {
+      for (let i = 0; i < searchTokens.length; i++) {
+        let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
+        stmt.params["tokenBegin" + i] = escapedToken + "%";
+        stmt.params["tokenBoundary" + i] =  "% " + escapedToken + "%";
+        stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
+      }
+    } else {
+      // no additional params need to be substituted into the query when the
+      // length is zero or one
+    }
+
+    let pending = stmt.executeAsync({
+      handleResult : function (aResultSet) {
+        for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+          let value = row.getResultByName("value");
+          let frecency = row.getResultByName("frecency");
+          let entry = {
+            text :          value,
+            textLowerCase : value.toLowerCase(),
+            frecency :      frecency,
+            totalScore :    Math.round(frecency * row.getResultByName("boundaryBonuses"))
+          };
+          if (aCallbacks && aCallbacks.handleResult) {
+            aCallbacks.handleResult(entry);
+          }
+        }
+      },
+
+      handleError : function (aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
+        }
+      },
+
+      handleCompletion : function (aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+        }
+      }
+    });
+    return pending;
+  },
+
+  get schemaVersion() {
+    return dbConnection.schemaVersion;
+  },
+
+  // This is used only so that the test can verify deleted table support.
+  get _supportsDeletedTable() {
+    return supportsDeletedTable;
+  },
+  set _supportsDeletedTable(val) {
+    supportsDeletedTable = val;
+  },
+
+  // The remaining methods are called by FormHistoryStartup.js
+  updatePrefs: function updatePrefs() {
+    Prefs.initialized = false;
+  },
+
+  expireOldEntries: function expireOldEntries() {
+    log("expireOldEntries");
+
+    // Determine how many days of history we're supposed to keep.
+    // Calculate expireTime in microseconds
+    let expireTime = (Date.now() - Prefs.expireDays * DAY_IN_MS) * 1000;
+
+    sendNotification("formhistory-beforeexpireoldentries", expireTime);
+
+    FormHistory.count({}, {
+      handleResult: function(aBeginningCount) {
+        expireOldEntriesDeletion(expireTime, aBeginningCount);
+      },
+      handleError: function(aError) {
+        log("expireStartCountFailure");
+      }
+    });
+  },
+
+  shutdown: function shutdown() { dbClose(true); }
+};
+
+// Prevent add-ons from redefining this API
+Object.freeze(FormHistory);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+
+function FormHistoryStartup() { }
+
+FormHistoryStartup.prototype = {
+  classID: Components.ID("{3A0012EB-007F-4BB8-AA81-A07385F77A25}"),
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference,
+    Ci.nsIFrameMessageListener
+  ]),
+
+  observe: function(subject, topic, data) {
+    switch (topic) {
+      case "nsPref:changed":
+        FormHistory.updatePrefs();
+        break;
+      case "idle-daily":
+      case "formhistory-expire-now":
+        FormHistory.expireOldEntries();
+        break;
+      case "profile-before-change":
+        FormHistory.shutdown();
+        break;
+      case "profile-after-change":
+        this.init();
+      default:
+        break;
+    }
+  },
+
+  inited: false,
+
+  init: function()
+  {
+    if (this.inited)
+      return;
+    this.inited = true;
+
+    Services.prefs.addObserver("browser.formfill.", this, true);
+
+    // triggers needed service cleanup and db shutdown
+    Services.obs.addObserver(this, "profile-before-change", true);
+    Services.obs.addObserver(this, "formhistory-expire-now", true);
+
+    let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+                         getService(Ci.nsIMessageListenerManager);
+    messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
+    messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
+  },
+
+  receiveMessage: function(message) {
+    let entries = message.json;
+    let changes = entries.map(function(entry) {
+      return {
+        op : "bump",
+        fieldname : entry.name,
+        value : entry.value,
+      }
+    });
+
+    FormHistory.update(changes);
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormHistoryStartup]);
--- a/toolkit/components/satchel/Makefile.in
+++ b/toolkit/components/satchel/Makefile.in
@@ -20,21 +20,26 @@ LOCAL_INCLUDES = \
   $(NULL)
 
 CPPSRCS = \
   nsFormFillController.cpp \
   $(NULL)
 
 EXTRA_COMPONENTS = \
   nsFormAutoComplete.js \
+  FormHistoryStartup.js \
   nsInputListAutoComplete.js \
   satchel.manifest \
   $(NULL)
 
 EXTRA_PP_COMPONENTS = \
 	nsFormHistory.js \
 	$(NULL)
 
 EXTRA_JS_MODULES = \
   nsFormAutoCompleteResult.jsm \
   $(NULL)
 
+EXTRA_PP_JS_MODULES = \
+  FormHistory.jsm \
+	$(NULL)
+
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -5,60 +5,60 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+                                  "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+
 function FormAutoComplete() {
     this.init();
 }
 
 FormAutoComplete.prototype = {
     classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
 
-    __formHistory : null,
-    get _formHistory() {
-        if (!this.__formHistory)
-            this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"].
-                                 getService(Ci.nsIFormHistory2);
-        return this.__formHistory;
-    },
-
     _prefBranch         : null,
     _debug              : true, // mirrors browser.formfill.debug
     _enabled            : true, // mirrors browser.formfill.enable preference
     _agedWeight         : 2,
     _bucketSize         : 1,
     _maxTimeGroupings   : 25,
     _timeGroupingSize   : 7 * 24 * 60 * 60 * 1000 * 1000,
     _expireDays         : null,
     _boundaryWeight     : 25,
     _prefixWeight       : 5,
 
+    // Only one query is performed at a time, which will be stored in _pendingQuery
+    // while the query is being performed. It will be cleared when the query finishes,
+    // is cancelled, or an error occurs. If a new query occurs while one is already
+    // pending, the existing one is cancelled. The pending query will be an
+    // mozIStoragePendingStatement object.
+    _pendingQuery       : null,
+
     init : function() {
         // Preferences. Add observer so we get notified of changes.
         this._prefBranch = Services.prefs.getBranch("browser.formfill.");
         this._prefBranch.addObserver("", this.observer, true);
         this.observer._self = this;
 
         this._debug            = this._prefBranch.getBoolPref("debug");
         this._enabled          = this._prefBranch.getBoolPref("enable");
         this._agedWeight       = this._prefBranch.getIntPref("agedWeight");
         this._bucketSize       = this._prefBranch.getIntPref("bucketSize");
         this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
         this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
         this._expireDays       = this._prefBranch.getIntPref("expire_days");
-
-        this._dbStmts = {};
-
-        Services.obs.addObserver(this.observer, "profile-before-change", true);
     },
 
     observer : {
         _self : null,
 
         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
                                                 Ci.nsISupportsWeakReference]),
 
@@ -91,22 +91,16 @@ FormAutoComplete.prototype = {
                         self._boundaryWeight = self._prefBranch.getIntPref(prefName);
                         break;
                     case "prefixWeight":
                         self._prefixWeight = self._prefBranch.getIntPref(prefName);
                         break;
                     default:
                         self.log("Oops! Pref not handled, change ignored.");
                 }
-            } else if (topic == "profile-before-change") {
-                for each (let stmt in self._dbStmts) {
-                    stmt.finalize();
-                }
-                self._dbStmts = {};
-                self.__formHistory = null;
             }
         }
     },
 
 
     /*
      * log
      *
@@ -116,43 +110,74 @@ FormAutoComplete.prototype = {
     log : function (message) {
         if (!this._debug)
             return;
         dump("FormAutoComplete: " + message + "\n");
         Services.console.logStringMessage("FormAutoComplete: " + message);
     },
 
 
+    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
+      Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
+
+      let result = null;
+      let listener = {
+        onSearchCompletion: function (r) result = r
+      };
+      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
+
+      // Just wait for the result to to be available.
+      let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread;
+      while (!result && this._pendingQuery) {
+        thread.processNextEvent(true);
+      }
+
+      return result;
+    },
+
+    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
+      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
+    },
+
     /*
-     * autoCompleteSearch
+     * autoCompleteSearchShared
      *
      * aInputName    -- |name| attribute from the form input being autocompleted.
      * aUntrimmedSearchString -- current value of the input
      * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
      * aPreviousResult -- previous search result, if any.
-     *
-     * Returns: an nsIAutoCompleteResult
+     * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
+     *              that may be returned asynchronously.
      */
-    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
+    _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
         function sortBytotalScore (a, b) {
             return b.totalScore - a.totalScore;
         }
 
-        if (!this._enabled)
-            return null;
+        let result = null;
+        if (!this._enabled) {
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
+            return;
+        }
 
         // don't allow form inputs (aField != null) to get results from search bar history
         if (aInputName == 'searchbar-history' && aField) {
             this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
-            return null;
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
+            return;
         }
 
         this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
         let searchString = aUntrimmedSearchString.trim().toLowerCase();
-        let result = null;
 
         // reuse previous results if:
         // a) length greater than one character (others searches are special cases) AND
         // b) the the new results will be a subset of the previous results
         if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
             searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
             this.log("Using previous autocomplete result");
             result = aPreviousResult;
@@ -171,155 +196,90 @@ FormAutoComplete.prototype = {
                     continue;
                 this._calculateScore(entry, searchString, searchTokens);
                 this.log("Reusing autocomplete entry '" + entry.text +
                          "' (" + entry.frecency +" / " + entry.totalScore + ")");
                 filteredEntries.push(entry);
             }
             filteredEntries.sort(sortBytotalScore);
             result.wrappedJSObject.entries = filteredEntries;
+
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
         } else {
             this.log("Creating new autocomplete search result.");
-            let entries = this.getAutoCompleteValues(aInputName, searchString);
-            result = new FormAutoCompleteResult(this._formHistory, entries, aInputName, aUntrimmedSearchString);
-            if (aField && aField.maxLength > -1) {
-                let original = result.wrappedJSObject.entries;
-                let filtered = original.filter(function (el) el.text.length <= this.maxLength, aField);
-                result.wrappedJSObject.entries = filtered;
+
+            // Start with an empty list.
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+
+            let processEntry = function(aEntries) {
+              if (aField && aField.maxLength > -1) {
+                result.entries =
+                  aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
+              } else {
+                result.entries = aEntries;
+              }
+
+              if (aListener) {
+                aListener.onSearchCompletion(result);
+              }
             }
+
+            this.getAutoCompleteValues(aInputName, searchString, processEntry);
         }
-
-        return result;
     },
 
-    getAutoCompleteValues : function (fieldName, searchString) {
-        let values = [];
-        let searchTokens;
+    stopAutoCompleteSearch : function () {
+        if (this._pendingQuery) {
+            this._pendingQuery.cancel();
+            this._pendingQuery = null;
+        }
+    },
 
+    /*
+     * Get the values for an autocomplete list given a search string.
+     *
+     *  fieldName - fieldname field within form history (the form input name)
+     *  searchString - string to search for
+     *  callback - called when the values are available. Passed an array of objects,
+     *             containing properties for each result. The callback is only called
+     *             when successful.
+     */
+    getAutoCompleteValues : function (fieldName, searchString, callback) {
         let params = {
             agedWeight:         this._agedWeight,
             bucketSize:         this._bucketSize,
             expiryDate:         1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
             fieldname:          fieldName,
             maxTimeGroupings:   this._maxTimeGroupings,
-            now:                Date.now() * 1000,          // convert from ms to microseconds
-            timeGroupingSize:   this._timeGroupingSize
+            timeGroupingSize:   this._timeGroupingSize,
+            prefixWeight:       this._prefixWeight,
+            boundaryWeight:     this._boundaryWeight
         }
 
-        // only do substring matching when more than one character is typed
-        let where = ""
-        let boundaryCalc = "";
-        if (searchString.length > 1) {
-            searchTokens = searchString.split(/\s+/);
-
-            // build up the word boundary and prefix match bonus calculation
-            boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
-            // for each word, calculate word boundary weights for the SELECT clause and
-            // add word to the WHERE clause of the query
-            let tokenCalc = [];
-            for (let i = 0; i < searchTokens.length; i++) {
-                tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
-                                "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
-                where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
-            }
-            // add more weight if we have a traditional prefix match and
-            // multiply boundary bonuses by boundary weight
-            boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
-            params.prefixWeight = this._prefixWeight;
-            params.boundaryWeight = this._boundaryWeight;
-        } else if (searchString.length == 1) {
-            where = "AND (value LIKE :valuePrefix ESCAPE '/') ";
-            boundaryCalc = "1";
-        } else {
-            where = "";
-            boundaryCalc = "1";
-        }
-        /* Three factors in the frecency calculation for an entry (in order of use in calculation):
-         * 1) average number of times used - items used more are ranked higher
-         * 2) how recently it was last used - items used recently are ranked higher
-         * 3) additional weight for aged entries surviving expiry - these entries are relevant
-         *    since they have been used multiple times over a large time span so rank them higher
-         * The score is then divided by the bucket size and we round the result so that entries
-         * with a very similar frecency are bucketed together with an alphabetical sort. This is
-         * to reduce the amount of moving around by entries while typing.
-         */
-
-        let query = "/* do not warn (bug 496471): can't use an index */ " +
-                    "SELECT value, " +
-                    "ROUND( " +
-                        "timesUsed / MAX(1.0, (lastUsed - firstUsed) / :timeGroupingSize) * " +
-                        "MAX(1.0, :maxTimeGroupings - (:now - lastUsed) / :timeGroupingSize) * "+
-                        "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
-                        ":bucketSize "+
-                    ", 3) AS frecency, " +
-                    boundaryCalc + " AS boundaryBonuses " +
-                    "FROM moz_formhistory " +
-                    "WHERE fieldname=:fieldname " + where +
-                    "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
-
-        let stmt;
-        try {
-            stmt = this._dbCreateStatement(query, params);
+        this.stopAutoCompleteSearch();
 
-            // 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.
-            if (searchString.length >= 1)
-                stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
-            if (searchString.length > 1) {
-                for (let i = 0; i < searchTokens.length; i++) {
-                    let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
-                    stmt.params["tokenBegin" + i] = escapedToken + "%";
-                    stmt.params["tokenBoundary" + i] =  "% " + escapedToken + "%";
-                    stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
-                }
-            } else {
-                // no addional params need to be substituted into the query when the
-                // length is zero or one
+        let results = [];
+        let processResults = {
+          handleResult: aResult => {
+            results.push(aResult);
+          },
+          handleError: aError => {
+            this.log("getAutocompleteValues failed: " + aError.message);
+          },
+          handleCompletion: aReason => {
+            this._pendingQuery = null;
+            if (!aReason) {
+              callback(results);
             }
-
-            while (stmt.executeStep()) {
-                let entry = {
-                    text:           stmt.row.value,
-                    textLowerCase:  stmt.row.value.toLowerCase(),
-                    frecency:       stmt.row.frecency,
-                    totalScore:     Math.round(stmt.row.frecency * stmt.row.boundaryBonuses)
-                }
-                values.push(entry);
-            }
+          }
+        };
 
-        } catch (e) {
-            this.log("getValues failed: " + e.name + " : " + e.message);
-            throw "DB failed getting form autocomplete values";
-        } finally {
-            if (stmt) {
-                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) {
-            let stmtparams = stmt.params;
-            for (let i in params)
-                stmtparams[i] = params[i];
-        }
-        return stmt;
+        this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
     },
 
     /*
      * _calculateScore
      *
      * entry    -- an nsIAutoCompleteResult entry
      * aSearchString -- current value of the input (lowercase)
      * searchTokens -- array of tokens of the search string
@@ -417,15 +377,18 @@ FormAutoCompleteResult.prototype = {
         return "";
     },
 
     removeValueAt : function (index, removeFromDB) {
         this._checkIndexBounds(index);
 
         let [removedEntry] = this.entries.splice(index, 1);
 
-        if (removeFromDB)
-            this.formHistory.removeEntry(this.fieldName, removedEntry.text);
+        if (removeFromDB) {
+          this.formHistory.update({ op: "remove",
+                                    fieldname: this.fieldName,
+                                    value: removedEntry.text });
+        }
     }
 };
 
 let component = [FormAutoComplete];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -33,21 +33,22 @@
 #include "nsEmbedCID.h"
 #include "nsIDOMNSEditableElement.h"
 #include "mozilla/dom/Element.h"
 #include "nsContentUtils.h"
 #include "nsDOMEvent.h"
 
 using namespace mozilla::dom;
 
-NS_IMPL_ISUPPORTS5(nsFormFillController,
+NS_IMPL_ISUPPORTS6(nsFormFillController,
                    nsIFormFillController,
                    nsIAutoCompleteInput,
                    nsIAutoCompleteSearch,
                    nsIDOMEventListener,
+                   nsIFormAutoCompleteObserver,
                    nsIMutationObserver)
 
 nsFormFillController::nsFormFillController() :
   mFocusedInput(nullptr),
   mFocusedInputNode(nullptr),
   mListNode(nullptr),
   mTimeout(50),
   mMinResultsForPopup(1),
@@ -597,71 +598,92 @@ nsFormFillController::StartSearch(const 
 
   // If the login manager has indicated it's responsible for this field, let it
   // handle the autocomplete. Otherwise, handle with form history.
   bool dummy;
   if (mPwmgrInputs.Get(mFocusedInputNode, &dummy)) {
     // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting
     // satchel manage the field?
     rv = mLoginManager->AutoCompleteSearch(aSearchString,
-                                         aPreviousResult,
-                                         mFocusedInput,
-                                         getter_AddRefs(result));
+                                           aPreviousResult,
+                                           mFocusedInput,
+                                           getter_AddRefs(result));
+    NS_ENSURE_SUCCESS(rv, rv);
+    if (aListener) {
+      aListener->OnSearchResult(this, result);
+    }
   } else {
-    nsCOMPtr<nsIAutoCompleteResult> formHistoryResult;
+    mLastListener = aListener;
 
     // It appears that mFocusedInput is always null when we are focusing a XUL
     // element. Scary :)
     if (!mFocusedInput || nsContentUtils::IsAutocompleteEnabled(mFocusedInput)) {
       nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
         do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
       NS_ENSURE_SUCCESS(rv, rv);
 
-      rv = formAutoComplete->AutoCompleteSearch(aSearchParam,
+      formAutoComplete->AutoCompleteSearchAsync(aSearchParam,
                                                 aSearchString,
                                                 mFocusedInput,
                                                 aPreviousResult,
-                                                getter_AddRefs(formHistoryResult));
+                                                this);
+      mLastFormAutoComplete = formAutoComplete;
+    } else {
+      mLastSearchString = aSearchString;
 
-      NS_ENSURE_SUCCESS(rv, rv);
+      // Even if autocomplete is disabled, handle the inputlist anyway as that was
+      // specifically requested by the page. This is so a field can have the default
+      // autocomplete disabled and replaced with a custom inputlist autocomplete.
+      return PerformInputListAutoComplete(aPreviousResult);
     }
+  }
 
-    mLastSearchResult = formHistoryResult;
-    mLastListener = aListener;
-    mLastSearchString = aSearchString;
+  return NS_OK;
+}
 
-    nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
-      do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
+nsresult
+nsFormFillController::PerformInputListAutoComplete(nsIAutoCompleteResult* aPreviousResult)
+{
+  // If an <input> is focused, check if it has a list="<datalist>" which can
+  // provide the list of suggestions.
+
+  nsresult rv;
+  nsCOMPtr<nsIAutoCompleteResult> result;
 
-    rv = inputListAutoComplete->AutoCompleteSearch(formHistoryResult,
-                                                   aSearchString,
-                                                   mFocusedInput,
-                                                   getter_AddRefs(result));
-
-    if (mFocusedInput) {
-      nsCOMPtr<nsIDOMHTMLElement> list;
-      mFocusedInput->GetList(getter_AddRefs(list));
+  nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
+    do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = inputListAutoComplete->AutoCompleteSearch(aPreviousResult,
+                                                 mLastSearchString,
+                                                 mFocusedInput,
+                                                 getter_AddRefs(result));
+  NS_ENSURE_SUCCESS(rv, rv);
 
-      nsCOMPtr<nsINode> node = do_QueryInterface(list);
-      if (mListNode != node) {
-        if (mListNode) {
-          mListNode->RemoveMutationObserver(this);
-          mListNode = nullptr;
-        }
-        if (node) {
-          node->AddMutationObserverUnlessExists(this);
-          mListNode = node;
-        }
+  if (mFocusedInput) {
+    nsCOMPtr<nsIDOMHTMLElement> list;
+    mFocusedInput->GetList(getter_AddRefs(list));
+
+    // Add a mutation observer to check for changes to the items in the <datalist>
+    // and update the suggestions accordingly.
+    nsCOMPtr<nsINode> node = do_QueryInterface(list);
+    if (mListNode != node) {
+      if (mListNode) {
+        mListNode->RemoveMutationObserver(this);
+        mListNode = nullptr;
+      }
+      if (node) {
+        node->AddMutationObserverUnlessExists(this);
+        mListNode = node;
       }
     }
   }
-  NS_ENSURE_SUCCESS(rv, rv);
 
-  aListener->OnSearchResult(this, result);
+  if (mLastListener) {
+    mLastListener->OnSearchResult(this, result);
+  }
 
   return NS_OK;
 }
 
 class UpdateSearchResultRunnable : public nsRunnable
 {
 public:
   UpdateSearchResultRunnable(nsIAutoCompleteObserver* aObserver,
@@ -704,20 +726,42 @@ void nsFormFillController::RevalidateDat
   nsCOMPtr<nsIRunnable> event =
     new UpdateSearchResultRunnable(mLastListener, this, result);
   NS_DispatchToCurrentThread(event);
 }
 
 NS_IMETHODIMP
 nsFormFillController::StopSearch()
 {
+  // Make sure to stop and clear this, otherwise the controller will prevent
+  // mLastFormAutoComplete from being deleted.
+  if (mLastFormAutoComplete) {
+    mLastFormAutoComplete->StopAutoCompleteSearch();
+    mLastFormAutoComplete = nullptr;
+  }
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////
+//// nsIFormAutoCompleteObserver
+
+NS_IMETHODIMP
+nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult)
+{
+  nsCOMPtr<nsIAutoCompleteResult> resultParam = do_QueryInterface(aResult);
+
+  nsAutoString searchString;
+  resultParam->GetSearchString(searchString);
+  mLastSearchResult = aResult;
+  mLastSearchString = searchString;
+
+  return PerformInputListAutoComplete(resultParam);
+}
+
+////////////////////////////////////////////////////////////////////////
 //// nsIDOMEventListener
 
 NS_IMETHODIMP
 nsFormFillController::HandleEvent(nsIDOMEvent* aEvent)
 {
   nsAutoString type;
   aEvent->GetType(type);
 
@@ -1162,9 +1206,8 @@ static const mozilla::Module::ContractID
 
 static const mozilla::Module kSatchelModule = {
   mozilla::Module::kVersion,
   kSatchelCIDs,
   kSatchelContracts
 };
 
 NSMODULE_DEFN(satchel) = &kSatchelModule;
-
--- a/toolkit/components/satchel/nsFormFillController.h
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -6,16 +6,17 @@
 #ifndef __nsFormFillController__
 #define __nsFormFillController__
 
 #include "nsIFormFillController.h"
 #include "nsIAutoCompleteInput.h"
 #include "nsIAutoCompleteSearch.h"
 #include "nsIAutoCompleteController.h"
 #include "nsIAutoCompletePopup.h"
+#include "nsIFormAutoComplete.h"
 #include "nsIDOMEventListener.h"
 #include "nsCOMPtr.h"
 #include "nsDataHashtable.h"
 #include "nsIDocShell.h"
 #include "nsIDOMWindow.h"
 #include "nsIDOMHTMLInputElement.h"
 #include "nsILoginManager.h"
 #include "nsIMutationObserver.h"
@@ -28,23 +29,25 @@
 
 class nsFormHistory;
 class nsINode;
 
 class nsFormFillController : public nsIFormFillController,
                              public nsIAutoCompleteInput,
                              public nsIAutoCompleteSearch,
                              public nsIDOMEventListener,
+                             public nsIFormAutoCompleteObserver,
                              public nsIMutationObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIFORMFILLCONTROLLER
   NS_DECL_NSIAUTOCOMPLETESEARCH
   NS_DECL_NSIAUTOCOMPLETEINPUT
+  NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
   NS_DECL_NSIDOMEVENTLISTENER
   NS_DECL_NSIMUTATIONOBSERVER
 
   nsresult Focus(nsIDOMEvent* aEvent);
   nsresult KeyPress(nsIDOMEvent* aKeyEvent);
   nsresult MouseDown(nsIDOMEvent* aMouseEvent);
 
   nsFormFillController();
@@ -55,16 +58,18 @@ protected:
   void RemoveWindowListeners(nsIDOMWindow *aWindow);
 
   void AddKeyListener(nsINode* aInput);
   void RemoveKeyListener();
 
   void StartControllingInput(nsIDOMHTMLInputElement *aInput);
   void StopControllingInput();
 
+  nsresult PerformInputListAutoComplete(nsIAutoCompleteResult* aPreviousResult);
+
   void RevalidateDataList();
   bool RowMatch(nsFormHistory *aHistory, uint32_t aIndex, const nsAString &aInputName, const nsAString &aInputValue);
 
   inline nsIDocShell *GetDocShellForInput(nsIDOMHTMLInputElement *aInput);
   inline nsIDOMWindow *GetWindowForDocShell(nsIDocShell *aDocShell);
   inline int32_t GetIndexOfDocShell(nsIDocShell *aDocShell);
 
   void MaybeRemoveMutationObserver(nsINode* aNode);
@@ -74,25 +79,34 @@ protected:
                                                      void* aUserData);
   bool IsEventTrusted(nsIDOMEvent *aEvent);
   // members //////////////////////////////////////////
 
   nsCOMPtr<nsIAutoCompleteController> mController;
   nsCOMPtr<nsILoginManager> mLoginManager;
   nsIDOMHTMLInputElement* mFocusedInput;
   nsINode* mFocusedInputNode;
+
+  // mListNode is a <datalist> element which, is set, has the form fill controller
+  // as a mutation observer for it.
   nsINode* mListNode;
   nsCOMPtr<nsIAutoCompletePopup> mFocusedPopup;
 
   nsTArray<nsCOMPtr<nsIDocShell> > mDocShells;
   nsTArray<nsCOMPtr<nsIAutoCompletePopup> > mPopups;
 
   //these are used to dynamically update the autocomplete
   nsCOMPtr<nsIAutoCompleteResult> mLastSearchResult;
+
+  // The observer passed to StartSearch. It will be notified when the search is
+  // complete or the data from a datalist changes.
   nsCOMPtr<nsIAutoCompleteObserver> mLastListener;
+
+  // This is cleared by StopSearch().
+  nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete;
   nsString mLastSearchString;
 
   nsDataHashtable<nsPtrHashKey<const nsINode>, bool> mPwmgrInputs;
 
   uint32_t mTimeout;
   uint32_t mMinResultsForPopup;
   uint32_t mMaxRows;
   bool mDisableAutoComplete;
--- a/toolkit/components/satchel/nsFormHistory.js
+++ b/toolkit/components/satchel/nsFormHistory.js
@@ -22,17 +22,16 @@ FormHistory.prototype = {
     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormHistory2,
                                               Ci.nsIObserver,
                                               Ci.nsIMessageListener,
                                               Ci.nsISupportsWeakReference,
                                               ]),
 
     debug          : true,
     enabled        : true,
-    saveHttpsForms : true,
 
     // The current database schema.
     dbSchema : {
         tables : {
             moz_formhistory: {
                 "id"        : "INTEGER PRIMARY KEY",
                 "fieldname" : "TEXT NOT NULL",
                 "value"     : "TEXT NOT NULL",
@@ -77,53 +76,24 @@ FormHistory.prototype = {
         if (!this.debug)
             return;
         dump("FormHistory: " + message + "\n");
         Services.console.logStringMessage("FormHistory: " + message);
     },
 
 
     init : function init() {
-        Services.prefs.addObserver("browser.formfill.", this, true);
-
         this.updatePrefs();
 
         this.dbStmts = {};
 
-        this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
-                              getService(Ci.nsIMessageListenerManager);
-        this.messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
-        this.messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
-
-        // Add observers
+        // Add observer
         Services.obs.addObserver(this, "profile-before-change", true);
-        Services.obs.addObserver(this, "idle-daily", true);
-        Services.obs.addObserver(this, "formhistory-expire-now", true);
     },
 
-    /* ---- message listener ---- */
-
-
-    receiveMessage: function receiveMessage(message) {
-        // Open a transaction so multiple adds happen in one commit
-        this.dbConnection.beginTransaction();
-
-        try {
-            let entries = message.json;
-            for (let i = 0; i < entries.length; i++) {
-                this.addEntry(entries[i].name, entries[i].value);
-            }
-        } finally {
-            // Don't need it to be atomic if there was an error.  Commit what
-            // we managed to put in the table.
-            this.dbConnection.commitTransaction();
-        }
-    },
-
-
     /* ---- nsIFormHistory2 interfaces ---- */
 
 
     get hasEntries() {
         return (this.countAllEntries() > 0);
     },
 
 
@@ -429,20 +399,16 @@ FormHistory.prototype = {
     /* ---- nsIObserver interface ---- */
 
 
     observe : function observe(subject, topic, data) {
         switch(topic) {
         case "nsPref:changed":
             this.updatePrefs();
             break;
-        case "idle-daily":
-        case "formhistory-expire-now":
-            this.expireOldEntries();
-            break;
         case "profile-before-change":
             this._dbClose(false);
             break;
         default:
             this.log("Oops! Unexpected notification: " + topic);
             break;
         }
     },
@@ -564,66 +530,19 @@ FormHistory.prototype = {
             }
         }
 
         this.log("countAllEntries: counted entries: " + numEntries);
         return numEntries;
     },
 
 
-    expireOldEntries : function () {
-        this.log("expireOldEntries");
-
-        // Determine how many days of history we're supposed to keep.
-        let expireDays = 180;
-        try {
-            expireDays = Services.prefs.getIntPref("browser.formfill.expire_days");
-        } catch (e) { /* ignore */ }
-
-        let expireTime = Date.now() - expireDays * DAY_IN_MS;
-        expireTime *= 1000; // switch to microseconds
-
-        this.sendIntNotification("before-expireOldEntries", expireTime);
-
-        let beginningCount = this.countAllEntries();
-
-        // Purge the form history...
-        let stmt;
-        let query = "DELETE FROM moz_formhistory WHERE lastUsed <= :expireTime";
-        let params = { expireTime : expireTime };
-
-        try {
-            stmt = this.dbCreateStatement(query, params);
-            stmt.execute();
-        } catch (e) {
-            this.log("expireOldEntries failed: " + e);
-            throw e;
-        } finally {
-            if (stmt) {
-                stmt.reset();
-            }
-        }
-
-        let endingCount = this.countAllEntries();
-
-        // If we expired a large batch of entries, shrink the DB to reclaim wasted
-        // space. This is expected to happen when entries predating timestamps
-        // (added in the v.1 schema) expire in mass, 180 days after the DB was
-        // upgraded -- entries not used since then expire all at once.
-        if (beginningCount - endingCount > 500)
-            this.dbConnection.executeSimpleSQL("VACUUM");
-
-        this.sendIntNotification("expireOldEntries", expireTime);
-    },
-
-
     updatePrefs : function () {
         this.debug          = Services.prefs.getBoolPref("browser.formfill.debug");
         this.enabled        = Services.prefs.getBoolPref("browser.formfill.enable");
-        this.saveHttpsForms = Services.prefs.getBoolPref("browser.formfill.saveHttpsForms");
     },
 
 //**************************************************************************//
     // Database Creation & Access
 
     /*
      * dbCreateStatement
      *
--- a/toolkit/components/satchel/nsIFormAutoComplete.idl
+++ b/toolkit/components/satchel/nsIFormAutoComplete.idl
@@ -1,22 +1,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 #include "nsISupports.idl"
 
 interface nsIAutoCompleteResult;
+interface nsIFormAutoCompleteObserver;
 interface nsIDOMHTMLInputElement;
 
-[scriptable, uuid(997c0c05-5d1d-47e5-9cbc-765c0b8ec699)]
+[scriptable, uuid(c079f18f-40ab-409d-800e-878889b83b58)]
 
 interface nsIFormAutoComplete: nsISupports {
+
     /**
-     * Generate results for a form input autocomplete menu.
+     * Generate results for a form input autocomplete menu synchronously.
+     * This method is deprecated in favour of autoCompleteSearchAsync.
+     */
+    nsIAutoCompleteResult autoCompleteSearch(in AString aInputName,
+                                             in AString aSearchString,
+                                             in nsIDOMHTMLInputElement aField,
+                                             in nsIAutoCompleteResult aPreviousResult);
+
+    /**
+     * Generate results for a form input autocomplete menu asynchronously.
      */
-    nsIAutoCompleteResult autoCompleteSearch(
-                                    in AString aInputName,
-                                    in AString aSearchString,
-                                    in nsIDOMHTMLInputElement aField,
-                                    in nsIAutoCompleteResult aPreviousResult);
+    void autoCompleteSearchAsync(in AString aInputName,
+                                 in AString aSearchString,
+                                 in nsIDOMHTMLInputElement aField,
+                                 in nsIAutoCompleteResult aPreviousResult,
+                                 in nsIFormAutoCompleteObserver aListener);
+
+    /**
+     * If a search is in progress, stop it. Otherwise, do nothing. This is used
+     * to cancel an existing search, for example, in preparation for a new search.
+     */
+    void stopAutoCompleteSearch();
 };
+
+[scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)]
+interface nsIFormAutoCompleteObserver : nsISupports
+{
+  /*
+   * Called when a search is complete and the results are ready even if the
+   * result set is empty. If the search is cancelled or a new search is
+   * started, this is not called.
+   *
+   * @param result - The search result object
+   */
+  void onSearchCompletion(in nsIAutoCompleteResult result);
+};
--- a/toolkit/components/satchel/satchel.manifest
+++ b/toolkit/components/satchel/satchel.manifest
@@ -1,6 +1,10 @@
 component {0c1bb408-71a2-403f-854a-3a0659829ded} nsFormHistory.js
 contract @mozilla.org/satchel/form-history;1 {0c1bb408-71a2-403f-854a-3a0659829ded}
 component {c11c21b2-71c9-4f87-a0f8-5e13f50495fd} nsFormAutoComplete.js
 contract @mozilla.org/satchel/form-autocomplete;1 {c11c21b2-71c9-4f87-a0f8-5e13f50495fd}
 component {bf1e01d0-953e-11df-981c-0800200c9a66} nsInputListAutoComplete.js
 contract @mozilla.org/satchel/inputlist-autocomplete;1 {bf1e01d0-953e-11df-981c-0800200c9a66}
+component {3a0012eb-007f-4bb8-aa81-a07385f77a25} FormHistoryStartup.js
+contract @mozilla.org/satchel/form-history-startup;1 {3a0012eb-007f-4bb8-aa81-a07385f77a25}
+category profile-after-change formHistoryStartup @mozilla.org/satchel/form-history-startup;1
+category idle-daily formHistoryStartup @mozilla.org/satchel/form-history-startup;1
--- a/toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js
+++ b/toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js
@@ -1,36 +1,55 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+let FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+
 /** Test for Bug 472396 **/
 function test() {
   // initialization
   waitForExplicitFinish();
   let windowsToClose = [];
   let testURI =
     "http://example.com/tests/toolkit/components/satchel/test/subtst_privbrowsing.html";
-  let formHistory = Cc["@mozilla.org/satchel/form-history;1"].
-    getService(Ci.nsIFormHistory2);
 
   function doTest(aIsPrivateMode, aShouldValueExist, aWindow, aCallback) {
     aWindow.gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
       aWindow.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
 
+      let checks = 0;
+      function doneCheck() {
+        checks++;
+        if (checks == 2) {
+          executeSoon(aCallback);
+        }
+      }
+
       // Wait for the second load of the page to call the callback,
       // because the first load submits the form and the page reloads after
       // the form submission.
       aWindow.gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
         aWindow.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
-        executeSoon(aCallback);
+        doneCheck();
       }, true);
 
-      is(formHistory.entryExists("field", "value"), aShouldValueExist,
-        "Checking value exists in form history");
+      let count = 0;
+      FormHistory.count({ fieldname: "field", value: "value" },
+        { handleResult: function(result) {
+            count = result;
+          },
+          handleError: function (error) {
+            do_throw("Error occurred searching form history: " + error);
+          },
+          handleCompletion: function(num) {
+            is(count >= 1, aShouldValueExist, "Checking value exists in form history");
+            doneCheck();
+          }
+        });
     }, true);
 
     aWindow.gBrowser.selectedBrowser.loadURI(testURI);
   }
 
   function testOnWindow(aOptions, aCallback) {
     whenNewWindowLoaded(aOptions, function(aWin) {
       windowsToClose.push(aWin);
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -66,56 +66,57 @@ function getAutocompletePopup() {
     autocompleteMenu = chromeWin.document.getElementById("PopupAutoComplete");
     ok(autocompleteMenu, "Got autocomplete popup");
 
     return autocompleteMenu;
 }
 
 
 function cleanUpFormHist() {
-  var formhist = SpecialPowers.Cc["@mozilla.org/satchel/form-history;1"].
-                 getService(SpecialPowers.Ci.nsIFormHistory2);
-  formhist.removeAllEntries();
+  SpecialPowers.formHistory.update({ op : "remove" });
 }
 cleanUpFormHist();
 
 
 var checkObserver = {
   verifyStack: [],
   callback: null,
 
   waitForChecks: function(callback) {
     if (this.verifyStack.length == 0)
       callback();
     else
       this.callback = callback;
   },
 
   observe: function(subject, topic, data) {
-    if (data != "addEntry" && data != "modifyEntry")
+    if (data != "formhistory-add" && data != "formhistory-update")
       return;
     ok(this.verifyStack.length > 0, "checking if saved form data was expected");
 
     // Make sure that every piece of data we expect to be saved is saved, and no
     // more. Here it is assumed that for every entry satchel saves or modifies, a
     // message is sent.
     //
     // We don't actually check the content of the message, but just that the right
     // quantity of messages is received.
     // - if there are too few messages, test will time out
     // - if there are too many messages, test will error out here
     //
     var expected = this.verifyStack.shift();
-    ok(fh.entryExists(expected.name, expected.value), expected.message);
 
-    if (this.verifyStack.length == 0) {
-      var callback = this.callback;
-      this.callback = null;
-      callback();
-    }
+    countEntries(expected.name, expected.value,
+      function(num) {
+        ok(num > 0, expected.message);
+        if (checkObserver.verifyStack.length == 0) {
+          var callback = checkObserver.callback;
+          checkObserver.callback = null;
+          callback();
+        }
+      });
   }
 };
 
 function checkForSave(name, value, message) {
   checkObserver.verifyStack.push({ name : name, value: value, message: message });
 }
 
 
@@ -126,8 +127,35 @@ function getFormSubmitButton(formNum) {
   // we can't just call form.submit(), because that doesn't seem to
   // invoke the form onsubmit handler.
   var button = form.firstChild;
   while (button && button.type != "submit") { button = button.nextSibling; }
   ok(button != null, "getting form submit button");
 
   return button;
 }
+
+// Count the number of entries with the given name and value, and call then(number)
+// when done. If name or value is null, then the value of that field does not matter.
+function countEntries(name, value, then) {
+  var obj = {};
+  if (name !== null)
+    obj.fieldname = name;
+  if (value !== null)
+    obj.value = value;
+
+  var count = 0;
+  SpecialPowers.formHistory.count(obj, { handleResult: function (result) { count = result },
+                                         handleError: function (error) {
+                                           do_throw("Error occurred searching form history: " + error);
+                                         },
+                                         handleCompletion: function (reason) { if (!reason) then(count); }
+                                       });
+}
+
+// Wrapper around FormHistory.update which handles errors. Calls then() when done.
+function updateFormHistory(changes, then) {
+  SpecialPowers.formHistory.update(changes, { handleError: function (error) {
+                                                do_throw("Error occurred updating form history: " + error);
+                                              },
+                                              handleCompletion: function (reason) { if (!reason) then(); },
+                                            });
+}
--- a/toolkit/components/satchel/test/test_bug_511615.html
+++ b/toolkit/components/satchel/test/test_bug_511615.html
@@ -31,30 +31,30 @@ Form History test: form field autocomple
 /** Test for Form History autocomplete **/
 
 var autocompletePopup = getAutocompletePopup();
 autocompletePopup.style.direction = "ltr";
 
 var input = $_(1, "field1");
 
 // Get the form history service
-var fh = SpecialPowers.Cc["@mozilla.org/satchel/form-history;1"].
-         getService(SpecialPowers.Ci.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("field1", "value5");
-fh.addEntry("field1", "value6");
-fh.addEntry("field1", "value7");
-fh.addEntry("field1", "value8");
-fh.addEntry("field1", "value9");
+function setupFormHistory(aCallback) {
+  updateFormHistory([
+    { op : "remove" },
+    { op : "add", fieldname : "field1", value : "value1" },
+    { op : "add", fieldname : "field1", value : "value2" },
+    { op : "add", fieldname : "field1", value : "value3" },
+    { op : "add", fieldname : "field1", value : "value4" },
+    { op : "add", fieldname : "field1", value : "value5" },
+    { op : "add", fieldname : "field1", value : "value6" },
+    { op : "add", fieldname : "field1", value : "value7" },
+    { op : "add", fieldname : "field1", value : "value8" },
+    { op : "add", fieldname : "field1", value : "value9" },
+  ], aCallback);
+}
 
 function checkForm(expectedValue) {
   var formID = input.parentNode.id;
   is(input.value, expectedValue, "Checking " + formID + " input");
 }
 
 function checkPopupOpen(isOpen, expectedIndex) {
     var actuallyOpen = autocompletePopup.popupOpen;
@@ -112,25 +112,50 @@ function doClickUnprivileged() {
     upEvent.initMouseEvent("mouseup",    true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
     ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
 
     input.dispatchEvent(dnEvent);
     input.dispatchEvent(upEvent);
     input.dispatchEvent(ckEvent);
 }
 
+var testNum = 0;
+var expectingPopup = false;
+
+function expectPopup()
+{
+  info("expecting popup for test " + testNum);
+  expectingPopup = true;
+}
+
+function popupShownListener()
+{
+  info("popup shown for test " + testNum);
+  if (expectingPopup) {
+    expectingPopup = false;
+    SimpleTest.executeSoon(runTest);
+  }
+  else {
+    ok(false, "Autocomplete popup not expected" + testNum);
+  }
+}
+
+SpecialPowers.addAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
+
 /*
  * 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) {
+function runTest() {
+  testNum++;
+
   ok(true, "Starting test #" + testNum);
 
   switch(testNum) {
     //
     // Check initial state
     //
     case 1:
         input.value = "";
@@ -213,17 +238,19 @@ function runTest(testNum) {
     case 19:
         checkPopupOpen(false);
         checkForm("");
         break;
     case 20:
         // We're privileged for this test, so open the popup.
         checkPopupOpen(false);
         checkForm("");
+        expectPopup();
         doKey("down");
+        return;
         break;
     case 21:
         checkPopupOpen(true, -1);
         checkForm("");
         testNum = 99;
         break;
 
     //
@@ -331,30 +358,32 @@ function runTest(testNum) {
         // Need to use doKey(), even though this test is not privileged.
         doKey("escape");
         break;
     case 211:
         checkPopupOpen(false);
         checkForm("");
         is(autocompletePopup.style.direction, "rtl", "direction should have been changed from ltr to rtl");
 
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
         SimpleTest.finish();
         return;
 
     default:
         ok(false, "Unexpected invocation of test #" + testNum);
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
         SimpleTest.finish();
         return;
   }
 
-  setTimeout(runTest, 50, testNum + 1);
+  SimpleTest.executeSoon(runTest);
 }
 
 function startTest() {
-  runTest(1);
+  setupFormHistory(runTest);
 }
 
 window.onload = startTest;
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
--- a/toolkit/components/satchel/test/test_bug_787624.html
+++ b/toolkit/components/satchel/test/test_bug_787624.html
@@ -60,23 +60,23 @@ Form History Layout test: form field aut
 
 var autocompletePopup = getAutocompletePopup();
 autocompletePopup.style.direction = "ltr";
 
 var input = $_(1, "field1");
 var rect = input.getBoundingClientRect();
 
 // Get the form history service
-var fh = SpecialPowers.Cc["@mozilla.org/satchel/form-history;1"].
-         getService(SpecialPowers.Ci.nsIFormHistory2);
-ok(fh != null, "got form history service");
-
-fh.removeAllEntries();
-fh.addEntry("field1", "value1");
-fh.addEntry("field1", "value2");
+function setupFormHistory() {
+  updateFormHistory([
+    { op : "remove" },
+    { op : "add", fieldname : "field1", value : "value1" },
+    { op : "add", fieldname : "field1", value : "value2" },
+  ], function() runTest(1));
+}
 
 function checkForm(expectedValue) {
   var formID = input.parentNode.id;
   if (input.value != expectedValue)
     return false;
 
   is(input.value, expectedValue, "Checking " + formID + " input");
   return true;
@@ -167,17 +167,17 @@ function runTest(testNum) {
 
   if (!retry)
     testNum++;
 
   setTimeout(runTest, 0, testNum);
 }
 
 function startTest() {
-  runTest(1);
+  setupFormHistory();
 }
 
 window.onload = startTest;
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
--- a/toolkit/components/satchel/test/test_form_autocomplete.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -6,18 +6,19 @@
   <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">
+<!-- We presumably can't hide the content for this test. The large top padding is to allow
+     listening for scrolls to occur. -->
+<div id="content" style="padding-top: 20000px;">
 
   <!-- 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) -->
@@ -115,50 +116,49 @@ Form History test: form field autocomple
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Form History autocomplete **/
 
 var input = $_(1, "field1");
 const shiftModifier = Event.SHIFT_MASK;
 
-// Get the form history service
-var fh = SpecialPowers.Cc["@mozilla.org/satchel/form-history;1"].
-         getService(SpecialPowers.Ci.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", "aaz");
-fh.addEntry("field3", "aa\xe6"); // 0xae == latin ae pair (0xc6 == AE)
-fh.addEntry("field3", "az");
-fh.addEntry("field3", "z");
-fh.addEntry("field4", "a\xe6");
-fh.addEntry("field4", "aa a\xe6");
-fh.addEntry("field4", "aba\xe6");
-fh.addEntry("field4", "bc d\xe6");
-fh.addEntry("field5", "1");
-fh.addEntry("field5", "12");
-fh.addEntry("field5", "123");
-fh.addEntry("field5", "1234");
-fh.addEntry("field6", "value");
-fh.addEntry("field7", "value");
-fh.addEntry("field8", "value");
-fh.addEntry("field9", "value");
-fh.addEntry("field10", "42");
-fh.addEntry("field11", "2010-10-10");
-fh.addEntry("field12", "21:21");
-fh.addEntry("field13", "32"); // not used, since type=range doesn't have a drop down menu
-fh.addEntry("searchbar-history", "blacklist test");
+function setupFormHistory(aCallback) {
+  updateFormHistory([
+    { op : "remove" },
+    { op : "add", fieldname : "field1", value : "value1" },
+    { op : "add", fieldname : "field1", value : "value2" },
+    { op : "add", fieldname : "field1", value : "value3" },
+    { op : "add", fieldname : "field1", value : "value4" },
+    { op : "add", fieldname : "field2", value : "value1" },
+    { op : "add", fieldname : "field3", value : "a" },
+    { op : "add", fieldname : "field3", value : "aa" },
+    { op : "add", fieldname : "field3", value : "aaz" },
+    { op : "add", fieldname : "field3", value : "aa\xe6" }, // 0xae == latin ae pair (0xc6 == AE)
+    { op : "add", fieldname : "field3", value : "az" },
+    { op : "add", fieldname : "field3", value : "z" },
+    { op : "add", fieldname : "field4", value : "a\xe6" },
+    { op : "add", fieldname : "field4", value : "aa a\xe6" },
+    { op : "add", fieldname : "field4", value : "aba\xe6" },
+    { op : "add", fieldname : "field4", value : "bc d\xe6" },
+    { op : "add", fieldname : "field5", value : "1" },
+    { op : "add", fieldname : "field5", value : "12" },
+    { op : "add", fieldname : "field5", value : "123" },
+    { op : "add", fieldname : "field5", value : "1234" },
+    { op : "add", fieldname : "field6", value : "value" },
+    { op : "add", fieldname : "field7", value : "value" },
+    { op : "add", fieldname : "field8", value : "value" },
+    { op : "add", fieldname : "field9", value : "value" },
+    { op : "add", fieldname : "field10", value : "42" },
+    { op : "add", fieldname : "field11", value : "2010-10-10" },
+    { op : "add", fieldname : "field12", value : "21:21" },
+    { op : "add", fieldname : "field13", value : "32" },  // not used, since type=range doesn't have a drop down menu
+    { op : "add", fieldname : "searchbar-history", value : "blacklist test" },
+  ], aCallback);
+}
 
 // All these non-implemeted types might need autocomplete tests in the future.
 var todoTypes = [ "datetime", "month", "week", "datetime-local", "color" ];
 var todoInput = document.createElement("input");
 for (var type of todoTypes) {
   todoInput.type = type;
   todo_is(todoInput.type, type, type + " type shouldn't be implemented");
 }
@@ -177,125 +177,162 @@ function restoreForm() {
 }
 
 // Check for expected form data.
 function checkForm(expectedValue) {
   var formID = input.parentNode.id;
   is(input.value, expectedValue, "Checking " + formID + " input");
 }
 
+var testNum = 0;
+var expectingPopup = false;
+
+function expectPopup()
+{
+  info("expecting popup for test " + testNum);
+  expectingPopup = true;
+}
+
+function popupShownListener()
+{
+  info("popup shown for test " + testNum);
+  if (expectingPopup) {
+    expectingPopup = false;
+    SimpleTest.executeSoon(runTest);
+  }
+  else {
+    ok(false, "Autocomplete popup not expected during test " + testNum);
+  }
+}
+
+SpecialPowers.addAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
 
 /*
  * Main section of test...
  *
- * This is a bit hacky, because the events are either being sent or
- * processed asynchronously, so we need to interrupt our flow with lots of
- * setTimeout() calls. The case statements are executed in order, one per
- * timeout.
+ * This is a bit hacky, as many operations happen asynchronously.
+ * Various mechanisms call runTests as a result of operations:
+ *   - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
+ *   - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it 
+ *   - addEntry calls runs the test when an entry has been added
+ *   - some tests scroll the window. This is because the form fill controller happens to scroll
+ *     the field into view near the end of the search, and there isn't any other good notification
+ *     to listen to for when the search is complete.
+ *   - some items still use setTimeout
  */
-function runTest(testNum) {
+function runTest() {
+  testNum++;
+
   ok(true, "Starting test #" + testNum);
 
   switch(testNum) {
     case 1:
         // Make sure initial form is empty.
         checkForm("");
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 2:
-        checkMenuEntries(["value1", "value2", "value3", "value4"]);
+        checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
         // Check first entry
         doKey("down");
         checkForm(""); // value shouldn't update
         doKey("return"); // not "enter"!
         checkForm("value1");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 3:
         // Check second entry
         doKey("down");
         doKey("down");
         doKey("return"); // not "enter"!
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 4:
         // Check third entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("value3");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 5:
         // Check fourth entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("value4");
 
         // Trigger autocomplete popup
+        expectPopup();
         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
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 7:
         // Check the last entry via arrow-up
         doKey("up");
         doKey("return");
         checkForm("value4");
 
         // Trigger autocomplete popup
+        expectPopup();
         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
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 9:
         // Check the last entry via arrow-up (wraparound)
         doKey("down");
         doKey("up"); // deselects
@@ -304,555 +341,695 @@ function runTest(testNum) {
         doKey("up");
         doKey("up"); // first entry
         doKey("up"); // deselects
         doKey("up"); // last entry
         doKey("return");
         checkForm("value4");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 10:
         // Set first entry w/o triggering autocomplete
         doKey("down");
         doKey("right");
         checkForm("value1");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 11:
         // Set first entry w/o triggering autocomplete
         doKey("down");
         doKey("left");
         checkForm("value1");
 
         // Trigger autocomplete popup
+        expectPopup();
         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
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 13:
         // Check last entry (page down)
         doKey("down");
         doKey("page_down");
         doKey("return");
         checkForm("value4");
 
         // Trigger autocomplete popup
+        testNum = 49;
+        expectPopup();
         restoreForm();
         doKey("down");
-        testNum = 49;
         break;
 
     /* Test removing entries from the dropdown */
 
     case 50:
-        checkMenuEntries(["value1", "value2", "value3", "value4"]);
+        checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
         // Delete the first entry (of 4)
         setForm("value");
         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.
         if (SpecialPowers.OS == "Darwin")
           doKey("back_space", shiftModifier);
         else
           doKey("delete", shiftModifier);
 
         // This tests that on OS X shift-backspace didn't delete the last character
         // in the input (bug 480262).
-        checkForm("value");
+        waitForMenuChange(3);
+        break;
 
-        ok(!fh.entryExists("field1", "value1"), "checking that f1/v1 was deleted");
+    case 51:
+        checkForm("value");
+        countEntries("field1", "value1",
+          function (num) {
+            ok(!num, testNum + " checking that f1/v1 was deleted");
+            runTest();
+          });
+        break;
+
+    case 52:
         doKey("return");
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 51:
-        checkMenuEntries(["value2", "value3", "value4"]);
+    case 53:
+        checkMenuEntries(["value2", "value3", "value4"], testNum);
         // Check the new first entry (of 3)
         doKey("down");
         doKey("return");
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 52:
+    case 54:
         // Delete the second entry (of 3)
         doKey("down");
         doKey("down");
         doKey("delete", shiftModifier);
+        waitForMenuChange(2);
+        break;
+
+    case 55:
         checkForm("");
-        ok(!fh.entryExists("field1", "value3"), "checking that f1/v3 was deleted");
+        countEntries("field1", "value3",
+          function (num) {
+            ok(!num, testNum + " checking that f1/v3 was deleted");
+            runTest();
+          });
+        break;
+
+    case 56:
         doKey("return");
         checkForm("value4")
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 53:
-        checkMenuEntries(["value2", "value4"]);
+    case 57:
+        checkMenuEntries(["value2", "value4"], testNum);
         // Check the new first entry (of 2)
         doKey("down");
         doKey("return");
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 54:
+    case 58:
         // 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");
+        waitForMenuChange(1);
+        break;
+
+    case 59:
+        countEntries("field1", "value4",
+          function (num) {
+            ok(!num, testNum + " checking that f1/v4 was deleted");
+            runTest();
+          });
+        break;
+
+    case 60:
         doKey("return");
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 55:
-        checkMenuEntries(["value2"]);
+    case 61:
+        checkMenuEntries(["value2"], testNum);
         // Check the new first entry (of 1)
         doKey("down");
         doKey("return");
         checkForm("value2");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
-    case 56:
+    case 62:
         // Delete the only remaining entry
         doKey("down");
         doKey("delete", shiftModifier);
+        waitForMenuChange(0);
+        break;
+
+    case 63:
         checkForm("");
-        ok(!fh.entryExists("field1", "value2"), "checking that f1/v2 was deleted");
+        countEntries("field1", "value2",
+          function (num) {
+            ok(!num, testNum + " checking that f1/v2 was deleted");
+            runTest();
+          });
+        break;
 
+    case 64:
         // Look at form 2, trigger autocomplete popup
         input = $_(2, "field2");
+        testNum = 99;
+        expectPopup();
         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();
+        // Sometimes, this will fail if scrollTo(0, 0) is called, so that doesn't
+        // happen here. Fortunately, a different input is used from the last test,
+        // so a scroll should still occur.
         doKey("down");
+        waitForScroll();
         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");
+        waitForMenuChange(0);
         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;
+        expectPopup();
         input.focus();
         sendChar("a");
         break;
 
     /* Test filtering as characters are typed. */
 
     case 200:
-        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"]);
+        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
         input.focus();
         sendChar("a");
+        waitForMenuChange(3);
         break;
 
     case 201:
-        checkMenuEntries(["aa", "aaz", "aa\xe6"]);
+        checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
         input.focus();
         sendChar("\xc6");
+        waitForMenuChange(1);
         break;
 
     case 202:
-        checkMenuEntries(["aa\xe6"]);
+        checkMenuEntries(["aa\xe6"], testNum);
         doKey("back_space");
+        waitForMenuChange(3);
         break;
 
     case 203:
-        checkMenuEntries(["aa", "aaz", "aa\xe6"]);
+        checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
         doKey("back_space");
+        waitForMenuChange(5);
         break;
 
     case 204:
-        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"]);
+        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
         input.focus();
         sendChar("z");
+        waitForMenuChange(2);
         break;
 
     case 205:
-        ok(getMenuEntries().length > 0, "checking typing in middle of text");
+        checkMenuEntries(["az", "aaz"], testNum);
         input.focus();
         doKey("left");
+        expectPopup();
         sendChar("a");
         break;
 
     case 206:
-        checkMenuEntries(["aaz"]);
-        fh.addEntry("field3", "aazq");
-        input.focus();
-        doKey("right");
-        sendChar("q");
+        checkMenuEntries(["aaz"], testNum);
+        addEntry("field3", "aazq");
         break;
 
     case 207:
-        // check that results were cached
-        checkMenuEntries([]);
-        fh.addEntry("field3", "aazqq");
         input.focus();
+        doKey("right");
         sendChar("q");
+        waitForMenuChange(0);
         break;
 
     case 208:
+        // check that results were cached
+        checkMenuEntries([], testNum);
+        addEntry("field3", "aazqq");
+        break;
+
+    case 209:
+        input.focus();
+        window.scrollTo(0, 0);
+        sendChar("q");
+        waitForScroll();
+        break;
+
+    case 210:
         // check that empty results were cached - bug 496466
-        checkMenuEntries([]);
+        checkMenuEntries([], testNum);
         doKey("escape");
 
         // Look at form 6, try to trigger autocomplete popup
         input = $_(6, "field4");
         restoreForm();
         testNum = 249;
+        expectPopup();
         input.focus();
         sendChar("a");
         break;
 
     /* Test substring matches and word boundary bonuses */
 
     case 250:
         // alphabetical results for first character
-        checkMenuEntries(["aa a\xe6", "aba\xe6", "a\xe6"]);
+        checkMenuEntries(["aa a\xe6", "aba\xe6", "a\xe6"], testNum);
         input.focus();
+
+        // for this test, hide the popup first as it contains the same number of menu 
+
         sendChar("\xc6");
+        waitForMenuChange(3, "a\xe6");
         break;
 
     case 251:
         // prefix match comes first, then word boundary match
         // followed by substring match
-        checkMenuEntries(["a\xe6", "aa a\xe6", "aba\xe6"]);
+        checkMenuEntries(["a\xe6", "aa a\xe6", "aba\xe6"], testNum);
 
         restoreForm();
         input.focus();
         sendChar("b");
+        waitForMenuChange(1, "bc d\xe6");
         break;
 
     case 252:
-        checkMenuEntries(["bc d\xe6"]);
+        checkMenuEntries(["bc d\xe6"], testNum);
         input.focus();
         sendChar(" ");
+        setTimeout(runTest, 300);
         break;
 
     case 253:
         // check that trailing space has no effect after single char.
-        checkMenuEntries(["bc d\xe6"]);
+        checkMenuEntries(["bc d\xe6"], testNum);
         input.focus();
         sendChar("\xc6");
+        waitForMenuChange(2);
         break;
 
     case 254:
         // check multi-word substring matches
         checkMenuEntries(["bc d\xe6", "aba\xe6"]);
         input.focus();
+        expectPopup();
         doKey("left");
         sendChar("d");
         break;
 
     case 255:
         // check inserting in multi-word searches
-        checkMenuEntries(["bc d\xe6"]);
+        checkMenuEntries(["bc d\xe6"], testNum);
         input.focus();
         sendChar("z");
+        waitForMenuChange(0);
         break;
 
     case 256:
-        checkMenuEntries([]);
+        checkMenuEntries([], testNum);
 
         // Look at form 7, try to trigger autocomplete popup
         input = $_(7, "field5");
+        testNum = 299;
+        expectPopup();
         restoreForm();
         doKey("down");
-        testNum = 299;
         break;
 
     case 300:
-        checkMenuEntries(["1", "12", "123", "1234"]);
+        checkMenuEntries(["1", "12", "123", "1234"], testNum);
         input.maxLength = 4;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 301:
-        checkMenuEntries(["1", "12", "123", "1234"]);
+        checkMenuEntries(["1", "12", "123", "1234"], testNum);
         input.maxLength = 3;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 302:
-        checkMenuEntries(["1", "12", "123"]);
+        checkMenuEntries(["1", "12", "123"], testNum);
         input.maxLength = 2;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 303:
-        checkMenuEntries(["1", "12"]);
+        checkMenuEntries(["1", "12"], testNum);
         input.maxLength = 1;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 304:
-        checkMenuEntries(["1"]);
+        checkMenuEntries(["1"], testNum);
         input.maxLength = 0;
         doKey("escape");
         doKey("down");
+        waitForMenuChange(0);
         break;
 
     case 305:
-        checkMenuEntries([]);
+        checkMenuEntries([], testNum);
         input.maxLength = 4;
 
         // now again with a character typed
         input.focus();
         sendChar("1");
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 306:
-        checkMenuEntries(["1", "12", "123", "1234"]);
+        checkMenuEntries(["1", "12", "123", "1234"], testNum);
         input.maxLength = 3;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 307:
-        checkMenuEntries(["1", "12", "123"]);
+        checkMenuEntries(["1", "12", "123"], testNum);
         input.maxLength = 2;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 308:
-        checkMenuEntries(["1", "12"]);
+        checkMenuEntries(["1", "12"], testNum);
         input.maxLength = 1;
+        expectPopup();
         doKey("escape");
         doKey("down");
         break;
 
     case 309:
-        checkMenuEntries(["1"]);
+        checkMenuEntries(["1"], testNum);
         input.maxLength = 0;
         doKey("escape");
         doKey("down");
+        waitForMenuChange(0);
         break;
 
     case 310:
-        checkMenuEntries([]);
+        checkMenuEntries([], testNum);
 
         input = $_(8, "field6");
         testNum = 399;
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 400:
     case 401:
     case 402:
     case 403:
-        checkMenuEntries(["value"]);
+        checkMenuEntries(["value"], testNum);
         doKey("down");
         doKey("return");
         checkForm("value");
 
         if (testNum == 400) {
           input = $_(9, "field7");
         } else if (testNum == 401) {
           input = $_(10, "field8");
         } else if (testNum == 402) {
           input = $_(11, "field9");
         } else if (testNum == 403) {
           input = $_(12, "field10");
         }
 
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 404:
-        checkMenuEntries(["42"]);
+        checkMenuEntries(["42"], testNum);
         doKey("down");
         doKey("return");
         checkForm("42");
 
         input = $_(14, "field11");
         restoreForm();
+        expectPopup();
         doKey("down");
         break;
 
     case 405:
         checkMenuEntries(["2010-10-10"]);
         doKey("down");
         doKey("return");
         checkForm("2010-10-10");
 
         input = $_(15, "field12");
         restoreForm();
+        expectPopup();
         doKey("down");
         break;
 
     case 406:
         checkMenuEntries(["21:21"]);
         doKey("down");
         doKey("return");
         checkForm("21:21");
 
         input = $_(16, "field13");
         restoreForm();
         doKey("down");
+        waitForMenuChange(0);
         break;
 
-      case 407:
+    case 407:
         checkMenuEntries([]); // type=range does not have a drop down menu
         doKey("down");
         doKey("return");
         checkForm("30"); // default (midway between minimum (0) and maximum (64)) - step
 
+        addEntry("field1", "value1");
+        break;
+
+    case 408:
+        input = $_(1, "field1");
         // Go to test 500.
-        fh.addEntry("field1", "value1");
-        input = $_(1, "field1");
         testNum = 499;
 
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     // Check that the input event is fired.
     case 500:
       input.addEventListener("input", function(event) {
         input.removeEventListener("input", arguments.callee, false);
-        ok(true, "oninput should have been received");
-        ok(event.bubbles, "input event should bubble");
-        ok(event.cancelable, "input event should be cancelable");
+        ok(true, testNum + " oninput should have been received");
+        ok(event.bubbles, testNum + " input event should bubble");
+        ok(event.cancelable, testNum + " input event should be cancelable");
       }, false);
 
       doKey("down");
       checkForm("");
       doKey("return");
       checkForm("value1");
       testNum = 599;
+      setTimeout(runTest, 100);
       break;
 
     case 600:
         // check we don't show autocomplete for searchbar-history
         input = $_(13, "searchbar-history");
 
         // Trigger autocomplete popup
         checkForm("");
         restoreForm();
         doKey("down");
+        waitForMenuChange(0);
         break;
 
     case 601:
-        checkMenuEntries([]);
+        checkMenuEntries([], testNum);
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
         SimpleTest.finish();
         return;
 
     default:
         ok(false, "Unexpected invocation of test #" + testNum);
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
         SimpleTest.finish();
         return;
   }
+}
 
-  setTimeout(runTest, 50, testNum + 1); // XXX 40ms was too slow, why?
+function addEntry(name, value)
+{
+  updateFormHistory({ op : "add", fieldname : name, value: value }, runTest);
+}
+
+// Runs the next test when scroll event occurs
+function waitForScroll()
+{
+  addEventListener("scroll", function() {
+    if (!window.pageYOffset)
+      return;
+
+    removeEventListener("scroll", arguments.callee, false);
+    setTimeout(runTest, 50);
+  }, false);
 }
 
-function checkMenuEntries(expectedValues) {
+function waitForMenuChange(expectedCount, expectedFirstValue)
+{
+  if (autocompleteMenu.tree.view.rowCount != expectedCount) {
+    SimpleTest.executeSoon(function () waitForMenuChange(expectedCount, expectedFirstValue));
+  }
+  else if (expectedFirstValue && autocompleteMenu.tree.view.rowCount > 1 &&
+           autocompleteMenu.tree.view.getValueAt(0, autocompleteMenu.tree.columns[0]) != expectedFirstValue) {
+    SimpleTest.executeSoon(function () waitForMenuChange(expectedCount, expectedFirstValue));
+  }
+  else {
+    runTest();
+  }
+}
+
+function checkMenuEntries(expectedValues, testNum) {
     var actualValues = getMenuEntries();
-    is(actualValues.length, expectedValues.length, "Checking length of expected menu");
+    is(actualValues.length, expectedValues.length, testNum + " Checking length of expected menu");
     for (var i = 0; i < expectedValues.length; i++)
-        is(actualValues[i], expectedValues[i], "Checking menu entry #"+i);
+        is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #"+i);
 }
 
 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() {
-    runTest(1);
+    setupFormHistory(function() {
+        runTest();
+    });
 }
 
 window.onload = startTest;
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
--- a/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
@@ -39,29 +39,26 @@ Form History test: form field autocomple
   </datalist>
 </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.nsIDOMEvent.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", "historyvalue");
-fh.addEntry("field2", "othervalue");
+function setupFormHistory(aCallback) {
+  updateFormHistory([
+    { op : "remove" },
+    { op : "add", fieldname : "field1", value : "historyvalue" },
+    { op : "add", fieldname : "field2", value : "othervalue" },
+  ], aCallback);
+}
 
 function setForm(value) {
     input.value = value;
     input.focus();
 }
 
 var autocompleteMenu = getAutocompletePopup();
 
@@ -71,194 +68,240 @@ function restoreForm() {
 }
 
 // Check for expected form data.
 function checkForm(expectedValue) {
     var formID = input.parentNode.id;
     is(input.value, expectedValue, "Checking " + formID + " input");
 }
 
-function nextTest(aTestNum) {
-  setTimeout(runTest, 50, aTestNum + 1); // XXX 40ms was too slow, why?
+var testNum = 0;
+var prevValue;
+var expectingPopup = false;
+
+function expectPopup()
+{
+  info("expecting popup for test " + testNum);
+  expectingPopup = true;
 }
 
+function popupShownListener()
+{
+  info("popup shown for test " + testNum);
+  if (expectingPopup) {
+    expectingPopup = false;
+    SimpleTest.executeSoon(runTest);
+  }
+  else {
+    ok(false, "Autocomplete popup not expected during test " + testNum);
+  }
+}
+
+SpecialPowers.addAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
+
 /*
 * Main section of test...
 *
-* This is a bit hacky, because the events are either being sent or
-* processed asynchronously, so we need to interrupt our flow with lots of
-* setTimeout() calls. The case statements are executed in order, one per
-* timeout.
+* This is a bit hacky, as many operations happen asynchronously.
+* Various mechanisms call runTests as a result of operations:
+*   - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
+*   - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it 
 */
-function runTest(testNum) {
+function runTest() {
+    testNum++;
+
     // Seems we need to enable this again, or sendKeyEvent() complaints.
     netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
     info("Starting test #" + testNum);
 
     switch(testNum) {
     case 1:
         // Make sure initial form is empty.
         checkForm("");
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
-
     case 2:
-        checkMenuEntries(["historyvalue", "PASS1", "PASS2", "final"]);
+        checkMenuEntries(["historyvalue", "PASS1", "PASS2", "final"], testNum);
         // Check first entry
         doKey("down");
         checkForm(""); // value shouldn't update
         doKey("return"); // not "enter"!
         checkForm("historyvalue");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 3:
         // Check second entry
         doKey("down");
         doKey("down");
         doKey("return"); // not "enter"!
         checkForm("Google");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 4:
         // Check third entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("Reddit");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 5:
         // Check fourth entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("final");
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
         
     case 6:
         //Delete the first entry (of 3)
         doKey("down");
         doKey("delete", shiftModifier);
+        waitForMenuChange(3);
+        break;
+
+    case 7:
         checkForm("");
-        ok(!fh.entryExists("field1", "historyvalue"), "checking that form history value was deleted");
+        countEntries("field1", "historyvalue",
+          function (num) {
+            ok(!num, testNum + " checking that form history value was deleted");
+            runTest();
+          });
+        break;
+
+    case 8:
         doKey("return");
         checkForm("Google")
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
-    case 7:
+
+    case 9:
         //Test deletion
-        checkMenuEntries(["PASS1", "PASS2", "final"]);
+        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
         // Check the new first entry (of 3)
         doKey("down");
         doKey("return");
         checkForm("Google");
 
         // Trigger autocomplete popup
         // Look at form 3, try to trigger autocomplete popup
         input.value = "";
         input = $_(3, "field2");
         testNum = 99;
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 100:
-        checkMenuEntries(["PASS1", "PASS2", "final"]);
+        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
         // Check first entry
         doKey("down");
         checkForm(""); // value shouldn't update
         doKey("return"); // not "enter"!
         checkForm("Google");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 101:
         // Check second entry
         doKey("down");
         doKey("down");
         doKey("return"); // not "enter"!
         checkForm("Reddit");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 102:
         // Check third entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("final");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 103:
-        checkMenuEntries(["PASS1", "PASS2", "final"]);
+        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
         // Check first entry
         doKey("down");
         checkForm(""); // value shouldn't update
         doKey("return"); // not "enter"!
         checkForm("Google");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 104:
         // Check second entry
         doKey("down");
         doKey("down");
         doKey("return"); // not "enter"!
         checkForm("Reddit");
 
         // Trigger autocomplete popup
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     case 105:
         // Check third entry
         doKey("down");
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("final");
 
         testNum = 199;
+        expectPopup();
         restoreForm();
         doKey("down");
         break;
 
     // Test dynamic updates.
     // For some reasons, when there is an update of the list, the selection is
     // lost so we need to go down like if we were at the beginning of the list
     // again.
@@ -273,21 +316,21 @@ function runTest(testNum) {
       SimpleTest.executeSoon(function() {
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("final");
 
         // Restore the element.
         datalist.insertBefore(toRemove, datalist.children[1]);
+        expectPopup();
         restoreForm();
         doKey("down");
-        nextTest(testNum);
       });
-      return;
+      break;
 
     case 201:
       // Adding an attribute after the first one while on the first then going
       // down and push enter. Value should be the on from the new suggestion.
       doKey("down");
       var datalist = document.getElementById('suggest');
       var added = Option("Foo");
       datalist.insertBefore(added, datalist.children[1]);
@@ -295,156 +338,169 @@ function runTest(testNum) {
       SimpleTest.executeSoon(function() {
         doKey("down");
         doKey("down");
         doKey("return");
         checkForm("Foo");
 
         // Remove the element.
         datalist.removeChild(added);
+        expectPopup();
         restoreForm();
         doKey("down");
-        nextTest(testNum);
       });
-      return;
+      break;
 
     case 202:
       // Change the first element value attribute.
       doKey("down");
       var datalist = document.getElementById('suggest');
-      var prevValue = datalist.children[0].value;
+      prevValue = datalist.children[0].value;
       datalist.children[0].value = "foo";
-
-      SimpleTest.executeSoon(function() {
-        doKey("down");
-        doKey("return");
-        checkForm("foo");
-
-        datalist.children[0].value = prevValue;
-        restoreForm();
-        doKey("down");
-        nextTest(testNum);
-      });
-      return;
+      expectPopup();
+      break;
 
     case 203:
+      doKey("down");
+      doKey("return");
+      checkForm("foo");
+
+      var datalist = document.getElementById('suggest');
+      datalist.children[0].value = prevValue;
+      expectPopup();
+      restoreForm();
+      doKey("down");
+      break;
+
+    case 204:
       // Change the textContent to update the value attribute.
       doKey("down");
       var datalist = document.getElementById('suggest');
-      var prevValue = datalist.children[0].getAttribute('value');
+      prevValue = datalist.children[0].getAttribute('value');
       datalist.children[0].removeAttribute('value');
       datalist.children[0].textContent = "foobar";
+      expectPopup();
+      break;
 
-      SimpleTest.executeSoon(function() {
-        doKey("down");
-        doKey("return");
-        checkForm("foobar");
+    case 205:
+      doKey("down");
+      doKey("return");
+      checkForm("foobar");
 
-        datalist.children[0].setAttribute('value', prevValue);
-        testNum = 299;
-        restoreForm();
-        doKey("down");
-        nextTest(testNum);
-      });
-      return;
+      var datalist = document.getElementById('suggest');
+      datalist.children[0].setAttribute('value', prevValue);
+      testNum = 299;
+      expectPopup();
+      restoreForm();
+      doKey("down");
+      break;
 
     // Tests for filtering (or not).
     case 300:
       // Filters with first letter of the word.
       synthesizeKey("f", {});
-      setTimeout(function() {
-        doKey("down");
-        doKey("return");
-        checkForm("final");
-        restoreForm();
-        doKey("down");
-        nextTest(testNum);
-      }, 500);
-      return;
+      expectPopup();
       break;
 
     case 301:
-      // Filter with a leterr in the middle of the word.
+      doKey("down");
+      doKey("return");
+      checkForm("final");
+      expectPopup();
+      restoreForm();
+      doKey("down");
+      break;
+
+    case 302:
+      // Filter with a letter in the middle of the word.
       synthesizeKey("i", {});
       synthesizeKey("n", {});
       setTimeout(function() {
         doKey("down");
         doKey("return");
         checkForm("final");
+        expectPopup();
         restoreForm();
         doKey("down");
-        nextTest(testNum);
       }, 500);
-      return;
       break;
 
-    case 302:
+    case 303:
       // Filter is disabled with mozNoFilter.
       input.setAttribute('mozNoFilter', 'true');
       synthesizeKey("f", {});
       setTimeout(function() {
         doKey("down");
         doKey("return");
         checkForm("Google");
         input.removeAttribute('mozNoFilter');
+        testNum = 399;
+        expectPopup();
         restoreForm();
         doKey("down");
-        testNum = 399;
-        nextTest(testNum);
       }, 500);
-      return;
       break;
 
     case 400:
       // Check that the input event is fired.
       input.addEventListener("input", function(event) {
         input.removeEventListener("input", arguments.callee, false);
         ok(true, "oninput should have been received");
         ok(event.bubbles, "input event should bubble");
         ok(event.cancelable, "input event should be cancelable");
         checkForm("Google");
+        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
         SimpleTest.finish();
       }, false);
 
       doKey("down");
       checkForm("");
       doKey("return");
-      return;
+      break;
 
     default:
-        ok(false, "Unexpected invocation of test #" + testNum);
-        SimpleTest.finish();
-        return;
+      ok(false, "Unexpected invocation of test #" + testNum);
+      SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
+      SimpleTest.finish();
+      return;
     }
-
-    nextTest(testNum);
 }
 
-function checkMenuEntries(expectedValues) {
+function waitForMenuChange(expectedCount)
+{
+  if (autocompleteMenu.tree.view.rowCount != expectedCount) {
+    SimpleTest.executeSoon(function () waitForMenuChange(expectedCount));
+  }
+  else {
+    runTest();
+  }
+}
+
+function checkMenuEntries(expectedValues, testNum) {
     var actualValues = getMenuEntries();
-    is(actualValues.length, expectedValues.length, "Checking length of expected menu");
+    is(actualValues.length, expectedValues.length, testNum + " Checking length of expected menu");
     for (var i = 0; i < expectedValues.length; i++)
-        is(actualValues[i], expectedValues[i], "Checking menu entry #"+i);
+        is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #"+i);
 }
 
 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() {
-    runTest(1);
+    setupFormHistory(runTest);
 }
 
 window.onload = startTest;
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
--- a/toolkit/components/satchel/test/test_form_submission.html
+++ b/toolkit/components/satchel/test/test_form_submission.html
@@ -288,20 +288,25 @@ var ccNumbers = {
     "9946177098017064", "4081194386488872",
     "3095975979578034", "3662215692222536",
     "6723210018630429", "4411962856225025",
     "8276996369036686", "4449796938248871",
     "3350852696538147", "5011802870046957"
   ],
 };
 
+function checkInitialState() {
+  countEntries(null, null,
+    function (num) {
+      ok(!num, "checking for initially empty storage");
+      startTest();
+    });
+}
+
 function startTest() {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  ok(!fh.hasEntries, "checking for initially empty storage");
-
   // Fill in values for the various fields. We could just set the <input>'s
   // value attribute, but we don't save default form values (and we want to
   // ensure unsaved values are because of autocomplete=off or whatever).
   $_(1, "test1").value = "dontSaveThis";
   $_(2, "test1").value = "dontSaveThis";
   $_(3, "test1").value = "dontSaveThis";
   $_(4, "test1").value = "dontSaveThis";
   $_(5, "test1").value = "";
@@ -364,17 +369,16 @@ function startTest() {
 
 // Called by each form's onsubmit handler.
 function checkSubmit(formNum) {
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
   ok(true, "form " + formNum + " submitted");
   numSubmittedForms++;
 
-
   // Check for expected storage state.
   switch (formNum) {
     // Test 1-24 should not save anything.
     case 1:
     case 2:
     case 3:
     case 4:
     case 5:
@@ -392,17 +396,22 @@ function checkSubmit(formNum) {
     case 17:
     case 18:
     case 19:
     case 20:
     case 21:
     case 22:
     case 23:
     case 24:
-        ok(!fh.hasEntries, "checking for empty storage");
+      countEntries(null, null,
+        function (num) {
+          ok(!num, "checking for empty storage");
+          submitForm(formNum);
+        });
+        return false;
         break;
     case 100:
         checkForSave("subtest2", "subtestValue", "checking saved subtest value");
         break;
     case 101:
         checkForSave("test1", "savedValue", "checking saved value");
         break;
     case 102:
@@ -436,50 +445,55 @@ function checkSubmit(formNum) {
     case 110:
         checkForSave("test10", "savedValue", "checking saved value");
         break;
     default:
         ok(false, "Unexpected form submission");
         break;
   }
 
+  return submitForm(formNum);
+}
+
+function submitForm(formNum)
+{
   // Forms 13 and 14 would trigger a save-password notification. Temporarily
   // disable pwmgr, then reenable it.
   if (formNum == 12)
-      prefBranch.setBoolPref("signon.rememberSignons", false);
+      SpecialPowers.setBoolPref("signon.rememberSignons", false);
   if (formNum == 14)
-      prefBranch.clearUserPref("signon.rememberSignons");
+      SpecialPowers.clearUserPref("signon.rememberSignons");
 
   // Forms 20 and 21 requires browser.formfill.saveHttpsForms to be false
   if (formNum == 19)
-    prefBranch.setBoolPref("browser.formfill.saveHttpsForms", false);
+    SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", false);
   // Reset preference now that 20 and 21 are over
   if (formNum == 21)
-    prefBranch.clearUserPref("browser.formfill.saveHttpsForms");
+    SpecialPowers.clearUserPref("browser.formfill.saveHttpsForms");
 
   // End the test now on SeaMonkey.
   if (formNum == 21 && navigator.userAgent.match(/ SeaMonkey\//)) {
     Services.obs.removeObserver(checkObserver, "satchel-storage-changed");
     is(numSubmittedForms, 21, "Ensuring all forms were submitted.");
 
     todo(false, "Skipping remaining checks on SeaMonkey ftb. (Bug 589471)");
     // finish(), yet let the test actually end first, to be safe.
     SimpleTest.executeSoon(SimpleTest.finish);
 
     return false; // return false to cancel current form submission
   }
 
   // Form 109 requires browser.formfill.save_https_forms to be true;
   // Form 110 requires it to be false.
   if (formNum == 108)
-    prefBranch.setBoolPref("browser.formfill.saveHttpsForms", true);
+    SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", true);
   if (formNum == 109)
-    prefBranch.setBoolPref("browser.formfill.saveHttpsForms", false);
+    SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", false);
   if (formNum == 110)
-    prefBranch.clearUserPref("browser.formfill.saveHttpsForms");
+    SpecialPowers.clearUserPref("browser.formfill.saveHttpsForms");
 
   // End the test at the last form.
   if (formNum == 110) {
     is(numSubmittedForms, 35, "Ensuring all forms were submitted.");
     Services.obs.removeObserver(checkObserver, "satchel-storage-changed");
     SimpleTest.finish();
     return false; // return false to cancel current form submission
   }
@@ -489,18 +503,16 @@ function checkSubmit(formNum) {
   // chain of submits will happen recursively since the submit is dispatched
   // immediately.
   //
   // This in itself is fine, but if there are errors in the code, mochitests
   // will in some cases give you "server too busy", which is hard to debug!
   //
   setTimeout(function() {
     checkObserver.waitForChecks(function() {
-      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
       var nextFormNum = formNum == 24 ? 100 : (formNum + 1);
 
       // Submit the next form. Special cases are Forms 21 and 100, which happen
       // from an HTTPS domain in an iframe.
       if (nextFormNum == 21 || nextFormNum == 100) {
         ok(true, "submitting iframe test " + nextFormNum);
         document.getElementById("iframe").contentWindow.clickButton(nextFormNum);
       }
@@ -509,27 +521,18 @@ function checkSubmit(formNum) {
         button.click();
       }
     });
   }, 0);
 
   return false; // cancel current form submission
 }
 
-netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
-var fh = Components.classes["@mozilla.org/satchel/form-history;1"].
-         getService(Components.interfaces.nsIFormHistory2);
-ok(fh != null, "Got formHistory service");
-
-var prefBranch = Components.classes["@mozilla.org/preferences-service;1"].
-                 getService(Components.interfaces.nsIPrefBranch);
-
 Services.obs.addObserver(checkObserver, "satchel-storage-changed", false);
 
-window.onload = startTest;
+window.onload = checkInitialState;
 
 SimpleTest.waitForExplicitFinish();
 
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/components/satchel/test/test_form_submission_cap.html
+++ b/toolkit/components/satchel/test/test_form_submission_cap.html
@@ -23,20 +23,25 @@
    Generate numInputFields = MAX_FIELDS_SAVED + 1 fields, change all values,
    and test that only MAX_FIELDS_SAVED are actually saved and that
    field # numInputFields was not saved.
 */
 
 var numSubmittedForms = 0;
 var numInputFields = 101;
 
+function checkInitialState() {
+  countEntries(null, null,
+    function (num) {
+      ok(!num, "checking for initially empty storage");
+      startTest();
+    });
+}
+
 function startTest() {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  ok(!fh.hasEntries, "checking for initially empty storage");
-
   var form = document.getElementById("form1");
   for (i = 1; i <= numInputFields; i++) {
     var newField = document.createElement("input");
     newField.setAttribute("type", "text");
     newField.setAttribute("name", "test" + i);
     form.appendChild(newField);
   }
 
@@ -50,52 +55,31 @@ function startTest() {
   // submit the first form.
   var button = getFormSubmitButton(1);
   button.click();
 }
 
 
 // Called by each form's onsubmit handler.
 function checkSubmit(formNum) {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
   ok(true, "form " + formNum + " submitted");
   numSubmittedForms++;
 
   // check that the first (numInputFields - 1) CHANGED fields are saved
   for (i = 1; i < numInputFields; i++) { // check all but last
     checkForSave("test" + i, i, "checking saved value " + i);
   }
 
-  // End the test at the last form.
-  if (formNum == 1) {
-    is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
-    Services.obs.removeObserver(checkObserver, "satchel-storage-changed");
-    SimpleTest.finish();
-    return false; // return false to cancel current form submission
-  }
-
-  checkObserver.waitForChecks(function() {
-    // submit the next form.
-    var button = getFormSubmitButton(formNum + 1);
-    button.click();
-  });
-
-  return false; // cancel current form submission
+  // End the test.
+  is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
+  SimpleTest.finish();
+  return false; // return false to cancel current form submission
 }
 
 
-netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
-var fh = Components.classes["@mozilla.org/satchel/form-history;1"].
-         getService(Components.interfaces.nsIFormHistory2);
-ok(fh != null, "Got formHistory service");
-
-window.onload = startTest;
-
-Services.obs.addObserver(checkObserver, "satchel-storage-changed", false);
+window.onload = checkInitialState;
 
 SimpleTest.waitForExplicitFinish();
 
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/components/satchel/test/test_form_submission_cap2.html
+++ b/toolkit/components/satchel/test/test_form_submission_cap2.html
@@ -121,73 +121,70 @@
 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 var numSubmittedForms = 0;
 var numInputFields = 101;
 
+function checkInitialState() {
+  countEntries(null, null,
+    function (num) {
+      ok(!num, "checking for initially empty storage");
+      startTest();
+    });
+}
+
 function startTest() {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  ok(!fh.hasEntries, "checking for initially empty storage");
-
   // Fill in values for the various fields. We could just set the <input>'s
   // value attribute, but we don't save default form values (and we want to
   // ensure unsaved values are because of autocomplete=off or whatever).
   $_(1, "test" + numInputFields).value = numInputFields + " changed";
 
   // submit the first form.
   var button = getFormSubmitButton(1);
   button.click();
 }
 
+// Make sure that the first (numInputFields - 1) were not saved (as they were not changed).
+// Call done() when finished.
+function checkCountEntries(formNum, index, done)
+{
+  countEntries("test" + index, index,
+    function (num) {
+      ok(!num, "checking unsaved value " + index);
+      if (index < numInputFields) {
+        checkCountEntries(formNum, index + 1, done);
+      }
+      else {
+        done(formNum);
+      }
+    });
+}
 
 // Called by each form's onsubmit handler.
 function checkSubmit(formNum) {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
   ok(true, "form " + formNum + " submitted");
   numSubmittedForms++;
 
-  // make sure that the first (numInputFields - 1) were not saved (as they were not changed)
-  for (i = 1; i < numInputFields; i++) {
-    ok(!fh.entryExists("test" + i, i), "checking unsaved value " + i);
-  }
-
   // make sure that the field # numInputFields was saved
   checkForSave("test" + numInputFields, numInputFields + " changed", "checking saved value " + numInputFields);
 
-
-  // End the test at the last form.
-  if (formNum == 1) {
-    is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
-    Services.obs.removeObserver(checkObserver, "satchel-storage-changed");
-    SimpleTest.finish();
-    return false; // return false to cancel current form submission
-  }
-
-  checkObserver.waitForChecks(function() {
-    // submit the next form.
-    var button = getFormSubmitButton(formNum + 1);
-    button.click();
-  });
+  checkCountEntries(formNum, 1, checkSubmitCounted);
 
   return false; // cancel current form submission
 }
 
-
-netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+function checkSubmitCounted(formNum) {
+  is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
+  SimpleTest.finish();
+  return false;
+}
 
-var fh = Components.classes["@mozilla.org/satchel/form-history;1"].
-         getService(Components.interfaces.nsIFormHistory2);
-ok(fh != null, "Got formHistory service");
-
-window.onload = startTest;
-
-Services.obs.addObserver(checkObserver, "satchel-storage-changed", false);
+window.onload = checkInitialState;
 
 SimpleTest.waitForExplicitFinish();
 
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..07b43c20960daf506467364e7b04a8016eec2ffa
GIT binary patch
literal 98304
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
E4|aI^pa1{>
index 43aa5c835b7ad9eec68b182cf63f7a18eecb7ae4..00daf03c27e7957cefc872d28e46b1058aa63a0c
GIT binary patch
literal 5120
zc%1E5O=uHA6rP#4*-b>Tq7aIRd#aWgiB%JNbCaxTOiW_3N!ofSTf1qu`7_CGZM{hG
z?#Zio@Zir)ya+<YlL%e}5yXSwL5d#5yE9F@O*Y$lQ7W<@Oy>RWdvAWiyuy{VZfIhq
z*{-XGcouO8V{}0j5kf<Bc+fTkI!F(X=>W7lhEVX!u><4<;_w85LnMJ;QQ|+u4UgcL
z0(kYH-k8&t>&=@pY)KdNj?rwd%*>nm+>DdP6U)nzB8$mPT%Htnb`!H1yH-5O)T67w
z3H~IFeB3c|P3;(FM{|x2yGHMKb<BX76sgH*8w7u3$MT(o+^(^A4q4H)+FV1eYom14
zOMiYNS8G`C2Lkx^K|`IbX}0(_mTVTGNYS)Bn5#ZV%TwfpoELNX<Yg&8C8p%5Q=(Oa
zsK}Fwn90&nOs7wYOKQy=5E^=2>#$X}KDMo^=<V(fIOw)E$3ewT&PJHg1LQOEk>iLw
zgx7F~G@%5iNdkB(a^Ygc=S9PZF(E;A3WM=Vvx$Xw!n<<56;8{lDTTty{B%Sf5&G<+
z)Q-S9ms*L$GuIO58<&gaP}3Y+40F`(DCYSfJO07g#(@vUFygL$O7362zcpV!pI6M-
z<aM)HK3B|*m%_NuPwM(f1Ae)s=u{|MZA?@P>SCZQ(-KZNS2omFrnfKs7%P)Yaj8r$
z8*dbri|J~@n7*hLM)*EEiP^zAtWQTxeSAVzre&>^Q_W1+)0b|%n{Ip{-Gnh~l(JLv
z3)OnLl~CIepnk_Jzi8JlIyT^VW$98QRh~7k$@NyN5~@UqqvM$Y$CXqo-YO@wOsH%w
z+)OF^X!SmLfZ#big(vU;e!y4w2yfx#-#=U~T)6lxcp&sY*T*I<<?T)an_!*a54?57
zJ-rOq%RnzfdKuik|Mw95gm3T}-oqPMgJ<xFid?vGv6t8f!?^qI@>yieYmrgGA|;-Y
z-N%N<B4eCIMq!&I7AbAs{|f-$>CYP6g%7X@x1b1*_quo&F8+NSfgrYb30@|&cRm6W
n+WQ2a3GKZO$AtC=K5Tr`_IM{uXm5G&W>)qV&BuiHhQsp<z~j$7
deleted file mode 100644
index e07279718c7fff64becfcc0b95cbafdeb0fceb9a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 1aa6c60c6bc0b9c85b956ff2f3637ca28b2c9bcc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index bfc28b55c3172e75fe83c307d0e5f0be1df11d01..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 44021be414b5edc1714532b799a95d4e3c1b9802..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c42be66eea9af6cd1394c4096575e5e17d62cffc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index e143a4d525e310952b787a4766af89104744c7a3..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index a6528d5e2a5c842ec4f7f407f29ec5c65e0c3f65..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/toolkit/components/satchel/test/unit/head_satchel.js
+++ b/toolkit/components/satchel/test/unit/head_satchel.js
@@ -1,34 +1,102 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/FormHistory.jsm");
+
 const Ci = Components.interfaces;
 const Cc = Components.classes;
+const Cu = Components.utils;
 
 const CURRENT_SCHEMA = 4;
 const PR_HOURS = 60 * 60 * 1000000;
 
 do_get_profile();
 
 var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
              getService(Ci.nsIProperties);
 
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+                         getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
 function getDBVersion(dbfile) {
     var ss = Cc["@mozilla.org/storage/service;1"].
              getService(Ci.mozIStorageService);
     var dbConnection = ss.openDatabase(dbfile);
     var version = dbConnection.schemaVersion;
     dbConnection.close();
 
     return version;
 }
 
 const isGUID = /[A-Za-z0-9\+\/]{16}/;
 
-function getGUIDforID(conn, id) {
-    var stmt = conn.createStatement("SELECT guid from moz_formhistory WHERE id = " + id);
-    stmt.executeStep();
-    var guid = stmt.getString(0);
-    stmt.finalize();
-    return guid;
+// Find form history entries.
+function searchEntries(terms, params, iter) {
+  let results = [];
+  FormHistory.search(terms, params, { handleResult: function (result) results.push(result),
+                                      handleError: function (error) {
+                                        do_throw("Error occurred searching form history: " + error);
+                                      },
+                                      handleCompletion: function (reason) { if (!reason) iter.send(results); }
+                                    });
+}
+
+// Count the number of entries with the given name and value, and call then(number)
+// when done. If name or value is null, then the value of that field does not matter.
+function countEntries(name, value, then) {
+  var obj = {};
+  if (name !== null)
+    obj.fieldname = name;
+  if (value !== null)
+    obj.value = value;
+
+  let count = 0;
+  FormHistory.count(obj, { handleResult: function (result) count = result,
+                           handleError: function (error) {
+                             do_throw("Error occurred searching form history: " + error);
+                           },
+                           handleCompletion: function (reason) { if (!reason) then(count); }
+                         });
 }
+
+// Perform a single form history update and call then() when done.
+function updateEntry(op, name, value, then) {
+  var obj = { op: op };
+  if (name !== null)
+    obj.fieldname = name;
+  if (value !== null)
+    obj.value = value;
+  updateFormHistory(obj, then);
+}
+
+// Add a single form history entry with the current time and call then() when done.
+function addEntry(name, value, then) {
+  let now = Date.now() * 1000;
+  updateFormHistory({ op: "add", fieldname: name, value: value, timesUsed: 1,
+                      firstUsed: now, lastUsed: now }, then);
+}
+
+// Wrapper around FormHistory.update which handles errors. Calls then() when done.
+function updateFormHistory(changes, then) {
+  FormHistory.update(changes, { handleError: function (error) {
+                                  do_throw("Error occurred updating form history: " + error);
+                                },
+                                handleCompletion: function (reason) { if (!reason) then(); },
+                              });
+}
+
+/**
+ * Logs info to the console in the standard way (includes the filename).
+ *
+ * @param aMessage
+ *        The message to log to the console.
+ */
+function do_log_info(aMessage) {
+  print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_async_expire.js
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var dbFile, oldSize;
+var currentTestIndex = 0;
+
+function triggerExpiration() {
+  // We can't easily fake a "daily idle" event, so for testing purposes form
+  // history listens for another notification to trigger an immediate
+  // expiration.
+  Services.obs.notifyObservers(null, "formhistory-expire-now", null);
+}
+
+let checkExists = function(num) { do_check_true(num > 0); next_test(); }
+let checkNotExists = function(num) { do_check_true(!num); next_test(); }
+
+var TestObserver = {
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+  observe : function (subject, topic, data) {
+    do_check_eq(topic, "satchel-storage-changed");
+
+    if (data == "formhistory-expireoldentries") {
+      next_test();
+    }
+  }
+};
+
+function test_finished() {
+  // Make sure we always reset prefs.
+  if (Services.prefs.prefHasUserValue("browser.formfill.expire_days"))
+    Services.prefs.clearUserPref("browser.formfill.expire_days");
+
+  do_test_finished();
+}
+
+let iter = tests();
+
+function run_test()
+{
+  do_test_pending();
+  iter.next();
+}
+
+function next_test()
+{
+  iter.next();
+}
+
+function tests()
+{
+  Services.obs.addObserver(TestObserver, "satchel-storage-changed", true);
+
+  // ===== test init =====
+  var testfile = do_get_file("asyncformhistory_expire.sqlite");
+  var profileDir = do_get_profile();
+
+  // Cleanup from any previous tests or failures.
+  dbFile = profileDir.clone();
+  dbFile.append("formhistory.sqlite");
+  if (dbFile.exists())
+    dbFile.remove(false);
+
+  testfile.copyTo(profileDir, "formhistory.sqlite");
+  do_check_true(dbFile.exists());
+
+  // We're going to clear this at the end, so it better have the default value now.
+  do_check_false(Services.prefs.prefHasUserValue("browser.formfill.expire_days"));
+
+  // Sanity check initial state
+  yield countEntries(null, null, function(num) { do_check_eq(508, num); next_test(); });
+  yield countEntries("name-A", "value-A", checkExists); // lastUsed == distant past
+  yield countEntries("name-B", "value-B", checkExists); // lastUsed == distant future
+
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+  // Add a new entry
+  yield countEntries("name-C", "value-C", checkNotExists);
+  yield addEntry("name-C", "value-C", next_test);
+  yield countEntries("name-C", "value-C", checkExists);
+
+  // Update some existing entries to have ages relative to when the test runs.
+  var now = 1000 * Date.now();
+  let updateLastUsed = function updateLastUsedFn(results, age)
+  {
+    let lastUsed = now - age * 24 * PR_HOURS;
+
+    let changes = [ ];
+    for (let r = 0; r < results.length; r++) {
+      changes.push({ op: "update", lastUsed: lastUsed, guid: results[r].guid });
+    }
+
+    return changes;
+  }
+
+  let results = yield searchEntries(["guid"], { lastUsed: 181 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 181), next_test);
+
+  results = yield searchEntries(["guid"], { lastUsed: 179 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 179), next_test);
+
+  results = yield searchEntries(["guid"], { lastUsed: 31 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 31), next_test);
+
+  results = yield searchEntries(["guid"], { lastUsed: 29 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 29), next_test);
+
+  results = yield searchEntries(["guid"], { lastUsed: 9999 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 11), next_test);
+
+  results = yield searchEntries(["guid"], { lastUsed: 9 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 9), next_test);
+
+  yield countEntries("name-A", "value-A", checkExists);
+  yield countEntries("181DaysOld", "foo", checkExists);
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(509, num); next_test(); });
+
+  // 2 entries are expected to expire.
+  triggerExpiration();
+  yield;
+
+  yield countEntries("name-A", "value-A", checkNotExists);
+  yield countEntries("181DaysOld", "foo", checkNotExists);
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+  // And again. No change expected.
+  triggerExpiration();
+  yield;
+
+  yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+  // Set formfill pref to 30 days.
+  Services.prefs.setIntPref("browser.formfill.expire_days", 30);
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries("bar", "31days", checkExists);
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+  triggerExpiration();
+  yield;
+
+  yield countEntries("179DaysOld", "foo", checkNotExists);
+  yield countEntries("bar", "31days", checkNotExists);
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+  // Set override pref to 10 days and expire. This expires a large batch of
+  // entries, and should trigger a VACCUM to reduce file size.
+  Services.prefs.setIntPref("browser.formfill.expire_days", 10);
+
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries("9DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+  triggerExpiration();
+  yield;
+
+  yield countEntries("bar", "29days", checkNotExists);
+  yield countEntries("9DaysOld", "foo", checkExists);
+  yield countEntries("name-B", "value-B", checkExists);
+  yield countEntries("name-C", "value-C", checkExists);
+  yield countEntries(null, null, function(num) { do_check_eq(3, num); next_test(); });
+
+  test_finished();
+};
--- a/toolkit/components/satchel/test/unit/test_autocomplete.js
+++ b/toolkit/components/satchel/test/unit/test_autocomplete.js
@@ -1,236 +1,237 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var testnum = 0;
-var fh;
 var fac;
 var prefs;
 
-const DEFAULT_EXPIRE_DAYS = 180;
+let numRecords, timeGroupingSize, now;
 
-function countAllEntries() {
-    let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory");
-    do_check_true(stmt.executeStep());
-    let numEntries = stmt.row.numEntries;
-    stmt.finalize();
-    return numEntries;
-}
+const DEFAULT_EXPIRE_DAYS = 180;
 
 function padLeft(number, length) {
     var str = number + '';
     while (str.length < length)
         str = '0' + str;
     return str;
 }
 
-function getFormExpiryDays () {
+function getFormExpiryDays() {
     if (prefs.prefHasUserValue("browser.formfill.expire_days"))
         return prefs.getIntPref("browser.formfill.expire_days");
     else
         return DEFAULT_EXPIRE_DAYS;
 }
 
 function run_test() {
-    try {
+    // ===== test init =====
+    var testfile = do_get_file("formhistory_autocomplete.sqlite");
+    var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
-        // ===== test init =====
-        var testfile = do_get_file("formhistory_autocomplete.sqlite");
-        var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+    // Cleanup from any previous tests or failures.
+    var destFile = profileDir.clone();
+    destFile.append("formhistory.sqlite");
+    if (destFile.exists())
+      destFile.remove(false);
 
-        // Cleanup from any previous tests or failures.
-        var destFile = profileDir.clone();
-        destFile.append("formhistory.sqlite");
-        if (destFile.exists())
-          destFile.remove(false);
+    testfile.copyTo(profileDir, "formhistory.sqlite");
 
-        testfile.copyTo(profileDir, "formhistory.sqlite");
+    fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].
+          getService(Ci.nsIFormAutoComplete);
+    prefs = Cc["@mozilla.org/preferences-service;1"].
+            getService(Ci.nsIPrefBranch);
 
-        fh = Cc["@mozilla.org/satchel/form-history;1"].
-             getService(Ci.nsIFormHistory2);
-        fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].
-              getService(Ci.nsIFormAutoComplete);
-        prefs = Cc["@mozilla.org/preferences-service;1"].
-                getService(Ci.nsIPrefBranch);
+    timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
+
+    run_next_test();
+}
 
-        var timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
-        var maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
-        var bucketSize = prefs.getIntPref("browser.formfill.bucketSize");
+add_test(function test0() {
+    var maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
+    var bucketSize = prefs.getIntPref("browser.formfill.bucketSize");
 
-        // ===== Tests with constant timesUsed and varying lastUsed date =====
-        // insert 2 records per bucket to check alphabetical sort within
-        var now = 1000 * Date.now();
-        var numRecords = Math.ceil(maxTimeGroupings / bucketSize) * 2;
+    // ===== Tests with constant timesUsed and varying lastUsed date =====
+    // insert 2 records per bucket to check alphabetical sort within
+    now = 1000 * Date.now();
+    numRecords = Math.ceil(maxTimeGroupings / bucketSize) * 2;
 
-        fh.DBConnection.beginTransaction();
-        for (let i = 0; i < numRecords; i+=2) {
-            let useDate = now - (i/2 * bucketSize * timeGroupingSize);
+    let changes = [ ];
+    for (let i = 0; i < numRecords; i+=2) {
+        let useDate = now - (i/2 * bucketSize * timeGroupingSize);
 
-            fh.DBConnection.executeSimpleSQL(
-              "INSERT INTO moz_formhistory "+
-              "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-              "VALUES ("+
-                  "'field1', " +
-                  "'value" + padLeft(numRecords - 1 - i, 2) + "', " +
-                  "1, " +
-                  (useDate + 1) + ", " +
-                  (useDate + 1) +
-              ");");
+        changes.push({ op : "add", fieldname: "field1", value: "value" + padLeft(numRecords - 1 - i, 2),
+                       timesUsed: 1, firstUsed: useDate, lastUsed: useDate });
+        changes.push({ op : "add", fieldname: "field1", value: "value" + padLeft(numRecords - 2 - i, 2),
+                       timesUsed: 1, firstUsed: useDate, lastUsed: useDate });
+    }
+
+    updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test1() {
+    do_log_info("Check initial state is as expected");
 
-              fh.DBConnection.executeSimpleSQL(
-              "INSERT INTO moz_formhistory "+
-              "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-              "VALUES ("+
-                  "'field1', " +
-                  "'value" + padLeft(numRecords - 2 - i, 2) + "', " +
-                  "1, " +
-                  useDate + ", " +
-                  useDate +
-              ");");
+    countEntries(null, null, function (count) {
+      countEntries("field1", null, function (count) {
+        do_check_true(count > 0);
+        run_next_test();
+      });
+    });
+});
+
+add_test(function test2() {
+    do_log_info("Check search contains all entries");
+
+    fac.autoCompleteSearchAsync("field1", "", null, null, {
+        onSearchCompletion : function(aResults) {
+            do_check_eq(numRecords, aResults.matchCount);
+            run_next_test();
         }
-        fh.DBConnection.commitTransaction();
+    });
+});
 
-        // ===== 1 =====
-        // Check initial state is as expected
-        testnum++;
-        do_check_true(fh.hasEntries);
-        do_check_eq(numRecords, countAllEntries());
-        do_check_true(fh.nameExists("field1"));
+add_test(function test3() {
+    do_log_info("Check search result ordering with empty search term");
 
-        // ===== 2 =====
-        // Check search contains all entries
-        testnum++;
-        var results = fac.autoCompleteSearch("field1", "", null, null);
-        do_check_eq(numRecords, results.matchCount);
+    let lastFound = numRecords;
+    fac.autoCompleteSearchAsync("field1", "", null, null, {
+        onSearchCompletion : function(aResults) {
+            for (let i = 0; i < numRecords; i+=2) {
+                do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+                do_check_eq(parseInt(aResults.getValueAt(i).substr(5), 10), --lastFound);
+            }
+            run_next_test();
+        }
+    });
+});
+
+add_test(function test4() {
+    do_log_info("Check search result ordering with \"v\"");
 
-        // ===== 3 =====
-        // Check search result ordering with empty search term
-        testnum++;
-        results = fac.autoCompleteSearch("field1", "", null, null);
-        let lastFound = numRecords;
-        for (let i = 0; i < numRecords; i+=2) {
-            do_check_eq(parseInt(results.getValueAt(i + 1).substr(5), 10), --lastFound);
-            do_check_eq(parseInt(results.getValueAt(i).substr(5), 10), --lastFound);
+    let lastFound = numRecords;
+    fac.autoCompleteSearchAsync("field1", "v", null, null, {
+        onSearchCompletion : function(aResults) {
+            for (let i = 0; i < numRecords; i+=2) {
+                do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+                do_check_eq(parseInt(aResults.getValueAt(i).substr(5), 10), --lastFound);
+            }
+            run_next_test();
         }
+    });
+});
 
-        // ===== 4 =====
-        // Check search result ordering with "v"
-        testnum++;
-        results = fac.autoCompleteSearch("field1", "v", null, null);
-        lastFound = numRecords;
-        for (let i = 0; i < numRecords; i+=2) {
-            do_check_eq(parseInt(results.getValueAt(i + 1).substr(5), 10), --lastFound);
-            do_check_eq(parseInt(results.getValueAt(i).substr(5), 10), --lastFound);
-        }
+const timesUsedSamples = 20;
 
-        // ===== Tests with constant use dates and varying timesUsed =====
+add_test(function test5() {
+    do_log_info("Begin tests with constant use dates and varying timesUsed");
 
-        let timesUsedSamples = 20;
-        fh.DBConnection.beginTransaction();
-        for (let i = 0; i < timesUsedSamples; i++) {
-            let timesUsed = (timesUsedSamples - i);
-            fh.DBConnection.executeSimpleSQL(
-              "INSERT INTO moz_formhistory "+
-              "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-              "VALUES ("+
-                  "'field2', "+
-                  "'value" + (timesUsedSamples - 1 -  i) + "', " +
-                  timesUsed * timeGroupingSize + ", " +
-                  now + ", " +
-                  now +
-              ");");
+    let changes =  [];
+    for (let i = 0; i < timesUsedSamples; i++) {
+        let timesUsed = (timesUsedSamples - i);
+        let change = { op : "add", fieldname: "field2", value: "value" + (timesUsedSamples - 1 -  i),
+                       timesUsed: timesUsed * timeGroupingSize, firstUsed: now, lastUsed: now };
+        changes.push(change);
+    }
+    updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test6() {
+    do_log_info("Check search result ordering with empty search term");
+
+    let lastFound = timesUsedSamples;
+    fac.autoCompleteSearchAsync("field2", "", null, null, {
+        onSearchCompletion : function(aResults) {
+            for (let i = 0; i < timesUsedSamples; i++) {
+                do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --lastFound);
+            }
+            run_next_test();
         }
-        fh.DBConnection.commitTransaction();
+    });
+});
 
-        // ===== 5 =====
-        // Check search result ordering with empty search term
-        testnum++;
-        results = fac.autoCompleteSearch("field2", "", null, null);
-        lastFound = timesUsedSamples;
-        for (let i = 0; i < timesUsedSamples; i++) {
-            do_check_eq(parseInt(results.getValueAt(i).substr(5)), --lastFound);
-        }
+add_test(function test7() {
+    do_log_info("Check search result ordering with \"v\"");
 
-        // ===== 6 =====
-        // Check search result ordering with "v"
-        testnum++;
-        results = fac.autoCompleteSearch("field2", "v", null, null);
-        lastFound = timesUsedSamples;
-        for (let i = 0; i < timesUsedSamples; i++) {
-            do_check_eq(parseInt(results.getValueAt(i).substr(5)), --lastFound);
+    let lastFound = timesUsedSamples;
+    fac.autoCompleteSearchAsync("field2", "v", null, null, {
+        onSearchCompletion : function(aResults) {
+            for (let i = 0; i < timesUsedSamples; i++) {
+                do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --lastFound);
+            }
+            run_next_test();
         }
+    });
+});
 
-        // ===== 7 =====
-        // Check that "senior citizen" entries get a bonus (browser.formfill.agedBonus)
-        testnum++;
+add_test(function test8() {
+    do_log_info("Check that \"senior citizen\" entries get a bonus (browser.formfill.agedBonus)");
+
+    let agedDate = 1000 * (Date.now() - getFormExpiryDays() * 24 * 60 * 60 * 1000);
+
+    let changes = [ ];
+    changes.push({ op : "add", fieldname: "field3", value: "old but not senior",
+                   timesUsed: 100, firstUsed: (agedDate + 60 * 1000 * 1000), lastUsed: now });
+    changes.push({ op : "add", fieldname: "field3", value: "senior citizen",
+                   timesUsed: 100, firstUsed: (agedDate - 60 * 1000 * 1000), lastUsed: now });
+    updateFormHistory(changes, run_next_test);
+});
 
-        let agedDate = 1000 * (Date.now() - getFormExpiryDays() * 24 * 60 * 60 * 1000);
-        fh.DBConnection.executeSimpleSQL(
-          "INSERT INTO moz_formhistory "+
-          "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-          "VALUES ("+
-              "'field3', " +
-              "'old but not senior', " +
-              "100, " +
-              (agedDate + 60 * 1000 * 1000) + ", " +
-              now +
-          ");");
-        fh.DBConnection.executeSimpleSQL(
-          "INSERT INTO moz_formhistory "+
-          "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-          "VALUES ("+
-              "'field3', " +
-              "'senior citizen', " +
-              "100, " +
-              (agedDate - 60 * 1000 * 1000) + ", " +
-              now +
-          ");");
+add_test(function test9() {
+    fac.autoCompleteSearchAsync("field3", "", null, null, {
+        onSearchCompletion : function(aResults) {
+            do_check_eq(aResults.getValueAt(0), "senior citizen");
+            do_check_eq(aResults.getValueAt(1), "old but not senior");
+            run_next_test();
+        }
+    });
+});
 
-        results = fac.autoCompleteSearch("field3", "", null, null);
-        do_check_eq(results.getValueAt(0), "senior citizen");
-        do_check_eq(results.getValueAt(1), "old but not senior");
+add_test(function test10() {
+    do_log_info("Check entries that are really old or in the future");
+
+    let changes = [ ];
+    changes.push({ op : "add", fieldname: "field4", value: "date of 0",
+                   timesUsed: 1, firstUsed: 0, lastUsed: 0 });
+    changes.push({ op : "add", fieldname: "field4", value: "in the future 1",
+                   timesUsed: 1, firstUsed: 0, lastUsed: now * 2 });
+    changes.push({ op : "add", fieldname: "field4", value: "in the future 2",
+                   timesUsed: 1, firstUsed: now * 2, lastUsed: now * 2 });
+    updateFormHistory(changes, run_next_test);
+});
 
-        // ===== 8 =====
-        // Check entries that are really old or in the future
-        testnum++;
-        fh.DBConnection.executeSimpleSQL(
-          "INSERT INTO moz_formhistory "+
-          "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-          "VALUES ("+
-              "'field4', " +
-              "'date of 0', " +
-              "1, " +
-              0 + ", " +
-              0 +
-          ");");
+add_test(function test11() {
+    fac.autoCompleteSearchAsync("field4", "", null, null, {
+        onSearchCompletion : function(aResults) {
+            do_check_eq(aResults.matchCount, 3);
+            run_next_test();
+        }
+    });
+});
+
+let syncValues = ["sync1", "sync1a", "sync2", "sync3"]
+
+add_test(function test12() {
+    do_log_info("Check old synchronous api");
 
-        fh.DBConnection.executeSimpleSQL(
-          "INSERT INTO moz_formhistory "+
-          "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-          "VALUES ("+
-              "'field4', " +
-              "'in the future 1', " +
-              "1, " +
-              0 + ", " +
-              (now * 2) +
-          ");");
+    let changes = [ ];
+    for (let value of syncValues) {
+      changes.push({ op : "add", fieldname: "field5", value: value });
+    }
+    updateFormHistory(changes, run_next_test);
+});
 
-        fh.DBConnection.executeSimpleSQL(
-          "INSERT INTO moz_formhistory "+
-          "(fieldname, value, timesUsed, firstUsed, lastUsed) " +
-          "VALUES ("+
-              "'field4', " +
-              "'in the future 2', " +
-              "1, " +
-              (now * 2) + ", " +
-              (now * 2) +
-          ");");
+add_test(function test13() {
+  let autocompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].getService(Ci.nsIFormAutoComplete);
+  let results = autocompleteService.autoCompleteSearch("field5", "", null, null);
+  do_check_eq(results.matchCount, syncValues.length, "synchronous matchCount");
+  for (let i = 0; i < results.matchCount; i++) {
+    do_check_eq(results.getValueAt(i), syncValues[i]);
+  }
 
-        results = fac.autoCompleteSearch("field4", "", null, null);
-        do_check_eq(results.matchCount, 3);
-
-
-    } catch (e) {
-      throw "FAILED in test #" + testnum + " -- " + e;
-    }
-}
+  let results = autocompleteService.autoCompleteSearch("field5", "sync1", null, null);
+  do_check_eq(results.matchCount, 2, "synchronous matchCount");
+  do_check_eq(results.getValueAt(0), "sync1");
+  do_check_eq(results.getValueAt(1), "sync1a");
+  run_next_test();
+});
--- a/toolkit/components/satchel/test/unit/test_db_corrupt.js
+++ b/toolkit/components/satchel/test/unit/test_db_corrupt.js
@@ -1,66 +1,89 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-function run_test()
-{
-  try {
-  var testnum = 0;
+let bakFile;
 
+function run_test() {
   // ===== test init =====
-  var testfile = do_get_file("formhistory_CORRUPT.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+  let testfile = do_get_file("formhistory_CORRUPT.sqlite");
+  let profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
+  let destFile = profileDir.clone();
   destFile.append("formhistory.sqlite");
   if (destFile.exists())
     destFile.remove(false);
 
-  var bakFile = profileDir.clone();
+  bakFile = profileDir.clone();
   bakFile.append("formhistory.sqlite.corrupt");
   if (bakFile.exists())
     bakFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
+  run_next_test();
+}
 
-  // ===== 1 =====
-  testnum++;
-  // Open the DB, ensure that a backup of the corrupt DB is made.
-  do_check_false(bakFile.exists());
-  var fh = Cc["@mozilla.org/satchel/form-history;1"].
-           getService(Ci.nsIFormHistory2);
+add_test(function test_corruptFormHistoryDB_lazyCorruptInit1() {
+  do_log_info("ensure FormHistory backs up a corrupt DB on initialization.");
+
   // DB init is done lazily so the DB shouldn't be created yet.
   do_check_false(bakFile.exists());
   // Doing any request to the DB should create it.
-  fh.DBConnection;
+  countEntries(null, null, run_next_test);
+});
+
+add_test(function test_corruptFormHistoryDB_lazyCorruptInit2() {
   do_check_true(bakFile.exists());
   bakFile.remove(false);
-
-  // ===== 2 =====
-  testnum++;
-  // File should be empty
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.entryExists("name-A", "value-A"));
+  run_next_test();
+});
 
 
-  // ===== 3 =====
-  testnum++;
-  // Try adding an entry
-  fh.addEntry("name-A", "value-A");
-  do_check_true(fh.hasEntries);
-  do_check_true(fh.entryExists("name-A", "value-A"));
+add_test(function test_corruptFormHistoryDB_emptyInit() {
+  do_log_info("test that FormHistory initializes an empty DB in place of corrupt DB.");
 
+  FormHistory.count({}, {
+    handleResult : function(aNumEntries) {
+      do_check_true(aNumEntries == 0);
+      FormHistory.count({ fieldname : "name-A", value : "value-A" }, {
+        handleResult : function(aNumEntries2) {
+          do_check_true(aNumEntries2 == 0);
+          run_next_test();
+        },
+        handleError : function(aError2) {
+          do_throw("DB initialized after reading a corrupt DB file found an entry.");
+        }
+      });
+    },
+    handleError : function (aError) {
+      do_throw("DB initialized after reading a corrupt DB file is not empty.");
+    }
+  });
+});
 
-  // ===== 4 =====
-  testnum++;
-  // Try removing an entry
-  fh.removeEntry("name-A", "value-A");
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.entryExists("name-A", "value-A"));
+add_test(function test_corruptFormHistoryDB_addEntry() {
+  do_log_info("test adding an entry to the empty DB.");
+
+  updateEntry("add", "name-A", "value-A",
+    function() {
+      countEntries("name-A", "value-A",
+        function(count) {
+          do_check_true(count == 1);
+          run_next_test();
+        });
+    });
+  });
 
+add_test(function test_corruptFormHistoryDB_removeEntry() {
+  do_log_info("test removing an entry to the empty DB.");
 
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
+  updateEntry("remove", "name-A", "value-A",
+    function() {
+      countEntries("name-A", "value-A",
+        function(count) {
+          do_check_true(count == 0);
+          run_next_test();
+        });
+    });
+  });
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v1.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Returns true if the timestamp is within 30 seconds of now.
-function is_about_now(timestamp) {
-  var delta = Math.abs(timestamp - 1000 * Date.now());
-  var seconds = 30 * 1000000;
-  return delta < seconds;
-}
-
-var testnum = 0;
-var fh;
-var timesUsed, firstUsed, lastUsed;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v0.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(0, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-  // Check for expected contents.
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  do_check_true(fh.entryExists("name-C", "value-C1"));
-  do_check_true(fh.entryExists("name-C", "value-C2"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-
-
-  // ===== 2 =====
-  testnum++;
-  // Check that timestamps were created correctly.
-
-  var query = "SELECT timesUsed, firstUsed, lastUsed " +
-              "FROM moz_formhistory WHERE fieldname = 'name-A'";
-  var stmt = fh.DBConnection.createStatement(query);
-  stmt.executeStep();
-
-  timesUsed = stmt.getInt32(0);
-  firstUsed = stmt.getInt64(1);
-  lastUsed  = stmt.getInt64(2);
-  stmt.finalize();
-
-  do_check_eq(1, timesUsed);
-  do_check_true(firstUsed == lastUsed);
-  // Upgraded entries timestamped 24 hours in the past.
-  do_check_true(is_about_now(firstUsed + 24 * PR_HOURS));
-
-
-  // ===== 3 =====
-  testnum++;
-  // Exercise adding and removing a name/value pair
-  do_check_false(fh.entryExists("name-D", "value-D"));
-  fh.addEntry("name-D", "value-D");
-  do_check_true(fh.entryExists("name-D", "value-D"));
-  fh.removeEntry("name-D", "value-D");
-  do_check_false(fh.entryExists("name-D", "value-D"));
-
-
-  // ===== 4 =====
-  testnum++;
-  // Add a new entry, check expected properties
-  do_check_false(fh.entryExists("name-E", "value-E"));
-  fh.addEntry("name-E", "value-E");
-  do_check_true(fh.entryExists("name-E", "value-E"));
-
-  query = "SELECT timesUsed, firstUsed, lastUsed " +
-          "FROM moz_formhistory WHERE fieldname = 'name-E'";
-  stmt = fh.DBConnection.createStatement(query);
-  stmt.executeStep();
-
-  timesUsed = stmt.getInt32(0);
-  firstUsed = stmt.getInt64(1);
-  lastUsed  = stmt.getInt64(2);
-  stmt.finalize();
-
-  do_check_eq(1, timesUsed);
-  do_check_true(firstUsed == lastUsed);
-  do_check_true(is_about_now(firstUsed));
-
-  // The next test adds the entry again, and check to see that the lastUsed
-  // field is updated. Unfortunately, on Windows PR_Now() is granular
-  // (robarnold says usually 16.5ms, sometimes 10ms), so if we execute the
-  // test too soon the timestamp will be the same! So, we'll wait a short
-  // period of time to make sure the timestamp will differ.
-  do_test_pending();
-  do_timeout(50, delayed_test);
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
-
-function delayed_test() {
-  try {
-
-  // ===== 5 =====
-  testnum++;
-  // Add entry again, check for updated properties.
-  do_check_true(fh.entryExists("name-E", "value-E"));
-  fh.addEntry("name-E", "value-E");
-  do_check_true(fh.entryExists("name-E", "value-E"));
-
-  var query = "SELECT timesUsed, firstUsed, lastUsed " +
-              "FROM moz_formhistory WHERE fieldname = 'name-E'";
-  var stmt = fh.DBConnection.createStatement(query);
-  stmt.executeStep();
-
-  timesUsed = stmt.getInt32(0);
-  var firstUsed2 = stmt.getInt64(1);
-  var lastUsed2  = stmt.getInt64(2);
-  stmt.finalize();
-
-  do_check_eq(2, timesUsed);
-  do_check_true(is_about_now(lastUsed2));
-
-  do_check_true(firstUsed  == firstUsed2); //unchanged
-  do_check_true(lastUsed   != lastUsed2);  //changed
-  do_check_true(firstUsed2 != lastUsed2);
-
-  do_test_finished();
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v1b.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Returns true if the timestamp is within 30 seconds of now.
-function is_about_now(timestamp) {
-  var delta = Math.abs(timestamp - 1000 * Date.now());
-  var seconds = 30 * 1000000;
-  return delta < seconds;
-}
-
-var testnum = 0;
-var fh;
-var timesUsed, firstUsed, lastUsed;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v0v1.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(0, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-  // Check for expected contents.
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  do_check_true(fh.entryExists("name-C", "value-C1"));
-  do_check_true(fh.entryExists("name-C", "value-C2"));
-  do_check_true(fh.entryExists("name-D", "value-D"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-
-  // ===== 2 =====
-  testnum++;
-
-  // The name-D entry was added by v0 code, so no timestamps were set. Make
-  // sure the upgrade set timestamps on that entry.
-  var query = "SELECT timesUsed, firstUsed, lastUsed " +
-              "FROM moz_formhistory WHERE fieldname = 'name-D'";
-  var stmt = fh.DBConnection.createStatement(query);
-  stmt.executeStep();
-
-  timesUsed = stmt.getInt32(0);
-  firstUsed = stmt.getInt64(1);
-  lastUsed  = stmt.getInt64(2);
-  stmt.finalize();
-
-  do_check_eq(1, timesUsed);
-  do_check_true(firstUsed == lastUsed);
-  // Upgraded entries timestamped 24 hours in the past.
-  do_check_true(is_about_now(firstUsed + 24 * PR_HOURS));
-
-
-  // ===== 3 =====
-  testnum++;
-
-  // Check to make sure the existing timestamps are unmodified.
-  var query = "SELECT timesUsed, firstUsed, lastUsed " +
-              "FROM moz_formhistory WHERE fieldname = 'name-A'";
-  var stmt = fh.DBConnection.createStatement(query);
-  stmt.executeStep();
-
-  timesUsed = stmt.getInt32(0);
-  firstUsed = stmt.getInt64(1);
-  lastUsed  = stmt.getInt64(2);
-  stmt.finalize();
-
-  do_check_eq(1, timesUsed);
-  do_check_eq(lastUsed,  1231984073012182);
-  do_check_eq(firstUsed, 1231984073012182);
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v2.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var testnum = 0;
-var fh;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v1.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(1, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-
-  // Check that the index was added (which is all the v2 upgrade does)
-  do_check_true(fh.DBConnection.indexExists("moz_formhistory_lastused_index"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-  // Check that old table was removed
-  do_check_false(fh.DBConnection.tableExists("moz_dummy_table"));
-
-
-  // ===== 2 =====
-  testnum++;
-
-  // Just sanity check for expected contents and that DB is working.
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_false(fh.entryExists("name-B", "value-B"));
-  fh.addEntry("name-B", "value-B");
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  fh.removeEntry("name-B", "value-B");
-  do_check_false(fh.entryExists("name-B", "value-B"));
-
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v2b.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var testnum = 0;
-var fh;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v1v2.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(1, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-
-  // Check that the index was added / still exists (which is all the v2 upgrade does)
-  do_check_true(fh.DBConnection.indexExists("moz_formhistory_lastused_index"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-  // Check that old table was removed
-  do_check_false(fh.DBConnection.tableExists("moz_dummy_table"));
-
-
-  // ===== 2 =====
-  testnum++;
-
-  // Just sanity check for expected contents and that DB is working.
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_false(fh.entryExists("name-B", "value-B"));
-  fh.addEntry("name-B", "value-B");
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  fh.removeEntry("name-B", "value-B");
-  do_check_false(fh.entryExists("name-B", "value-B"));
-
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v3.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var testnum = 0;
-var fh;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v2.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(2, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-
-  // Check that the index was added
-  do_check_true(fh.DBConnection.indexExists("moz_formhistory_guid_index"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  var guid = getGUIDforID(fh.DBConnection, 1);
-  do_check_true(isGUID.test(guid));
-
-  // Add a new entry and check that it gets a GUID
-  do_check_false(fh.entryExists("name-B", "value-B"));
-  fh.addEntry("name-B", "value-B");
-  do_check_true(fh.entryExists("name-B", "value-B"));
-
-  guid = getGUIDforID(fh.DBConnection, 2);
-  do_check_true(isGUID.test(guid));
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_db_update_v3b.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var testnum = 0;
-var fh;
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_v2v3.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var destFile = profileDir.clone();
-  destFile.append("formhistory.sqlite");
-  if (destFile.exists())
-    destFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_eq(2, getDBVersion(testfile));
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
-  // ===== 1 =====
-  testnum++;
-
-  // Check that the index was added
-  do_check_true(fh.DBConnection.indexExists("moz_formhistory_guid_index"));
-  // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-
-  // Entry added by v3 code, has a GUID that shouldn't be changed.
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  var guid = getGUIDforID(fh.DBConnection, 1);
-  do_check_eq(guid, "dgdaRfzsTnOOZ7wK");
-
-  // Entry added  by v2 code after a downgrade, GUID should be assigned on upgrade.
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  guid = getGUIDforID(fh.DBConnection, 2);
-  do_check_true(isGUID.test(guid));
-
-  // Add a new entry and check that it gets a GUID
-  do_check_false(fh.entryExists("name-C", "value-C"));
-  fh.addEntry("name-C", "value-C");
-  do_check_true(fh.entryExists("name-C", "value-C"));
-
-  guid = getGUIDforID(fh.DBConnection, 3);
-  do_check_true(isGUID.test(guid));
-
-  } catch (e) {
-    throw "FAILED in test #" + testnum + " -- " + e;
-  }
-}
--- a/toolkit/components/satchel/test/unit/test_db_update_v4.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4.js
@@ -1,42 +1,60 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var testnum = 0;
-var fh;
+
+let iter;
 
 function run_test()
 {
+  do_test_pending();
+  iter = next_test();
+  iter.next();
+}
+
+function next_test()
+{
   try {
 
   // ===== test init =====
   var testfile = do_get_file("formhistory_v3.sqlite");
   var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
   var destFile = profileDir.clone();
   destFile.append("formhistory.sqlite");
   if (destFile.exists())
     destFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
   do_check_eq(3, getDBVersion(testfile));
 
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
   // ===== 1 =====
   testnum++;
 
-  // Check that the index was added
-  do_check_true(fh.DBConnection.tableExists("moz_deleted_formhistory"));
+  destFile = profileDir.clone();
+  destFile.append("formhistory.sqlite");
+  let dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
   // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+  // Check that the index was added
+  do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+  dbConnection.close();
+
+  // check for upgraded schema.
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
   // check that an entry still exists
-  do_check_true(fh.entryExists("name-A", "value-A"));
+  yield countEntries("name-A", "value-A",
+    function (num) {
+      do_check_true(num > 0);
+      do_test_finished();
+    }
+  );
 
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
 }
--- a/toolkit/components/satchel/test/unit/test_db_update_v4b.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4b.js
@@ -1,42 +1,58 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var testnum = 0;
-var fh;
+
+let iter;
 
 function run_test()
 {
+  do_test_pending();
+  iter = next_test();
+  iter.next();
+}
+
+function next_test()
+{
   try {
 
   // ===== test init =====
   var testfile = do_get_file("formhistory_v3v4.sqlite");
   var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
   var destFile = profileDir.clone();
   destFile.append("formhistory.sqlite");
   if (destFile.exists())
     destFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
   do_check_eq(3, getDBVersion(testfile));
 
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-
   // ===== 1 =====
   testnum++;
 
-  // Check that the index was added
-  do_check_true(fh.DBConnection.tableExists("moz_deleted_formhistory"));
+  destFile = profileDir.clone();
+  destFile.append("formhistory.sqlite");
+  dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
   // check for upgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+  // Check that the index was added
+  do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+  dbConnection.close();
+
   // check that an entry still exists
-  do_check_true(fh.entryExists("name-A", "value-A"));
+  yield countEntries("name-A", "value-A",
+    function (num) {
+      do_check_true(num > 0);
+      do_test_finished();
+    }
+  );
 
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
 }
--- a/toolkit/components/satchel/test/unit/test_db_update_v999a.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999a.js
@@ -6,55 +6,70 @@
  * This test uses a formhistory.sqlite with schema version set to 999 (a
  * future version). This exercies the code that allows using a future schema
  * version as long as the expected columns are present.
  *
  * Part A tests this when the columns do match, so the DB is used.
  * Part B tests this when the columns do *not* match, so the DB is reset.
  */
 
+let iter = tests();
+
 function run_test()
 {
+  do_test_pending();
+  iter.next();
+}
+
+function next_test()
+{
+  iter.next();
+}
+
+function tests()
+{
   try {
   var testnum = 0;
 
   // ===== test init =====
   var testfile = do_get_file("formhistory_v999a.sqlite");
   var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
   var destFile = profileDir.clone();
   destFile.append("formhistory.sqlite");
   if (destFile.exists())
     destFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
   do_check_eq(999, getDBVersion(testfile));
 
-  var fh = Cc["@mozilla.org/satchel/form-history;1"].
-           getService(Ci.nsIFormHistory2);
-
+  let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+  let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
 
   // ===== 1 =====
   testnum++;
   // Check for expected contents.
-  do_check_true(fh.hasEntries);
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  do_check_true(fh.entryExists("name-C", "value-C1"));
-  do_check_true(fh.entryExists("name-C", "value-C2"));
-  do_check_true(fh.entryExists("name-E", "value-E"));
+  yield countEntries(null, null, function(num) { do_check_true(num > 0); next_test(); });
+  yield countEntries("name-A", "value-A", checkOne);
+  yield countEntries("name-B", "value-B", checkOne);
+  yield countEntries("name-C", "value-C1", checkOne);
+  yield countEntries("name-C", "value-C2", checkOne);
+  yield countEntries("name-E", "value-E", checkOne);
+
   // check for downgraded schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
   // ===== 2 =====
   testnum++;
   // Exercise adding and removing a name/value pair
-  do_check_false(fh.entryExists("name-D", "value-D"));
-  fh.addEntry("name-D", "value-D");
-  do_check_true(fh.entryExists("name-D", "value-D"));
-  fh.removeEntry("name-D", "value-D");
-  do_check_false(fh.entryExists("name-D", "value-D"));
+  yield countEntries("name-D", "value-D", checkZero);
+  yield updateEntry("add", "name-D", "value-D", next_test);
+  yield countEntries("name-D", "value-D", checkOne);
+  yield updateEntry("remove", "name-D", "value-D", next_test);
+  yield countEntries("name-D", "value-D", checkZero);
 
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
+
+  do_test_finished();
 }
--- a/toolkit/components/satchel/test/unit/test_db_update_v999b.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999b.js
@@ -6,18 +6,31 @@
  * This test uses a formhistory.sqlite with schema version set to 999 (a
  * future version). This exercies the code that allows using a future schema
  * version as long as the expected columns are present.
  *
  * Part A tests this when the columns do match, so the DB is used.
  * Part B tests this when the columns do *not* match, so the DB is reset.
  */
 
+let iter = tests();
+
 function run_test()
 {
+  do_test_pending();
+  iter.next();
+}
+
+function next_test()
+{
+  iter.next();
+}
+
+function tests()
+{
   try {
   var testnum = 0;
 
   // ===== test init =====
   var testfile = do_get_file("formhistory_v999b.sqlite");
   var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
@@ -29,48 +42,51 @@ function run_test()
   var bakFile = profileDir.clone();
   bakFile.append("formhistory.sqlite.corrupt");
   if (bakFile.exists())
     bakFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
   do_check_eq(999, getDBVersion(testfile));
 
+  let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+  let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
+
   // ===== 1 =====
   testnum++;
+
   // Open the DB, ensure that a backup of the corrupt DB is made.
-  do_check_false(bakFile.exists());
-  var fh = Cc["@mozilla.org/satchel/form-history;1"].
-           getService(Ci.nsIFormHistory2);
   // DB init is done lazily so the DB shouldn't be created yet.
   do_check_false(bakFile.exists());
   // Doing any request to the DB should create it.
-  fh.DBConnection;
+  yield countEntries("", "", next_test);
+
   do_check_true(bakFile.exists());
   bakFile.remove(false);
 
   // ===== 2 =====
   testnum++;
   // File should be empty
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.entryExists("name-A", "value-A"));
+  yield countEntries(null, null, function(num) { do_check_false(num); next_test(); });
+  yield countEntries("name-A", "value-A", checkZero);
   // check for current schema.
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
+  do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
   // ===== 3 =====
   testnum++;
   // Try adding an entry
-  fh.addEntry("name-A", "value-A");
-  do_check_true(fh.hasEntries);
-  do_check_true(fh.entryExists("name-A", "value-A"));
-
+  yield updateEntry("add", "name-A", "value-A", next_test);
+  yield countEntries(null, null, checkOne);
+  yield countEntries("name-A", "value-A", checkOne);
 
   // ===== 4 =====
   testnum++;
   // Try removing an entry
-  fh.removeEntry("name-A", "value-A");
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.entryExists("name-A", "value-A"));
+  yield updateEntry("remove", "name-A", "value-A", next_test);
+  yield countEntries(null, null, checkZero);
+  yield countEntries("name-A", "value-A", checkZero);
 
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
+
+  do_test_finished();
 }
deleted file mode 100644
--- a/toolkit/components/satchel/test/unit/test_expire.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var testnum = 0;
-var fh, prefs;
-
-function countAllEntries() {
-    let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory");
-    do_check_true(stmt.executeStep());
-    let numEntries = stmt.row.numEntries;
-    stmt.finalize();
-    return numEntries;
-}
-
-function triggerExpiration() {
-    // We can't easily fake a "daily idle" event, so for testing purposes form
-    // history listens for another notification to trigger an immediate
-    // expiration.
-    var os = Cc["@mozilla.org/observer-service;1"].
-             getService(Ci.nsIObserverService);
-    os.notifyObservers(null, "formhistory-expire-now", null);
-}
-
-function run_test()
-{
-  try {
-
-  // ===== test init =====
-  var testfile = do_get_file("formhistory_expire.sqlite");
-  var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
-
-  // Cleanup from any previous tests or failures.
-  var dbFile = profileDir.clone();
-  dbFile.append("formhistory.sqlite");
-  if (dbFile.exists())
-    dbFile.remove(false);
-
-  testfile.copyTo(profileDir, "formhistory.sqlite");
-  do_check_true(dbFile.exists());
-
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
-  prefs = Cc["@mozilla.org/preferences-service;1"].
-          getService(Ci.nsIPrefBranch);
-
-  // We're going to clear this at the end, so it better have the default value now.
-  do_check_false(prefs.prefHasUserValue("browser.formfill.expire_days"));
-
-
-  // ===== 1 =====
-  testnum++;
-
-  // Sanity check initial state
-  do_check_eq(CURRENT_SCHEMA, fh.DBConnection.schemaVersion);
-  do_check_eq(508, countAllEntries());
-  do_check_true(fh.entryExists("name-A", "value-A")); // lastUsed == distant past
-  do_check_true(fh.entryExists("name-B", "value-B")); // lastUsed == distant future
-
-  // Add a new entry
-  do_check_false(fh.entryExists("name-C", "value-C"));
-  fh.addEntry("name-C", "value-C");
-  do_check_true(fh.entryExists("name-C", "value-C"));
-
-  // Check the original db size.
-  // Do a vacuum to make sure the db has current page size.
-  fh.DBConnection.executeSimpleSQL("VACUUM");
-  var oldSize = dbFile.clone().fileSize;
-
-  // Update some existing entries to have ages relative to when the test runs.
-  var now = 1000 * Date.now();
-  var age181 = now - 181 * 24 * PR_HOURS;
-  var age179 = now - 179 * 24 * PR_HOURS;
-  var age31  = now -  31 * 24 * PR_HOURS;
-  var age29  = now -  29 * 24 * PR_HOURS;
-  var age11  = now -  11 * 24 * PR_HOURS;
-  var age9   = now -   9 * 24 * PR_HOURS;
-
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age181 + " WHERE lastUsed=181");
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age179 + " WHERE lastUsed=179");
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age31  + " WHERE lastUsed=31");
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age29  + " WHERE lastUsed=29");
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age11  + " WHERE lastUsed=9999");
-  fh.DBConnection.executeSimpleSQL("UPDATE moz_formhistory SET lastUsed=" + age9   + " WHERE lastUsed=9");
-
-
-  // ===== 2 =====
-  testnum++;
-
-  // Expire history with default pref (180 days)
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("181DaysOld", "foo"));
-  do_check_true(fh.entryExists("179DaysOld", "foo"));
-  do_check_eq(509, countAllEntries());
-
-  // 2 entries are expected to expire.
-  triggerExpiration();
-
-  do_check_false(fh.entryExists("name-A", "value-A"));
-  do_check_false(fh.entryExists("181DaysOld", "foo"));
-  do_check_true(fh.entryExists("179DaysOld", "foo"));
-  do_check_eq(507, countAllEntries());
-
-
-  // ===== 3 =====
-  testnum++;
-
-  // And again. No change expected.
-  triggerExpiration();
-  do_check_eq(507, countAllEntries());
-
-
-  // ===== 4 =====
-  testnum++;
-
-  // Set formfill pref to 30 days.
-  prefs.setIntPref("browser.formfill.expire_days", 30);
-  do_check_true(fh.entryExists("179DaysOld", "foo"));
-  do_check_true(fh.entryExists("bar", "31days"));
-  do_check_true(fh.entryExists("bar", "29days"));
-  do_check_eq(507, countAllEntries());
-
-  triggerExpiration();
-
-  do_check_false(fh.entryExists("179DaysOld", "foo"));
-  do_check_false(fh.entryExists("bar", "31days"));
-  do_check_true(fh.entryExists("bar", "29days"));
-  do_check_eq(505, countAllEntries());
-
-
-  // ===== 5 =====
-  testnum++;
-
-  // Set override pref to 10 days and expire. This expires a large batch of
-  // entries, and should trigger a VACCUM to reduce file size.
-  prefs.setIntPref("browser.formfill.expire_days", 10);
-
-  do_check_true(fh.entryExists("bar", "29days"));
-  do_check_true(fh.entryExists("9DaysOld", "foo"));
-  do_check_eq(505, countAllEntries());
-
-  triggerExpiration();
-
-  do_check_false(fh.entryExists("bar", "29days"));
-  do_check_true(fh.entryExists("9DaysOld", "foo"));
-  do_check_true(fh.entryExists("name-B", "value-B"));
-  do_check_true(fh.entryExists("name-C", "value-C"));
-  do_check_eq(3, countAllEntries());
-
-  // Check that the file size was reduced.
-  // Need to clone the nsIFile because the size is being cached on Windows.
-  dbFile = dbFile.clone();
-  do_check_true(dbFile.fileSize < oldSize);
-
-
-  } catch (e) {
-      throw "FAILED in test #" + testnum + " -- " + e;
-  } finally {
-      // Make sure we always reset prefs.
-      if (prefs.prefHasUserValue("browser.formfill.expire_days"))
-        prefs.clearUserPref("browser.formfill.expire_days");
-  }
-}
--- a/toolkit/components/satchel/test/unit/test_history_api.js
+++ b/toolkit/components/satchel/test/unit/test_history_api.js
@@ -1,125 +1,397 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var testnum = 0;
-var fh;
+let dbConnection; // used for deleted table tests
+
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+function countDeletedEntries(expected)
+{
+  let deferred = Promise.defer();
+  let stmt = dbConnection.createAsyncStatement("SELECT COUNT(*) AS numEntries FROM moz_deleted_formhistory");
+  stmt.executeAsync({
+    handleResult: function(resultSet) {
+      do_check_eq(expected, resultSet.getNextRow().getResultByName("numEntries"));
+      deferred.resolve();
+    },
+    handleError : function () {
+      do_throw("Error occurred counting deleted entries: " + error);
+      deferred.reject();
+    },
+    handleCompletion : function () {
+      stmt.finalize();
+    }
+  });
+  return deferred.promise;
+}
+
+function checkTimeDeleted(guid, checkFunction)
+{
+  let deferred = Promise.defer();
+  let stmt = dbConnection.createAsyncStatement("SELECT timeDeleted FROM moz_deleted_formhistory WHERE guid = :guid");
+  stmt.params.guid = guid;
+  stmt.executeAsync({
+    handleResult: function(resultSet) {
+      checkFunction(resultSet.getNextRow().getResultByName("timeDeleted"));
+      deferred.resolve();
+    },
+    handleError : function () {
+      do_throw("Error occurred getting deleted entries: " + error);
+      deferred.reject();
+    },
+    handleCompletion : function () {
+      stmt.finalize();
+    }
+  });
+  return deferred.promise;
+}
 
-function run_test()
+function promiseUpdateEntry(op, name, value)
+{
+  var change = { op: op };
+  if (name !== null)
+    change.fieldname = name;
+  if (value !== null)
+    change.value = value;
+  return promiseUpdate(change);
+}
+
+function promiseUpdate(change)
+{
+  let deferred = Promise.defer();
+  FormHistory.update(change,
+                     { handleError: function (error) {
+                         do_throw("Error occurred updating form history: " + error);
+                         deferred.reject(error);
+                       },
+                       handleCompletion: function (reason) { if (!reason) deferred.resolve(); }
+                     });
+  return deferred.promise;
+}
+
+function promiseSearchEntries(terms, params)
 {
+  let deferred = Promise.defer();
+  let results = [];
+  FormHistory.search(terms, params,
+                     { handleResult: function(result) results.push(result),
+                       handleError: function (error) {
+                         do_throw("Error occurred searching form history: " + error);
+                         deferred.reject(error);
+                       },
+                       handleCompletion: function (reason) { if (!reason) deferred.resolve(results); }
+                     });
+  return deferred.promise;
+}
+
+function promiseCountEntries(name, value, checkFn)
+{
+  let deferred = Promise.defer();
+  countEntries(name, value, function (result) { checkFn(result); deferred.resolve(); } );
+  return deferred.promise;
+}
+
+add_task(function ()
+{
+  let oldSupportsDeletedTable = FormHistory._supportsDeletedTable;
+  FormHistory._supportsDeletedTable = true;
+
   try {
 
   // ===== test init =====
   var testfile = do_get_file("formhistory_apitest.sqlite");
   var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
 
   // Cleanup from any previous tests or failures.
   var destFile = profileDir.clone();
   destFile.append("formhistory.sqlite");
   if (destFile.exists())
     destFile.remove(false);
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
 
-  fh = Cc["@mozilla.org/satchel/form-history;1"].
-       getService(Ci.nsIFormHistory2);
-
+  function checkExists(num) { do_check_true(num > 0); }
+  function checkNotExists(num) { do_check_true(num == 0); }
 
   // ===== 1 =====
   // Check initial state is as expected
   testnum++;
-  do_check_true(fh.hasEntries);
-  do_check_true(fh.nameExists("name-A"));
-  do_check_true(fh.nameExists("name-B"));
-  do_check_true(fh.nameExists("name-C"));
-  do_check_true(fh.nameExists("name-D"));
-  do_check_true(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("name-B", "value-B1"));
-  do_check_true(fh.entryExists("name-B", "value-B2"));
-  do_check_true(fh.entryExists("name-C", "value-C"));
-  do_check_true(fh.entryExists("name-D", "value-D"));
+  yield promiseCountEntries("name-A", null, checkExists);
+  yield promiseCountEntries("name-B", null, checkExists);
+  yield promiseCountEntries("name-C", null, checkExists);
+  yield promiseCountEntries("name-D", null, checkExists);
+  yield promiseCountEntries("name-A", "value-A", checkExists);
+  yield promiseCountEntries("name-B", "value-B1", checkExists);
+  yield promiseCountEntries("name-B", "value-B2", checkExists);
+  yield promiseCountEntries("name-C", "value-C", checkExists);
+  yield promiseCountEntries("name-D", "value-D", checkExists);
   // time-A/B/C/D checked below.
 
+  // Delete anything from the deleted table
+  let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+  dbFile.append("formhistory.sqlite");
+  dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+
+  let deferred = Promise.defer();
+
+  let stmt = dbConnection.createAsyncStatement("DELETE FROM moz_deleted_formhistory");
+  stmt.executeAsync({
+    handleResult: function(resultSet) { },
+    handleError : function () {
+      do_throw("Error occurred counting deleted all entries: " + error);
+    },
+    handleCompletion : function () {
+      stmt.finalize();
+      deferred.resolve();
+    }
+  });
+  yield deferred.promise;
+
   // ===== 2 =====
   // Test looking for nonexistent / bogus data.
   testnum++;
-  do_check_false(fh.nameExists("blah"));
-  do_check_false(fh.nameExists(""));
-  do_check_false(fh.nameExists(null));
-  do_check_false(fh.entryExists("name-A", "blah"));
-  do_check_false(fh.entryExists("name-A", ""));
-  do_check_false(fh.entryExists("name-A", null));
-  do_check_false(fh.entryExists("blah", "value-A"));
-  do_check_false(fh.entryExists("", "value-A"));
-  do_check_false(fh.entryExists(null, "value-A"));
+  yield promiseCountEntries("blah", null, checkNotExists);
+  yield promiseCountEntries("", null, checkNotExists);
+  yield promiseCountEntries("name-A", "blah", checkNotExists);
+  yield promiseCountEntries("name-A", "", checkNotExists);
+  yield promiseCountEntries("name-A", null, checkExists);
+  yield promiseCountEntries("blah", "value-A", checkNotExists);
+  yield promiseCountEntries("", "value-A", checkNotExists);
+  yield promiseCountEntries(null, "value-A", checkExists);
+
+  // Cannot use promiseCountEntries when name and value are null because it treats null values as not set
+  // and here a search should be done explicity for null.
+  deferred = Promise.defer();
+  yield FormHistory.count({ fieldname: null, value: null },
+                          { handleResult: function(result) checkNotExists(result),
+                            handleError: function (error) {
+                              do_throw("Error occurred searching form history: " + error);
+                            },
+                            handleCompletion: function(reason) { if (!reason) deferred.resolve() }
+                          });
+  yield deferred.promise;
 
   // ===== 3 =====
   // Test removeEntriesForName with a single matching value
   testnum++;
-  fh.removeEntriesForName("name-A");
-  do_check_false(fh.entryExists("name-A", "value-A"));
-  do_check_true(fh.entryExists("name-B", "value-B1"));
-  do_check_true(fh.entryExists("name-B", "value-B2"));
-  do_check_true(fh.entryExists("name-C", "value-C"));
-  do_check_true(fh.entryExists("name-D", "value-D"));
+  yield promiseUpdateEntry("remove", "name-A", null);
+
+  yield promiseCountEntries("name-A", "value-A", checkNotExists);
+  yield promiseCountEntries("name-B", "value-B1", checkExists);
+  yield promiseCountEntries("name-B", "value-B2", checkExists);
+  yield promiseCountEntries("name-C", "value-C", checkExists);
+  yield promiseCountEntries("name-D", "value-D", checkExists);
+  yield countDeletedEntries(1);
 
   // ===== 4 =====
   // Test removeEntriesForName with multiple matching values
   testnum++;
-  fh.removeEntriesForName("name-B");
-  do_check_false(fh.entryExists("name-A", "value-A"));
-  do_check_false(fh.entryExists("name-B", "value-B1"));
-  do_check_false(fh.entryExists("name-B", "value-B2"));
-  do_check_true(fh.entryExists("name-C", "value-C"));
-  do_check_true(fh.entryExists("name-D", "value-D"));
+  yield promiseUpdateEntry("remove", "name-B", null);
+
+  yield promiseCountEntries("name-A", "value-A", checkNotExists);
+  yield promiseCountEntries("name-B", "value-B1", checkNotExists);
+  yield promiseCountEntries("name-B", "value-B2", checkNotExists);
+  yield promiseCountEntries("name-C", "value-C", checkExists);
+  yield promiseCountEntries("name-D", "value-D", checkExists);
+  yield countDeletedEntries(3);
 
   // ===== 5 =====
   // Test removing by time range (single entry, not surrounding entries)
   testnum++;
-  do_check_true(fh.nameExists("time-A")); // firstUsed=1000, lastUsed=1000
-  do_check_true(fh.nameExists("time-B")); // firstUsed=1000, lastUsed=1099
-  do_check_true(fh.nameExists("time-C")); // firstUsed=1099, lastUsed=1099
-  do_check_true(fh.nameExists("time-D")); // firstUsed=2001, lastUsed=2001
-  fh.removeEntriesByTimeframe(1050, 2000);
-  do_check_true(fh.nameExists("time-A"));
-  do_check_true(fh.nameExists("time-B"));
-  do_check_false(fh.nameExists("time-C"));
-  do_check_true(fh.nameExists("time-D"));
+  yield promiseCountEntries("time-A", null, checkExists); // firstUsed=1000, lastUsed=1000
+  yield promiseCountEntries("time-B", null, checkExists); // firstUsed=1000, lastUsed=1099
+  yield promiseCountEntries("time-C", null, checkExists); // firstUsed=1099, lastUsed=1099
+  yield promiseCountEntries("time-D", null, checkExists); // firstUsed=2001, lastUsed=2001
+  yield promiseUpdate({ op : "remove", firstUsedStart: 1050, firstUsedEnd: 2000 });
+
+  yield promiseCountEntries("time-A", null, checkExists);
+  yield promiseCountEntries("time-B", null, checkExists);
+  yield promiseCountEntries("time-C", null, checkNotExists);
+  yield promiseCountEntries("time-D", null, checkExists);
+  yield countDeletedEntries(4);
 
   // ===== 6 =====
   // Test removing by time range (multiple entries)
   testnum++;
-  fh.removeEntriesByTimeframe(1000, 2000);
-  do_check_false(fh.nameExists("time-A"));
-  do_check_false(fh.nameExists("time-B"));
-  do_check_false(fh.nameExists("time-C"));
-  do_check_true(fh.nameExists("time-D"));
+  yield promiseUpdate({ op : "remove", firstUsedStart: 1000, firstUsedEnd: 2000 });
+
+  yield promiseCountEntries("time-A", null, checkNotExists);
+  yield promiseCountEntries("time-B", null, checkNotExists);
+  yield promiseCountEntries("time-C", null, checkNotExists);
+  yield promiseCountEntries("time-D", null, checkExists);
+  yield countDeletedEntries(6);
 
   // ===== 7 =====
   // test removeAllEntries
   testnum++;
-  fh.removeAllEntries();
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.nameExists("name-C"));
-  do_check_false(fh.nameExists("name-D"));
-  do_check_false(fh.entryExists("name-C", "value-C"));
-  do_check_false(fh.entryExists("name-D", "value-D"));
+  yield promiseUpdateEntry("remove", null, null);
+
+  yield promiseCountEntries("name-C", null, checkNotExists);
+  yield promiseCountEntries("name-D", null, checkNotExists);
+  yield promiseCountEntries("name-C", "value-C", checkNotExists);
+  yield promiseCountEntries("name-D", "value-D", checkNotExists);
+
+  yield promiseCountEntries(null, null, checkNotExists);
+  yield countDeletedEntries(6);
 
   // ===== 8 =====
   // Add a single entry back
   testnum++;
-  fh.addEntry("newname-A", "newvalue-A");
-  do_check_true(fh.hasEntries);
-  do_check_true(fh.entryExists("newname-A", "newvalue-A"));
+  yield promiseUpdateEntry("add", "newname-A", "newvalue-A");
+  yield promiseCountEntries("newname-A", "newvalue-A", checkExists);
 
   // ===== 9 =====
   // Remove the single entry
   testnum++;
-  fh.removeEntry("newname-A", "newvalue-A");
-  do_check_false(fh.hasEntries);
-  do_check_false(fh.entryExists("newname-A", "newvalue-A"));
+  yield promiseUpdateEntry("remove", "newname-A", "newvalue-A");
+  yield promiseCountEntries("newname-A", "newvalue-A", checkNotExists);
+
+  // ===== 10 =====
+  // Add a single entry
+  testnum++;
+  yield promiseUpdateEntry("add", "field1", "value1");
+  yield promiseCountEntries("field1", "value1", checkExists);
+
+  let processFirstResult = function processResults(results)
+  {
+    // Only handle the first result
+    if (results.length > 0) {
+      let result = results[0];
+      return [result.timesUsed, result.firstUsed, result.lastUsed, result.guid];
+    }
+  }
+
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field1", value: "value1" });
+  let [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+  do_check_eq(1, timesUsed);
+  do_check_true(firstUsed > 0);
+  do_check_true(lastUsed > 0);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 1));
+
+  // ===== 11 =====
+  // Add another single entry
+  testnum++;
+  yield promiseUpdateEntry("add", "field1", "value1b");
+  yield promiseCountEntries("field1", "value1", checkExists);
+  yield promiseCountEntries("field1", "value1b", checkExists);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 2));
+
+  // ===== 12 =====
+  // Update a single entry
+  testnum++;
+
+  results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1" });
+  let guid = processFirstResult(results)[3];
+
+  yield promiseUpdate({ op : "update", guid: guid, value: "modifiedValue" });
+  yield promiseCountEntries("field1", "modifiedValue", checkExists);
+  yield promiseCountEntries("field1", "value1", checkNotExists);
+  yield promiseCountEntries("field1", "value1b", checkExists);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 2));
+
+  // ===== 13 =====
+  // Add a single entry with times
+  testnum++;
+  yield promiseUpdate({ op : "add", fieldname: "field2", value: "value2",
+                        timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field2", value: "value2" });
+  [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+
+  do_check_eq(20, timesUsed);
+  do_check_eq(100, firstUsed);
+  do_check_eq(500, lastUsed);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 3));
+
+  // ===== 14 =====
+  // Bump an entry, which updates its lastUsed field
+  testnum++;
+  yield promiseUpdate({ op : "bump", fieldname: "field2", value: "value2",
+                        timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field2", value: "value2" });
+  [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+  do_check_eq(21, timesUsed);
+  do_check_eq(100, firstUsed);
+  do_check_true(lastUsed > 500);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 3));
+
+  // ===== 15 =====
+  // Bump an entry that does not exist
+  testnum++;
+  yield promiseUpdate({ op : "bump", fieldname: "field3", value: "value3",
+                        timesUsed: 10, firstUsed: 50, lastUsed: 400 });
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field3", value: "value3" });
+  [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+  do_check_eq(10, timesUsed);
+  do_check_eq(50, firstUsed);
+  do_check_eq(400, lastUsed);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 4));
+
+  // ===== 16 =====
+  // Bump an entry with a guid
+  testnum++;
+  results = yield promiseSearchEntries(["guid"], { fieldname: "field3", value: "value3" });
+  guid = processFirstResult(results)[3];
+  yield promiseUpdate({ op : "bump", guid: guid, timesUsed: 20, firstUsed: 55, lastUsed: 400 });
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field3", value: "value3" });
+  [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+  do_check_eq(11, timesUsed);
+  do_check_eq(50, firstUsed);
+  do_check_true(lastUsed > 400);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 4));
+
+  // ===== 17 =====
+  // Remove an entry
+  testnum++;
+  yield countDeletedEntries(7);
+
+  results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1b" });
+  guid = processFirstResult(results)[3];
+
+  yield promiseUpdate({ op : "remove", guid: guid});
+  yield promiseCountEntries("field1", "modifiedValue", checkExists);
+  yield promiseCountEntries("field1", "value1b", checkNotExists);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 3));
+
+  yield countDeletedEntries(8);
+  yield checkTimeDeleted(guid, function (timeDeleted) do_check_true(timeDeleted > 10000));
+
+  // ===== 18 =====
+  // Add yet another single entry
+  testnum++;
+  yield promiseUpdate({ op : "add", fieldname: "field4", value: "value4",
+                        timesUsed: 5, firstUsed: 230, lastUsed: 600 });
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 4));
+
+  // ===== 19 =====
+  // Remove an entry by time
+  testnum++;
+  results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                       { fieldname: "field1", value: "modifiedValue" });
+  [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+
+  yield promiseUpdate({ op : "remove", firstUsedStart: 60, firstUsedEnd: 250 });
+  yield promiseCountEntries("field1", "modifiedValue", checkExists);
+  yield promiseCountEntries("field2", "value2", checkNotExists);
+  yield promiseCountEntries("field3", "value3", checkExists);
+  yield promiseCountEntries("field4", "value4", checkNotExists);
+  yield promiseCountEntries(null, null, function(num) do_check_eq(num, 2));
+  yield countDeletedEntries(10);
 
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
-}
+  finally {
+    FormHistory._supportsDeletedTable = oldSupportsDeletedTable;
+    dbConnection.asyncClose(do_test_finished);
+  }
+});
+
+function run_test() run_next_test();
--- a/toolkit/components/satchel/test/unit/test_notify.js
+++ b/toolkit/components/satchel/test/unit/test_notify.js
@@ -1,214 +1,160 @@
 /*
  * Test suite for satchel notifications
  *
  * Tests notifications dispatched when modifying form history.
  *
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
 var expectedNotification;
-var expectedBeforeNotification = null;
 var expectedData;
 
 var TestObserver = {
   QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
   observe : function (subject, topic, data) {
     do_check_eq(topic, "satchel-storage-changed");
-
-    // ensure that the "before-" notification comes before the other
-    dump(expectedBeforeNotification + " : " + expectedNotification + "\n");
-    if (!expectedBeforeNotification)
-        do_check_eq(data, expectedNotification);
-    else
-        do_check_eq(data, expectedBeforeNotification);
+    do_check_eq(data, expectedNotification);
 
     switch (data) {
-        case "addEntry":
-            do_check_true(subject instanceof Ci.nsIMutableArray);
-            do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString));
-            do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString));
-            do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString()));
-            break;
-        case "modifyEntry":
-            do_check_true(subject instanceof Ci.nsIMutableArray);
-            do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString));
-            do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString));
-            do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString()));
-            break;
-        case "before-removeEntry":
-        case "removeEntry":
-            do_check_true(subject instanceof Ci.nsIMutableArray);
-            do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString));
-            do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString));
-            do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString()));
+        case "formhistory-add":
+        case "formhistory-update":
+            do_check_true(subject instanceof Ci.nsISupportsString);
+            do_check_true(isGUID.test(subject.toString()));
             break;
-        case "before-removeAllEntries":
-        case "removeAllEntries":
-            do_check_eq(subject, expectedData);
-            break;
-        case "before-removeEntriesForName":
-        case "removeEntriesForName":
-            do_check_true(subject instanceof Ci.nsISupportsString);
-            do_check_eq(subject, expectedData);
-            break;
-        case "before-removeEntriesByTimeframe":
-        case "removeEntriesByTimeframe":
-            do_check_true(subject instanceof Ci.nsIMutableArray);
-            do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsPRInt64));
-            do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsPRInt64));
-            break;
-        case "before-expireOldEntries":
-        case "expireOldEntries":
-            do_check_true(subject instanceof Ci.nsISupportsPRInt64);
-            do_check_true(subject.data > 0);
+        case "formhistory-remove":
+            do_check_eq(null, subject);
             break;
         default:
             do_throw("Unhandled notification: " + data + " / " + topic);
     }
-    // ensure a duplicate is flagged as unexpected
-    if (expectedBeforeNotification) {
-        expectedBeforeNotification = null;
-    } else {
-        expectedNotification = null;
-        expectedData = null;
-    }
+
+    expectedNotification = null;
+    expectedData = null;
   }
 };
 
-function countAllEntries() {
-    let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory");
-    do_check_true(stmt.step());
-    let numEntries = stmt.row.numEntries;
-    stmt.finalize();
-    return numEntries;
+let testIterator = null;
+
+function run_test() {
+  do_test_pending();
+  testIterator = run_test_steps();
+  testIterator.next();
 }
 
-function triggerExpiration() {
-    // We can't easily fake a "daily idle" event, so for testing purposes form
-    // history listens for another notification to trigger an immediate
-    // expiration.
-    var os = Cc["@mozilla.org/observer-service;1"].
-             getService(Ci.nsIObserverService);
-    os.notifyObservers(null, "formhistory-expire-now", null);
+function next_test()
+{
+  testIterator.next();
 }
 
-function run_test() {
+function run_test_steps() {
 
 try {
 
 var testnum = 0;
 var testdesc = "Setup of test form history entries";
-fh = Cc["@mozilla.org/satchel/form-history;1"].
-     getService(Ci.nsIFormHistory2);
-
-do_check_true(fh != null);
 
 var entry1 = ["entry1", "value1"];
 var entry2 = ["entry2", "value2"];
 
 
+/* ========== 1 ========== */
+var testnum = 1;
+var testdesc = "Initial connection to storage module"
+
+yield updateEntry("remove", null, null, next_test);
+yield countEntries(null, null, function (num) { do_check_false(num, "Checking initial DB is empty"); next_test(); });
+
 // Add the observer
 var os = Cc["@mozilla.org/observer-service;1"].
          getService(Ci.nsIObserverService);
 os.addObserver(TestObserver, "satchel-storage-changed", false);
 
-
-/* ========== 1 ========== */
-var testnum = 1;
-var testdesc = "Initial connection to storage module"
-
-fh.DBConnection.executeSimpleSQL("DELETE FROM moz_formhistory");
-do_check_eq(countAllEntries(), 0, "Checking initial DB is empty");
-
 /* ========== 2 ========== */
 testnum++;
 testdesc = "addEntry";
 
-expectedNotification = "addEntry";
+expectedNotification = "formhistory-add";
 expectedData = entry1;
-fh.addEntry(entry1[0], entry1[1]);
-do_check_true(fh.entryExists(entry1[0], entry1[1]));
+
+yield updateEntry("add", entry1[0], entry1[1], next_test);
 do_check_eq(expectedNotification, null); // check that observer got a notification
 
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
 /* ========== 3 ========== */
 testnum++;
 testdesc = "modifyEntry";
 
-expectedNotification = "modifyEntry";
+expectedNotification = "formhistory-update";
 expectedData = entry1;
-fh.addEntry(entry1[0], entry1[1]); // will update previous entry
+// will update previous entry
+yield updateEntry("update", entry1[0], entry1[1], next_test);
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
 do_check_eq(expectedNotification, null);
 
 /* ========== 4 ========== */
 testnum++;
 testdesc = "removeEntry";
 
-expectedNotification = "removeEntry";
-expectedBeforeNotification = "before-" + expectedNotification;
+expectedNotification = "formhistory-remove";
 expectedData = entry1;
-fh.removeEntry(entry1[0], entry1[1]);
+yield updateEntry("remove", entry1[0], entry1[1], next_test);
+
 do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
-do_check_true(!fh.entryExists(entry1[0], entry1[1]));
+yield countEntries(entry1[0], entry1[1], function(num) { do_check_false(num, "doesn't exist after remove"); next_test(); });
 
 /* ========== 5 ========== */
 testnum++;
 testdesc = "removeAllEntries";
 
-expectedNotification = "removeAllEntries";
-expectedBeforeNotification = "before-" + expectedNotification;
+expectedNotification = "formhistory-remove";
 expectedData = null; // no data expected
-fh.removeAllEntries();
+yield updateEntry("remove", null, null, next_test);
+
 do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
 
 /* ========== 6 ========== */
 testnum++;
 testdesc = "removeAllEntries (again)";
 
-expectedNotification = "removeAllEntries";
-expectedBeforeNotification = "before-" + expectedNotification;
+expectedNotification = "formhistory-remove";
 expectedData = null;
-fh.removeAllEntries();
+yield updateEntry("remove", null, null, next_test);
+
 do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
 
 /* ========== 7 ========== */
 testnum++;
 testdesc = "removeEntriesForName";
 
-expectedNotification = "removeEntriesForName";
-expectedBeforeNotification = "before-" + expectedNotification;
+expectedNotification = "formhistory-remove";
 expectedData = "field2";
-fh.removeEntriesForName("field2");
+yield updateEntry("remove", null, "field2", next_test);
+
 do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
 
 /* ========== 8 ========== */
 testnum++;
 testdesc = "removeEntriesByTimeframe";
 
-expectedNotification = "removeEntriesByTimeframe";
-expectedBeforeNotification = "before-" + expectedNotification;
+expectedNotification = "formhistory-remove";
 expectedData = [10, 99999999999];
-fh.removeEntriesByTimeframe(expectedData[0], expectedData[1]);
-do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
 
-/* ========== 9 ========== */
-testnum++;
-testdesc = "expireOldEntries";
+yield FormHistory.update({ op: "remove", firstUsedStart: expectedData[0], firstUsedEnd: expectedData[1] },
+                         { handleCompletion: function(reason) { if (!reason) next_test() },
+                           handleErrors: function (error) {
+                             do_throw("Error occurred updating form history: " + error);
+                           }
+                         });
 
-expectedNotification = "expireOldEntries";
-expectedBeforeNotification = "before-" + expectedNotification;
-expectedData = null; // TestObserver checks expiryDate > 0
-triggerExpiration();
 do_check_eq(expectedNotification, null);
-do_check_eq(expectedBeforeNotification, null);
+
+os.removeObserver(TestObserver, "satchel-storage-changed", false);
+
+do_test_finished();
 
 } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e;
 }
 };
--- a/toolkit/components/satchel/test/unit/xpcshell.ini
+++ b/toolkit/components/satchel/test/unit/xpcshell.ini
@@ -1,19 +1,13 @@
 [DEFAULT]
 head = head_satchel.js
 tail = 
 
+[test_async_expire.js]
 [test_autocomplete.js]
 [test_db_corrupt.js]
-[test_db_update_v1.js]
-[test_db_update_v1b.js]
-[test_db_update_v2.js]
-[test_db_update_v2b.js]
-[test_db_update_v3.js]
-[test_db_update_v3b.js]
 [test_db_update_v4.js]
 [test_db_update_v4b.js]
 [test_db_update_v999a.js]
 [test_db_update_v999b.js]
-[test_expire.js]
 [test_history_api.js]
 [test_notify.js]