diff options
Diffstat (limited to 'toolkit/components/search/tests/xpcshell/test_searchSuggest.js')
-rw-r--r-- | toolkit/components/search/tests/xpcshell/test_searchSuggest.js | 891 |
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" + ); +} |