summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/tests/xpcshell/test_searchSuggest.js')
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest.js891
1 files changed, 891 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
new file mode 100644
index 0000000000..161e7d6c63
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
@@ -0,0 +1,891 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * Testing search suggestions from SearchSuggestionController.jsm.
+ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+);
+const { SearchSuggestionController } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchSuggestionController.sys.mjs"
+);
+const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const ENGINE_NAME = "other";
+const SEARCH_TELEMETRY_LATENCY = "SEARCH_SUGGESTIONS_LATENCY_MS";
+
+// 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 getEngine, postEngine, unresolvableEngine, alternateJSONEngine;
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ // These tests intentionally test broken connections.
+ consoleAllowList = consoleAllowList.concat([
+ "Non-200 status or empty HTTP response: 404",
+ "Non-200 status or empty HTTP response: 500",
+ "Unexpected response, searchString does not match remote response",
+ "HTTP request timeout",
+ "HTTP error",
+ ]);
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ await AddonTestUtils.promiseStartupManager();
+
+ registerCleanupFunction(async () => {
+ // Remove added form history entries
+ await updateSearchHistory("remove", null);
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+});
+
+add_task(async 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",
+ };
+
+ let alternateJSONSuggestEngineData = {
+ baseURL: gDataUrl,
+ name: "Alternative JSON suggestion type",
+ method: "GET",
+ alternativeJSONType: true,
+ };
+
+ getEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(getEngineData)}`,
+ });
+ postEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(postEngineData)}`,
+ });
+ unresolvableEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(unresolvableEngineData)}`,
+ });
+ alternateJSONEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(
+ alternateJSONSuggestEngineData
+ )}`,
+ });
+});
+
+// Begin tests
+
+add_task(async function simple_no_result_promise() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("no remote", false, getEngine);
+ Assert.equal(result.term, "no remote");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "Mozilla");
+ Assert.equal(result.remote[1].value, "modern");
+ Assert.equal(result.remote[2].value, "mom");
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result_telemetry() {
+ Services.telemetry.clearScalars();
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ await controller.fetch("mo", false, getEngine);
+
+ let scalars = {};
+ const key = "browser.search.data_transferred";
+
+ await TestUtils.waitForCondition(() => {
+ scalars =
+ Services.telemetry.getSnapshotForKeyedScalars("main", false).parent || {};
+ return key in scalars;
+ }, "should have the expected keyed scalars");
+
+ const scalar = scalars[key];
+ Assert.ok(`sggt-${ENGINE_NAME}` in scalar, "correct telemetry category");
+ Assert.notEqual(scalar[`sggt-${ENGINE_NAME}`], 0, "bandwidth logged");
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function simple_remote_no_local_result_alternative_type() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, alternateJSONEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "Mozilla");
+ Assert.equal(result.remote[1].value, "modern");
+ Assert.equal(result.remote[2].value, "mom");
+});
+
+add_task(async function remote_term_case_mismatch() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("Query Case Mismatch", false, getEngine);
+ Assert.equal(result.term, "Query Case Mismatch");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "Query Case Mismatch");
+});
+
+add_task(async function simple_local_no_remote_result() {
+ await updateSearchHistory("bump", "no remote entries");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("no remote", false, getEngine);
+ Assert.equal(result.term, "no remote");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "no remote entries");
+ Assert.equal(result.remote.length, 0);
+
+ await updateSearchHistory("remove", "no remote entries");
+});
+
+add_task(async function simple_non_ascii() {
+ await updateSearchHistory("bump", "I ❤️ XUL");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("I ❤️", false, getEngine);
+ Assert.equal(result.term, "I ❤️");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "I ❤️ XUL");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "I ❤️ Mozilla");
+});
+
+add_task(async function both_local_remote_result_dedupe() {
+ await updateSearchHistory("bump", "Mozilla");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Mozilla");
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "modern");
+ Assert.equal(result.remote[1].value, "mom");
+});
+
+add_task(async function POST_both_local_remote_result_dedupe() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, postEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Mozilla");
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "modern");
+ Assert.equal(result.remote[1].value, "mom");
+});
+
+add_task(async function both_local_remote_result_dedupe2() {
+ await updateSearchHistory("bump", "mom");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 2);
+ Assert.equal(result.local[0].value, "mom");
+ Assert.equal(result.local[1].value, "Mozilla");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "modern");
+});
+
+add_task(async function both_local_remote_result_dedupe3() {
+ // All of the server entries also exist locally
+ await updateSearchHistory("bump", "modern");
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("mo", false, getEngine);
+ Assert.equal(result.term, "mo");
+ Assert.equal(result.local.length, 3);
+ Assert.equal(result.local[0].value, "modern");
+ Assert.equal(result.local[1].value, "mom");
+ Assert.equal(result.local[2].value, "Mozilla");
+ Assert.equal(result.remote.length, 0);
+});
+
+add_task(async function valid_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tail query", false, getEngine);
+ Assert.equal(result.term, "tail query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tail query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tail query tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[2].value, "tail query tail 2");
+ Assert.equal(result.remote[2].matchPrefix, "… ");
+ Assert.equal(result.remote[2].tail, "tail 2");
+});
+
+add_task(async function alt_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailalt query", false, getEngine);
+ Assert.equal(result.term, "tailalt query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailalt query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailalt query tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[2].value, "tailalt query tail 2");
+ Assert.equal(result.remote[2].matchPrefix, "… ");
+ Assert.equal(result.remote[2].tail, "tail 2");
+});
+
+add_task(async function invalid_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailjunk query", false, getEngine);
+ Assert.equal(result.term, "tailjunk query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailjunk query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailjunk query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "tailjunk query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function too_few_tail_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tailjunk few query", false, getEngine);
+ Assert.equal(result.term, "tailjunk few query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "tailjunk few query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "tailjunk few query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "tailjunk few query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function empty_rich_results() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("richempty query", false, getEngine);
+ Assert.equal(result.term, "richempty query");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[0].value, "richempty query normal");
+ Assert.ok(!result.remote[0].matchPrefix);
+ Assert.ok(!result.remote[0].tail);
+ Assert.equal(result.remote[1].value, "richempty query tail 1");
+ Assert.ok(!result.remote[1].matchPrefix);
+ Assert.ok(!result.remote[1].tail);
+ Assert.equal(result.remote[2].value, "richempty query tail 2");
+ Assert.ok(!result.remote[2].matchPrefix);
+ Assert.ok(!result.remote[2].tail);
+});
+
+add_task(async function tail_offset_index() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("tail tail 1 t", false, getEngine);
+ Assert.equal(result.term, "tail tail 1 t");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 3);
+ Assert.equal(result.remote[1].value, "tail tail 1 t tail 1");
+ Assert.equal(result.remote[1].matchPrefix, "… ");
+ Assert.equal(result.remote[1].tail, "tail 1");
+ Assert.equal(result.remote[1].tailOffsetIndex, 14);
+});
+
+add_task(async function fetch_twice_in_a_row() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Two entries since the first will match the first fetch but not the second.
+ await updateSearchHistory("bump", "delay local");
+ await 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);
+ await resultPromise1.then(results => Assert.equal(null, results));
+
+ let result = await resultPromise2;
+ Assert.equal(result.term, "delayed ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "delayed local");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "delayed ");
+
+ // Only the second fetch's latency should be recorded since the first fetch
+ // was aborted and latencies for aborted fetches are not recorded.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async 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++
+ ) {
+ await updateSearchHistory(
+ "bump",
+ "letter " + String.fromCharCode(charCode)
+ );
+ }
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 7;
+ controller.maxRemoteResults = 10;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 7);
+ for (let i = 0; i < controller.maxLocalResults; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.local.length + result.remote.length, 10);
+ for (let i = 0; i < result.remote.length; i++) {
+ Assert.equal(
+ result.remote[i].value,
+ "letter " +
+ String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i)
+ );
+ }
+});
+
+add_task(async function noremote_maxLocal() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2; // (should be ignored because no remote results)
+ controller.maxRemoteResults = 0;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < result.local.length; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function someremote_maxLocal() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2;
+ controller.maxRemoteResults = 4;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 2);
+ for (let i = 0; i < result.local.length; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(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++) {
+ Assert.equal(
+ result.remote[i].value,
+ "letter " + String.fromCharCode("C".charCodeAt() + i)
+ );
+ }
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function one_of_each() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "letter A");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter B");
+});
+
+add_task(async function local_result_returned_remote_result_disabled() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(
+ async function local_result_returned_remote_result_disabled_after_creation_of_controller() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ }
+);
+
+add_task(
+ async function one_of_each_disabled_before_creation_enabled_after_creation_of_controller() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ 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 = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "letter A");
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter B");
+
+ assertLatencyHistogram(histogram, true);
+
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ }
+);
+
+add_task(async function one_local_zero_remote() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 0;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ Assert.equal(
+ result.local[i].value,
+ "letter " + String.fromCharCode("A".charCodeAt() + i)
+ );
+ }
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function zero_local_one_remote() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = await controller.fetch("letter ", false, getEngine);
+ Assert.equal(result.term, "letter ");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 1);
+ Assert.equal(result.remote[0].value, "letter A");
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function stop_search() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController(result => {
+ do_throw("The callback shouldn't be called after stop()");
+ });
+ let resultPromise = controller.fetch("mo", false, getEngine);
+ controller.stop();
+ await resultPromise.then(result => {
+ Assert.equal(null, result);
+ });
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function empty_searchTerm() {
+ // Empty searches don't go to the server but still get form history.
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("", false, getEngine);
+ Assert.equal(result.term, "");
+ Assert.ok(!!result.local.length);
+ Assert.equal(result.remote.length, 0);
+ assertLatencyHistogram(histogram, false);
+});
+
+add_task(async function slow_timeout() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer than the timeout of
+ // the suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Add a local result.
+ let localValue = searchString + " local result";
+ await updateSearchHistory("bump", localValue);
+
+ // Do a search. The remote fetch should time out but the local result should
+ // be returned.
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch(searchString, false, getEngine);
+ Assert.equal(result.term, searchString);
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, localValue);
+ Assert.equal(result.remote.length, 0);
+
+ // The remote fetch isn't done yet, so the latency histogram should not be
+ // updated.
+ assertLatencyHistogram(histogram, false);
+
+ // Wait for the remote fetch to finish.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Now the latency histogram should be updated.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function slow_timeout_2() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer the timeout of the
+ // suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Add a local result.
+ let localValue = searchString + " local result";
+ await updateSearchHistory("bump", localValue);
+
+ // Do two searches using the same controller. Both times, the remote fetches
+ // should time out and only the local result should be returned. The second
+ // search should abort the remote fetch of the first search, and the remote
+ // fetch of the second search should be ongoing when the second search
+ // finishes.
+ let controller = new SearchSuggestionController();
+ for (let i = 0; i < 2; i++) {
+ let result = await controller.fetch(searchString, false, getEngine);
+ Assert.equal(result.term, searchString);
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, localValue);
+ Assert.equal(result.remote.length, 0);
+ }
+
+ // The remote fetch of the second search isn't done yet, so the latency
+ // histogram should not be updated.
+ assertLatencyHistogram(histogram, false);
+
+ // Wait for the second remote fetch to finish.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Now the latency histogram should be updated, and only the remote fetch of
+ // the second search should be recorded.
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function slow_stop() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ // Make the server return suggestions on a delay longer the timeout of the
+ // suggestion controller.
+ let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT;
+ let searchString = `delay${delayMs} `;
+
+ // Do a search but stop it before it finishes. Wait a tick before stopping it
+ // to better simulate the real world.
+ let controller = new SearchSuggestionController();
+ let resultPromise = controller.fetch(searchString, false, getEngine);
+ await TestUtils.waitForTick();
+ controller.stop();
+ let result = await resultPromise;
+ Assert.equal(result, null, "No result should be returned");
+
+ // The remote fetch should have been aborted by stopping the controller, but
+ // wait for the timeout period just to make sure it's done.
+ await new Promise(r => setTimeout(r, delayMs));
+
+ // Since the latencies of aborted fetches are not recorded, the latency
+ // histogram should not be updated.
+ assertLatencyHistogram(histogram, false);
+});
+
+// Error handling
+
+add_task(async function remote_term_mismatch() {
+ await updateSearchHistory("bump", "Query Mismatch Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("Query Mismatch", false, getEngine);
+ Assert.equal(result.term, "Query Mismatch");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Query Mismatch Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function http_404() {
+ await updateSearchHistory("bump", "HTTP 404 Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("HTTP 404", false, getEngine);
+ Assert.equal(result.term, "HTTP 404");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "HTTP 404 Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function http_500() {
+ await updateSearchHistory("bump", "HTTP 500 Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("HTTP 500", false, getEngine);
+ Assert.equal(result.term, "HTTP 500");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "HTTP 500 Entry");
+ Assert.equal(result.remote.length, 0);
+
+ assertLatencyHistogram(histogram, true);
+});
+
+add_task(async function unresolvable_server() {
+ await updateSearchHistory("bump", "Unresolvable Server Entry");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ SEARCH_TELEMETRY_LATENCY
+ );
+
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch(
+ "Unresolvable Server",
+ false,
+ unresolvableEngine
+ );
+ Assert.equal(result.term, "Unresolvable Server");
+ Assert.equal(result.local.length, 1);
+ Assert.equal(result.local[0].value, "Unresolvable Server Entry");
+ Assert.equal(result.remote.length, 0);
+
+ // This latency assert fails on Windows 7 (NT version 6.1), so skip it there.
+ if (!AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
+ assertLatencyHistogram(histogram, true);
+ }
+});
+
+// Exception handling
+
+add_task(async function missing_pb() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No privacy");
+ }, /priva/i);
+});
+
+add_task(async function missing_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No engine", false);
+ }, /engine/i);
+});
+
+add_task(async function invalid_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("invalid engine", false, {});
+ }, /engine/i);
+});
+
+add_task(async 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(async 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(async function test_userContextId() {
+ let controller = new SearchSuggestionController();
+ controller._fetchRemote = function (
+ searchTerm,
+ engine,
+ privateMode,
+ userContextId
+ ) {
+ Assert.equal(userContextId, 1);
+ return PromiseUtils.defer();
+ };
+
+ controller.fetch("test", false, getEngine, 1);
+});
+
+// Non-English characters
+
+add_task(async function suggestions_contain_escaped_unicode() {
+ let controller = new SearchSuggestionController();
+ let result = await controller.fetch("stü", false, getEngine);
+ Assert.equal(result.term, "stü");
+ Assert.equal(result.local.length, 0);
+ Assert.equal(result.remote.length, 2);
+ Assert.equal(result.remote[0].value, "stühle");
+ Assert.equal(result.remote[1].value, "stüssy");
+});
+
+// Helpers
+
+function updateSearchHistory(operation, value) {
+ return FormHistory.update({
+ op: operation,
+ fieldname: "searchbar-history",
+ value,
+ });
+}
+
+function assertLatencyHistogram(histogram, shouldRecord) {
+ let snapshot = histogram.snapshot();
+ info("Checking latency snapshot: " + JSON.stringify(snapshot));
+
+ // Build a map from engine ID => number of non-zero values recorded for it.
+ let valueCountByEngineId = Object.entries(snapshot).reduce(
+ (memo, [key, data]) => {
+ memo[key] = Object.values(data.values).filter(v => v != 0);
+ return memo;
+ },
+ {}
+ );
+
+ let expected = shouldRecord ? { [ENGINE_NAME]: [1] } : {};
+ Assert.deepEqual(
+ valueCountByEngineId,
+ expected,
+ shouldRecord ? "Latency histogram updated" : "Latency histogram not updated"
+ );
+}