/* 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 best match quick suggest results. "Best match" refers to two different // concepts: // // (1) The "best match" UI treatment (labeled "top pick" in the UI) that makes a // result's row larger than usual and sets `suggestedIndex` to 1. // (2) The quick suggest config in remote settings can contain a `best_match` // object that tells Firefox to use the best match UI treatment if the // user's search string is a certain length. // // This file tests aspects of both concepts. // // See also test_quicksuggest_topPicks.js. "Top picks" refer to a similar // concept but it is not related to (2). "use strict"; const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults"); // This search string length needs to be >= 4 to trigger its suggestion as a // best match instead of a usual quick suggest. const BEST_MATCH_POSITION_SEARCH_STRING = "bestmatchposition"; const BEST_MATCH_POSITION = Math.round(MAX_RESULT_COUNT / 2); const REMOTE_SETTINGS_RESULTS = [ { id: 1, url: "http://example.com/", title: "Fullkeyword title", keywords: [ "fu", "ful", "full", "fullk", "fullke", "fullkey", "fullkeyw", "fullkeywo", "fullkeywor", "fullkeyword", ], click_url: "http://example.com/click", impression_url: "http://example.com/impression", advertiser: "TestAdvertiser", }, { id: 2, url: "http://example.com/best-match-position", title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`, keywords: [BEST_MATCH_POSITION_SEARCH_STRING], click_url: "http://example.com/click", impression_url: "http://example.com/impression", advertiser: "TestAdvertiser", position: BEST_MATCH_POSITION, }, ]; const EXPECTED_BEST_MATCH_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, isBestMatch: true, payload: { telemetryType: "adm_sponsored", url: "http://example.com/", originalUrl: "http://example.com/", title: "Fullkeyword title", icon: null, isSponsored: true, sponsoredImpressionUrl: "http://example.com/impression", sponsoredClickUrl: "http://example.com/click", sponsoredBlockId: 1, sponsoredAdvertiser: "TestAdvertiser", 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("bestMatchBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, displayUrl: "http://example.com", source: "remote-settings", }, }; const EXPECTED_NON_BEST_MATCH_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_sponsored", url: "http://example.com/", originalUrl: "http://example.com/", title: "Fullkeyword title", qsSuggestion: "fullkeyword", icon: null, isSponsored: true, sponsoredImpressionUrl: "http://example.com/impression", sponsoredClickUrl: "http://example.com/click", sponsoredBlockId: 1, sponsoredAdvertiser: "TestAdvertiser", 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://example.com", source: "remote-settings", }, }; const EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, isBestMatch: true, payload: { telemetryType: "adm_sponsored", url: "http://example.com/best-match-position", originalUrl: "http://example.com/best-match-position", title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`, icon: null, isSponsored: true, sponsoredImpressionUrl: "http://example.com/impression", sponsoredClickUrl: "http://example.com/click", sponsoredBlockId: 2, sponsoredAdvertiser: "TestAdvertiser", 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("bestMatchBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, displayUrl: "http://example.com/best-match-position", source: "remote-settings", }, }; add_task(async function init() { UrlbarPrefs.set("quicksuggest.enabled", true); UrlbarPrefs.set("bestMatch.enabled", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("suggest.bestmatch", true); // Disable search suggestions so we don't hit the network. Services.prefs.setBoolPref("browser.search.suggest.enabled", false); await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsResults: [ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ], config: QuickSuggestTestUtils.BEST_MATCH_CONFIG, }); }); // Tests a best match result. add_task(async function bestMatch() { let context = createContext("fullkeyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_BEST_MATCH_URLBAR_RESULT], }); let result = context.results[0]; // The title should not include the full keyword and em dash, and the part of // the title that the search string matches should be highlighted. Assert.equal(result.title, "Fullkeyword title", "result.title"); Assert.deepEqual( result.titleHighlights, [[0, "fullkeyword".length]], "result.titleHighlights" ); Assert.equal(result.suggestedIndex, 1, "result.suggestedIndex"); Assert.equal( !!result.isSuggestedIndexRelativeToGroup, false, "result.isSuggestedIndexRelativeToGroup" ); }); // Tests a usual, non-best match quick suggest result. add_task(async function nonBestMatch() { // Search for a substring of the full search string so we can test title // highlights. let context = createContext("fu", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], }); let result = context.results[0]; // 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. Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title"); Assert.deepEqual( result.titleHighlights, [["fu".length, "fullkeyword".length - "fu".length]], "result.titleHighlights" ); Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex"); Assert.equal( result.isSuggestedIndexRelativeToGroup, true, "result.isSuggestedIndexRelativeToGroup" ); }); // Tests prefix keywords leading up to a best match. add_task(async function prefixKeywords() { let sawNonBestMatch = false; let sawBestMatch = false; for (let keyword of REMOTE_SETTINGS_RESULTS[0].keywords) { info(`Searching for "${keyword}"`); let context = createContext(keyword, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); let expectedResult; if (keyword.length < 4) { expectedResult = EXPECTED_NON_BEST_MATCH_URLBAR_RESULT; sawNonBestMatch = true; } else { expectedResult = EXPECTED_BEST_MATCH_URLBAR_RESULT; sawBestMatch = true; } await check_results({ context, matches: [expectedResult], }); } Assert.ok(sawNonBestMatch, "Sanity check: Saw a non-best match"); Assert.ok(sawBestMatch, "Sanity check: Saw a best match"); }); // When tab-to-search is shown in the same search, both it and the best match // will have a `suggestedIndex` value of 1. The TTS should appear first. add_task(async function tabToSearch() { // 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); // 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.fullkeyword.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("fullkeyword", { isPrivate: false, }); await check_results({ context, matches: [ // search heuristic makeSearchResult(context, { engineName: Services.search.defaultEngine.name, engineIconUri: Services.search.defaultEngine.iconURI?.spec, heuristic: true, }), // tab to search makeSearchResult(context, { engineName: engine.name, engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), providesSearchMode: true, query: "", providerName: "TabToSearch", satisfiesAutofillThreshold: true, }), // best match EXPECTED_BEST_MATCH_URLBAR_RESULT, // visit makeVisitResult(context, { uri: engineURL, title: `test visit for ${engineURL}`, }), ], }); await cleanupPlaces(); await extension.unload(); UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft"); }); // When the best match feature gate is disabled, quick suggest results should be // shown as the usual non-best match results. add_task(async function disabled_featureGate() { UrlbarPrefs.set("bestMatch.enabled", false); await doDisabledTest(); UrlbarPrefs.set("bestMatch.enabled", true); }); // When the best match suggestions are disabled, quick suggest results should be // shown as the usual non-best match results. add_task(async function disabled_suggestions() { UrlbarPrefs.set("suggest.bestmatch", false); await doDisabledTest(); UrlbarPrefs.set("suggest.bestmatch", true); }); // When best match is disabled, quick suggest results should be shown as the // usual, non-best match results. async function doDisabledTest() { let context = createContext("fullkeywor", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], }); let result = context.results[0]; // 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. Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title"); Assert.deepEqual( result.titleHighlights, [["fullkeywor".length, 1]], "result.titleHighlights" ); Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex"); Assert.equal( result.isSuggestedIndexRelativeToGroup, true, "result.isSuggestedIndexRelativeToGroup" ); } // `suggestion.position` should be ignored when the suggestion is a best match. add_task(async function position() { Assert.greater( BEST_MATCH_POSITION, 1, "Precondition: `suggestion.position` > the best match index" ); UrlbarPrefs.set("quicksuggest.allowPositionInSuggestions", true); let context = createContext(BEST_MATCH_POSITION_SEARCH_STRING, { isPrivate: false, }); // Add some visits to fill up the view. let maxResultCount = UrlbarPrefs.get("maxRichResults"); let visitResults = []; for (let i = 0; i < maxResultCount; i++) { let url = `http://example.com/${BEST_MATCH_POSITION_SEARCH_STRING}-${i}`; await PlacesTestUtils.addVisits(url); visitResults.unshift( makeVisitResult(context, { uri: url, title: `test visit for ${url}`, }) ); } // Do a search. await check_results({ context, matches: [ // search heuristic makeSearchResult(context, { engineName: Services.search.defaultEngine.name, engineIconUri: Services.search.defaultEngine.iconURI?.spec, heuristic: true, }), // best match whose backing suggestion has a `position` EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT, // visits ...visitResults.slice(0, MAX_RESULT_COUNT - 2), ], }); await cleanupPlaces(); UrlbarPrefs.clear("quicksuggest.allowPositionInSuggestions"); }); // Tests a suggestion that is blocked from being a best match. add_task(async function blockedAsBestMatch() { let config = QuickSuggestTestUtils.BEST_MATCH_CONFIG; config.best_match.blocked_suggestion_ids = [1]; await QuickSuggestTestUtils.withConfig({ config, callback: async () => { let context = createContext("fullkeyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], }); }, }); }); // Tests without a best_match config to make sure nothing breaks. add_task(async function noConfig() { await QuickSuggestTestUtils.withConfig({ config: {}, callback: async () => { let context = createContext("fullkeyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], }); }, }); });