/* 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 SEARCH_STRING = "frab"; const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest; const REMOTE_SETTINGS_RESULTS = [ QuickSuggestTestUtils.ampRemoteSettings({ keywords: [SEARCH_STRING], }), ]; const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = makeAmpResult({ keyword: SEARCH_STRING, }); const EXPECTED_MERINO_URLBAR_RESULT = makeAmpResult({ source: "merino", provider: "adm", requestId: "request_id", }); // `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino // fetch, so it's easiest to create `gClient` lazily too. ChromeUtils.defineLazyGetter( this, "gClient", () => UrlbarProviderQuickSuggest._test_merino ); add_setup(async () => { await MerinoTestUtils.server.start(); // Set up the remote settings client with the test data. await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsRecords: [ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ], prefs: [ ["suggest.quicksuggest.nonsponsored", true], ["suggest.quicksuggest.sponsored", true], ], }); Assert.equal( typeof DEFAULT_SUGGESTION_SCORE, "number", "Sanity check: DEFAULT_SUGGESTION_SCORE is defined" ); }); // Tests with the Merino endpoint URL set to an empty string, which disables // fetching from Merino. add_task(async function merinoDisabled() { let mockEndpointUrl = UrlbarPrefs.get("merino.endpointURL"); UrlbarPrefs.set("merino.endpointURL", ""); UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); // Clear the remote settings suggestions so that if Merino is actually queried // -- which would be a bug -- we don't accidentally mask the Merino suggestion // by also matching an RS suggestion with the same or higher score. await QuickSuggestTestUtils.setRemoteSettingsRecords([]); 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: UrlbarProviderQuickSuggest._test_merino, }); UrlbarPrefs.set("merino.endpointURL", mockEndpointUrl); await QuickSuggestTestUtils.setRemoteSettingsRecords([ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ]); }); // Tests with Merino enabled but with data collection disabled. Results should // not be fetched from Merino in that case. add_task(async function dataCollectionDisabled() { UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, false); // Clear the remote settings suggestions so that if Merino is actually queried // -- which would be a bug -- we don't accidentally mask the Merino suggestion // by also matching an RS suggestion with the same or higher score. await QuickSuggestTestUtils.setRemoteSettingsRecords([]); let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [], }); await QuickSuggestTestUtils.setRemoteSettingsRecords([ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ]); }); // 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_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = 2 * 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_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = 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_DATA_COLLECTION_ENABLED, true); let histograms = MerinoTestUtils.getAndClearHistograms(); MerinoTestUtils.server.response.body.suggestions[0].score = 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_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_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_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(); }); // When Merino returns multiple suggestions, the one with the largest score // should be used. add_task(async function multipleMerinoSuggestions() { 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", iab_category: "22 - Shopping", 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", iab_category: "22 - Shopping", 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", iab_category: "22 - Shopping", is_sponsored: true, score: 0.2, }, ]; let context = createContext("test", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [ makeAmpResult({ keyword: "multipleMerinoSuggestions 1 full_keyword", title: "multipleMerinoSuggestions 1 title", url: "multipleMerinoSuggestions 1 url", originalUrl: "multipleMerinoSuggestions 1 url", icon: "multipleMerinoSuggestions 1 icon", impressionUrl: "multipleMerinoSuggestions 1 impression_url", clickUrl: "multipleMerinoSuggestions 1 click_url", blockId: 1, advertiser: "multipleMerinoSuggestions 1 advertiser", requestId: "request_id", source: "merino", provider: "adm", }), ], }); 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_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_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); await QuickSuggestTestUtils.forceSync(); gClient.resetSession(); }); // Test whether the blocking for Merino results works. add_task(async function block() { UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); // Make sure the Merino suggestions have different URLs from the remote // settings suggestion. let { suggestions } = MerinoTestUtils.server.response.body; for (let i = 0; i < suggestions.length; i++) { let suggestion = suggestions[i]; suggestion.url = "https://example.com/merino-url-" + i; 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 top pick/best match. add_task(async function bestMatch() { UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); // Set up a suggestion with `is_top_pick` and an unknown provider so that // UrlbarProviderQuickSuggest will make a default result for it. MerinoTestUtils.server.response.body.suggestions = [ { is_top_pick: true, provider: "some_top_pick_provider", full_keyword: "full_keyword", title: "title", url: "url", icon: null, score: 1, }, ]; let context = createContext(SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [ { isBestMatch: true, type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "some_top_pick_provider", title: "title", url: "url", icon: null, qsSuggestion: "full_keyword", isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, isManageable: true, displayUrl: "url", source: "merino", provider: "some_top_pick_provider", }, }, ], }); // 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"); MerinoTestUtils.server.reset(); gClient.resetSession(); });