/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Testing search suggestions from SearchSuggestionController.jsm. */ "use strict"; Cu.import("resource://gre/modules/FormHistory.jsm"); Cu.import("resource://gre/modules/SearchSuggestionController.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); // We must make sure the FormHistoryStartup component is // initialized in order for it to respond to FormHistory // requests from nsFormAutoComplete.js. var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"]. getService(Ci.nsIObserver); formHistoryStartup.observe(null, "profile-after-change", null); var httpServer = new HttpServer(); var getEngine, postEngine, unresolvableEngine; function run_test() { Services.prefs.setBoolPref("browser.search.suggest.enabled", true); removeMetadata(); updateAppInfo(); let server = useHttpServer(); server.registerContentType("sjs", "sjs"); do_register_cleanup(() => Task.spawn(function* cleanup() { // Remove added form history entries yield updateSearchHistory("remove", null); FormHistory.shutdown(); Services.prefs.clearUserPref("browser.search.suggest.enabled"); })); run_next_test(); } add_task(function* add_test_engines() { let getEngineData = { baseURL: gDataUrl, name: "GET suggestion engine", method: "GET", }; let postEngineData = { baseURL: gDataUrl, name: "POST suggestion engine", method: "POST", }; let unresolvableEngineData = { baseURL: "http://example.invalid/", name: "Offline suggestion engine", method: "GET", }; [getEngine, postEngine, unresolvableEngine] = yield addTestEngines([ { name: getEngineData.name, xmlFileName: "engineMaker.sjs?" + JSON.stringify(getEngineData), }, { name: postEngineData.name, xmlFileName: "engineMaker.sjs?" + JSON.stringify(postEngineData), }, { name: unresolvableEngineData.name, xmlFileName: "engineMaker.sjs?" + JSON.stringify(unresolvableEngineData), }, ]); }); // Begin tests add_task(function* simple_no_result_callback() { let deferred = Promise.defer(); let controller = new SearchSuggestionController((result) => { do_check_eq(result.term, "no remote"); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 0); deferred.resolve(); }); controller.fetch("no remote", false, getEngine); yield deferred.promise; }); add_task(function* simple_no_result_callback_and_promise() { // Make sure both the callback and promise get results let deferred = Promise.defer(); let controller = new SearchSuggestionController((result) => { do_check_eq(result.term, "no results"); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 0); deferred.resolve(); }); let result = yield controller.fetch("no results", false, getEngine); do_check_eq(result.term, "no results"); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 0); yield deferred.promise; }); add_task(function* simple_no_result_promise() { let controller = new SearchSuggestionController(); let result = yield controller.fetch("no remote", false, getEngine); do_check_eq(result.term, "no remote"); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 0); }); add_task(function* simple_remote_no_local_result() { let controller = new SearchSuggestionController(); let result = yield controller.fetch("mo", false, getEngine); do_check_eq(result.term, "mo"); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 3); do_check_eq(result.remote[0], "Mozilla"); do_check_eq(result.remote[1], "modern"); do_check_eq(result.remote[2], "mom"); }); add_task(function* remote_term_case_mismatch() { let controller = new SearchSuggestionController(); let result = yield controller.fetch("Query Case Mismatch", false, getEngine); do_check_eq(result.term, "Query Case Mismatch"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "Query Case Mismatch"); }); add_task(function* simple_local_no_remote_result() { yield updateSearchHistory("bump", "no remote entries"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("no remote", false, getEngine); do_check_eq(result.term, "no remote"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "no remote entries"); do_check_eq(result.remote.length, 0); yield updateSearchHistory("remove", "no remote entries"); }); add_task(function* simple_non_ascii() { yield updateSearchHistory("bump", "I ❤️ XUL"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("I ❤️", false, getEngine); do_check_eq(result.term, "I ❤️"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "I ❤️ XUL"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "I ❤️ Mozilla"); }); add_task(function* both_local_remote_result_dedupe() { yield updateSearchHistory("bump", "Mozilla"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("mo", false, getEngine); do_check_eq(result.term, "mo"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "Mozilla"); do_check_eq(result.remote.length, 2); do_check_eq(result.remote[0], "modern"); do_check_eq(result.remote[1], "mom"); }); add_task(function* POST_both_local_remote_result_dedupe() { let controller = new SearchSuggestionController(); let result = yield controller.fetch("mo", false, postEngine); do_check_eq(result.term, "mo"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "Mozilla"); do_check_eq(result.remote.length, 2); do_check_eq(result.remote[0], "modern"); do_check_eq(result.remote[1], "mom"); }); add_task(function* both_local_remote_result_dedupe2() { yield updateSearchHistory("bump", "mom"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("mo", false, getEngine); do_check_eq(result.term, "mo"); do_check_eq(result.local.length, 2); do_check_eq(result.local[0], "mom"); do_check_eq(result.local[1], "Mozilla"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "modern"); }); add_task(function* both_local_remote_result_dedupe3() { // All of the server entries also exist locally yield updateSearchHistory("bump", "modern"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("mo", false, getEngine); do_check_eq(result.term, "mo"); do_check_eq(result.local.length, 3); do_check_eq(result.local[0], "modern"); do_check_eq(result.local[1], "mom"); do_check_eq(result.local[2], "Mozilla"); do_check_eq(result.remote.length, 0); }); add_task(function* fetch_twice_in_a_row() { // Two entries since the first will match the first fetch but not the second. yield updateSearchHistory("bump", "delay local"); yield updateSearchHistory("bump", "delayed local"); let controller = new SearchSuggestionController(); let resultPromise1 = controller.fetch("delay", false, getEngine); // A second fetch while the server is still waiting to return results leads to an abort. let resultPromise2 = controller.fetch("delayed ", false, getEngine); yield resultPromise1.then((results) => do_check_null(results)); let result = yield resultPromise2; do_check_eq(result.term, "delayed "); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "delayed local"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "delayed "); }); add_task(function* fetch_twice_subset_reuse_formHistoryResult() { // This tests if we mess up re-using the cached form history result. // Two entries since the first will match the first fetch but not the second. yield updateSearchHistory("bump", "delay local"); yield updateSearchHistory("bump", "delayed local"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("delay", false, getEngine); do_check_eq(result.term, "delay"); do_check_eq(result.local.length, 2); do_check_eq(result.local[0], "delay local"); do_check_eq(result.local[1], "delayed local"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "delay"); // Remove the entry from the DB but it should remain in the cached formHistoryResult. yield updateSearchHistory("remove", "delayed local"); let result2 = yield controller.fetch("delayed ", false, getEngine); do_check_eq(result2.term, "delayed "); do_check_eq(result2.local.length, 1); do_check_eq(result2.local[0], "delayed local"); do_check_eq(result2.remote.length, 1); do_check_eq(result2.remote[0], "delayed "); }); add_task(function* both_identical_with_more_than_max_results() { // Add letters A through Z to form history which will match the server for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) { yield updateSearchHistory("bump", "letter " + String.fromCharCode(charCode)); } let controller = new SearchSuggestionController(); controller.maxLocalResults = 7; controller.maxRemoteResults = 10; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 7); for (let i = 0; i < controller.maxLocalResults; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.local.length + result.remote.length, 10); for (let i = 0; i < result.remote.length; i++) { do_check_eq(result.remote[i], "letter " + String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i)); } }); add_task(function* noremote_maxLocal() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 2; // (should be ignored because no remote results) controller.maxRemoteResults = 0; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 26); for (let i = 0; i < result.local.length; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.remote.length, 0); }); add_task(function* someremote_maxLocal() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 2; controller.maxRemoteResults = 4; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 2); for (let i = 0; i < result.local.length; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.remote.length, 2); // "A" and "B" will have been de-duped, start at C for remote results for (let i = 0; i < result.remote.length; i++) { do_check_eq(result.remote[i], "letter " + String.fromCharCode("C".charCodeAt() + i)); } }); add_task(function* one_of_each() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 1; controller.maxRemoteResults = 2; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "letter A"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "letter B"); }); add_task(function* local_result_returned_remote_result_disabled() { Services.prefs.setBoolPref("browser.search.suggest.enabled", false); let controller = new SearchSuggestionController(); controller.maxLocalResults = 1; controller.maxRemoteResults = 1; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 26); for (let i = 0; i < 26; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.remote.length, 0); Services.prefs.setBoolPref("browser.search.suggest.enabled", true); }); add_task(function* local_result_returned_remote_result_disabled_after_creation_of_controller() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 1; controller.maxRemoteResults = 1; Services.prefs.setBoolPref("browser.search.suggest.enabled", false); let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 26); for (let i = 0; i < 26; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.remote.length, 0); Services.prefs.setBoolPref("browser.search.suggest.enabled", true); }); add_task(function* one_of_each_disabled_before_creation_enabled_after_creation_of_controller() { Services.prefs.setBoolPref("browser.search.suggest.enabled", false); let controller = new SearchSuggestionController(); controller.maxLocalResults = 1; controller.maxRemoteResults = 2; Services.prefs.setBoolPref("browser.search.suggest.enabled", true); let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "letter A"); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "letter B"); }); add_task(function* reset_suggestions_pref() { Services.prefs.setBoolPref("browser.search.suggest.enabled", true); }); add_task(function* one_local_zero_remote() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 1; controller.maxRemoteResults = 0; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 26); for (let i = 0; i < 26; i++) { do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i)); } do_check_eq(result.remote.length, 0); }); add_task(function* zero_local_one_remote() { let controller = new SearchSuggestionController(); controller.maxLocalResults = 0; controller.maxRemoteResults = 1; let result = yield controller.fetch("letter ", false, getEngine); do_check_eq(result.term, "letter "); do_check_eq(result.local.length, 0); do_check_eq(result.remote.length, 1); do_check_eq(result.remote[0], "letter A"); }); add_task(function* stop_search() { let controller = new SearchSuggestionController((result) => { do_throw("The callback shouldn't be called after stop()"); }); let resultPromise = controller.fetch("mo", false, getEngine); controller.stop(); yield resultPromise.then((result) => { do_check_null(result); }); }); add_task(function* empty_searchTerm() { // Empty searches don't go to the server but still get form history. let controller = new SearchSuggestionController(); let result = yield controller.fetch("", false, getEngine); do_check_eq(result.term, ""); do_check_true(result.local.length > 0); do_check_eq(result.remote.length, 0); }); add_task(function* slow_timeout() { let d = Promise.defer(); function check_result(result) { do_check_eq(result.term, "slow "); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "slow local result"); do_check_eq(result.remote.length, 0); } yield updateSearchHistory("bump", "slow local result"); let controller = new SearchSuggestionController(); setTimeout(function check_timeout() { // The HTTP response takes 10 seconds so check that we already have results after 2 seconds. check_result(result); d.resolve(); }, 2000); let result = yield controller.fetch("slow ", false, getEngine); check_result(result); yield d.promise; }); add_task(function* slow_stop() { let d = Promise.defer(); let controller = new SearchSuggestionController(); let resultPromise = controller.fetch("slow ", false, getEngine); setTimeout(function check_timeout() { // The HTTP response takes 10 seconds but we timeout in less than a second so just use 0. controller.stop(); d.resolve(); }, 0); yield resultPromise.then((result) => { do_check_null(result); }); yield d.promise; }); // Error handling add_task(function* remote_term_mismatch() { yield updateSearchHistory("bump", "Query Mismatch Entry"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("Query Mismatch", false, getEngine); do_check_eq(result.term, "Query Mismatch"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "Query Mismatch Entry"); do_check_eq(result.remote.length, 0); }); add_task(function* http_404() { yield updateSearchHistory("bump", "HTTP 404 Entry"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("HTTP 404", false, getEngine); do_check_eq(result.term, "HTTP 404"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "HTTP 404 Entry"); do_check_eq(result.remote.length, 0); }); add_task(function* http_500() { yield updateSearchHistory("bump", "HTTP 500 Entry"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("HTTP 500", false, getEngine); do_check_eq(result.term, "HTTP 500"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "HTTP 500 Entry"); do_check_eq(result.remote.length, 0); }); add_task(function* unresolvable_server() { yield updateSearchHistory("bump", "Unresolvable Server Entry"); let controller = new SearchSuggestionController(); let result = yield controller.fetch("Unresolvable Server", false, unresolvableEngine); do_check_eq(result.term, "Unresolvable Server"); do_check_eq(result.local.length, 1); do_check_eq(result.local[0], "Unresolvable Server Entry"); do_check_eq(result.remote.length, 0); }); // Exception handling add_task(function* missing_pb() { Assert.throws(() => { let controller = new SearchSuggestionController(); controller.fetch("No privacy"); }, /priva/i); }); add_task(function* missing_engine() { Assert.throws(() => { let controller = new SearchSuggestionController(); controller.fetch("No engine", false); }, /engine/i); }); add_task(function* invalid_engine() { Assert.throws(() => { let controller = new SearchSuggestionController(); controller.fetch("invalid engine", false, {}); }, /engine/i); }); add_task(function* no_results_requested() { Assert.throws(() => { let controller = new SearchSuggestionController(); controller.maxLocalResults = 0; controller.maxRemoteResults = 0; controller.fetch("No results requested", false, getEngine); }, /result/i); }); add_task(function* minus_one_results_requested() { Assert.throws(() => { let controller = new SearchSuggestionController(); controller.maxLocalResults = -1; controller.fetch("-1 results requested", false, getEngine); }, /result/i); }); add_task(function* test_userContextId() { let controller = new SearchSuggestionController(); controller._fetchRemote = function(searchTerm, engine, privateMode, userContextId) { Assert.equal(userContextId, 1); return Promise.defer(); }; controller.fetch("test", false, getEngine, 1); }); // Helpers function updateSearchHistory(operation, value) { let deferred = Promise.defer(); FormHistory.update({ op: operation, fieldname: "searchbar-history", value: value, }, { handleError: function (error) { do_throw("Error occurred updating form history: " + error); deferred.reject(error); }, handleCompletion: function (reason) { if (!reason) deferred.resolve(); } }); return deferred.promise; }