/* 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/. */ // Basic tests for the quick suggest provider using the remote settings source. // See also test_quicksuggest_merino.js. "use strict"; ChromeUtils.defineESModuleGetters(this, { AmpMatchingStrategy: "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", AmpSuggestions: "resource:///modules/urlbar/private/AmpSuggestions.sys.mjs", SuggestionProvider: "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs", }); const SPONSORED_SEARCH_STRING = "amp"; const NONSPONSORED_SEARCH_STRING = "wikipedia"; const SPONSORED_AND_NONSPONSORED_SEARCH_STRING = "sponsored and non-sponsored"; const HTTP_SEARCH_STRING = "http prefix"; const HTTPS_SEARCH_STRING = "https prefix"; const PREFIX_SUGGESTIONS_STRIPPED_URL = "example.com/prefix-test"; const ONE_CHAR_SEARCH_STRINGS = ["x", "x ", " x", " x "]; const { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = AmpSuggestions; const TIMESTAMP_SEARCH_STRING = "timestamp"; const TIMESTAMP_SUGGESTION_URL = `http://example.com/timestamp-${TIMESTAMP_TEMPLATE}`; const TIMESTAMP_SUGGESTION_CLICK_URL = `http://click.reporting.test.com/timestamp-${TIMESTAMP_TEMPLATE}-foo`; const REMOTE_SETTINGS_RESULTS = [ QuickSuggestTestUtils.ampRemoteSettings({ keywords: [ SPONSORED_SEARCH_STRING, SPONSORED_AND_NONSPONSORED_SEARCH_STRING, ], }), QuickSuggestTestUtils.wikipediaRemoteSettings({ keywords: [ NONSPONSORED_SEARCH_STRING, SPONSORED_AND_NONSPONSORED_SEARCH_STRING, ], }), { id: 3, url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, title: "HTTP Suggestion", keywords: [HTTP_SEARCH_STRING], full_keywords: [[HTTP_SEARCH_STRING, 1]], click_url: "http://example.com/http-click", impression_url: "http://example.com/http-impression", advertiser: "HttpAdvertiser", iab_category: "22 - Shopping", icon: "1234", }, { id: 4, url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL, title: "https suggestion", keywords: [HTTPS_SEARCH_STRING], full_keywords: [[HTTPS_SEARCH_STRING, 1]], click_url: "http://click.reporting.test.com/prefix", impression_url: "http://impression.reporting.test.com/prefix", advertiser: "TestAdvertiserPrefix", iab_category: "22 - Shopping", icon: "1234", }, { id: 5, url: TIMESTAMP_SUGGESTION_URL, title: "Timestamp suggestion", keywords: [TIMESTAMP_SEARCH_STRING], full_keywords: [[TIMESTAMP_SEARCH_STRING, 1]], click_url: TIMESTAMP_SUGGESTION_CLICK_URL, impression_url: "http://impression.reporting.test.com/timestamp", advertiser: "TestAdvertiserTimestamp", iab_category: "22 - Shopping", icon: "1234", }, QuickSuggestTestUtils.ampRemoteSettings({ keywords: [...ONE_CHAR_SEARCH_STRINGS, "12", "a longer keyword"], title: "Suggestion with 1-char keyword", url: "http://example.com/1-char-keyword", }), QuickSuggestTestUtils.ampRemoteSettings({ keywords: [ "amp full key", "amp full keyw", "amp full keywo", "amp full keywor", "amp full keyword", "xyz", ], full_keywords: [ ["amp full keyword", 5], ["xyz", 1], ], title: "AMP suggestion with full keyword and prefix keywords", url: "https://example.com/amp-full-keyword", }), QuickSuggestTestUtils.wikipediaRemoteSettings({ keywords: [ "wikipedia full key", "wikipedia full keyw", "wikipedia full keywo", "wikipedia full keywor", "wikipedia full keyword", ], full_keywords: [["wikipedia full keyword", 5]], title: "Wikipedia suggestion with full keyword and prefix keywords", url: "https://example.com/wikipedia-full-keyword", }), ]; let gMaxResultsSuggestionsCount; function expectedSponsoredPriorityResult() { return { ...QuickSuggestTestUtils.ampResult(), isBestMatch: true, suggestedIndex: 1, isSuggestedIndexRelativeToGroup: false, }; } function expectedHttpResult() { let suggestion = REMOTE_SETTINGS_RESULTS[2]; return QuickSuggestTestUtils.ampResult({ keyword: HTTP_SEARCH_STRING, title: suggestion.title, url: suggestion.url, originalUrl: suggestion.url, impressionUrl: suggestion.impression_url, clickUrl: suggestion.click_url, blockId: suggestion.id, advertiser: suggestion.advertiser, }); } function expectedHttpsResult() { let suggestion = REMOTE_SETTINGS_RESULTS[3]; return QuickSuggestTestUtils.ampResult({ keyword: HTTPS_SEARCH_STRING, title: suggestion.title, url: suggestion.url, originalUrl: suggestion.url, impressionUrl: suggestion.impression_url, clickUrl: suggestion.click_url, blockId: suggestion.id, advertiser: suggestion.advertiser, }); } add_setup(async function init() { // Add a bunch of suggestions that have the same keyword so we can verify the // provider respects its `queryContext.maxResults` cap when adding results. let maxResults = UrlbarPrefs.get("maxRichResults"); Assert.greater(maxResults, 0, "This test expects maxRichResults to be > 0"); gMaxResultsSuggestionsCount = 2 * maxResults; for (let i = 0; i < gMaxResultsSuggestionsCount; i++) { REMOTE_SETTINGS_RESULTS.push( QuickSuggestTestUtils.ampRemoteSettings({ keywords: ["maxresults"], title: "maxresults " + i, url: "https://example.com/maxresults/" + i, }) ); } // Install a default test engine. let engine = await addTestSuggestionsEngine(); await Services.search.setDefault( engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN ); UrlbarPrefs.set("scotchBonnet.enableOverride", false); UrlbarPrefs.set("quicksuggest.ampTopPickCharThreshold", 0); await QuickSuggestTestUtils.ensureQuickSuggestInit(); await resetRemoteSettingsData(); }); add_task(async function telemetryType_sponsored() { Assert.equal( QuickSuggest.getFeature("AmpSuggestions").getSuggestionTelemetryType({}), "adm_sponsored", "Telemetry type should be 'adm_sponsored'" ); }); add_task(async function telemetryType_nonsponsored() { Assert.equal( QuickSuggest.getFeature( "OfflineWikipediaSuggestions" ).getSuggestionTelemetryType({}), "adm_nonsponsored", "Telemetry type should be 'adm_nonsponsored'" ); }); // Tests with only non-sponsored suggestions enabled with a matching search // string. add_task(async function nonsponsoredOnly_match() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(NONSPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.wikipediaResult()], }); // The title should include the full keyword and em dash, and the part of the // title that the search string does not match should be highlighted. let result = context.results[0]; Assert.equal( result.title, `${NONSPONSORED_SEARCH_STRING} — Wikipedia Suggestion`, "result.title should be correct" ); Assert.deepEqual( result.titleHighlights, [], "result.titleHighlights should be correct" ); }); // Tests with only non-sponsored suggestions enabled with a non-matching search // string. add_task(async function nonsponsoredOnly_noMatch() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); }); // Tests with only sponsored suggestions enabled with a matching search string. add_task(async function sponsoredOnly_sponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.ampResult()], }); // The title should include the full keyword and em dash, and the part of the // title that the search string does not match should be highlighted. let result = context.results[0]; Assert.equal( result.title, `${SPONSORED_SEARCH_STRING} — Amp Suggestion`, "result.title should be correct" ); Assert.deepEqual( result.titleHighlights, [], "result.titleHighlights should be correct" ); }); // Tests with only sponsored suggestions enabled with a non-matching search // string. add_task(async function sponsoredOnly_nonsponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(NONSPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); }); // Tests with both sponsored and non-sponsored suggestions enabled with a // search string that matches the sponsored suggestion. add_task(async function both_sponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.ampResult()], }); }); // Tests with both sponsored and non-sponsored suggestions enabled with a // search string that matches the non-sponsored suggestion. add_task(async function both_nonsponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(NONSPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.wikipediaResult()], }); }); // Tests with both sponsored and non-sponsored suggestions enabled with a // search string that doesn't match either suggestion. add_task(async function both_noMatch() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext("this doesn't match anything", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); }); // Tests with both the main and sponsored prefs disabled with a search string // that matches the sponsored suggestion. add_task(async function neither_sponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); }); // Tests with both the main and sponsored prefs disabled with a search string // that matches the non-sponsored suggestion. add_task(async function neither_nonsponsored() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); let context = createContext(NONSPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [] }); }); // Search string matching should be case insensitive and ignore leading spaces. add_task(async function caseInsensitiveAndLeadingSpaces() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(" " + SPONSORED_SEARCH_STRING.toUpperCase(), { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.ampResult()], }); }); // The provider should not be active for search strings that are empty or // contain only spaces. add_task(async function emptySearchStringsAndSpaces() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let searchStrings = ["", " ", " ", " "]; for (let str of searchStrings) { let msg = JSON.stringify(str) + ` (length = ${str.length})`; info("Testing search string: " + msg); let context = createContext(str, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [], }); Assert.ok( !(await UrlbarProviderQuickSuggest.isActive(context)), "Provider should not be active for search string: " + msg ); } }); // Results should be returned even when `browser.search.suggest.enabled` is // false. add_task(async function browser_search_suggest_disabled() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("browser.search.suggest.enabled", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], }); UrlbarPrefs.clear("browser.search.suggest.enabled"); }); // Results should be returned even when `browser.urlbar.suggest.searches` is // false. add_task(async function browser_suggest_searches_disabled() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("suggest.searches", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 })], }); UrlbarPrefs.clear("suggest.searches"); }); // Neither sponsored nor non-sponsored results should appear in private contexts // even when suggestions in private windows are enabled. add_task(async function privateContext() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); for (let privateSuggestionsEnabled of [true, false]) { UrlbarPrefs.set( "browser.search.suggest.enabled.private", privateSuggestionsEnabled ); let context = createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: true, }); await check_results({ context, matches: [], }); } UrlbarPrefs.clear("browser.search.suggest.enabled.private"); }); // When search suggestions come before general results and the only general // result is a quick suggest result, it should come last. add_task(async function suggestionsBeforeGeneral_only() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("browser.search.suggest.enabled", true); UrlbarPrefs.set("suggest.searches", true); UrlbarPrefs.set("showSearchSuggestionsFirst", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); await check_results({ context, matches: [ makeSearchResult(context, { heuristic: true, query: SPONSORED_SEARCH_STRING, engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " foo", engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " bar", engineName: Services.search.defaultEngine.name, }), QuickSuggestTestUtils.ampResult(), ], }); UrlbarPrefs.clear("browser.search.suggest.enabled"); UrlbarPrefs.clear("suggest.searches"); UrlbarPrefs.clear("showSearchSuggestionsFirst"); }); // When search suggestions come before general results and there are other // general results besides quick suggest, the quick suggest result should come // last. add_task(async function suggestionsBeforeGeneral_others() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("browser.search.suggest.enabled", true); UrlbarPrefs.set("suggest.searches", true); UrlbarPrefs.set("showSearchSuggestionsFirst", true); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); // Add some history that will match our query below. let maxResults = UrlbarPrefs.get("maxRichResults"); let historyResults = []; for (let i = 0; i < maxResults; i++) { let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; historyResults.push( makeVisitResult(context, { uri: url, title: "test visit for " + url, }) ); await PlacesTestUtils.addVisits(url); } historyResults = historyResults.reverse().slice(0, historyResults.length - 4); await check_results({ context, matches: [ makeSearchResult(context, { heuristic: true, query: SPONSORED_SEARCH_STRING, engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " foo", engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " bar", engineName: Services.search.defaultEngine.name, }), QuickSuggestTestUtils.ampResult(), ...historyResults, ], }); UrlbarPrefs.clear("browser.search.suggest.enabled"); UrlbarPrefs.clear("suggest.searches"); UrlbarPrefs.clear("showSearchSuggestionsFirst"); await PlacesUtils.history.clear(); }); // When general results come before search suggestions and the only general // result is a quick suggest result, it should come before suggestions. add_task(async function generalBeforeSuggestions_only() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("browser.search.suggest.enabled", true); UrlbarPrefs.set("suggest.searches", true); UrlbarPrefs.set("showSearchSuggestionsFirst", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); await check_results({ context, matches: [ makeSearchResult(context, { heuristic: true, query: SPONSORED_SEARCH_STRING, engineName: Services.search.defaultEngine.name, }), QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " foo", engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " bar", engineName: Services.search.defaultEngine.name, }), ], }); UrlbarPrefs.clear("browser.search.suggest.enabled"); UrlbarPrefs.clear("suggest.searches"); UrlbarPrefs.clear("showSearchSuggestionsFirst"); }); // When general results come before search suggestions and there are other // general results besides quick suggest, the quick suggest result should be the // last general result. add_task(async function generalBeforeSuggestions_others() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("browser.search.suggest.enabled", true); UrlbarPrefs.set("suggest.searches", true); UrlbarPrefs.set("showSearchSuggestionsFirst", false); await QuickSuggestTestUtils.forceSync(); let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); // Add some history that will match our query below. let maxResults = UrlbarPrefs.get("maxRichResults"); let historyResults = []; for (let i = 0; i < maxResults; i++) { let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; historyResults.push( makeVisitResult(context, { uri: url, title: "test visit for " + url, }) ); await PlacesTestUtils.addVisits(url); } historyResults = historyResults.reverse().slice(0, historyResults.length - 4); await check_results({ context, matches: [ makeSearchResult(context, { heuristic: true, query: SPONSORED_SEARCH_STRING, engineName: Services.search.defaultEngine.name, }), ...historyResults, QuickSuggestTestUtils.ampResult({ suggestedIndex: -1 }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " foo", engineName: Services.search.defaultEngine.name, }), makeSearchResult(context, { query: SPONSORED_SEARCH_STRING, suggestion: SPONSORED_SEARCH_STRING + " bar", engineName: Services.search.defaultEngine.name, }), ], }); UrlbarPrefs.clear("browser.search.suggest.enabled"); UrlbarPrefs.clear("suggest.searches"); UrlbarPrefs.clear("showSearchSuggestionsFirst"); await PlacesUtils.history.clear(); }); // The provider should not add more than `queryContext.maxResults` results. add_task(async function maxResults() { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let searchString = "maxresults"; let suggestions = await QuickSuggest.rustBackend.query(searchString); Assert.equal( suggestions.length, gMaxResultsSuggestionsCount, "The backend should return all matching suggestions" ); let context = createContext(searchString, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); // Spy on `muxer.sort()` so we can verify the provider limited the number of // results it added to the query. let muxerName = context.muxer || "UnifiedComplete"; let muxer = UrlbarProvidersManager.muxers.get(muxerName); Assert.ok(!!muxer, "Muxer should exist"); let sandbox = sinon.createSandbox(); let spy = sandbox.spy(muxer, "sort"); // Use `check_results()` to do the query. await check_results({ context, matches: [ QuickSuggestTestUtils.ampResult({ keyword: "maxresults", title: "maxresults 0", url: "https://example.com/maxresults/0", }), ], }); // Check the `sort()` calls. let calls = spy.getCalls(); Assert.greater( calls.length, 0, "muxer.sort() should have been called at least once" ); for (let c of calls) { let unsortedResults = c.args[1]; Assert.lessOrEqual( unsortedResults.length, UrlbarPrefs.get("maxRichResults"), "Provider should have added no more than maxRichResults results" ); } sandbox.restore(); }); // When the Suggest provider adds more than one result and they are not hidden // exposures, the muxer should add the first one to the final results list and // discard the rest, and the discarded results should not prevent the muxer from // adding other non-Suggest results. add_task(async function manySuggestResults_visible() { await doManySuggestResultsTest({ expectedSuggestResults: [ QuickSuggestTestUtils.ampResult({ keyword: "maxresults", title: "maxresults 0", url: "https://example.com/maxresults/0", }), ], expectedOtherResultsCount: UrlbarPrefs.get("maxRichResults") - 1, }); }); // When the Suggest provider adds more than one result and they are hidden // exposures, the muxer should add up to `queryContext.maxResults` of them to // the final results list, and they should not prevent the muxer from adding // other non-Suggest results. add_task(async function manySuggestResults_hiddenExposures() { UrlbarPrefs.set("exposureResults", "rust_adm_sponsored"); UrlbarPrefs.set("showExposureResults", false); // Build the list of expected Suggest results. let results = []; let maxResults = UrlbarPrefs.get("maxRichResults"); let suggestResultsCount = Math.min(gMaxResultsSuggestionsCount, maxResults); for (let i = 0; i < suggestResultsCount; i++) { let index = maxResults - 1 - i; results.push({ ...QuickSuggestTestUtils.ampResult({ keyword: "maxresults", title: "maxresults " + index, url: "https://example.com/maxresults/" + index, }), exposureTelemetry: UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN, }); } await doManySuggestResultsTest({ expectedSuggestResults: results, expectedOtherResultsCount: maxResults, }); UrlbarPrefs.clear("exposureResults"); UrlbarPrefs.clear("showExposureResults"); }); async function doManySuggestResultsTest({ expectedSuggestResults, expectedOtherResultsCount, }) { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); // Make sure many Suggest suggestions match the search string. let searchString = "maxresults"; let suggestions = await QuickSuggest.rustBackend.query(searchString); Assert.equal( suggestions.length, gMaxResultsSuggestionsCount, "Sanity check: The backend should return all matching suggestions" ); Assert.greater( suggestions.length, 1, "Sanity check: There should be more than 1 matching suggestion" ); // Register a test provider that adds a bunch of history results. let otherResults = []; let maxResults = UrlbarPrefs.get("maxRichResults"); for (let i = 0; i < maxResults; i++) { otherResults.push( new UrlbarResult( UrlbarUtils.RESULT_TYPE.URL, UrlbarUtils.RESULT_SOURCE.HISTORY, { url: "http://example.com/history/" + i } ) ); } let provider = new UrlbarTestUtils.TestProvider({ results: otherResults }); UrlbarProvidersManager.registerProvider(provider); // Do a search that matches all the Suggest suggestions and the test // provider's results. The Suggest suggestion(s) should be first since its // `suggestedIndex` is 0. await check_results({ context: createContext(searchString, { providers: [UrlbarProviderQuickSuggest.name, provider.name], isPrivate: false, }), matches: [ ...expectedSuggestResults, ...otherResults.slice(0, expectedOtherResultsCount), ], }); UrlbarProvidersManager.unregisterProvider(provider); } add_task(async function dedupeAgainstURL_samePrefix() { await doDedupeAgainstURLTest({ searchString: HTTP_SEARCH_STRING, expectedQuickSuggestResult: expectedHttpResult(), otherPrefix: "http://", expectOther: false, }); }); add_task(async function dedupeAgainstURL_higherPrefix() { await doDedupeAgainstURLTest({ searchString: HTTPS_SEARCH_STRING, expectedQuickSuggestResult: expectedHttpsResult(), otherPrefix: "http://", expectOther: false, }); }); add_task(async function dedupeAgainstURL_lowerPrefix() { await doDedupeAgainstURLTest({ searchString: HTTP_SEARCH_STRING, expectedQuickSuggestResult: expectedHttpResult(), otherPrefix: "https://", expectOther: true, }); }); /** * Tests how the muxer dedupes URL results against quick suggest results. * Depending on prefix rank, quick suggest results should be preferred over * other URL results with the same stripped URL: Other results should be * discarded when their prefix rank is lower than the prefix rank of the quick * suggest. They should not be discarded when their prefix rank is higher, and * in that case both results should be included. * * This function adds a visit to the URL formed by the given `otherPrefix` and * `PREFIX_SUGGESTIONS_STRIPPED_URL`. The visit's title will be set to the given * `searchString` so that both the visit and the quick suggest will match it. * * @param {object} options * Options object. * @param {string} options.searchString * The search string that should trigger one of the mock prefix-test quick * suggest results. * @param {object} options.expectedQuickSuggestResult * The expected quick suggest result. * @param {string} options.otherPrefix * The visit will be created with a URL with this prefix, e.g., "http://". * @param {boolean} options.expectOther * Whether the visit result should appear in the final results. */ async function doDedupeAgainstURLTest({ searchString, expectedQuickSuggestResult, otherPrefix, expectOther, }) { // Disable search suggestions. This means the expected suggestedIndex for // sponsored suggestions will now be -1. We assume expectedQuickSuggestResult // is sponsored, so set its suggestedIndex now. UrlbarPrefs.set("suggest.searches", false); expectedQuickSuggestResult.suggestedIndex = -1; // Add a visit that will match our query below. let otherURL = otherPrefix + PREFIX_SUGGESTIONS_STRIPPED_URL; await PlacesTestUtils.addVisits({ uri: otherURL, title: searchString }); // First, do a search with quick suggest disabled to make sure the search // string matches the visit. info("Doing first query"); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); let context = createContext(searchString, { isPrivate: false }); await check_results({ context, matches: [ makeSearchResult(context, { heuristic: true, query: searchString, engineName: Services.search.defaultEngine.name, }), makeVisitResult(context, { uri: otherURL, title: searchString, }), ], }); // Now do another search with quick suggest enabled. UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); context = createContext(searchString, { isPrivate: false }); let expectedResults = [ makeSearchResult(context, { heuristic: true, query: searchString, engineName: Services.search.defaultEngine.name, }), ]; if (expectOther) { expectedResults.push( makeVisitResult(context, { uri: otherURL, title: searchString, }) ); } // The expected result is last since its expected suggestedIndex is -1. expectedResults.push(expectedQuickSuggestResult); info("Doing second query"); await check_results({ context, matches: expectedResults }); UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); await QuickSuggestTestUtils.forceSync(); UrlbarPrefs.clear("suggest.searches"); await PlacesUtils.history.clear(); } // Timestamp templates in URLs should be replaced with real timestamps. add_task(async function timestamps() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); // Do a search. let context = createContext(TIMESTAMP_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); let controller = UrlbarTestUtils.newMockController(); 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: TIMESTAMP_SUGGESTION_URL, sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, }); }); // Real quick suggest URLs include a timestamp template that // UrlbarProviderQuickSuggest fills in when it fetches suggestions. When the // user picks a quick suggest, its URL with its particular timestamp is added to // history. If the user triggers the quick suggest again later, its new // timestamp may be different from the one in the user's history. In that case, // the two URLs should be treated as dupes and only the quick suggest should be // shown, not the URL from history. add_task(async function dedupeAgainstURL_timestamps() { // Disable search suggestions. This means the expected suggestedIndex for // sponsored suggestions will now be -1. UrlbarPrefs.set("suggest.searches", false); // Add a visit that will match the query below and dupe the quick suggest. let dupeURL = TIMESTAMP_SUGGESTION_URL.replace( TIMESTAMP_TEMPLATE, "2013051113" ); // Add other visits that will match the query and almost dupe the quick // suggest but not quite because they have invalid timestamps. let badTimestamps = [ // not numeric digits "x".repeat(TIMESTAMP_LENGTH), // too few digits "5".repeat(TIMESTAMP_LENGTH - 1), // empty string, too few digits "", ]; let badTimestampURLs = badTimestamps.map(str => TIMESTAMP_SUGGESTION_URL.replace(TIMESTAMP_TEMPLATE, str) ); await PlacesTestUtils.addVisits( [dupeURL, ...badTimestampURLs].map(uri => ({ uri, title: TIMESTAMP_SEARCH_STRING, })) ); // First, do a search with quick suggest disabled to make sure the search // string matches all the other URLs. info("Doing first query"); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); let context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); let expectedHeuristic = makeSearchResult(context, { heuristic: true, query: TIMESTAMP_SEARCH_STRING, engineName: Services.search.defaultEngine.name, }); let expectedDupeResult = makeVisitResult(context, { uri: dupeURL, title: TIMESTAMP_SEARCH_STRING, }); let expectedBadTimestampResults = [...badTimestampURLs].reverse().map(uri => makeVisitResult(context, { uri, title: TIMESTAMP_SEARCH_STRING, }) ); await check_results({ context, matches: [ expectedHeuristic, ...expectedBadTimestampResults, expectedDupeResult, ], }); // Now do another search with quick suggest enabled. info("Doing second query"); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); let expectedQuickSuggest = QuickSuggestTestUtils.ampResult({ originalUrl: TIMESTAMP_SUGGESTION_URL, keyword: TIMESTAMP_SEARCH_STRING, title: "Timestamp suggestion", impressionUrl: "http://impression.reporting.test.com/timestamp", blockId: 5, advertiser: "TestAdvertiserTimestamp", iabCategory: "22 - Shopping", // suggestedIndex is -1 since search suggestions are disabled. suggestedIndex: -1, }); let expectedResults = [expectedHeuristic, ...expectedBadTimestampResults]; const QUICK_SUGGEST_INDEX = expectedResults.length; expectedResults.push(expectedQuickSuggest); let controller = UrlbarTestUtils.newMockController(); await controller.startQuery(context); info("Actual results: " + JSON.stringify(context.results)); Assert.equal( context.results.length, expectedResults.length, "Found the expected number of results" ); function getPayload(result, keysToIgnore = []) { keysToIgnore.push("suggestionObject"); let payload = {}; for (let [key, value] of Object.entries(result.payload)) { if (value !== undefined && !keysToIgnore.includes(key)) { payload[key] = value; } } return payload; } // Check actual vs. expected result properties. for (let i = 0; i < expectedResults.length; i++) { let actual = context.results[i]; let expected = expectedResults[i]; info( `Comparing results at index ${i}:` + " actual=" + JSON.stringify(actual) + " expected=" + JSON.stringify(expected) ); Assert.equal( actual.type, expected.type, `result.type at result index ${i}` ); Assert.equal( actual.source, expected.source, `result.source at result index ${i}` ); Assert.equal( actual.heuristic, expected.heuristic, `result.heuristic at result index ${i}` ); // Check payloads except for the quick suggest. if (i != QUICK_SUGGEST_INDEX) { Assert.deepEqual( getPayload(context.results[i], ["lastVisit"]), getPayload(expectedResults[i], ["lastVisit"]), "Payload at index " + i ); } } // Check the quick suggest's payload excluding the timestamp-related // properties. let actualQuickSuggest = context.results[QUICK_SUGGEST_INDEX]; let timestampKeys = [ "displayUrl", "sponsoredClickUrl", "url", "urlTimestampIndex", ]; Assert.deepEqual( getPayload(actualQuickSuggest, timestampKeys), getPayload(expectedQuickSuggest, timestampKeys), "Quick suggest payload excluding timestamp-related keys" ); // Now check the timestamps in the payload. QuickSuggestTestUtils.assertTimestampsReplaced(actualQuickSuggest, { url: TIMESTAMP_SUGGESTION_URL, sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, }); // Clean up. UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); await QuickSuggestTestUtils.forceSync(); UrlbarPrefs.clear("suggest.searches"); await PlacesUtils.history.clear(); }); // Tests `UrlbarResult` dismissal. add_task(async function dismissResult() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let tests = [ // [suggestion, expected result] [REMOTE_SETTINGS_RESULTS[0], QuickSuggestTestUtils.ampResult()], [REMOTE_SETTINGS_RESULTS[1], QuickSuggestTestUtils.wikipediaResult()], [REMOTE_SETTINGS_RESULTS[2], expectedHttpResult()], [REMOTE_SETTINGS_RESULTS[3], expectedHttpsResult()], ]; for (let [suggestion, expectedResult] of tests) { info("Testing suggestion: " + JSON.stringify(suggestion)); // Do a search to get a real `UrlbarResult` created for the suggestion. let context = createContext(suggestion.keywords[0], { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [expectedResult], }); // Dismiss it. await QuickSuggest.dismissResult(context.results[0]); Assert.ok( await QuickSuggest.isResultDismissed(context.results[0]), "isResultDismissed should return true" ); Assert.ok( await QuickSuggest.canClearDismissedSuggestions(), "canClearDismissedSuggestions should return true" ); // Do another search. The result shouldn't be added. await check_results({ context: createContext(suggestion.keywords[0], { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [], }); await QuickSuggest.clearDismissedSuggestions(); Assert.ok( !(await QuickSuggest.isResultDismissed(context.results[0])), "isResultDismissed should return false" ); Assert.ok( !(await QuickSuggest.canClearDismissedSuggestions()), "canClearDismissedSuggestions should return false" ); } }); // Tests dismissing a `UrlbarResult` whose URL has a timestamp template. add_task(async function dismissResultWithTimestamp() { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); await QuickSuggestTestUtils.forceSync(); // Do a search. let context = createContext(TIMESTAMP_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); let controller = UrlbarTestUtils.newMockController(); 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: TIMESTAMP_SUGGESTION_URL, sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, }); Assert.ok(result.payload.originalUrl, "The actual result has an originalUrl"); Assert.equal( result.payload.originalUrl, REMOTE_SETTINGS_RESULTS[4].url, "The actual result's originalUrl should be the raw suggestion URL with a timestamp template" ); // Dismiss the result. await QuickSuggest.dismissResult(result); Assert.ok( await QuickSuggest.isResultDismissed(result), "isResultDismissed should return true" ); Assert.ok( await QuickSuggest.canClearDismissedSuggestions(), "canClearDismissedSuggestions should return true" ); // Do another search. The result shouldn't be added. await check_results({ context: createContext(TIMESTAMP_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [], }); await QuickSuggest.clearDismissedSuggestions(); Assert.ok( !(await QuickSuggest.isResultDismissed(context.results[0])), "isResultDismissed should return false" ); Assert.ok( !(await QuickSuggest.canClearDismissedSuggestions()), "canClearDismissedSuggestions should return false" ); }); add_task(async function sponsoredPriority_normal() { await doSponsoredPriorityTest({ searchWord: SPONSORED_SEARCH_STRING, remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], expectedMatches: [expectedSponsoredPriorityResult()], }); }); add_task(async function sponsoredPriority_nonsponsoredSuggestion() { // Not affect to except sponsored suggestion. await doSponsoredPriorityTest({ searchWord: NONSPONSORED_SEARCH_STRING, remoteSettingsData: [REMOTE_SETTINGS_RESULTS[1]], expectedMatches: [QuickSuggestTestUtils.wikipediaResult()], }); }); add_task(async function sponsoredPriority_sponsoredIndex() { await doSponsoredPriorityTest({ nimbusSettings: { quickSuggestSponsoredIndex: 2 }, searchWord: SPONSORED_SEARCH_STRING, remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], expectedMatches: [expectedSponsoredPriorityResult()], }); }); async function doSponsoredPriorityTest({ remoteSettingsConfig = {}, nimbusSettings = {}, searchWord, remoteSettingsData, expectedMatches, }) { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ ...nimbusSettings, quickSuggestSponsoredPriority: true, }); await resetRemoteSettingsData(remoteSettingsData); await QuickSuggestTestUtils.setConfig(remoteSettingsConfig); await check_results({ context: createContext(searchWord, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: expectedMatches, }); await cleanUpNimbusEnable(); await resetRemoteSettingsData(); await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); } // When a Suggest best match and a tab-to-search (TTS) are shown in the same // search, both will have a `suggestedIndex` value of 1. The TTS should appear // first. add_task(async function tabToSearch() { // We'll use a sponsored priority result as the best match result. Different // types of Suggest results can appear as best matches, and they all should // have the same behavior. UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); Services.prefs.setBoolPref( "browser.urlbar.quicksuggest.sponsoredPriority", true ); // Disable tab-to-search onboarding results so we get a regular TTS result, // which we can test a little more easily with `makeSearchResult()`. UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 0); // Disable search suggestions so we don't need to expect them below. Services.prefs.setBoolPref("browser.search.suggest.enabled", false); // Install a test engine. The main part of its domain name needs to match the // best match result too so we can trigger both its TTS and the best match. let engineURL = `https://foo.${SPONSORED_SEARCH_STRING}.com/`; let extension = await SearchTestUtils.installSearchExtension( { name: "Test", search_url: engineURL, }, { skipUnload: true } ); let engine = Services.search.getEngineByName("Test"); // Also need to add a visit to trigger TTS. await PlacesTestUtils.addVisits(engineURL); let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false, }); await check_results({ context, matches: [ // search heuristic makeSearchResult(context, { engineName: Services.search.defaultEngine.name, engineIconUri: await Services.search.defaultEngine.getIconURL(), heuristic: true, }), // tab to search makeSearchResult(context, { engineName: engine.name, engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost( engine.searchUrlDomain ), providesSearchMode: true, query: "", providerName: "TabToSearch", satisfiesAutofillThreshold: true, }), // Suggest best match expectedSponsoredPriorityResult(), // visit makeVisitResult(context, { uri: engineURL, title: `test visit for ${engineURL}`, }), ], }); await cleanupPlaces(); await extension.unload(); UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft"); Services.prefs.clearUserPref("browser.search.suggest.enabled"); Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority"); }); // The `Amp` and `Wikipedia` Rust providers should be passed to the Rust // component when querying depending on whether sponsored and non-sponsored // suggestions are enabled. add_task(async function rustProviders() { await doRustProvidersTests({ searchString: SPONSORED_AND_NONSPONSORED_SEARCH_STRING, tests: [ { prefs: { "suggest.quicksuggest.nonsponsored": true, "suggest.quicksuggest.sponsored": true, }, expectedUrls: [ "https://example.com/amp", "https://example.com/wikipedia", ], }, { prefs: { "suggest.quicksuggest.nonsponsored": true, "suggest.quicksuggest.sponsored": false, }, expectedUrls: ["https://example.com/wikipedia"], }, { prefs: { "suggest.quicksuggest.nonsponsored": false, "suggest.quicksuggest.sponsored": true, }, expectedUrls: ["https://example.com/amp"], }, { prefs: { "suggest.quicksuggest.nonsponsored": false, "suggest.quicksuggest.sponsored": false, }, expectedUrls: [], }, ], }); }); // Tests the keyword/search-string-length threshold. Keywords/search strings // must be at least two characters long to be matched. add_task(async function keywordLengthThreshold() { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); let tests = [ ...ONE_CHAR_SEARCH_STRINGS.map(keyword => ({ keyword, expected: false })), { keyword: "12", expected: true }, { keyword: "a longer keyword", expected: true }, ]; for (let { keyword, expected } of tests) { await check_results({ context: createContext(keyword, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: !expected ? [] : [ QuickSuggestTestUtils.ampResult({ keyword, title: "Suggestion with 1-char keyword", url: "http://example.com/1-char-keyword", originalUrl: "http://example.com/1-char-keyword", }), ], }); } }); // AMP should be a top pick when `quicksuggest.ampTopPickCharThreshold` is // non-zero and the query length meets the threshold; otherwise it should not be // a top pick. It shouldn't matter whether the query is one of the suggestion's // full keywords. add_task(async function ampTopPickCharThreshold() { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); await QuickSuggestTestUtils.forceSync(); UrlbarPrefs.set( "quicksuggest.ampTopPickCharThreshold", "amp full keywo".length ); let tests = [ // No top pick: Matches an AMP suggestion but the query is shorter than the // threshold. { keyword: "amp full key", amp: true, isTopPick: false }, { keyword: "amp full keyw", amp: true, isTopPick: false }, { keyword: " amp full key", amp: true, isTopPick: false }, { keyword: " amp full keyw", amp: true, isTopPick: false }, // Top pick: Matches an AMP suggestion and the query meets the threshold. { keyword: "amp full keywo", amp: true, isTopPick: true }, { keyword: "amp full keywor", amp: true, isTopPick: true }, { keyword: "amp full keyword", amp: true, isTopPick: true }, { keyword: "AmP FuLl KeYwOrD", amp: true, isTopPick: true }, { keyword: " amp full keywo", amp: true, isTopPick: true }, { keyword: " amp full keywor", amp: true, isTopPick: true }, { keyword: " amp full keyword", amp: true, isTopPick: true }, { keyword: " AmP FuLl KeYwOrD", amp: true, isTopPick: true }, // No top pick: Matches an AMP suggestion but the query is shorter than the // threshold. It doesn't matter that the query is equal to the suggestion's // full keyword. { keyword: "xyz", fullKeyword: "xyz", amp: true, isTopPick: false }, { keyword: "XyZ", fullKeyword: "xyz", amp: true, isTopPick: false }, { keyword: " xyz", fullKeyword: "xyz", amp: true, isTopPick: false, }, { keyword: " XyZ", fullKeyword: "xyz", amp: true, isTopPick: false, }, // No top pick: Matches a Wikipedia suggestion and some queries meet the // threshold, but Wikipedia should not be top pick. { keyword: "wikipedia full key", isTopPick: false }, { keyword: "wikipedia full keyw", isTopPick: false }, { keyword: "wikipedia full keywo", isTopPick: false }, { keyword: "wikipedia full keywor", isTopPick: false }, { keyword: "wikipedia full keyword", isTopPick: false }, // No match: These shouldn't match anything at all since they have extra // spaces at the end, but they're included for completeness. { keyword: " amp full key ", noMatch: true }, { keyword: " amp full keyw ", noMatch: true }, { keyword: " amp full keywo ", noMatch: true }, { keyword: " amp full keywor ", noMatch: true }, { keyword: " amp full keyword ", noMatch: true }, { keyword: " AmP FuLl KeYwOrD ", noMatch: true }, { keyword: " xyz ", noMatch: true }, { keyword: " XyZ ", noMatch: true }, ]; for (let { keyword, fullKeyword, amp, isTopPick, noMatch } of tests) { fullKeyword ??= amp ? "amp full keyword" : "wikipedia full keyword"; info( "Running subtest: " + JSON.stringify({ keyword, fullKeyword, amp, isTopPick }) ); let expectedResult; if (!noMatch) { if (!amp) { expectedResult = QuickSuggestTestUtils.wikipediaResult({ keyword, fullKeyword, title: "Wikipedia suggestion with full keyword and prefix keywords", url: "https://example.com/wikipedia-full-keyword", }); } else if (isTopPick) { expectedResult = QuickSuggestTestUtils.ampResult({ keyword, fullKeyword, title: "AMP suggestion with full keyword and prefix keywords", url: "https://example.com/amp-full-keyword", suggestedIndex: 1, isSuggestedIndexRelativeToGroup: false, isBestMatch: true, descriptionL10n: null, }); } else { expectedResult = QuickSuggestTestUtils.ampResult({ keyword, fullKeyword, title: "AMP suggestion with full keyword and prefix keywords", url: "https://example.com/amp-full-keyword", }); } } await check_results({ context: createContext(keyword, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: expectedResult ? [expectedResult] : [], }); } UrlbarPrefs.clear("quicksuggest.ampTopPickCharThreshold"); }); // AMP should not be shown as a top pick when the threshold is zero. add_task(async function ampTopPickCharThreshold_zero() { UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); await QuickSuggestTestUtils.forceSync(); UrlbarPrefs.set("quicksuggest.ampTopPickCharThreshold", 0); let tests = [ { keyword: "amp full key", amp: true }, { keyword: "amp full keyw", amp: true }, { keyword: "amp full keywo", amp: true }, { keyword: "amp full keywor", amp: true }, { keyword: "amp full keyword", amp: true }, { keyword: "AmP FuLl KeYwOrD", amp: true }, { keyword: "xyz", fullKeyword: "xyz", amp: true }, { keyword: "XyZ", fullKeyword: "xyz", amp: true }, { keyword: "wikipedia full key" }, { keyword: "wikipedia full keyw" }, { keyword: "wikipedia full keywo" }, { keyword: "wikipedia full keywor" }, { keyword: "wikipedia full keyword" }, ]; for (let { keyword, fullKeyword, amp } of tests) { fullKeyword ??= amp ? "amp full keyword" : "wikipedia full keyword"; info("Running subtest: " + JSON.stringify({ keyword, fullKeyword, amp })); let expectedResult; if (!amp) { expectedResult = QuickSuggestTestUtils.wikipediaResult({ keyword, fullKeyword, title: "Wikipedia suggestion with full keyword and prefix keywords", url: "https://example.com/wikipedia-full-keyword", }); } else { expectedResult = QuickSuggestTestUtils.ampResult({ keyword, fullKeyword, title: "AMP suggestion with full keyword and prefix keywords", url: "https://example.com/amp-full-keyword", }); } await check_results({ context: createContext(keyword, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [expectedResult], }); } UrlbarPrefs.clear("quicksuggest.ampTopPickCharThreshold"); }); // Tests `ampMatchingStrategy`. add_task(async function ampMatchingStrategy() { UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); // Test each strategy in `AmpMatchingStrategy`. There are only a few. for (let [key, value] of Object.entries(AmpMatchingStrategy)) { await doAmpMatchingStrategyTest({ key, value }); // Reset back to the default strategy just to make sure that works. await doAmpMatchingStrategyTest({ key: "(default)", value: 0, }); } // Test an invalid strategy integer value. The default strategy should // actually be used. First we need to set a valid non-default strategy. await doAmpMatchingStrategyTest({ key: "FTS_AGAINST_TITLE", value: AmpMatchingStrategy.FTS_AGAINST_TITLE, }); await doAmpMatchingStrategyTest({ key: "(invalid)", value: 99, expectedStrategy: 0, }); Services.prefs.clearUserPref( "browser.urlbar.quicksuggest.ampMatchingStrategy" ); await QuickSuggestTestUtils.forceSync(); }); async function doAmpMatchingStrategyTest({ key, value, expectedStrategy = value, }) { info("Doing ampMatchingStrategy test: " + JSON.stringify({ key, value })); let sandbox = sinon.createSandbox(); let ingestSpy = sandbox.spy(QuickSuggest.rustBackend._test_store, "ingest"); // Set the strategy. It should trigger ingest. (Assuming it's different from // the current strategy. If it's not, ingest won't happen.) Services.prefs.setIntPref( "browser.urlbar.quicksuggest.ampMatchingStrategy", value ); let ingestCall = await TestUtils.waitForCondition(() => { return ingestSpy.getCalls().find(call => { let ingestConstraints = call.args[0]; return ingestConstraints?.providers[0] == SuggestionProvider.AMP; }); }, "Waiting for ingest() to be called with Amp provider"); // Check the provider constraints in the ingest constraints. let { providerConstraints } = ingestCall.args[0]; if (!expectedStrategy) { Assert.ok( !providerConstraints, "ingest() should not have been called with provider constraints" ); } else { Assert.ok( providerConstraints, "ingest() should have been called with provider constraints" ); Assert.strictEqual( providerConstraints.ampAlternativeMatching, expectedStrategy, "ampAlternativeMatching should have been set" ); } // Now do a query to make sure it also uses the correct provider constraints. // No need to use `check_results()`. We only need to trigger a query, and // checking the right results unnecessarily complicates things. let querySpy = sandbox.spy( QuickSuggest.rustBackend._test_store, "queryWithMetrics" ); let controller = UrlbarTestUtils.newMockController(); await controller.startQuery( createContext(SPONSORED_SEARCH_STRING, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }) ); let queryCalls = querySpy.getCalls(); Assert.equal(queryCalls.length, 1, "query() should have been called once"); let query = queryCalls[0].args[0]; Assert.ok(query, "query() should have been called with a query object"); Assert.ok( query.providerConstraints, "query() should have been called with provider constraints" ); if (!expectedStrategy) { Assert.strictEqual( query.providerConstraints.ampAlternativeMatching, null, "ampAlternativeMatching should not have been set on query provider constraints" ); } else { Assert.strictEqual( query.providerConstraints.ampAlternativeMatching, expectedStrategy, "ampAlternativeMatching should have been set on query provider constraints" ); } sandbox.restore(); } async function resetRemoteSettingsData(data = REMOTE_SETTINGS_RESULTS) { let isAmp = suggestion => suggestion.iab_category == "22 - Shopping"; await QuickSuggestTestUtils.setRemoteSettingsRecords([ { collection: QuickSuggestTestUtils.RS_COLLECTION.AMP, type: QuickSuggestTestUtils.RS_TYPE.AMP, attachment: data.filter(isAmp), }, { collection: QuickSuggestTestUtils.RS_COLLECTION.OTHER, type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA, attachment: data.filter(s => !isAmp(s)), }, ]); }