☠☠ backed out by fd5a919c42ec ☠ ☠ | |
author | Gavin Sharp <gavin@gavinsharp.com> |
Fri, 19 Apr 2013 15:21:30 -0700 | |
changeset 130638 | 555f2b757639248c396ed4370db2bf1a3098faa5 |
parent 130637 | b342f87552a33951b0d372255372dfdd16c20bd6 |
child 130639 | 9c426bd08d289acaf72e49929881dccb4aa5c079 |
push id | 24627 |
push user | ryanvm@gmail.com |
push date | Fri, 03 May 2013 01:34:43 +0000 |
treeherder | mozilla-central@cdd14d9b3aae [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 566746, 697377, 851641 |
milestone | 23.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
|
--- 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 @@ -4656,17 +4656,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; @@ -4726,32 +4726,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; @@ -4765,37 +4776,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 */ @@ -4822,50 +4837,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 @@ -338,16 +338,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 zc%1CL2f*uAdH=5?Wha*evI!wK5Rwq*Vr|KgKs;pGulJA_WO!SW_qLg~*=;B*jIz>{ zSw?{n2owkeD5C+w4p1QMUG|3l{<3|b_q;M~FFh@3|54uPbM=+q_c<QtIY*Mek5Y-S zKCB+Bb^5L1@E}JVMF2otaqu8P9CaK)5Z4g@wj68yPad`ntp6$E-(9S~ap)`O9iSxQ zD4-KSC*MO}PCe=0|DFG%%OU}eJ=5U;%lxodYF1mF`PJ(ey`c|=o&Id!Ch1Ko?zD0+ z?e>I~gZp+KyjZUs3`Wz6sw5A_lfj5PnLij(@|Pa0>D6YXU2Ihkrj=a!U^Hg^uZ6>x z9vl~&BbzwXTh)O!sQ%%Pt^YuOFhr$halcX@S@G9@>wyc3I`+(<11!$d+m-5cUpuc} z|LVy4)vI^yJqkX>VAQAN_CLjazj`nhwZ90zZ`cF-<gsT;RwGW{*@#^t_3y_1<01d> z`aaNlL*qC<9h0MuJ=6V1pTqhU-}yw2{&%0qzx~?IXS3e@z@x3;mys`Dw?U<TL;ak* zlll?$J@WI^H>s~sUm!nC-9deV`Uv$w@^<Py)H|rRP;a1KO@4@a8TAtC`P8$hYsvRg zPofsoBdHP9A>T>qRGGSx%2EmPW-3G})Wawj#gT8M4pA3T=TqlUr<1RtPNt5hDDrpY zFUXgZKO(<NejVovdH3>vbh&DEm9>7v>QZZ+U(T&{W*M;7d&@Jdb$U^;*2x8Ht>cUJ z<6{|X6%nk}apzjAt5*|it*koM+PA7%YtJfct=+4bwZ3BIx7H6^U2d%(y5g<%Wvd5R z>xZn?zw26^X07GbiPl<Lt%pEiIk49J(zMpxvSh8<W!_pd%XL<8F5TAp(9&hCFJ02s z+OfRIT3@ofueH8-d9t;>Xt^FvFI+svT0dYhvep+Yn%4S$iz}`5!G&h6&s&78^?es! zYkltG!Pfemg~M9kdvSkjefHws*7~f)J+1Ybi}hfA#?)G!*0ommY*?#PuC`VuN3GR8 z*3a(5hgz!>4q2<?*H7)()2-DpC#|2&@szrL#k|yasZUeyrd~rmms(OSs<4&sUhZD* zUjF0d@_SI{gOXm~%(VoU$CAXuC0<-7Bl;BT{^TDdMgZ%f_ko9toWy{2db>X&vJzPL z<og^hvN8wmXNjjHBIhE0ZixpD7a12zyx$T}LPVY?p1bb<M^o=xKLJ1WGwR#a7pRYI zH}78VUhZE0w=54nmLkcEP6qF>{{0~M@3Hmo^WneO^O#e&|0dRtxz{li2`)OhMp8!; z2gt=ymUJccG;%?{z#8{YqOP#!{}+<qA#bAINPd(W{#l>e-OK;+C3re@h~y_x)%1ys zprvL7k79uJd^)t=Bgw<UFzpS-%v9MZWH@kzwI{pYQ_YSdZ8xpQOtqAc1U$t0)KX!+ zr&F3LYS%mF%~3q<j^v0N*JrA<)2KrrH*R?|dN^U24t_555<Tki)T=fpx2z?){^el3 zky27|I;RwyQ%2Q^Ut4>wS?@^=!rgqV6`z@+Ud#$Cm|1%g>pjs9U+(&f9z|)DgE3DG ztPd8#>pk8qUFxNxdXn}gRc9gw)`trZU+)>|13t=3oO`BM3Ha(k;-%J}z<Lio+6zT0 z{&-Xo2Vu_e5pSZX+lb?T|AOV#k1+-TUnFD(JzCWmboNrj+pIn3{P9;gwSMx6ZaC*i zilr&tGvZRQdFcUb&q>6i2<zvS#OJ`VfCrBNj|cxk=Ez&AOR4Alcf8-fUoHX15oezR zNb=D7oXR2hW>#{@4UWnll}*<ScJ2?%ymU6qYm*dBZXbKh_OYJrV?EYmiSw_0=7a9I z=q!fz%|b$7DJr2>hmA@O>Nw*3QvpRDVr0fwoDE{lO0ClY;44=UH+AxXWYG*XGPJ14 ziDuPt)E^!!x%NHyMj`Bt^KvCn)LL>jJ!)Kd^l`+gX9EfxDi!-2Q?VYte&BiCxFa5M zlqYRXpDD>?>%wCWdmQWf{o~G+bDS8@jQkx%bZh=WqH*l;*0J7!b@boQ&1jloIbIMY z*>$K`Dp#ttdP6tNW~<%l_WFb2XgrzD=7-!KuTSx-f#6;!9Erx_iDW9RWwN<^VIAnA zlV`Pt=?F}N0<HRnM#I%O&K8Nm6G^KC$Md1=%qdyMOf+YW<86^xJdt>xI_@(;&Toq2 z);MD{POwGd@I-RPqock&lUt@rPfOKQ<3w8|9#5ovBrO<bX2>YLo>UR&#!0qF0-i`U zkKXRg6Um&}N<`Y_y><UTo^TV^&#S@d;IZJ_WSYE%x`ev!KiP>J2W(9h@if&Z?RDlp zqi*G@Sk}ntjeFQ4Nq8a&HCFcceLaCLc4%XqXq;?|Bx8D`rF6SoZU^dSu`4ATdgByZ zBp04Yjy++XNOTf;v&gH%Mohow@x=N2yFRAAuZW2mAM_@bSVI%XdHr7YP?*w8u-$k2 z+!Y~0v$7yqWAmx@P?++}Ueu%I=c+K%*a;gq^7?7^P?!=;aze{lR`)0ASk+PGefsJ4 zP?$1}zpBS+XMHZ2A*Y|ur4CSG;yB`Jg7^>+!1d&b<e2<2arJ+?_y0$WejnR2VKIFa zvXYueO(xEvsfH$n!Aw8H{-R+@nmIPuR%RZxq@@!5q2|!fw1>i!^ju!YuvtijlXS>m z3$zUVEPE(SS<llJD}Ey)nWm!<2y&u+wmlT4wC8F$+s#ou(xt1R2FvI4d)q@{%6kJ( zK<OvF2}9}WN}^xU&#`|3G3C8fFP{ttgc7e61BS}3PtynS^qO<+ZN`+<;*HGSG|h*+ zbkQGe<wooA|2S*vPrMSG3LbOBqyPWdqTkp4l3>cdV=ilEM0zr$t0Tr+&*|scLt)Ck zdfq2>bV1G24!=I8)A~VsC`>tv?*{bLkoU;6SX6qBh<?636sDY&8I)_O$XvB@R;Cy# zGWz}Op)lpFP|$P*eVKAZn~h^FbEx0n9tu;=@`l>wk~n8lW-l@eR-O6<_E4B|R@xiw z`YTbNV8&F_Xo&g)Y@s+zIZID?B7we>$!o2yoHvC7)XS~@A0>#lf(yW3fnSqV@^<PW zYvE7orHAYd$MklT21Ybq>SrRR!_0P4d-{d;P?%;GqxzK5X3<(-8_D8coYOC|hr*PO z^HC!>?0G~^>#>YeZt54?Lt#pDJ#VkqAB!$s3;8B`!mmHj9tu;M%M9mJebTGvlvZ09 zMjiSk_E4D8T!Zb^%dV`hE7lBx({j4Q9tu;M+e^2cy`WPqE3s~+=nm?a+CyPVbNf%z zSzW7ubvXUQvOWI4oUne5d<LM4wpW11^a?mLG$(RWwnC>&xtre8C3`4LnZ<fn-LXW* z?=)vzYR?zYWqT-0iM}=%GI}*$R5b_3`Z8YKWeX)>O7yhQ^frX9KdjA+{4A&G54MNG zlrLLqsG|;q$)2XF?P$NKKg1piQ@*4nxe#W1URF`74Z{`HFSCcjlrQ;`+r_eMU`VTK zsWP4Dm)k?(Iw)^Da?^Z<&uZ$FmuFG^q4rRi@?~_M%ofG6Lo$mg&B#<vu)48KJdv<| zt|MMbypebtaVv2<@kQeM#P7fXa4t9mE(Z~CC1`<1gKNPH!E3<V!ENB5!S}##$&<+Y zke8B|ks<QoWRrX}`84t+<m<_IlOHF)O8%6hsMDwiP$H#LIZCJI)L&39q+UzCo%%5K zMd}A=B3XY=;@?iZAGO*1!%2JU0Cg_;`+eZI$5N+)Qn7zD<S_@R^ZxK13;Ni@x0vG& z|NaF--}7YZ{)cVSCc4cu(QUScZgXvPoA038LKoc@d+4^*N4Mnxy6qYw&1LR|>}Kc@ zy3LHyZFYigb5nGipP}2r9NiWliEc}eLbv5dquZ{>AkAe@MRv3FW0CKgunh4yRA7k( zDsaRS6?kHW3Ig$XR1k?Lpn^m^5fx<ONvPl=o{Y4cI}O>-(Zo|wfgzrX3M}y#sK60_ zi3&V%Eh-4a(@;Sq{t6W&;_0X$6VE^e7x7G_;r!|7mw9O7S*XAe&qf87xDFLK;yLU7 z&lArfte;m9HxnNuK1Y0)_%%2Y+{fzq4+SxB4d{U-coujmxDmV;d<uL6{DM4=Je$0P zd<YpPuiVk^kEYJp(eK|$ebnCX|JZ^yQy`v)3L^1*RFH_jMg^I80V=qN7b35oa0aqp zpotfu0z<qQ6<FdWsK61|qXJL-4Jru44X7Xze~Std@lsTfiI<^*i+DNGaPdrJzep3W zKm~?)B`UDQt5AU>{tgv*;?<}i5U)W6k$5dCNW|+<K_>nl6<oyYk%miWA^Rnocmpah z#2Zn8C2m9oj(8I)@Wf51AP{dx1(A3QDoDi5s2~$>MFkh}Hl*S5*~osGCjJ2x7~<`y zz!L931&(+pD)7WDs2~vULIsg{H!4WPt*9Uq??D9@@m{3iu6v{30i=odp#nqv<NuoT ze-E=yi7*}4Iaoy+q|^bYiGE{T^y*jGLt#3qG187=v+NN1T1s!u6j68ELt#3q$;E1g zSlbg2Xm!?2WOUtQ4~6NdroC4Sgi6f;Z>rsXbtvmzTPP9JQB5XHtD|;n&oJ4EoZ$Vs z&mIcXQH^8lGgWb5B`R+-#<>%^Vh@GssHQP>?#1V>>6q@tM|wfm{q|6pj%v)3vsGi- z{WvXljhvXpeEu(LYq*5#3nnTTmXmv3hmz_Ci=~pT+Fv6~hhwosqSX;Pr7Z332tvNB z2kfCR9gf8Z&N}b$WM_0LQfmlvJ!lVwE6oKPY*7`bQ8Shdab>T*XAgxb&CLyo9XI#< zisG$$dJaYp*+XGUbKPR9QjUmbj!w}LF)8R_dnjDrsHlxyF^}e?wUEgRVTT^Ef7mgl zxk$b1c2=vMoEZ~5fiy?lMbH0bZLffg?b>QuuX$^%YF4#kELqfZ_E4BIOS&*`M0_2E z(^5>TsF-@*9tu;UXQe7vcPF^4R`gH0JgXP%p)e(S$;V{de%W0!9fNGJ63`!R4}~dT zwjA>!)2$bqbZ;1u#<czjdnio#(qB(X-fDVUGWEVBhP(Qe_E4DerQQuElwwlNYN{t0 z5d!*E_E4DeWrNq^p<*W(GFw$O5lrb<+e2Z>mmc3}FFL5qG_B{H_>1%3_kZd->Jsu6 zG7Y|M<$zPI1A@!{GyT6|Yo`m>Ou20;Y}>D{uTv^vzgw@{Lt%QOON`FuRNd90Xa0CC zT-Iy$P?$cx?JUp6vr+d<n+?1(k<+X8P?$PpoKa(ZS89s1Sc?t(GreLDg(=mfv<?&X zD=CFe$!;;z*30%#m=dU1lj8YSjE$PAORcpTy<`uCX;_#RW1Lx~M+zOv<N|G{UbKh8 zG}sBvaX-~?cN26koDYhoevQ5VW17Si@_|6xEr&bWtk7i&%=-La6+Dap4+DM>21$?y z*MK@`gAsTXSb?X4XMpE{mw=ap*Mc{Je*m|F4}#mlXTTT1*TMI|PpsnwKpsb)OrAlW zOI|=aNS1VwSCA?hB{lL9WSKO`9yui+OFogjmV7q(0`dm(Rpjf*w~+57-$&j?euDfQ z`DOB3<PXT7lfR>mqE4jlMV(C@q%NW!L<!Vol$YA05>$@5nyOJPYDhhjT2fD;o=!cN zdNK7f>NV7j)Z3_cQy-u{N`0F80`)cOyVQ@VU;US+Lr1zCLjm%5@Y|y)0M0#Ty*}^g zwXur0@u&@MqSiPn3$|{nW4AbNi{rOAVT%*DIBAOmTij!dleai!i+gSX9bBhwaoQHA zZ*j&JXKr!U7H4m9?=8;R;yzoPyTyICIB$!CTb#ee{kFLO78h*sfGsZE;-W1s-r|8< zT(X5@i%YjSw8ewAaBe|w!EC{9!EM2BA#5RTA#EXV;o9QCTRdco%eJ_Di-&IUur03G z!o7uO3-1=bEtD<%Tc}$Ewg_&qw?$}+@D`CRqFcnah;NbDBDqCsi}V)S7MU%wTjaLL zZ&BFd;afaniz~OdYKyD4xMqvu7Nsr9TU55FuKWKZfQtYwa0O676lmZPpbQMq15@x= z@I-Jecs6(exB<KhydJy-yc4_++y*`YJ_o)Gz6E{=en}Em|38I1i#$kPOwy!m_5J`E zCv)UAWP|KlegARflgX!(&m*rVUupIHo5^>PA0Tfh?;!8A`uz{dUs42h9CZqHmeuPo zrf5p0uAl-`-0Jh!Pz|a}O{vFGPqupe^Qh~oS5mL1Zl>O4_4nJUJE%LUZ&5#_e)&Jz z+tHrCe*NlOG`7&UFt#waXl~KkqP<0Di|!V^E&5vwwis?P+G4!LWQ*w*vn}RZJaUUi zZSm+W9<#+`q4o1|kPFBq<O=ffkWYYoBIJ`GpA7jF$frX71>|2sUJLm&$iISoI^;7T zp9%Ra$Y(=d2l*Vx=R!UY^7)W|4fz7d7ec-W^2LxZfxI5_Zy;}g{9DMELcR?0<&dv{ zd?n<oApZ{X)sU}&d@bbbApain^^k9Xd?VzIkZ*##3G&U5Z-KlS@~x0>gZu}`w?n=I z@|}>kK)ws|-H^9#&;Q>*5I2AWR*(Oh)py@+^~7iY+4_}j(RKU03)goFTYg;|%88mX zbBhV7pd0p3nC4wgeeCUUeW`7>a-LE-WSGYj_qW+`FrCn7i@_0B7h_7Q)Kd9?(cBRW z(=2&h>6-4Q&XyE!#WyKMjMk1=m?r<(o*d2d+8(PEO9Q`8G1@y~VLGO17<*hW8B$fV zTIx+(y3yGY3)7+6xS5N@0@Z9$>B)t3Qa8FgVqrQ0_3CA&9OQ;Ix)>;lePccTbDQgb z#9N6E5uYc%NBj=l1DppAfy+PyTm?Gdan>=w4S%@$|1<I!@=R;2caeLCee36S<So{y z{$=vV$hUs#*7*Gt>aVF+@BY@$ku2ziOOE&eD)7VyQ9&R+gbE_@PpBXfx1oYed>9p6 z#77SQIuU#P;a68#`W&R}3QZFqMFobq9TiyOW2nFpA4dhA_yj5l#3xZfBtC@-67gwN zkcrQrf{VBVX*h!xkzr`!v#7ujpF;(f_&h3b#6P0~PkaFt1mcURAQE3f1&O#56=dSe zsNf>Lf;5~x7unC!#8**)A-;wREb(<z;D~Ra0#AGs6$Ij2s2~#GMg@uZ4l2mRcTvGb zd=F_jcVDzZ4^4a@6&T`QP=O_WfC?P(Lsa02AEAOk{1_EP;wPve5kEx*nfTcO>SfmW zuMor=z`5WlR_`v7AEsPu;RqIEYEM|WypeLQpUG#1R?UnxddyHWdOMOMrZa*2OG6m_ z9kDQ-;Ouf8x7X2ecr-mca(TzbU`H%W^Bk^H$r)OGTBW@L!-tE;a7Qdm6CLL;T<SQ| zxgwpNg%c6Z8149M;5u)nPTkdd#GBV*a;%Y_7~>tWa9t%T`@#~ROr%UU;tVEQ#$-n< zOcR}0HkeTcWi@YlhyIx8w7S^C|Ma2r-(9xz|K}0J^T5%d557c($+uD$QqTHt3iqF0 zj0Ld2zPL<~T`3dxYr~;vwz8RK!)+|c9l<aqux@RT^~<GjPhmssv|t6Jb_By^FzinK zNnf!ZF|+e{BiA<;N9_oP%VHSI<z`M_d8V|uepTa*#nC&0Vam;Bdk{!8l$1}?%biKG zXe^G|5e!put_ZzetlIUrXh(gbG@8cZ*d4(zW#|1>jEu!`JAz?K&;3YK4Eov?nl9!g zS5n$u|8o%3v#1Nnw~}G-B`XUYZ6yT9jxKpmd%JMi`<fLgndk;Qs=`L<u6V>)oU$Vr zrnk4?Et&0V%AeENWN_+B8jF*61jALvq^J*?{LE?6)q*-NTfy$JBN(PGS*VO0?P+5w zXgaOYNr$mGup=0zWD|`J!*olZ7L=+Osd{3@;-nqHFs0FSiffCdn6FIF5~H~gHx?)E z2!?5Zhz81iHjwo>H8wufvt?s(!j52=W)=~y7xoFYfTk6l?xN2cAdcS=4AYcJ%=Qc+ z6P>rs(6AY2TEFlA*3W-5127i%+R-jdd1)5&D%E70E^4ECzgwCbi&O2vcuX%sC>=AZ zioxY*b|!TBnz1--M=(rpFGqJf0Z*In(`-JiMyw(E^c}%4eQ@0&*W5!_WS%ZE@fn*k z7H8}ThN(+t+Wd4cKj00sC`=X8%4}!u2!<)yaC)kpZRaJ^?1i%|V`a9pb_BzeMmw4@ z8LL&FW~O+RXM)D!>>a@{4G^+3A7<2=(?J)f*^n797Wdu}4A)nU>TPv8-V6Jcq7uj? zYDd)n&)Lx~OymF5mkaLY)JlWy$(3}}Wi0NqBN(oF3-$Ue9@ZzCS=9W6sr8DVYY&FY zi0kP0_smQ@;WVqk;a+)cEbhA_7^Y++cA05Ns&J~7>MHey*I1mlBN(P+6O_ktE8H3k zO|h#dCsku{a7Qpq$%dKHJ|WYSJLZT^vSrCwoWCO&retH5JbJbk%%@Fu>Wl_-V{yM7 z!7wEok&(wftvc6feI7IvE7{zCM=(surkkzx5+i5a(DYzmPU^ou{<qEl|ICZAaM)i2 zTwn07=HyDPo{I<RS#)CV3C7}*9l<btaD&CJTXIIbQ94!@=#lkVdf<*=xXv7T>B(ND znCqHFsSz_oV{!40V7RXRO$NEW#NI5VWt)CZw5CxP?Ffb`je4U-$0rPw8O`d=VBC6U z7w+gKm<9+jHtjiU`8ln%26@h99sfUIM=(r7UoGRRhm!e7g09YD*?81gT(Bb;rtyD& z&EfU+KbrU)Vf}o2r|<WMoxa~)e*dF)`u*SeU%m48r!L4982&uu$_Gyqzd!|s_$4Z^ z#II0+BYuqvJn<V;5QyKRf=K)h6(r(cQ9&mDA1b&2+Hm0@vR|M9Km`VnsK5dW6*zDd zD)8WFR1m;1s33x4Q9%O7p@IyKM+Fx+0cp5+KC)k=!HKBAfRj*x1qV=p1NT4$9-NE{ z0yqT~L~u`3kiflAK?bLyf(x96G+eqLvR|SBdVdXx0cW5B3(iCZ4xEJwJUAN_1aNOu z5CM8QS&{&HnO2ekdihgwf%_s2mytV$urdwM553F)^ur{x;QYJp`G3xy1SL$P<o@dL z#)97w4Ac0(zm~hP5OxH^G_#osVx(V8?zK%veKa(znT@z37^aD9q9A(AF&*-nReCQx zv}QKaj$oK3t~}ij$>yjPGmFDQn06Wqc}Flzqhvpp8D;$R*f6u2G?`k#Tswl{`W9Nw zB?a8GLPTkeqI1@o|37#~FibO>{dWyGqVxY=d%JM?l2RJyp80c)jy5Z~;{KGe@azbN z>Ft#TEv?QniIgcSOv}?S7VaIvaQVpSfZ|Hl3{@Fb%Jo*=SX{9q7^W`i@l{(BK9V15 zv#L>QTT}9f?Ffb`*`zxuW>25*g|(17!AqvGc<7E`xK1`E(#@LYj+c~>NoSh0vABFk zFiZnPdhD1+E1AehOO+FJ+AtQE?FfeHFv?SHDgLnT9h<68tPJMH;vqYNVLFVerp0C` z-b!}0s$OkMo+CQ{=iAXPOnE67PKM%gqC+bwE?iGejfG+lhRbgynZdHpsI^9BC>z!2 zma*{f2!`qHZN_t>R<&XcO}ZE=x?ILW-4P7e%`qY+rBM$|{TfR*YkJvO1a<_&)Fs=F zm?)$oS%%j8Qf#aki{Or6xcrPE*38s>QI!_^dfL%57JEB_VM?RTY(9`NWp7Q3iFzYz zWwy|cV3-Dopnp`Gb&Bqi5^MFdQr1|6cLc*$SF`IBil*kKi)Ah`mX4_ZM|QLe)A--1 z%oI;>mI|6;&><8G#v-~S7^Y-XoaZN1#vhXCkS8Pztz;9k1!Hl28^1o7wmie!Fhq~U zS=McRmf|~tVM;d6h&$&oMovNT7PMNeX)F>ug5fG&>`|2b+;kRAM?vt!;>IGmBN(P+ z6Jb1NBCoU>bgQE@x?y9H+7S#>vXR2Wk#%U(Et}bCx~^LTM0!UsOv#3oM&5=?OC}XZ zFB3{ejD@x%7^Y++_;}52&1<5jSdug&w?6-u2QmR<a0Li}ILLu(Km&Ba6g&<*89W_4 z4_psk30@Cw2JZqN0JnoXz@7g;cm8b_=%0Eo<oh815%T?zAAtNI<cA>t3Gz0`4?}(g z@}rQqLw*eM<B*?#{3PV3AU_TH8OS>zKMVOe$j?LmGvpT_zX<sy$U7mw4EYtvuR?we z^6QY_fcz%pw;;a_`5nmbLVgeO`;h+v`2)xwLjDNy$B;jP{3+zmAb$?|3&>wW{tEKf zkiUWaE#&VY{}uB8Z3*D}3;-kvNkJY3c{JoPkjFwE2YEc?36Lj3o&<RS@*a>UL!JV8 zPsn>g!drQQ(;!cWJOlDf$g?2NhP*fAIgt0c>t6r6(w+oy-2|!Nk|WL(*Q;owpi<(j zvE&guf?*oCLbVFp90tb&B{bzmH7mV8d`B=`w|5yZO;1QuT3VLzh5HU;QP>d-(`2LK zbdMTx(AQLo)nPkn&HwW|g5f%5t0*pCXr%FUZ|-UGZex+#F-l^ZAa>jOh@VvC937hI z*_bsY&+Z6@Y3e(i=Ob-@c}Qz&x8U<8j74TgFkH41hZ;|1RR?Q^s(Hpe+Ft+rJaL%w zj|OgQ9#FLMzzRGYybQb<yx$u2zYTtEjrVrv-}?HW-TAk^{)fr=_a`stwGJNayF4O* z3s6A>4?qP8T!;!XxCj+o;9}$wj0^pfE*d-#6&P>{DzLzT3LLl;6?kw66$J1gR1kp^ z6(m5Tf(#f`Z~^vkYa@qTyW?o&h2Ur!aHzll9u-(1paKU(RN#Sx3IdQ(K?E*Tkidgc zK?V;&1sAvsX*h$tVH`t)%Ta*=4@CtQJPZ{$a0M#xz>Nw5@SuVSyr>`nA1cT|K?N7^ zBMoPf4;IJLKt%-x1W<tmK~&(t9xCu4gbD%(qk;$`ciroMReKV|cM~+)Be;#?fL8fB z&s&G&wH?7QjsG5{=T6OyY)MJQ%I@IISk!j}!!)xIn|wNzEjdD_=rKop&sa2e1jBUT zI%*F}nNfq|%&P8frjo`&-w_PcfomWoMY(#mtShX$%BS+i!q^cE(<sTcBTSV`Hbtcs z7!Sv7V_`4Xz~b`%%;~b4sSR|mDP}4$Cv7b3^%_`Q{-5N0I4EfSfMkkwWj5iD==z`C z%E{vL|2Rb-CwtoSvFWIo&6r>;>>gDXm;a|n8)cqx7JW*#$Q5#|v9OzOSzP{~NV>%~ z=GD5()MtGk*Dw}#S22sr|0Bk^;Vkd(2bHW)sOWxUVYf@Oxcon+IO=*eN0?Wtu~bB} zUPgQ23Kp0DCl>VZ&craKnb}mzovIoOdqoZwm;cAdj<Z@iDCe|UKN{$?jD@|_2#d@A zqht%Nq2#R06}IP2PkhG0UPFb&<^S=|3*uy6clwm7Sv5xT5uN|H+pAez{vVBPI@*#n zFDnj5(A1{J!tSqTaru9my<l@RE_$uEm#Y>c9%ErIV8G(?|D;Njkm&NyLbSKwGbh&Z z|7=GvT>hVSCRWK;Y9qQBD~`FYv6$}&hU=Tn^+rlb#p#?<Y)%<fHx`fF5e!qZX?A-a z)@(QPiYPakuFF_FYDX|kX;fko5hF#5Iz3Zoa?d*cfAo%ExT;Q6G6`=jA9pFKOoQ=R z<NsrJ1j97+?Rnaro>2+3l^N#@b&MnG|Bv0#E==QpVoXoEhSNwWsg9rtLt{zo2!<)y zc=?*yW$B(^7L6WXjTlRNu>lsB|0kGE*5i6GE19X#Ea!0=OM9^a7MK6W)fK$0L{Z6` zEq^)MpBqbiu>lsB|EE<959ULku4=QGU<Sg*(q3$U#pVAQ#0q&&mSs%Zo2a@y17m3~ zHo)TY|1=$?-aHW))XkWW%P5Aiv=<v-aru9^Se==3aW$!=<a|K0UPgPd0T!44CpO?F zbl4NgnLXCs*<<dq=l_>>OFoOs|C1HmfoY)~8k^NxRcKks#%{@Haru8b&2*?C`TB<D z9UD`{Wi0KMd={7gXB4FClR!{y(<33-u3MSSZpmkH`G4qUPEU>nk98Q8oONP7V`;bK zv$*^}(GFkk`idS!X_bR9Ps~`_E%_`q|4+FsCe%=n(Td@qJ6blDc1u2s%m34jxf#Xp zR~^bo7gF_>v9#C!U~&0>@?l}<lB3S3rnWl`@BA?T&v%L6?!52cO*`-V*WP*Gzl(O> z_wUX>$N7KsRHqDLsNe!|<P0o#F|wbdK>`&RkVFL*q)>qaX;k2Wh6)16pn?dps33tH zD###@3NBDU8qOn^7_4J^@NiUMz#~wB1y`a12d+W|9$bwI0=NbhL{LNp36xMl24z%m zfeO-a0l5^y2{fpp0t0HOz=Ap|aG-$-JkU`=00t_Ez(fTJG*LkYEmUxUHqvkrxn#qM zH0YoL1G=cdf*vYxppObX7@&dxhNvKd5h_Svj0!TCpn?lbk%mjir6o?H!R#)3{(ot& zCBfqI|47~*7vIZsnpUlb>TQRywAYegaruA3X{K3hG!j`eq-J?ZF_!jP5-cwNkL;2C z(^5rC(X&vfnd}=&do2kTm;Z;~vnH&qSSzknYc(~g8%ukE2o{(Br&Vrx-KG>V6t(C# z#i+5g7l>eS`G0B=S7q)JLJVDOwx=~~{%<c3!Q%4&&_+p`6y&5&i%qoDgfo`*0ud}O z|Bt8dVP<|Q9#vFLYaZG8|E1mD&EoR^RELbOo@??I)8U?1eIa9Mw|BF+{69j)Y^Whe zXrP7ao{`gQEbaDg7MK4gtAt{mWIisKqN-;2xUsa`yIEZRAGhcT*Jds6SQ#bu{9WrP z*<MS6#pVA=YFWNhb}A)BOb2Pk>XP;<87waUPb6FP8{?)&(AYWO(dNd|UjKu|<^SQi zrj#tHLs1LGCSlQ<lG`hru(<p`y<Te=HhgrG&dMVp!5B+>%@!7y|0iTLvvkHcbSu4N zrBA1h<ow^#UQ2?-<^R!|vQPF5{3WgDPSictXUSeAgT>|lNs5vb)klGb5{uT{RV$di z{s)W8|D&cookU2AX0)m~_tmV-X0L3*;`0Bnd7oC~{ces9^}I!1FqW6@2!^Rks%(Je z%6%zq>X|m@u*UyGJA&b=*-{a*qnac*l@T*`x2??fpdG<5rO`=$?01j*dRk%QV>8__ zmd+i)FbxnXwVUjXI>SA!$jXU`HQAtd1j97+b%I{W;2oX}T}?-Vq52W^e`ZI!FpdAS zPCy-42gsVqHl${$XDrzr!EoKMy6O<irBqrlQ?qh6oidj8Vgnp5|4)%CBytgFu%Q*L zd?OEyrM=hyhs*zyDtV^WVcX|X)N!tvDjG|Bu>lU3|3@}T=5Xj>6fH{&-IVp7*^3Qu zxconZX;Yt7hk2SFHR@$PVl3^&1~^>)pH3v9%XOI@DXeaeXV#ExFE+s8^8eIRi7pe$ zvXZGf_T1irv9uQ(;BfhW)G5=8dWWrnHD^m2`TF|&Uk+SB-0lAP(-!EXxG&^+kOv{p zhrA!;{UI-ad;sKykQYH-4EaFFOCTMPmqH$bd=R7)l7?g;Sx63&hZG=1NC{GgbU{8C z@*$9yL0%5|P{@ZtUIFQb^gwzceUJ*IA5w)3Kn5ZAAVZL0$OvQ<G6orkOh6_fQ;=y$ z4Kf3nh0H<bAq$WXhkOL&m5^6KUJZE-WD&9iS%$1YRv~MUb;t&!4rxG|kWI)IWE-*r z*@f&u_8|w5L&(ux_xhi`BnOAf|5M_*AU%&q7^Rh&$1>KG++LD{!{z^x<Y8f$_6B2S zs%#W8oUyc*<lu1mf4Cfz&Txg2iyk@JS+_OWu$ScEaQS~+jDN4w<tvP$2CIAioUyc* z<lu1mf8ssUV>X(8k*+G89Md$G_L3YNF8`0B%p;>*H|x-5&hofgF_!j{92_qHPsBB7 z#PojEN3*U>Eu1ix_WC~@F8`0Oucg??lnK$j>97)K|A_x53CcVCKPQs+AsyspWQ4qm z>{uiGGsqjr8_D;QpCZ4y!~e6s{&!dL|8Nou9*GJZcoZt|;L)fcfXASM2p)?H5_lXc z$Y6mAF0e!{!N`c2hm&crLInmq9u-*d1XSR_6H$Q&PeKI&JQ)>4@Dx;#z*A8{27iGH zF7TH~!(E8!h;z~4T2x@b(@=p0e}xJhcseTZ;2EeOfM=qD2%d!s5_mQ$$ly9uaDnF> zZf)XEJp5_!G-8$FX&O8i6&Ub5RA9mLQGo-0jS4(?0V)XKg{UBc7oma#UW^JdcnK=F z!1YMO8N||MZGyi+1qR%J3M}|rRN%l%QGo|9yX#*6v)7X0aQS~CjlEKtW!i#L95~0B zs<E`!lHqXqe+v0b!i)uiWvy4`rzM}UwAYg1aQT1OL8H@gHTGPzx89y+d1Gm>CBxzJ z|D@UZxST8W8%nC@7}mzd(q15j!{z^>!_9%F<N{4vPbE3O-&ooU#BjL$KN%((_qly6 zr-(i!5^Nhwdx01Zm;Wc$RhUk95c1JgryA>8b2fW{7!H^JC)#ZVq~ts>G*h`MTaO>n z^*_74o5SV*;U`hm^ofk1rDg@s`Wu&adpC#6|Kso{`!mtciL|QIu`FXO?e=aCm;YzV zbR3~ZDH~KA?Y5TIjiue*&EfL@go^D-J`jk-%xXF3tu&0Ky_O7z%m0%Og^W@n>l2kx zQqFs=`M<qN4u{MClju&0MaehQ6w%pK@>64Jum8j0^8fVexqh%O);#7YF{#D_#?oHd zh{NUo$;1;h%c=FgSxvM4Hf=2JHJdnG{-1g@C!}({?7+-U6+K=)qVxauS`r*C{|{eP zQc{VXbWCr+KNUn{X|IyO;qw2OYM*l%fwW6YH7jOvZY=HfKR8_epCLCl{Rz$)Hobm* z&@LHEdu0<2m;Z;ZISUF~WZFt9(yzum#`3Bi!EjmflEN?;EC<`B!`usdt(nc$JA&al zXA{nsf?~gsRa$Z}>k1poYjy;~Rg#MdIwLiWtti>#m>Xt{WpPI^OanwMoO5`JxnxBP z8N*J?>i?x3!7vSd-Yi|}rJ{P0_9j(lB6dXmzr3Sen8trm6nl~8bdaPS{(wqby`-`u z7^Y<7i_#Tm-ZXp4h-t7*>w{}AHo)QX|0I+3K)(@_24-xk)_vCW&R%SQ!{z^JC)?g$ zr|01{w&(1R9mdjLY=Fb%{~7ht{wPyXWwSMO=kwMmX)iXw;qw2KHD^jr2iqBwEt&b6 z^)lLv4RE;pKZBlw)??*n!BoqxM%wy-*ozHtxconH=bUavhi<>ticV^-l(DoI8{lyH ze_FM%oME}T^~`dtH1FJX&;Q#k`5Z3)Pomu8o0&<lrBnl@@@&so+AaAUF8>efO!#s> ztvR68bUs#!8cVw+pTp(<DU5y1cq6?oHsF)By^684Tk<(v{-0<#;q5zUF`<M)!=@B7 zmUc@%hs*y{&KXLms@KzWD>uuutV43UC7;9P{}D2d@PN)oty%IUtvbBM(r(G;aQS}< zqQmQyN^>vGYN57YGM4uG9~>_K&nz9T<Y%<%rPVzjpY(6f|KCAaKfk~Kj{{r=BH${} z0gnUE_{05wz6O3q9%HNj^&;|h<Sn-PUm2=SP5-?0zt9t%B6tNVNZ^&IAcI$-f(!f| za{iS?Dri~rb?|CbV8Clofd#Kc1rEFp6?pLXs33sXqk;(DfC>_LBPz(?MpSTtHz5t@ zSY$s(gPTx+0dGbH7Q6)&IB+v6@ZhbeAb_`_f(ZTr6(sO>RFJ_tP{9S>i8P!?ijG;w z5#Sb7V8FXjfd%hI1rFSb3OslZDhS}cs33y(p@Ibd5fx<cepGOQ4<HQ}km6|8F$wq} zDlp(fsKA1MLIn=oh6+6RFe(V(Bd8#PkD`JEZbt<fd<+#_;Ny4Q^Z)i*5*#l7&#V_> zGxdC+NXNA1u&)_Qdo2kLm;Wcz7iUZ=Q0>rJiOXp-V`;A?!Qt}%#N<jO6D%odr8o`t zrM$7U*OK6H`G2O3ctY_F{dJ`!xf3#LEbRp%I9&c8Q}(b4tyd7tY}V+{ea6aOAcDi? z|B*W*!QpdHIkOt{djm0JWiJrHVe|iZMy}zAm03mg4>}IPSlJ6iaJc+GAxZIeg2{Z& zbi^m!N&iUB|F7)!ZVs3Kr^Y2prm#22n<F;v(EY~BZtv!B`G0t)FX^{tlDwjGzJS>` zR(5+ghs*!dD7TZ%YOEhNQ~t6V3L7iCy_>`3|4DPbcA%t8VoEg|8!Of`v)7W~aQT1C zXt6luT-B`R@Wn^|lCiQ^$>4DLe|Vp}k%*)<uQDn)tE^$H?Dao5T>c+7Jxq<#!G@Ri zR_Gv?Hdgk^CLAvRkKT0gtx?k3QpAuu7GjN+y=DuC%m334hJ*8-U+QStY^Lbz9?AK? zmA#e(hs*yH328%5#LG4{HcdO{ys@%Z$>4DLf1<slry$2l89HQe{l1mn?e#x6Z2li< zBnq;tq?pA)IiD#QD|=-V4wwHY;;ct&)oQ=1q$YzrA2e2{><EUbO9qOinM0m4K4s=g z`2uxgb<Z8aaNUw4xR;3)Tw>mgbv?03(OBJUM=(rjG_Q+Np`MPpOs_7uI4g~wx+55- z0ivQ#OT$vG!zig<D#B~V>a-ofFb#d;NUgSue4<GA+F5fFULXI5!S%%5-}Ap~F7SKW z6UZs#3~~<nNXSP)J{s~dkdK9Y9OMFW3AuuNJmeD~p9uLR$R|TS1@ftoe*yWIkk>*! z4f3xbpAPv9$Y(-63-Z~J*Fio9^0|=DgM2>ZUqikC@`aEuf_yRLOCYa@{2Ry{ApaKf zrI0U!d^zMRAYTdjD#*Wsd^O~2AYTjlI>^6=d_CkFAm0diBjlSPZ-RU?<Xa$bhI}jJ z+aUh|^6ilCfP5$9Es*bmd^hB+M>PJQz9TQ;Itd!Bw5zk2Q`bVpY#^>1t21^4!!+5* zCn8h1TNkUe*vserNn>R%Hp1ib|K&&0yyOoRL`|P~hl7r>vKJfSaryt+6Q7^W1ZELU ztqW>qVyx`NMtEHQzpB}cjdBGCr?mvp=kXdVd$AE7m;bL24SK|Yqmx$HRJYwu7%O|R z5gwQSk8_5_s9Bu0l~J7$YGY$%FE+yC^8bxTMx&f5@kOP|aH)nhjj|UT;c@x@8pUEG z9i}r*+A-^;n}xgV^}m(9BnOYn|KpWA?Rm3ZNz&d-vT0cT-(He~$L0SKyLm^ToU9G$ zBIiuU8DnKH$-(3D{{#bBJwC~JRc$t(Hx289YcI*c<MRJ>Dph~K8yp+7Bjio>YsSi6 zl7q+P|4Az4NLcT73TCTO%TBFcVlT<T<MRJ_oyD9uW~x=qD=<>BWvuKaIe1+DAF(Dl zt0te$nW1)c>hKvWd;K3Cm;a|4<MK(Gt2j+I=c2RI_55EXp78tne+R&QffHN-5}*VI z;PJ5j-#>!SSVsfDB2OUCAulB_hxPv+Yt8%rhI}LW9`cjq*U6v5`hPMNqzc3biJOU6 z5YM4ZYDPT4n&Cf}dO39y^<L@|)K{n<**g2-S0x0R2ERcrQ@C8<*Qg+aU!j5oeu)Yq z_yr<}H24H6FyND@z=BVq0tY^g3Ox7>DhS{XR1m>uQ9%NqLj@Up9u-{RpOK445^~=h zUZTMlP=NtoL<JUn2^BbSCo1sZ%cvlLub_emzKRMG_!=t6;OnU10^dLyE+hB%;bj_p z6BQWnEmUB^w^4xu-$4Z)d>0i2@I6!z!S_)?0{?;vGWY>1xWEsQhP!0+vqOU)p#lSb zj0!CH2`X^lr>MY#pP_;Petx(sNjT~7CJ3|(*<>D!Y_bnQHaX;;M*@F2vMD?i*%Tj! zY)V%ko3i^Zd;M=^uO-9d^8d`;%^2gEhSIc`uI57pV`Z-;!{hS*G}EHwFiX<N6rK50 zIcu!!wPbi){-0^MT1mL3gS0t{Ip{ssSlMgI@VNXxH6f}v2XSpc7c))SAsZ`uffyc_ z|7XuLEp&?kcA$ujg5k3cqwED@cwGLUP*9$DjYh$z%slhX*k!Ej1!8zy{+~`-m>LbO zpEPHJ*@{+;mAya=kIVlvqGMTytKJ>cYR|LhsUFewKfAq~$L0TN>aJp7n3$MKR(1&~ zzp=90yLnvxpL9$P`zQRQO~=M%S9fTv?DlRRm;Wc}lvP*UZ%VXi3c@^RtnBt~9+&?| zc27pGo;hjJBe|Y-(8kJMONPhg|4}rJ>6LSPeXZqp@|@Kr?NxGkT>c-v&1dp$CLB{@ zbZ6MxGgkKcKRhn~Ppn=Zo3lhetAu(C7b+Sndu1aYm;a}ij2EP!s#KMkVYb+kv9i}} z;&J(Z(n-Hc_apt3Inz5%=irFW|JiFv@VNXx1y{-8aBHcynacP(Evrk~t7P!F{6ASX zot@9KQeV^E38o%4R`&WIJTCu_=*opcWl`X@l#9<sW5&u}*@VaC|C#p-&UR)b_mx>b zUCdc8qi07jTz581X*pAKDg!zd;@lbQz}34W7^Y+s4mU%B&Nb`GsK{_JYe@F(2!<(* zCLEb=oXdKr^eExVtuHnzJAz>vAh?;5Oh@;aq}GZHtSe-!{5yhS8u~`bu$Wf-a@JJ$ zay1WgMEzgg(JoBmf4uJ=G3B7wWvbOsIH4P>z>Z*;l8xN1&Lu<jW)!d4)l#CdvKJfR zaru9yQ#BCoITAb_(tD$vHA}V^8{l#Ie`@ob+SB%okrFa}($qS#u@@WQaru9;eIHi~ z)B>y`CW@@TXRPeS26$ZlpF%r3_lb<Vq|M@~tb1;(?8OFnT>c-$=k@vmx)3+jMsm<> z7%O|R0Uq1^f7+ZGb%}|#VjY-O3YxL97aQPl`F|vdD>ekB+fa&~cDL!g>z@C&Tk?5q z{vWl|XjT*DxE2b!>gvo`*)91zF8@zp>`J$bX@}FAxSEM*##q@c`8+QFkCb4uH8qkE zm0~^~QmoIC-ICAa^8ZYHNnz4Uv(^WfEzK*|%*JlX=dt;JYO{hvFxZ)*_dD@&-&ol# z`8+QF4>uKKZmv6GG<`Np_9bIwx8(D<{6Bk%NkN;7tfHuFB$$jED|`J99+&@TUR6h- zS;Xiny`Vc=sjSccKL`{EPyh`u0Z#<iftP|C!L8t9;7;%Z@UP@a<T>Oeq>BuYS+Yis z$j6h<B5xpHPu@a)g!}^eJ@Ply3Dnut#gs((sWerl`qYAYI`tCjb<{hk+o;b|-=Ti? z=guX6p5^e<z2bqxPrHh=7ujTd$R?{Gn;dedc#&6;O(B46ia}&k+Cw(w5VGkCAKpgk z9*6Ig=m@gOM3GH4hHP@ky_6(Afouv%WK&Ebn^GFtlr?12l{vhP^2vwql<6$8$>fkt zHjiv_$ZeBk{^7``@Cam6yb{@zu0l5DtC3CDHHWv+b;{v8U33xIWJ<^;TShiH<PJ$L zzKU!LHDpt)Bb!nK*_3r;(`8uQ6ts)2>LEAy!#_RbUHAOIy_N)z%l{+S_d5NQHb`h{ zI2lV=M_l$=5<D*dPfzyo6M>f+rgzq&Yc*qKuO-3b^8X|g!-%X#dKtRNR0LneSlMey z@VNXxnMN=hPEEXXGnH;7TUldeFA%}w^8fJ3X{y%9v30uWiD}Nfv9cG4;BonXN>OP~ z9diAy=9N+b*_uY#3q<g^{6B+Hb?kTM+J4$wlX?^D(AQodg2(0m2|84cPV0NR(sRwG zy5oq>|J&`|JTCuF*Kp2qY$`BPRD&~yoUyXoyLnvxp9Y<hI*o9?s<CXUAL<w@yS<yo z<^N$jP2Y4}>||)Yl?`Ny#>#H*=5hId0-afAI4#dv#ZgF?TS;SOuO-3b^8Yl`n&J@R zp0Jr6?3HG5V`Z<B!Q=A(R5-;`$j~yYj9lG(sbH+^^*?xA{vSuD-j+v&x<^w7#c{Q1 ztn8IdcwGLUOq;IM9THd4deND;ZzUUh%@!V)|0l~9eZ{2OubM^9OofIgSkrSCc`-q@ zthxQ8$tRH4lGl+hB40+nmb{642l+no!{n!|dH&bQ?~^|xe@7in9iUF9&ZQne9ijy4 za!R2hltx`iRjC#=q8?2>fx4Euj(QRGGU~O|P1X^>`=}37pQ64%eVzI~StYL|H8Mgf z<mIGbg}i(DU$mS=9RrB-uYKl&?zreI5`KC7H;})E{1xOcA%6k+bI6}Tz8CU+kpBqz ze#j3%eh~6QkpBdE8{~%}KLYtt$lD=52KjNwPe6VW@>7tXhWrfV9gv@e{2b)xA^#cj z3y@!g{1W7ykY9%U3glNIzXthr$ZtS?6Y^V---i4S<aZ&z2l;)-e}Vh~<PRZ#1o>mg zpFsZ9dOuG*?$IZgCufld$T4v=c|1WL4`%>R`*VNN?q2>Zi?N#8M;%<Qvr)_(%$xqW zM0<0CZaHtoJ8M@XE?zzAIO5z>0R;}Ze8t%y)~wVzo%P>1Ug;|{U#y%>m{nSc=dHsi zdw~c6muDjqXT6!ZOPSKC@`&^FjMaR{i-T!O-izjfLE7D(nO;w_<_#FD*^XeC=GpnI z5!O0Vy{l!z112*+0bEa<4W3Gn_aZN{#^oegCm#i#N<M>p1^IUJW8^ojG5=oFMb=24 z0=H2OYlMF$^-AiU)F-L${MY~cAO3{@cP+=@S)WcFBKOZb!MDLzh)a*;FVLJU#T%j# zr>m2p>Q8KvJ$)oCIgL659IPS@QtE)yM87dEdLOoF+`g*GAh&sFu)!8paT+ya$q-le z1~=lKFi#{piM(0l)nOxc%0|@El_pc~#La51W8{XJjaVDIVjj&&Yax>t!jAAJQop}v zX5tB_Sq%>N%46b1n}?2lrYa5?D~U8?oIBxLi%y;!dt>QAkFFYCrp9vX-*<{WPWH6t zW7APFn=yg-*+z}Ha~@;l6clektJRvs?VCRkkB<8COm3MfJuOvJCv3zF&54|pt<Wh` z?xy#|&F^QrPN8UOZn{|J5@U(DZKGD(ia(c4&FI-caZL+g^IV<7aH-=+=ZbW87EVOC z;aa4Hrnez<{b6lp<Yzg}wGkz`5N3N`R#B@B!xeqV=3U%Z5(F$Z>b?@_AhJ>Sm3#$P zZq$7@6$a;S)Midkjs=fT$tGu=SPz`EKL7s!;<E%e2hczP4+lNsv*1bKh2V|g1H=!& z7r{@-6Uc+4NQTK0Ikk=s{??i!eAGG~_ziW>ZC<dl!egjsgQKaxqi(V0`rrLiy3l{a zvVOWOo93ITkQArY3gc+7;D;M^-vK%J{#xCCehGYa{q8WdZx#~zN>K^5I&4%TH)2_x zF+#spp+og~uMlC^zt7BQpOERv9dkq{*|J1jzlpW4_#fzR)P3b;!8PkY%Su(Q?oM!7 zt>~Y0c{aa!s5J66Tv{@zIC_~-GD3WEqx##7J024R+L0)=8)M?{H|oBde*(uw-FHg> z5L&DI9d6(S8<p;M0-iSCr`dd3jr56UZ`4?wnR9V9sifq5KnsIoHfmgHb8c^<NvE6= z%SDMVZPa}?AOf4z!R2duB_@=6%BVex`Ws-re!^U(k~6gWv`Tvgh7T79n}_PDcD9|D zOtTlxvW$ax?nd2rjU0Srt45eAmu!kkD=;38+r)P^>b{G2;Nu&0-?wbQ`!?_TzDh9Q zsT-B<hh%fqikZb>Axt}ow`|mXHw6agZ`6HtNx&1=PmTAf<31DQ{H8c=jWfpan}_bZ zUplz=Mvat|Mm;d~Yb@QY>1E>m8#R{8&78jSOlfibs>TzK-~8E<+r_eMU`VTKsWP2h zwt48j(%GP}QTKgc65PD`1Koh08uA{Q7K=*H5jl7O+(8^mTtEPm_*e44gx6Bq11 z`)@4m#$Q&#G}Q_Wy776#;bFB<-|vWxjHSKQ0xM#g{iv=~P8<2+Wm63b(UfE??G-Cn z5z~o3AtDR;`CgS)TG{Nx$Qny~Ee=-1bmDLSFho9`2w}>k#?oGCgB39y8H{IKG?42J zy{4mU#2s!bfU({7IvT8qY0|zYcj8_nJ(()ik>VdGt$6s_ZMVL&5~h=l?qMd~P#pzH z3uR`hVSGLQpAGILz@6ad<cY{ge*<|l`4RGK<gckys0*pfs3cXl=Ks&4US*B*pZq^F zd;dSatcL=@$7}A$NQ;_cNz#lQ@u~GtAgEJ!wI1>2wU``hq$i_|81}|_xe_R9EjgPW zHICZ+{=P&8?zvI-)yM_yjhda5hN+Rysifj*ZJGgrjao4qYRC~9Xra1i<n)3IH^FMG z?D6}00$uFT#yD}0jo2LbQw?`FLHEM>plBA?zb`WK*r!$JI<3!xh7u%h*r@wz#)I-k z-S>TK@UD#-oA#Ww{G8TWgFNSI5-;DVac(%v`};v9D-<fapZM`c&Grl-6P>rs(6AY2 zTEtT}YPl^Y)KHMois7I;S_UU<)P3e%@RIeD5NlH08tK`nsk+o!o7pz3$){7<k|Sh_ z9&^O^h;MGxE;9{D6;9PsU8Uae5-;AWsA(L$bffMo-UFV!2{svaBK-jErd!SMpv63L z{ZNTXM2r+I>hw&V$-OM`+l{*K+kfDr>;8WU(ITv$74bacb;Ns#&k;Wa$AJ3)76iam zU;v&BUJTv@KD5aYCz0oq63h@!BVS6sjl7-w#^zXHe}?$fjtsGBg15)T_wt;kRjZ+T z+X2p8KL^RjWZQn(T{9hnY_Jm8Okwtyd;-tdsNvzrV~)FJGdoS!^%n7ojk@oPZ{SlK zwW7Fup^?VZy}75!yNS<kR5Mui8MW5P3}vGl-6A$qLD`uPGiuH0po`ON$P5tA->8C? zR%e++$`lo*<!KPNZq$7?8}PP`>XH4^QbkMAvrwp+?1Qt{PfgBoVmveQcNEdB`3H$( zwqie%&kC)Y8Ef>Ip*G$8{=TmzfX$40|6R6$uu=DYg&BNcqwcF)2_Cmr_ur=zJanT9 zgH&cO*E8vC(@d9R;MMDfxx9{HvyciW>5#t`Xc=d29$GFmjEXUEYO`cit2Bt$Y#!Qj zwwt4Rq)S&r4VKTHy?JO?@U{{~C2zL;<!FBn$hEq^o*sC@Mim|5+N|XrE2G4ozdHaI zY}Alz?x8C(PZyc^j7<^OZPa!qR>@atBf1zXj=3)JtBtyEGy`X>RgbUQn(&eQP@7ea zQoBaHYu#LrZ?qR3RA!pib58ul`PG|;Dm`~<Ze&YJDpqy}XT&!)4-Im;PSz9y)(0f( z8|aVSJanJi0c<`Ao@z_+hjs7RRDEJ)Fel!=`2%gS;x{7JL5!mi2y)_Cn}_y2%*-#v zql&6&%?S_gyH@v?8UUN)MlWA8yDZ%k%%aibs}Zp2JNxTafR}AlUu`^%#Of`D?Hgh+ z2>k0Ncth=SNt`n&vlp2KtIqpx9vU?|K4GBDXjX3q<0A2j13(~-Bd#W>>!?e}TgWu{ zHt`|iYVa6L!;j7%Pn>W2Vm+3bj4zoeH-{0;qv`#`u<Oz}dnim7y7!-CvYuEF&&wm& z+l=YxW8y6s^MH%b(6OQ?B>HsL9tzW?`TgfZbjBVE(}j71jplPDE>@;za#9J0b=n>Z z(@eSH>13KSvt2Mlp;)OL*PZrIn2v!Hd28tRvU^c2rPq7?s{SBbC|o|faXgt5DkIu! zj>4Up-dK<S7ZSIw>;HcrP~dFvAm9Ue&;d^XF92@<?<Z~rUjRQLk0Z||Su#LgMGncQ zlGl@OCT}CZLjIDv2Xz7UU@A`4s7L+Vl>sbW<3GKu^V<H4L*TcY6sVV(a*!L==whHG z_Kof)1x_R5LaAyfw6`>JnJMtfwd!NXS?&L8@7rVE_s;r!@@`tE?MF3O_s5ukl|g@P zfcO&I3DXcs>^O<zB(~!?NlQB?aUA>DPMp}eX&d|gz7x07QH@(wfl616i9)#-)KOqn zrw-^~>ok=KIz(yN*hR2)6D=FNynbGK`JTMfehg2OHlp{hkDi=!k~cojbI$X5KF_#Y zn_8FsptFsr-^vG&#NPHQ1>TuXhITOZp952(c@SEGDX|+>!PM~&Vf9kMv^!U>ON06* zK#3O2=*!_F5G5z*=K&=eQ6sqVAq#KOy8yN4^;E2^t}>f6#wUsT%^YnTd;MbX%(azS z(Sqqv@5q<icKs1u@vb6PEA8skd>MeAA-3G{d4c`CDD-!)0WdC4&$CI@wv=<Vb{YI7 z81y<9PFJrY$u4$-tVY2Vs={_yZn+o7*q_8)*+*XjD4~Cf{wAQrO$_=RKvkvJ*^HYt z+0h*BazqdpDdXl=SH;1^a`9DKtLRmL+9!OLL{61`jgu7v$$(}8EwTqWwvv83xnT4k z06jCwpp`}kSr{9^%tQ4Y?U>?#^X#QBvkkXu&ZvJ4D4}YK-Uui`tU~_|P~!3my$n#I z;sbp!M@i-#`j>##j<u*C9a1dpeB<O&ZvnKOtuI@rv5<$+-WF}@{eTkUPUvlb_6>DZ z)3>y2a(&M@Q~v<avEhyUt}S9yy-b@GgaCDR4Y4xV4d~&Lr!${=`~PYGZ{6%<MbRcl zOvv@dr)gxF%YAZ={qgNo1zl1d5`KBLs+Hva%(A-H=Tu?-e{#F#f;M_G+vc0MQxzr6 z5zyi=ZXFZZx!<Sat8d&+RaAe|$zF~(v*i?*XS8|f`TFftMKv6BkEfL=SSQ*|8k3># zk8Y<bD1h29i;5s*>ewpp&zU*%hqqG|)!DSet6ljtvT0j$g~{SM^riRDzeU}B%iZ&( zcb7)!qttsS3!VNzKkMMO4+O&&6f2Quv*5PD1;Z5d3{CV=kGWnJp`{$Ew6VSaXuU$7 zdn$Nx+r5He3yNHWOk-)+=$!Avj&ZbK1-A`H7^a|I*rT_Nxy02H>(XqCUJ%@NQel{a zw&A^@ncRL~z2cT!Tb+8rEdz*Q3fj}iOSRyZ0K_l_ZEC}?(+hEM>|i0nvne0k@_(43 zq%RLy+C8sq#iz^B(pa;f%>Up2{U-V&=+7b=wR08#`kIIMAAJaY45ELo)GfWPl$Bmz zdTZ&&OFv)wNa=U(UV8UMcYpCtzw`d@MgRZPS01}|<9$PFtay!PYdp^=pA9k4wJcPR z<;GEVuF`NqQ$L)eWIhM|X+WFtHVIapSiW!-bsJFs?E!sOiVFQuK3Q+jv}RX@zgX;v z9R)K8Xvx}6^Z_s(N0p>oQI_&>w{jZYGJu2^HS_nA?OM<afD-al=!f$G)Gn@frO8sW zF2`Yn)0cy3zu8_brBl-~^?h$=qJIwPco?{Hz)flP!tbhU>YHHz!nqn?N^}%Q6F}E1 zHcY$6gRGvpY^_lr0yGG1dLpF_IWbmy+C-n9qkVOxsk5MVe=JyNlCnf^2b9qBK<@=~ z%!JNnquW?j?5ZduxH;>uq%_)|=CnK<sO)i%;2M|gb%5Y>r7=yc&*W%BM_YNb>r;^Z z*SMrF@#~IdIhBSZH7LmQYxTos=<i*LX6uu6Uq?W`AzM+PZv>Q}1fu@}=s03GS2yps zc-c$bhWd|y+D<z-hE3jLuhp$}MK904Ud`JzDz3Yo;vwJcj5%QMXq(<*$uco7JBP|~ z;-YSj4!8=dRNC{Y<;sgPUQs^;XseqFgZZw9opDIlo7ArX+F9<|jxALzc9i<|l6pO$ zL~lj}hu^^<?bDHep3APFxy>Q<h8LCpe{!qq#uSzRCqHWN<W?S!DJuU@@@9f3w<?27 zQTc!J<Q+Wul1C;h$cK<u^hM_XpWLb=GDYS8=W+8=i5sa#M_l(b&5F$bKe^RDWQxlF zk81b$q}gd%Y!)e-{76{bx^I;YnWFOlr1#7H|L3FEQRsCjL$60~ML&lA5&AXsiPCeW zFD%tcgVL&WDScDv@05O`^iMv;`pYwYn?K|NyoeWUy1S?&8rM#W&8f?JBQye(=s|?u z@PO{tJ-svRVB0=42P<^UJ;&f`*p9F)i`X%(V6uYz9ogy+{Z&9)^r^RUraSD2n_(bO z-vem3S*_@)X<^$m8>UPB`+V4;u18ybyRhljmgi?n1xyV-?RJ;l7?*Lc-;&U4!PM?f z<ik>#;a1un3Y8O>F8Gt~TfL@LHk0}yK;H)_p`C|*90nj_+$)&wWM;cNs1of~l;JK# zLqHGQiS9SGDNfkNw#T49mZM~sY!u|fju>TF-HD+<3+v&beQ_VaXK#iqct82T{ve<# zizl8mP-tQAkIM=5vw&{sCf^901FclOY86wz4#RFHz4N%LwrHn4h^84ZYDWof;!W5V zXjk4&nGnJKD5++J{vix-sCd<8)4tHAw%+hpphk=iOxRQ{IkId<>hH_cFXt#JK!)H_ zkJPO}znBk8W)CW0Iv1MwNUJUE(!GA_Q$GeM(fAr2UY!5Gt?OosI?t!m^D-O^s;m|1 z+0`2bPj1WO*`iLX(Rmv2R<!S9c9r&qICyfaGQbvfgd_>F!IRfMGFd_XKODJdiQO-b zw7HwxN3Br*f2)qb7Img~$HP?Voi)#Lht;N9E7bqrY9FvgT@BsD;zj!ZTV(^bsJkKG z6u7>&6t<Sr5Sk%=;Qyr5{oglG-$lKXdO!7H>f`7+^l~JkK3bp?dINeJdKdZy^pX3h z;Im49q4edYVaY9BOK&WFcj?`we_Hwts1bVp?kn!bcmCa1-+k-d+wcDVqx_%Wd*wUP zn7tmC)A@cQn@Krc9}yIOknJnc*X0}BxHZ_dR|AR1Lj?yL0cav)RSg!pEeUdF)Ml+I zf?b|e_C@aoln^~d?*!E3Vqr0qmh|O*=ix`xH^OA|>qe8dGH02L>Btz0T*gbOFIElR z#H?27>JfP5@j76y?ark|+x@C_G9kk<_Pooe@1HE}Snc__LwyIB#zHsaJYguCi%6+< zFqm#lrsrRKeM@cVRS{D^4d}6a9;_x>cJI7&dK^$cnWGjPmSa)8F6DB$i>)*D+xf7T zEa;bNw`It~jU^mse+oc_>(`9lt}L7TuCrJS09<vC*R)od$~J@b@bVHEG^|Or=Apf? zl7{Xz2k7}6oiKxu)R0(C79?5g#%KU&o0pvB?h?=mv*V9D^mp@NCw*GU;;0v3_EgiW z?G~7hbeiQ9Z;##a{eg={2AC3esecVj^>Mhi8l#1*VtNwN)K39Q=!zgX=aOae=r<nF z>bW;NR0HhrXBivRw*gA@Tt<HdmT*|EP3@`}v^sHAhxzjWrq{E7O1!BY#e6y_zcPP6 zDG-Z(`T>2mb{+wN&*mGQ^E_6M*j>APl%kjB!`h;vo2OGH#fo~GA1;3mOl5VrHD)6o z3yLzSj?fnYy4v*vaiwl)C+W|-JZi!KNo_EzT0K^F7KgSZM_@`QE~5A6rc<^X%a^9I z$nSVktNdpG9IofXxY^-p<+9T!ej|VXbT!di>)EI-o0APA(bRu^as2;V;ZLq8|9@Td z_|wh}>a=<-A4iwqg<Gjrt|<Rc=8S_EZWU{}qWu55QS}Ghc{;<H(sSEh@WQPYFISZR zCx1kR^8as@cDbVb|5|beOe!a=HC(gSAT5;tf2*U*73Kd)(j8SK|NmC{mMhBtll<%l z{~uD+3)Ht$@1WkB^MI%jm_U_>dO-BG=sVGmpr0XX0Z*T*1^g&v0$v0UDBpp|`eXDv z`DQf}$MJF*cRcLuL}TN>6il14JiFqGgUfTiIUG?R0JP7i_1d5^u#|YaX<gAvU;u5* z2Y8Y)a*~KEp8gX64uWo)GzHE|Sf)}7UjgqYdjBDC7-Yj&^tS*dTK6HSMVkxdRz+M~ zTQqB_aa%;NHP7octzvS?%F4R(g6mWN;sJeD)*OL@o3|D?cFp$GVvX)uGAN*!kR9&O zn;+0;8zZ7`%|F_F*6GBJT7PR@<~wH2{ADnmO>{i$beC4*R?iCw^#EOE<@mfg^OjFr zv{9yje?#88QQ!Z7J}a_|pu}~4l)}+woL_QFHLJHmNsc<)T%X=YLmSK4$irHSp!C+c z&Q31VRz+HEs(gB;ej#7(g7Mj@;@2bk(jKk*OD{KFRJksF9chdlO|HX<ioiuJ`m;q> z*l%n(syTMOkASC33UZ;30y?dWoyKLZ$x$}AvR&$3Fd31!*8z;$zLnPWwR~v^x;XdY z{o4^;-}Fz4ER-veD4|~h(?c*AE%p-)m$AJydtaKHF0G1L8+YRgW*2z2PSFO?X&|ev z?~UnbdYMEq+5$>Ql_FrMuPTjfQ|n5h?6_%c$kZ<ZO2`@^C}CXjNiuYI?T9XWCwUW6 zUjr!7C>VWnj*=a{5EKx&V<A*Wk+HK}-wnkk^%D>1v!^ZUSM%rpmDJZ$_kVBsjOhOh zQ5_A?3SH3~(RZVFqYt2eg?_j6*`?=8Ur`#Dg3?!)zNPd%rJpMOv(mr0qwZdI_o}<E zytD4`-Cw`^w!81Wd(Yhmf8TY$u)Jw)wf3f?E`1rCaF3_H7tj@NXKRtK$Qg5N#0Lcb z5EpxHZ)M7|oYgkdI7PqwfIi!j5&?<dO&Q6XhSe1=H?FRvBG?JmoW8((?{M#**-_bV zBS@T(O>Gd|6Oq0gwenxey6MN;t8Pqjq-*iA_b0)W@MuT^v_CC3ovE?1)F9nPG=jR8 zwN_tU>%7X)P8grp0fI1bJ*j)DG1GMV{=v6L0}5!~YtJ~&+pV>S6NQPw5Pb-ia7Y>7 z9XX0s=2&Ucs(~q?0*-zzHziZ`a9`*Rro1^#>;tyXvOiiP9Z*x>F{;gqSSj}!tvP~W zy~8<brqfiVGvQ)4F!hgfloV4#Ks($pO)VQUVN-4`s-sy2eJo!BNpu+j$R^&hZu`FM zGIh?Hz6!vqHBy~zSH+R5=v@<TAxTOu4KooPEzx%Zx{Qs`9q#+sY%dkHkD!KU<HgGP z*z31tNA67YHT7l~Ht2C(Lsb;)E|PQP#{ixyz0tLPKGMl3^XERW3w^pH*Nt_c$k}S? zMIQB@+|<{bT4!HjW>&PT%`gv9J$cZkJ_@EYN!<FpGNa3F|45G^LF$L~YLpqRNEXz_ zP-PL+`ue-RF5e$FY)fex8sCAket&mv8<OK`*uIW7(>?VxHziro41nXZ6E4gA-ZK60 zA`4#$V2oupZY%jx<$k-dd2#XoEg^v`D*tC5`n%aM8eOp9J2uY-FPv^CD=PnY;<t+1 z+qLc;x5s6pBzPfvWU_+de?3c%=h0%RS*>!<qG!Pi=_8XB6#qxscI0i&(;aOdQl&j9 zH2yz6vhIT7e=^}wX#9VxXv7tj|09{oRFU!jE!T)ED*s0+G35UL`CR|^Gb{fXq@b@t z2lVymE$E%-{piE!<E7_HFNb{K@>vxS>P^2_`=>us`}YS{0m(PJnARr?X)oAx=7vsi zML@0~*=-cmxM3mNwXAZ#D!W5dvsemv-?-Lfbt{?k_rI=Sf0Ynaa$#l9)q~EsZkgc< z8&~vdKnIR<=IX9(m8DVEUZAhdH%z>!L~ADAk7!{ROa;3Qri-R;ue-A`X3v#D$EV&3 zXng4C8`+dBf4Myw4h2PSVR?~U_0GY=Ly4Qn0s5nWj;2}Uu0)ac1=Xq7srNmg&tBqF zzXB-HKoEWZ1NyA^Ci=I4+Oy6C*IH|9DEJ#2Q$GjjN;LFcr>V%S=G8|U1%;Gk%Qf_q z`3DaDV{6Op#TZA^=3=P^V7g11ERMsOEYw0~SEqpd5+>a+J36hYb>ZB()}%h34-l2F z4zu%|kWNxfJkqZQu#z-}`lMaK7tZWS9T7l9-6{wE)Uy;N9_$^cgCI9DlmhDX57$)k z73(tOai@>&09}Ty1{SqIR-85#2IzAET_0-`mh<LVnX=+_i{Q4EO#M(FhM!n@DDb9c zGqk@Khew(EnS5b!Z{1NB;h~C^-QG=WO)xd$R<cc+Rf|n3`(qic!PKs@BYV)eSeHZV zB5-Rkb<4tXVUBrhUY5JDi9jJH_WNRMeqQ&m?_TEF6+sCy*|8nHDMz>6mKapa%?a&} zwXW(R2z9oa?$^8;o5`$?j~51sfD-)!(T@OX1*fCpN)|S&N|otSZ_1auoplTccQzAR zuxfhf-aVZEV~Y9^^)d7k^m(X?y1)ha2J~I%ozxG0`st_Ql33F2pZ`zt0X~pd{!(ww z83EM$p4|bczw!7UkS~msJ463DM@fY^^obljSo@53O)-*NoHepF^zj@ewSv)afh{4U zjD8)y2BJqb`r8laMQ(O4W)}<HGC!YFP;9de$LdCu_JP$p24}gBJ_}|mEB9w%rOaa0 zoiUA!`e6P=?znBERo<4Hmfz3ntt06A?8HgGZKbJzBYP5V8WhMj?S>6TZnu@PocX-z zMd**i0Hg9qD=$y6<zHmOh+tdT+3Q5lDx<*iyZf5qQ*Q%P$6PHn0Z(a@)&zqG0iWIa zV7F_Fej>9fd!9EaU<U4DKdYWrwJEKhe0yk9K=(`NU|D4<jwIXnr}imJ{U<;PVRH1V z`D95eN%lvf?pvyK^7Z!f01~Wm$kUU3>ky=vlZnymPMage^2MN&)DYZ*B;Iu2-3#j` z&h(Qz>L6e@CP|d`1Bni4yO!}fgT5d~_uYQ?nzY5OW!n=++M%a_YN61T;uTFhGqG-n z)YkzzoMe+WeGIT|iKlag0+kz*C=9p%hh|6K8!bJM6JOT%D-ho~4Ck|^%`%v7>29NW zKp=Q<NXoQ7T!!~M$g;i}OUQ=-h$@gVfCQiR0`EVE{>4paG@Wg{WmQ9<AC_`g+}62D zAGg~6Azq-@07~S)sgD49ooR7<nbMZBn@cU7`bBt1h~}N>M*;29-l@}7VmjlE*wYY{ zsgaGo(EIY`X5N{xE`#;n%4$My#C`FLivORMA8ANhQ2bApp$AW2`A8G_g5rO(dU7g3 zx0+gh*wgHJ@bndrOjc0*Po~U*r(gWYWCg|lq;n}U{(t)XBkL|G{wK>Zi;VxDzUq;6 z7Zm@Kq-LtX`2XoGPqbRp{Xfas%KiTrpbmvzi_&|a{}%M4=x0B9(~mkOp`@0aXYTqx zsRpF~FSI`XKYQg{?x~q=YtH;g)5`jAUR&t5VIz(Yi%GWOIPChZ&@?E>TW86*Jz!Q! z4M%BXHtQfS-=2@0jVA(qN@+J^THf^Y03=GbzYM_D4mU!dUC6DKbXsU3fZ<ddc3j($ zliGZ2_;4G2%!18wvZy+;%W&q#L*D`D=`ii-y^CtuVVe`y6qG@eYFr2^CdgY8bjl|q z1qMKtglq|hK(6q#Keijky+4-O(Gia~=*x2S%xuf*W?^^bavWP@AK8HNg0^>zUX06L zmIgcpT#d6d?e?b1YC|Wr)p$^+K(3JNc8-1^U)b3h-hbD0Ce!L<+KDR*Fx`xFSxVcU z9ElCR#v<S!ki8iZ(El#Omh2ia?O2(&-!0A*B!e#F{%Uril@_h^q^i+I@ReWI8(i+3 zyF9H(X|SmSSMjp#C<l(?EzEAjaFHMe&_rb?-mK%$Ln}=876PH3E4@@E>x19I!elk9 z&=j2G$PV@B9e@(Kas-S`vZWUSUDInIvd6I3mgSb)Ov()8g2?u;=shpa|9{mZ4M_^} z|75%vJbm>eP2>vl|76l2cshGzvV#0SS+x>8ojx*IQH8NYoll|ufBwk2i|VvPq~Hqm z|F3yu-39r7G8;n`>HlweqSd1A{|r~^SKNbg#de=v?&{B>S5PmZw9n}OL;XEUL;L^D zmEYbJSL+4+!b$HMJiWcUuhxs|qcyomqcI+sxD_s2TCxA$+XP3oUeKOHUYG<=Z*vaS zdO>CDq}F)w^zg`J1r@H7m%+i){UehVrTs%>FN3GaBa;>60ZGY{;OXv>{yzonTcjj* z9{<0rBv7By{rt45-|qseH&^)nG^_XbpV$Ax*Z+gBJeJgVC&v+`izN${P@bL97*HbV zi84Ts^l2AV0$nz@yv3)~f6q~)Qd8=qHWq0C^Za#+fTLmvqB!ug&gMRBS~uJQ0!FSO zFSWkE+s<*cS=(n1LCS?}cZlFp-!Qz+elrvgcz9MkDMc=zmR;R0EAm1X#$B2jAW+03 zH+lrE0!S~3ekBj7jT2q7S-)-2L$~Z!B`6U$uIXWJ?K>hCJo$2}QXmXOUNj*n!!nhf zk!0JnXE|(5*8&8kI;O=nF74@NYh4!iUx!J(H=oS3w#=X{n=)<I8p0%Af$5@ocGFmD z%F1yXnhvTypeI$Rw=XK)noIbkQJ;VTlCi&Ttyu-TmNPUQ`vII8{SLpVT23cW*w*L% 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]