/* 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/. */ // Tests Merino integration with UrlbarProviderQuickSuggest. "use strict"; // relative to `browser.urlbar` const PREF_DATA_COLLECTION_ENABLED = "quicksuggest.dataCollection.enabled"; const PREF_MERINO_ENABLED = "merino.enabled"; const PREF_REMOTE_SETTINGS_ENABLED = "quicksuggest.remoteSettings.enabled"; const SEARCH_STRING = "frab"; const REMOTE_SETTINGS_RESULTS = [ { id: 1, url: "http://test.com/q=frabbits", title: "frabbits", keywords: [SEARCH_STRING], click_url: "http://click.reporting.test.com/", impression_url: "http://impression.reporting.test.com/", advertiser: "TestAdvertiser", }, ]; const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_sponsored", qsSuggestion: SEARCH_STRING, title: "frabbits", url: "http://test.com/q=frabbits", originalUrl: "http://test.com/q=frabbits", icon: null, sponsoredImpressionUrl: "http://impression.reporting.test.com/", sponsoredClickUrl: "http://click.reporting.test.com/", sponsoredBlockId: 1, sponsoredAdvertiser: "TestAdvertiser", isSponsored: true, helpUrl: QuickSuggest.HELP_URL, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, displayUrl: "http://test.com/q=frabbits", source: "remote-settings", }, }; const EXPECTED_MERINO_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_sponsored", qsSuggestion: "full_keyword", title: "title", url: "url", originalUrl: "url", icon: null, sponsoredImpressionUrl: "impression_url", sponsoredClickUrl: "click_url", sponsoredBlockId: 1, sponsoredAdvertiser: "advertiser", isSponsored: true, helpUrl: QuickSuggest.HELP_URL, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, displayUrl: "url", requestId: "request_id", source: "merino", }, }; // `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino // fetch, so it's easiest to create `gClient` lazily too. XPCOMUtils.defineLazyGetter( this, "gClient", () => UrlbarProviderQuickSuggest._test_merino ); add_task(async function init() { UrlbarPrefs.set("quicksuggest.enabled", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false); await MerinoTestUtils.server.start(); // Set up the remote settings client with the test data. await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsResults: [ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ], }); Assert.equal( typeof QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, "number", "Sanity check: DEFAULT_SUGGESTION_SCORE is defined" ); }); // Tests with Merino enabled and remote settings disabled. add_task(async function oneEnabled_merino() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); // Use a score lower than the remote settings score to make sure the // suggestion is included regardless. MerinoTestUtils.server.response.body.suggestions[0].score = QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_MERINO_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // Tests with Merino disabled and remote settings enabled. add_task(async function oneEnabled_remoteSettings() { UrlbarPrefs.set(PREF_MERINO_ENABLED, false); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: null, latencyRecorded: false, client: gClient, }); }); // Tests with Merino enabled but with data collection disabled. Results should // not be fetched from Merino in that case. Also tests with remote settings // enabled. add_task(async function dataCollectionDisabled() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, false); let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); }); // When the Merino suggestion has a higher score than the remote settings // suggestion, the Merino suggestion should be used. add_task(async function higherScore() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_MERINO_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When the Merino suggestion has a lower score than the remote settings // suggestion, the remote settings suggestion should be used. add_task(async function lowerScore() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When the Merino and remote settings suggestions have the same score, the // remote settings suggestion should be used. add_task(async function sameScore() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When the Merino suggestion does not include a score, the remote settings // suggestion should be used. add_task(async function noMerinoScore() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); Assert.equal( typeof MerinoTestUtils.server.response.body.suggestions[0].score, "number", "Sanity check: First suggestion has a score" ); delete MerinoTestUtils.server.response.body.suggestions[0].score; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When remote settings doesn't return a suggestion but Merino does, the Merino // suggestion should be used. add_task(async function noSuggestion_remoteSettings() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); let context = createContext("this doesn't match remote settings", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_MERINO_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When Merino doesn't return a suggestion but remote settings does, the remote // settings suggestion should be used. add_task(async function noSuggestion_merino() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions = []; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "no_suggestion", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // Tests with both Merino and remote settings disabled. add_task(async function bothDisabled() { UrlbarPrefs.set(PREF_MERINO_ENABLED, false); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: null, latencyRecorded: false, client: gClient, }); }); // When Merino returns multiple suggestions, the one with the largest score // should be used. add_task(async function multipleMerinoSuggestions() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions = [ { provider: "adm", full_keyword: "multipleMerinoSuggestions 0 full_keyword", title: "multipleMerinoSuggestions 0 title", url: "multipleMerinoSuggestions 0 url", icon: "multipleMerinoSuggestions 0 icon", impression_url: "multipleMerinoSuggestions 0 impression_url", click_url: "multipleMerinoSuggestions 0 click_url", block_id: 0, advertiser: "multipleMerinoSuggestions 0 advertiser", is_sponsored: true, score: 0.1, }, { provider: "adm", full_keyword: "multipleMerinoSuggestions 1 full_keyword", title: "multipleMerinoSuggestions 1 title", url: "multipleMerinoSuggestions 1 url", icon: "multipleMerinoSuggestions 1 icon", impression_url: "multipleMerinoSuggestions 1 impression_url", click_url: "multipleMerinoSuggestions 1 click_url", block_id: 1, advertiser: "multipleMerinoSuggestions 1 advertiser", is_sponsored: true, score: 1, }, { provider: "adm", full_keyword: "multipleMerinoSuggestions 2 full_keyword", title: "multipleMerinoSuggestions 2 title", url: "multipleMerinoSuggestions 2 url", icon: "multipleMerinoSuggestions 2 icon", impression_url: "multipleMerinoSuggestions 2 impression_url", click_url: "multipleMerinoSuggestions 2 click_url", block_id: 2, advertiser: "multipleMerinoSuggestions 2 advertiser", is_sponsored: true, score: 0.2, }, ]; let context = createContext("test", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [ { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_sponsored", qsSuggestion: "multipleMerinoSuggestions 1 full_keyword", title: "multipleMerinoSuggestions 1 title", url: "multipleMerinoSuggestions 1 url", originalUrl: "multipleMerinoSuggestions 1 url", icon: "multipleMerinoSuggestions 1 icon", sponsoredImpressionUrl: "multipleMerinoSuggestions 1 impression_url", sponsoredClickUrl: "multipleMerinoSuggestions 1 click_url", sponsoredBlockId: 1, sponsoredAdvertiser: "multipleMerinoSuggestions 1 advertiser", isSponsored: true, helpUrl: QuickSuggest.HELP_URL, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, displayUrl: "multipleMerinoSuggestions 1 url", requestId: "request_id", source: "merino", }, }, ], }); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // Timestamp templates in URLs should be replaced with real timestamps. add_task(async function timestamps() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); // Set up the Merino response with template URLs. let suggestion = MerinoTestUtils.server.response.body.suggestions[0]; let { TIMESTAMP_TEMPLATE } = QuickSuggest; suggestion.url = `http://example.com/time-${TIMESTAMP_TEMPLATE}`; suggestion.click_url = `http://example.com/time-${TIMESTAMP_TEMPLATE}-foo`; // Do a search. let context = createContext("test", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); let controller = UrlbarTestUtils.newMockController({ input: { isPrivate: context.isPrivate, onFirstResult() { return false; }, getSearchSource() { return "dummy-search-source"; }, window: { location: { href: AppConstants.BROWSER_CHROME_URL, }, }, }, }); await controller.startQuery(context); // Should be one quick suggest result. Assert.equal(context.results.length, 1, "One result returned"); let result = context.results[0]; QuickSuggestTestUtils.assertTimestampsReplaced(result, { url: suggestion.click_url, sponsoredClickUrl: suggestion.click_url, }); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // When both suggestion types are disabled but data collection is enabled, we // should still send requests to Merino, and the requests should include an // empty `providers` to tell Merino not to fetch any suggestions. add_task(async function suggestedDisabled_dataCollectionEnabled() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); let histograms = MerinoTestUtils.getAndClearHistograms(); let context = createContext("test", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [], }); // Check that the request is received and includes an empty `providers`. MerinoTestUtils.server.checkAndClearRequests([ { params: { [MerinoTestUtils.SEARCH_PARAMS.QUERY]: "test", [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, [MerinoTestUtils.SEARCH_PARAMS.PROVIDERS]: "", }, }, ]); MerinoTestUtils.checkAndClearHistograms({ histograms, response: "success", latencyRecorded: true, client: gClient, }); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); gClient.resetSession(); }); // Test whether the blocking for Merino results works. add_task(async function block() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); for (const suggestion of MerinoTestUtils.server.response.body.suggestions) { await QuickSuggest.blockedSuggestions.add(suggestion.url); } const context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], }); await QuickSuggest.blockedSuggestions.clear(); MerinoTestUtils.server.reset(); gClient.resetSession(); }); // Tests a Merino suggestion that is a best match. add_task(async function bestMatch() { UrlbarPrefs.set(PREF_MERINO_ENABLED, true); UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); // Simply enabling the best match feature should make the mock suggestion a // best match because the search string length is greater than the required // best match length. UrlbarPrefs.set("bestMatch.enabled", true); UrlbarPrefs.set("suggest.bestmatch", true); let expectedResult = { ...EXPECTED_MERINO_URLBAR_RESULT }; expectedResult.payload = { ...EXPECTED_MERINO_URLBAR_RESULT.payload }; expectedResult.isBestMatch = true; delete expectedResult.payload.qsSuggestion; await QuickSuggestTestUtils.withConfig({ config: QuickSuggestTestUtils.BEST_MATCH_CONFIG, callback: async () => { let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [expectedResult], }); // This isn't necessary since `check_results()` checks `isBestMatch`, but // check it here explicitly for good measure. Assert.ok(context.results[0].isBestMatch, "Result is a best match"); }, }); UrlbarPrefs.clear("bestMatch.enabled"); UrlbarPrefs.clear("suggest.bestmatch"); MerinoTestUtils.server.reset(); gClient.resetSession(); });