Re-land patches from bug 566746 and bug 697377 now that bug 851641 is addressed

This commit is contained in:
Gavin Sharp 2013-04-19 15:21:30 -07:00
parent 181686bf1e
commit 6ff5e33807
59 changed files with 3641 additions and 1955 deletions

View File

@ -447,6 +447,7 @@
@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

View File

@ -6,6 +6,8 @@
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 = {
@ -16,9 +18,16 @@ Sanitizer.prototype = {
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: "",
@ -29,12 +38,10 @@ Sanitizer.prototype = {
},
/**
* 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);
@ -46,27 +53,42 @@ Sanitizer.prototype = {
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
@ -231,15 +253,14 @@ Sanitizer.prototype = {
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);
@ -251,17 +272,27 @@ Sanitizer.prototype = {
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;
}
},
@ -486,9 +517,7 @@ Sanitizer._checkAndSanitize = function()
// 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);
}
};

View File

@ -47,11 +47,13 @@ var gSanitizePromptDialog = {
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 =
@ -281,11 +283,13 @@ var gSanitizePromptDialog = {
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 =

View File

@ -2,9 +2,24 @@
* 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);
@ -29,13 +44,28 @@ function test() {
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();
}

View File

@ -1,8 +1,10 @@
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;
@ -11,14 +13,50 @@ 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() {
@ -47,6 +85,8 @@ function onHistoryReady() {
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"))),
@ -68,16 +108,19 @@ function onHistoryReady() {
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");
@ -95,6 +138,8 @@ function onHistoryReady() {
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"))),
@ -114,15 +159,15 @@ function onHistoryReady() {
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");
@ -140,6 +185,8 @@ function onHistoryReady() {
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"))),
@ -157,14 +204,14 @@ function onHistoryReady() {
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");
@ -179,6 +226,8 @@ function onHistoryReady() {
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"))),
@ -194,15 +243,14 @@ function onHistoryReady() {
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");
@ -216,6 +264,8 @@ function onHistoryReady() {
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"))),
@ -229,12 +279,12 @@ function onHistoryReady() {
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");
@ -247,6 +297,8 @@ function onHistoryReady() {
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"))),
@ -258,11 +310,11 @@ function onHistoryReady() {
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");
@ -275,6 +327,8 @@ function onHistoryReady() {
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) {
@ -283,12 +337,12 @@ function onHistoryReady() {
}
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");
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("4hour10minutes", "4hour10minutes form entry should be deleted", checkZero);
if (minutesSinceMidnight > 250)
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)
@ -298,6 +352,8 @@ function onHistoryReady() {
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
@ -307,28 +363,33 @@ function onHistoryReady() {
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) {
@ -363,73 +424,140 @@ function setupHistory(aCallback) {
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();
// 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");
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;
}
// 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() {

View File

@ -17,6 +17,11 @@
* 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);
@ -24,11 +29,11 @@ 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 = [
@ -80,7 +85,7 @@ var gAllTests = [
};
wh.onunload = function () {
yield promiseHistoryClearedState(uris, false);
blankSlate();
yield blankSlate();
yield promiseHistoryClearedState(uris, true);
};
wh.open();
@ -148,7 +153,7 @@ var gAllTests = [
ensureDownloadsClearedState(olderDownloadIDs, false);
// OK, done, cleanup after ourselves.
blankSlate();
yield blankSlate();
yield promiseHistoryClearedState(olderURIs, true);
ensureDownloadsClearedState(olderDownloadIDs, true);
};
@ -156,6 +161,23 @@ var gAllTests = [
});
},
/**
* 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.
@ -176,10 +198,6 @@ var gAllTests = [
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 () {
@ -207,10 +225,14 @@ var gAllTests = [
// 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);
};
@ -301,6 +323,19 @@ var gAllTests = [
});
},
/**
* 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.
@ -311,7 +346,6 @@ var gAllTests = [
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() {
@ -331,7 +365,9 @@ var gAllTests = [
};
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();
});
@ -365,9 +401,21 @@ var gAllTests = [
}
wh.open();
},
function () {
let formEntries = [ addFormEntryWithMinutesAgo(10) ];
/**
* Add form history entry for the next test.
*/
function () {
let iter = function() {
formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ];
yield;
doNextTest();
}();
iter.next();
},
function () {
let wh = new WindowHelper();
wh.onload = function() {
boolPrefIs("cpd.formdata", true,
@ -383,7 +431,8 @@ var gAllTests = [
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();
},
@ -739,7 +788,9 @@ WindowHelper.prototype = {
* 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;
@ -891,28 +942,59 @@ function addDownloadWithMinutesAgo(aMinutesAgo) {
* @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 + "'");
is(formhist.nameExists(name), true,
"Sanity check: form entry " + name + " should exist after creating it");
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;
}
/**
* 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;
}
/**
@ -982,22 +1064,6 @@ function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
});
}
/**
* 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.
*
@ -1026,8 +1092,8 @@ function visitTimeForMinutesAgo(aMinutesAgo) {
function test() {
requestLongerTimeout(2);
blankSlate();
waitForExplicitFinish();
blankSlate();
// Kick off all the tests in the gAllTests array.
waitForAsyncUpdates(doNextTest);
}

View File

@ -30,6 +30,12 @@ const UNEXPECTED_NOTIFICATIONS = [
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 = {

View File

@ -109,6 +109,9 @@
"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[
@ -455,12 +458,13 @@
// 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);
@ -554,7 +558,6 @@
]]></destructor>
<field name="_stringBundle"/>
<field name="_formHistSvc"/>
<field name="_prefBranch"/>
<field name="_suggestMenuItem"/>
<field name="_suggestEnabled"/>
@ -565,9 +568,6 @@
"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);
@ -748,10 +748,6 @@
},
isCommandEnabled: function(aCommand) {
if (aCommand == "cmd_clearhistory") {
var param = this._self.getAttribute("autocompletesearchparam");
return this._self._formHistSvc.nameExists(param);
}
return true;
},
@ -759,7 +755,10 @@
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":

View File

@ -5,6 +5,9 @@ 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();
@ -20,6 +23,8 @@ function test() {
var ss = Services.search;
let testIterator;
function observer(aSub, aTopic, aData) {
switch (aData) {
case "engine-added":
@ -182,33 +187,64 @@ function test() {
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) {

View File

@ -432,6 +432,7 @@
@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

View File

@ -37,7 +37,7 @@ SimpleTest.waitForFocus(function() {
function listener() {
popupShown = true;
}
SpecialPowers.addAutoCompletePopupEventListener(window, listener);
SpecialPowers.addAutoCompletePopupEventListener(window, "popupshowing", listener);
var event = document.createEvent("KeyboardEvent");
@ -49,7 +49,7 @@ SimpleTest.waitForFocus(function() {
hitEventLoop(function() {
ok(!popupShown, "Popup must not be opened");
SpecialPowers.removeAutoCompletePopupEventListener(window, listener);
SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshowing", listener);
SimpleTest.finish();
}, 100);
}, 0);

View File

@ -4667,7 +4667,7 @@ var FormAssistant = {
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();
@ -4737,8 +4737,14 @@ var FormAssistant = {
// 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":
@ -4746,13 +4752,18 @@ var FormAssistant = {
// 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;
let checkResultsInput = hasResults => {
if (hasResults)
return;
// If we're not showing autocomplete suggestions, hide the form assist popup
this._hideFormAssistPopup();
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
@ -4776,27 +4787,31 @@ var FormAssistant = {
},
// 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);
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;
// 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 });
}
// Supply a label and value, since they can differ for datalist suggestions
suggestions.push({ label: value, value: value });
aCallback(suggestions);
}
};
return suggestions;
this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
aSearchString, aElement, null,
resultsAvailable);
},
/**
@ -4833,40 +4848,47 @@ var FormAssistant = {
},
// 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;
aCallback(false);
return;
}
let autoCompleteSuggestions = this._getAutoCompleteSuggestions(aElement.value, aElement);
let listSuggestions = this._getListSuggestions(aElement);
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);
// 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) {
aCallback(false);
return;
}
// Return false if there are no suggestions to show
if (!suggestions.length)
return false;
sendMessageToJava({
type: "FormAssist:AutoComplete",
suggestions: suggestions,
rect: ElementTouchHelper.getBoundingContentRect(aElement)
});
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);
};
// Keep track of input element so we can fill it in if the user
// selects an autocomplete suggestion
this._currentInputElement = aElement;
return true;
this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
},
// Only show a validation message if the user submitted an invalid form,

View File

@ -345,6 +345,7 @@
@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

View File

@ -962,16 +962,21 @@ SpecialPowersAPI.prototype = {
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);

File diff suppressed because it is too large Load Diff

View File

@ -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]);

View File

@ -25,6 +25,7 @@ CPPSRCS = \
EXTRA_COMPONENTS = \
nsFormAutoComplete.js \
FormHistoryStartup.js \
nsInputListAutoComplete.js \
satchel.manifest \
$(NULL)
@ -37,4 +38,8 @@ EXTRA_JS_MODULES = \
nsFormAutoCompleteResult.jsm \
$(NULL)
EXTRA_PP_JS_MODULES = \
FormHistory.jsm \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -10,6 +10,11 @@ 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();
}
@ -18,14 +23,6 @@ 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
@ -37,6 +34,13 @@ FormAutoComplete.prototype = {
_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.");
@ -50,10 +54,6 @@ FormAutoComplete.prototype = {
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 : {
@ -96,12 +96,6 @@ FormAutoComplete.prototype = {
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;
}
}
},
@ -121,33 +115,64 @@ FormAutoComplete.prototype = {
},
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
@ -176,145 +201,80 @@ FormAutoComplete.prototype = {
}
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;
}
}
return result;
// 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);
}
},
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+/);
this.stopAutoCompleteSearch();
// 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 '/') ";
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);
}
// 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);
// 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
}
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);
},
/*
@ -422,8 +382,11 @@ FormAutoCompleteResult.prototype = {
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 });
}
}
};

View File

@ -38,11 +38,12 @@
using namespace mozilla::dom;
NS_IMPL_ISUPPORTS5(nsFormFillController,
NS_IMPL_ISUPPORTS6(nsFormFillController,
nsIFormFillController,
nsIAutoCompleteInput,
nsIAutoCompleteSearch,
nsIDOMEventListener,
nsIFormAutoCompleteObserver,
nsIMutationObserver)
nsFormFillController::nsFormFillController() :
@ -602,11 +603,15 @@ nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAStrin
// 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 :)
@ -615,48 +620,65 @@ nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAStrin
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.
rv = inputListAutoComplete->AutoCompleteSearch(formHistoryResult,
aSearchString,
mFocusedInput,
getter_AddRefs(result));
nsresult rv;
nsCOMPtr<nsIAutoCompleteResult> 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;
}
@ -709,9 +731,31 @@ void nsFormFillController::RevalidateDataList()
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
@ -1167,4 +1211,3 @@ static const mozilla::Module kSatchelModule = {
};
NSMODULE_DEFN(satchel) = &kSatchelModule;

View File

@ -11,6 +11,7 @@
#include "nsIAutoCompleteSearch.h"
#include "nsIAutoCompleteController.h"
#include "nsIAutoCompletePopup.h"
#include "nsIFormAutoComplete.h"
#include "nsIDOMEventListener.h"
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
@ -33,6 +34,7 @@ class nsFormFillController : public nsIFormFillController,
public nsIAutoCompleteInput,
public nsIAutoCompleteSearch,
public nsIDOMEventListener,
public nsIFormAutoCompleteObserver,
public nsIMutationObserver
{
public:
@ -40,6 +42,7 @@ public:
NS_DECL_NSIFORMFILLCONTROLLER
NS_DECL_NSIAUTOCOMPLETESEARCH
NS_DECL_NSIAUTOCOMPLETEINPUT
NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
NS_DECL_NSIDOMEVENTLISTENER
NS_DECL_NSIMUTATIONOBSERVER
@ -60,6 +63,8 @@ protected:
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);
@ -79,6 +84,9 @@ protected:
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;
@ -87,7 +95,13 @@ protected:
//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;

View File

@ -27,7 +27,6 @@ FormHistory.prototype = {
debug : true,
enabled : true,
saveHttpsForms : true,
// The current database schema.
dbSchema : {
@ -82,43 +81,14 @@ FormHistory.prototype = {
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 ---- */
@ -434,10 +404,6 @@ FormHistory.prototype = {
case "nsPref:changed":
this.updatePrefs();
break;
case "idle-daily":
case "formhistory-expire-now":
this.expireOldEntries();
break;
case "profile-before-change":
this._dbClose(false);
break;
@ -569,56 +535,9 @@ FormHistory.prototype = {
},
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");
},
//**************************************************************************//

View File

@ -6,17 +6,47 @@
#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);
nsIAutoCompleteResult autoCompleteSearch(in AString aInputName,
in AString aSearchString,
in nsIDOMHTMLInputElement aField,
in nsIAutoCompleteResult aPreviousResult);
/**
* Generate results for a form input autocomplete menu asynchronously.
*/
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);
};

View File

@ -4,3 +4,7 @@ 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

View File

@ -2,6 +2,8 @@
* 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
@ -9,23 +11,40 @@ function test() {
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);

View File

@ -71,9 +71,7 @@ function getAutocompletePopup() {
function cleanUpFormHist() {
var formhist = SpecialPowers.Cc["@mozilla.org/satchel/form-history;1"].
getService(SpecialPowers.Ci.nsIFormHistory2);
formhist.removeAllEntries();
SpecialPowers.formHistory.update({ op : "remove" });
}
cleanUpFormHist();
@ -90,7 +88,7 @@ var checkObserver = {
},
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");
@ -104,13 +102,16 @@ var checkObserver = {
// - 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();
}
});
}
};
@ -131,3 +132,30 @@ function getFormSubmitButton(formNum) {
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(); },
});
}

View File

@ -36,20 +36,20 @@ 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;
@ -117,6 +117,29 @@ function doClickUnprivileged() {
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...
*
@ -125,7 +148,9 @@ function doClickUnprivileged() {
* 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) {
@ -218,7 +243,9 @@ function runTest(testNum) {
// We're privileged for this test, so open the popup.
checkPopupOpen(false);
checkForm("");
expectPopup();
doKey("down");
return;
break;
case 21:
checkPopupOpen(true, -1);
@ -336,20 +363,22 @@ function runTest(testNum) {
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;

View File

@ -65,13 +65,13 @@ 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;
@ -172,7 +172,7 @@ function runTest(testNum) {
}
function startTest() {
runTest(1);
setupFormHistory();
}
window.onload = startTest;

View File

@ -11,8 +11,9 @@
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;">
@ -120,40 +121,39 @@ Form History test: form field 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" ];
@ -182,16 +182,45 @@ function checkForm(expectedValue) {
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) {
@ -199,12 +228,13 @@ function runTest(testNum) {
// 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
@ -212,6 +242,7 @@ function runTest(testNum) {
checkForm("value1");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -224,6 +255,7 @@ function runTest(testNum) {
checkForm("value2");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -237,6 +269,7 @@ function runTest(testNum) {
checkForm("value3");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -251,6 +284,7 @@ function runTest(testNum) {
checkForm("value4");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -267,6 +301,7 @@ function runTest(testNum) {
checkForm("value1");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -278,6 +313,7 @@ function runTest(testNum) {
checkForm("value4");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -291,6 +327,7 @@ function runTest(testNum) {
checkForm("value4");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -309,6 +346,7 @@ function runTest(testNum) {
checkForm("value4");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -320,6 +358,7 @@ function runTest(testNum) {
checkForm("value1");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -331,6 +370,7 @@ function runTest(testNum) {
checkForm("value1");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -344,6 +384,7 @@ function runTest(testNum) {
checkForm("value1");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -356,15 +397,16 @@ function runTest(testNum) {
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");
@ -378,95 +420,144 @@ function runTest(testNum) {
// 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);
checkForm("");
ok(!fh.entryExists("field1", "value2"), "checking that f1/v2 was deleted");
waitForMenuChange(0);
break;
case 63:
checkForm("");
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 */
@ -480,7 +571,11 @@ function runTest(testNum) {
// 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:
@ -493,6 +588,7 @@ function runTest(testNum) {
input = $_(4, "field2");
restoreForm();
doKey("down");
waitForMenuChange(0);
break;
case 102:
@ -505,6 +601,7 @@ function runTest(testNum) {
input = $_(5, "field3");
restoreForm();
testNum = 199;
expectPopup();
input.focus();
sendChar("a");
break;
@ -512,65 +609,81 @@ function runTest(testNum) {
/* 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;
@ -579,138 +692,159 @@ function runTest(testNum) {
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;
@ -719,7 +853,7 @@ function runTest(testNum) {
case 401:
case 402:
case 403:
checkMenuEntries(["value"]);
checkMenuEntries(["value"], testNum);
doKey("down");
doKey("return");
checkForm("value");
@ -734,18 +868,20 @@ function runTest(testNum) {
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;
@ -757,6 +893,7 @@ function runTest(testNum) {
input = $_(15, "field12");
restoreForm();
expectPopup();
doKey("down");
break;
@ -769,19 +906,24 @@ function runTest(testNum) {
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
// Go to test 500.
fh.addEntry("field1", "value1");
addEntry("field1", "value1");
break;
case 408:
input = $_(1, "field1");
// Go to test 500.
testNum = 499;
expectPopup();
restoreForm();
doKey("down");
break;
@ -790,9 +932,9 @@ function runTest(testNum) {
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");
@ -800,6 +942,7 @@ function runTest(testNum) {
doKey("return");
checkForm("value1");
testNum = 599;
setTimeout(runTest, 100);
break;
case 600:
@ -810,27 +953,59 @@ function runTest(testNum) {
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 checkMenuEntries(expectedValues) {
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 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() {
@ -847,7 +1022,9 @@ function getMenuEntries() {
}
function startTest() {
runTest(1);
setupFormHistory(function() {
runTest();
});
}
window.onload = startTest;

View File

@ -44,19 +44,16 @@ Form History test: form field autocomplete
/** 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;
@ -76,19 +73,41 @@ function checkForm(expectedValue) {
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);
@ -98,12 +117,12 @@ function runTest(testNum) {
// 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
@ -111,6 +130,7 @@ function runTest(testNum) {
checkForm("historyvalue");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -123,6 +143,7 @@ function runTest(testNum) {
checkForm("Google");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -136,6 +157,7 @@ function runTest(testNum) {
checkForm("Reddit");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -148,6 +170,7 @@ function runTest(testNum) {
doKey("down");
doKey("return");
checkForm("final");
expectPopup();
restoreForm();
doKey("down");
break;
@ -156,18 +179,31 @@ function runTest(testNum) {
//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");
@ -178,12 +214,13 @@ function runTest(testNum) {
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
@ -191,6 +228,7 @@ function runTest(testNum) {
checkForm("Google");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -203,6 +241,7 @@ function runTest(testNum) {
checkForm("Reddit");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -216,12 +255,13 @@ function runTest(testNum) {
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
@ -229,6 +269,7 @@ function runTest(testNum) {
checkForm("Google");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -241,6 +282,7 @@ function runTest(testNum) {
checkForm("Reddit");
// Trigger autocomplete popup
expectPopup();
restoreForm();
doKey("down");
break;
@ -254,6 +296,7 @@ function runTest(testNum) {
checkForm("final");
testNum = 199;
expectPopup();
restoreForm();
doKey("down");
break;
@ -278,11 +321,11 @@ function runTest(testNum) {
// 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
@ -300,83 +343,87 @@ function runTest(testNum) {
// 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", {});
@ -385,12 +432,11 @@ function runTest(testNum) {
doKey("return");
checkForm("Google");
input.removeAttribute('mozNoFilter');
testNum = 399;
expectPopup();
restoreForm();
doKey("down");
testNum = 399;
nextTest(testNum);
}, 500);
return;
break;
case 400:
@ -401,28 +447,38 @@ function runTest(testNum) {
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() {
@ -439,7 +495,7 @@ function getMenuEntries() {
}
function startTest() {
runTest(1);
setupFormHistory(runTest);
}
window.onload = startTest;

View File

@ -293,10 +293,15 @@ var ccNumbers = {
],
};
function startTest() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
ok(!fh.hasEntries, "checking for initially empty storage");
function checkInitialState() {
countEntries(null, null,
function (num) {
ok(!num, "checking for initially empty storage");
startTest();
});
}
function startTest() {
// 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).
@ -369,7 +374,6 @@ function checkSubmit(formNum) {
ok(true, "form " + formNum + " submitted");
numSubmittedForms++;
// Check for expected storage state.
switch (formNum) {
// Test 1-24 should not save anything.
@ -397,7 +401,12 @@ function checkSubmit(formNum) {
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");
@ -441,19 +450,24 @@ function checkSubmit(formNum) {
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\//)) {
@ -470,11 +484,11 @@ function checkSubmit(formNum) {
// 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) {
@ -494,8 +508,6 @@ function checkSubmit(formNum) {
//
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
@ -514,18 +526,9 @@ function checkSubmit(formNum) {
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();

View File

@ -28,10 +28,15 @@
var numSubmittedForms = 0;
var numInputFields = 101;
function startTest() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
ok(!fh.hasEntries, "checking for initially empty storage");
function checkInitialState() {
countEntries(null, null,
function (num) {
ok(!num, "checking for initially empty storage");
startTest();
});
}
function startTest() {
var form = document.getElementById("form1");
for (i = 1; i <= numInputFields; i++) {
var newField = document.createElement("input");
@ -55,8 +60,6 @@ function startTest() {
// Called by each form's onsubmit handler.
function checkSubmit(formNum) {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
ok(true, "form " + formNum + " submitted");
numSubmittedForms++;
@ -65,33 +68,14 @@ function checkSubmit(formNum) {
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();

View File

@ -126,10 +126,15 @@
var numSubmittedForms = 0;
var numInputFields = 101;
function startTest() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
ok(!fh.hasEntries, "checking for initially empty storage");
function checkInitialState() {
countEntries(null, null,
function (num) {
ok(!num, "checking for initially empty storage");
startTest();
});
}
function startTest() {
// 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).
@ -140,50 +145,42 @@ function startTest() {
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
}
function checkSubmitCounted(formNum) {
is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
SimpleTest.finish();
return false;
}
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();

View File

@ -2,8 +2,13 @@
* 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;
@ -13,6 +18,12 @@ 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);
@ -25,10 +36,67 @@ function getDBVersion(dbfile) {
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);
}

View File

@ -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();
};

View File

@ -3,19 +3,12 @@
* 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 + '';
@ -24,7 +17,7 @@ function padLeft(number, length) {
return str;
}
function getFormExpiryDays () {
function getFormExpiryDays() {
if (prefs.prefHasUserValue("browser.formfill.expire_days"))
return prefs.getIntPref("browser.formfill.expire_days");
else
@ -32,205 +25,213 @@ function getFormExpiryDays () {
}
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;
var timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
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;
fh.DBConnection.beginTransaction();
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) +
");");
fh.DBConnection.executeSimpleSQL(
"INSERT INTO moz_formhistory "+
"(fieldname, value, timesUsed, firstUsed, lastUsed) " +
"VALUES ("+
"'field1', " +
"'value" + padLeft(numRecords - 2 - i, 2) + "', " +
"1, " +
useDate + ", " +
useDate +
");");
}
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"));
// ===== 2 =====
// Check search contains all entries
testnum++;
var results = fac.autoCompleteSearch("field1", "", null, null);
do_check_eq(numRecords, results.matchCount);
// ===== 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);
}
// ===== 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);
}
// ===== 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 +
");");
}
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);
}
// ===== 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);
}
// ===== 7 =====
// Check that "senior citizen" entries get a bonus (browser.formfill.agedBonus)
testnum++;
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 +
");");
results = fac.autoCompleteSearch("field3", "", null, null);
do_check_eq(results.getValueAt(0), "senior citizen");
do_check_eq(results.getValueAt(1), "old but not senior");
// ===== 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 +
");");
fh.DBConnection.executeSimpleSQL(
"INSERT INTO moz_formhistory "+
"(fieldname, value, timesUsed, firstUsed, lastUsed) " +
"VALUES ("+
"'field4', " +
"'in the future 1', " +
"1, " +
0 + ", " +
(now * 2) +
");");
fh.DBConnection.executeSimpleSQL(
"INSERT INTO moz_formhistory "+
"(fieldname, value, timesUsed, firstUsed, lastUsed) " +
"VALUES ("+
"'field4', " +
"'in the future 2', " +
"1, " +
(now * 2) + ", " +
(now * 2) +
");");
results = fac.autoCompleteSearch("field4", "", null, null);
do_check_eq(results.matchCount, 3);
} catch (e) {
throw "FAILED in test #" + testnum + " -- " + e;
}
run_next_test();
}
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
now = 1000 * Date.now();
numRecords = Math.ceil(maxTimeGroupings / bucketSize) * 2;
let changes = [ ];
for (let i = 0; i < numRecords; i+=2) {
let useDate = now - (i/2 * bucketSize * timeGroupingSize);
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");
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();
}
});
});
add_test(function test3() {
do_log_info("Check search result ordering with empty search term");
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\"");
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();
}
});
});
const timesUsedSamples = 20;
add_test(function test5() {
do_log_info("Begin tests with constant use dates and varying timesUsed");
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();
}
});
});
add_test(function test7() {
do_log_info("Check search result ordering with \"v\"");
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();
}
});
});
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);
});
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();
}
});
});
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);
});
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");
let changes = [ ];
for (let value of syncValues) {
changes.push({ op : "add", fieldname: "field5", value: value });
}
updateFormHistory(changes, run_next_test);
});
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]);
}
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();
});

View File

@ -2,65 +2,88 @@
* 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();
}
add_test(function test_corruptFormHistoryDB_lazyCorruptInit1() {
do_log_info("ensure FormHistory backs up a corrupt DB on initialization.");
// ===== 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;
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();
});
});
});
} catch (e) {
throw "FAILED in test #" + testnum + " -- " + e;
}
}
add_test(function test_corruptFormHistoryDB_removeEntry() {
do_log_info("test removing an entry to the empty DB.");
updateEntry("remove", "name-A", "value-A",
function() {
countEntries("name-A", "value-A",
function(count) {
do_check_true(count == 0);
run_next_test();
});
});
});

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -3,9 +3,17 @@
* 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 {
@ -22,19 +30,29 @@ function run_test()
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;

View File

@ -3,9 +3,17 @@
* 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 {
@ -22,19 +30,27 @@ function run_test()
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;

View File

@ -11,7 +11,20 @@
* 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;
@ -29,32 +42,34 @@ function run_test()
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();
}

View File

@ -11,7 +11,20 @@
* 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;
@ -34,43 +47,46 @@ function run_test()
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();
}

View File

@ -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");
}
}

View File

@ -3,10 +3,101 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var testnum = 0;
var fh;
let dbConnection; // used for deleted table tests
function run_test()
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 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 =====
@ -21,105 +112,286 @@ function run_test()
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();

View File

@ -5,10 +5,7 @@
*
*/
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
var expectedNotification;
var expectedBeforeNotification = null;
var expectedData;
var TestObserver = {
@ -16,197 +13,146 @@ var TestObserver = {
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()));
break;
case "before-removeAllEntries":
case "removeAllEntries":
do_check_eq(subject, expectedData);
break;
case "before-removeEntriesForName":
case "removeEntriesForName":
case "formhistory-add":
case "formhistory-update":
do_check_true(subject instanceof Ci.nsISupportsString);
do_check_eq(subject, expectedData);
do_check_true(isGUID.test(subject.toString()));
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;
}
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);
}
let testIterator = null;
function run_test() {
do_test_pending();
testIterator = run_test_steps();
testIterator.next();
}
function next_test()
{
testIterator.next();
}
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"];
// 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");
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);
/* ========== 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;

View File

@ -2,18 +2,12 @@
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]