/* 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 dynamic Rust suggestions. const REMOTE_SETTINGS_RECORDS = [ // nonsponsored, no `bypassSuggestAll` { type: "dynamic-suggestions", suggestion_type: "aaa", score: 0.9, attachment: [ { keywords: ["aaa keyword", "aaa bbb keyword", "wikipedia"], data: { result: { payload: { title: "aaa title", url: "https://example.com/aaa", }, }, }, }, ], }, // sponsored, no `bypassSuggestAll` { type: "dynamic-suggestions", suggestion_type: "bbb", score: 0.1, attachment: [ { keywords: ["bbb keyword", "aaa bbb keyword", "wikipedia"], dismissal_key: "bbb-dismissal-key", data: { result: { isBestMatch: true, suggestedIndex: 1, isSuggestedIndexRelativeToGroup: false, isRichSuggestion: true, payload: { title: "bbb title", url: "https://example.com/bbb", isSponsored: true, telemetryType: "bbb_telemetry_type", }, }, }, }, ], }, // nonsponsored, `bypassSuggestAll: true` { type: "dynamic-suggestions", suggestion_type: "ccc", score: 0.9, attachment: [ { keywords: ["ccc keyword", "ccc ddd keyword"], data: { result: { bypassSuggestAll: true, payload: { title: "ccc title", url: "https://example.com/ccc", }, }, }, }, ], }, // sponsored, `bypassSuggestAll: true` { type: "dynamic-suggestions", suggestion_type: "ddd", score: 0.9, attachment: [ { keywords: ["ddd keyword", "ccc ddd keyword"], data: { result: { bypassSuggestAll: true, payload: { title: "ddd title", url: "https://example.com/ddd", isSponsored: true, }, }, }, }, ], }, { type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA, attachment: [QuickSuggestTestUtils.wikipediaRemoteSettings()], }, ]; const EXPECTED_AAA_RESULT = makeExpectedResult({ title: "aaa title", url: "https://example.com/aaa", telemetryType: "aaa", suggestionType: "aaa", }); const EXPECTED_BBB_RESULT = makeExpectedResult({ title: "bbb title", url: "https://example.com/bbb", isSponsored: true, telemetryType: "bbb_telemetry_type", suggestionType: "bbb", isBestMatch: true, suggestedIndex: 1, isSuggestedIndexRelativeToGroup: false, isRichSuggestion: true, }); const EXPECTED_CCC_RESULT = makeExpectedResult({ title: "ccc title", url: "https://example.com/ccc", telemetryType: "ccc", suggestionType: "ccc", }); const EXPECTED_DDD_RESULT = makeExpectedResult({ title: "ddd title", url: "https://example.com/ddd", isSponsored: true, telemetryType: "ddd", suggestionType: "ddd", }); add_setup(async function () { await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, prefs: [ ["quicksuggest.dynamicSuggestionTypes", "aaa,bbb,ccc,ddd"], ["suggest.quicksuggest.all", true], ["suggest.quicksuggest.sponsored", true], ["quicksuggest.ampTopPickCharThreshold", 0], ], }); }); // When a dynamic suggestion doesn't include `telemetryType`, its // `suggestionType` should be used as the telemetry type. add_task(async function telemetryType_default() { Assert.equal( QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({ suggestionType: "abcdefg", }), "abcdefg", "Telemetry type should be correct when using default" ); }); // When a dynamic suggestion includes `telemetryType`, it should be used as the // telemetry type. add_task(async function telemetryType_override() { Assert.equal( QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({ suggestionType: "abcdefg", data: { result: { payload: { telemetryType: "telemetry_type_override", }, }, }, }), "telemetry_type_override", "Telemetry type should be correct when overridden" ); }); add_task(async function basic() { let queries = [ { query: "no match", expected: [], }, { query: "aaa keyword", expected: [EXPECTED_AAA_RESULT], }, { query: "bbb keyword", expected: [EXPECTED_BBB_RESULT], }, { query: "aaa bbb keyword", // The "aaa" suggestion has a higher score than "bbb". expected: [EXPECTED_AAA_RESULT], }, { query: "ccc keyword", expected: [EXPECTED_CCC_RESULT], }, { query: "ddd keyword", expected: [EXPECTED_DDD_RESULT], }, { query: "ccc ddd keyword", // The "ccc" suggestion has a higher score than "ddd". expected: [EXPECTED_CCC_RESULT], }, ]; await doQueries(queries); }); // When only one dynamic suggestion type is enabled, only its result should be // returned. This task assumes multiples types were added to remote settings in // the setup task. add_task(async function oneSuggestionType() { await withSuggestionTypesPref("bbb", async () => { await doQueries([ { query: "aaa keyword", expected: [], }, { query: "bbb keyword", expected: [EXPECTED_BBB_RESULT], }, { query: "aaa bbb keyword", expected: [EXPECTED_BBB_RESULT], }, { query: "doesn't match", expected: [], }, ]); }); }); // When no dynamic suggestion types are enabled, no results should be added. add_task(async function disabled() { await withSuggestionTypesPref("", async () => { await doQueries( ["aaa keyword", "bbb keyword", "aaa bbb keyword"].map(query => ({ query, expected: [], })) ); }); }); // Dynamic suggestions shouldn't be added when `all` is disabled unless they // define `bypassSuggestAll`. add_task(async function allDisabled() { UrlbarPrefs.set("suggest.quicksuggest.all", false); // The sponsored pref shouldn't matter. for (let sponsoredEnabled of [true, false]) { UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsoredEnabled); await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => { await doQueries([ { query: "aaa keyword", expected: [], }, { query: "bbb keyword", expected: [], }, { query: "aaa bbb keyword", expected: [], }, { query: "ccc keyword", expected: [EXPECTED_CCC_RESULT], }, { query: "ddd keyword", expected: [EXPECTED_DDD_RESULT], }, { query: "ccc ddd keyword", // The "ccc" suggestion has a higher score than "ddd". expected: [EXPECTED_CCC_RESULT], }, ]); }); } UrlbarPrefs.set("suggest.quicksuggest.all", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); }); // Dynamic suggestions that are sponsored shouldn't be added when sponsored // suggestions are disabled unless they define `bypassSuggestAll`. add_task(async function sponsoredDisabled() { UrlbarPrefs.set("suggest.quicksuggest.all", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => { await doQueries([ { query: "aaa keyword", expected: [EXPECTED_AAA_RESULT], }, { query: "bbb keyword", expected: [], }, { query: "aaa bbb keyword", expected: [EXPECTED_AAA_RESULT], }, { query: "ccc keyword", expected: [EXPECTED_CCC_RESULT], }, { query: "ddd keyword", expected: [EXPECTED_DDD_RESULT], }, { query: "ccc ddd keyword", // The "ccc" suggestion has a higher score than "ddd". expected: [EXPECTED_CCC_RESULT], }, ]); }); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); await QuickSuggestTestUtils.forceSync(); }); // Tests the `quickSuggestDynamicSuggestionTypes` Nimbus variable. add_task(async function nimbus() { // Clear `dynamicSuggestionTypes` to make sure the value comes from the Nimbus // variable and not the pref. await withSuggestionTypesPref("", async () => { let cleanup = await UrlbarTestUtils.initNimbusFeature({ quickSuggestDynamicSuggestionTypes: "aaa,bbb", }); await QuickSuggestTestUtils.forceSync(); await doQueries([ { query: "aaa keyword", expected: [EXPECTED_AAA_RESULT], }, { query: "bbb keyword", expected: [EXPECTED_BBB_RESULT], }, { query: "aaa bbb keyword", // The "aaa" suggestion has a higher score than "bbb". expected: [EXPECTED_AAA_RESULT], }, { query: "doesn't match", expected: [], }, ]); await cleanup(); }); }); // Tests dismissals. Note that dynamic suggestions must define a `dismissal_key` // in order to be dismissable. add_task(async function dismissal() { // Do a search and get the actual result that's returned. let context = createContext("bbb keyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); await check_results({ context, matches: [EXPECTED_BBB_RESULT], }); let result = context.results[0]; let { suggestionObject } = result.payload; let { dismissalKey } = suggestionObject; Assert.equal( dismissalKey, "bbb-dismissal-key", "The suggestion should have the expected dismissal key" ); // It shouldn't be dismissed yet. Assert.ok( !(await QuickSuggest.isResultDismissed(result)), "The result should not be dismissed yet" ); Assert.ok( !(await QuickSuggest.rustBackend.isRustSuggestionDismissed( suggestionObject )), "The suggestion should not be dismissed yet" ); Assert.ok( !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)), "The dismissal key should not be registered yet" ); // Dismiss it. It should be dismissed by its dismissal key. await QuickSuggest.dismissResult(result); Assert.ok( await QuickSuggest.isResultDismissed(result), "The result should be dismissed" ); Assert.ok( await QuickSuggest.rustBackend.isRustSuggestionDismissed(suggestionObject), "The suggestion should be dismissed" ); await check_results({ context: createContext("bbb keyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [], }); // Clear dismissals and check again. await QuickSuggest.clearDismissedSuggestions(); await check_results({ context: createContext("bbb keyword", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [EXPECTED_BBB_RESULT], }); Assert.ok( !(await QuickSuggest.isResultDismissed(result)), "The result should not be dismissed after clearing dismissals" ); Assert.ok( !(await QuickSuggest.rustBackend.isRustSuggestionDismissed( suggestionObject )), "The suggestion should not be dismissed after clearing dismissals" ); Assert.ok( !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)), "The dismissal key should not be registered after clearing dismissals" ); }); // Tests whether the prefs DynamicSuggestion handles clears. add_task(async function clearDismissedSuggestions() { let feature = QuickSuggest.getFeature("DynamicSuggestions"); let sandbox = sinon.createSandbox(); sinon .stub(feature, "primaryUserControlledPreferences") .get(() => ["suggest.realtimeOptIn", "autoFill", "closeOtherPanelsOnOpen"]); UrlbarPrefs.set("suggest.realtimeOptIn", false); UrlbarPrefs.set("autoFill", false); UrlbarPrefs.set("closeOtherPanelsOnOpen", false); Assert.ok(await QuickSuggest.canClearDismissedSuggestions()); await QuickSuggest.clearDismissedSuggestions(); Assert.ok(UrlbarPrefs.get("suggest.realtimeOptIn")); Assert.ok(UrlbarPrefs.get("autoFill")); Assert.ok(UrlbarPrefs.get("closeOtherPanelsOnOpen")); sandbox.restore(); }); // Tests some suggestions with bad data that desktop ignores. add_task(async function badSuggestions() { await QuickSuggestTestUtils.setRemoteSettingsRecords([ { type: "dynamic-suggestions", suggestion_type: "bad", attachment: [ // Include a good suggestion so we can verify this record was actually // ingested. Change the keyword so we don't confuse ourselves by // searching for an "aaa" keyword and getting a urlbar result whose // telemetry type and dynamic suggestion type is "bad". { ...REMOTE_SETTINGS_RECORDS[0].attachment[0], keywords: ["good actually"], }, // `data` is missing -- Rust actually allows this since `data` is // defined as `Option`, but desktop doesn't. { keywords: ["bad"], }, // `data` isn't an object { data: 123, keywords: ["bad"], }, // `data.result` is missing { data: {}, keywords: ["bad"], }, // `data.result` isn't an object { data: { result: 123, }, keywords: ["bad"], }, // `data.result.payload` isn't an object { data: { result: { payload: 123, }, }, keywords: ["bad"], }, ], }, ]); await withSuggestionTypesPref("bad", async () => { // Verify the good suggestion was ingested. await check_results({ context: createContext("good actually", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [ { ...EXPECTED_AAA_RESULT, payload: { ...EXPECTED_AAA_RESULT.payload, telemetryType: "bad", suggestionType: "bad", }, }, ], }); // No "bad" suggestions should be matched. await check_results({ context: createContext("bad", { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: [], }); }); // Clean up. await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); }); async function doQueries(queries) { for (let { query, expected } of queries) { info( "Doing query: " + JSON.stringify({ query, expected, }) ); await check_results({ context: createContext(query, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }), matches: expected, }); } } async function withSuggestionTypesPref(prefValue, callback) { // Use `Services` to get the original pref value since `UrlbarPrefs` will // parse the string value into a `Set`. let originalPrefValue = Services.prefs.getCharPref( "browser.urlbar.quicksuggest.dynamicSuggestionTypes" ); // Changing the pref (or Nimbus variable) to a different value will trigger // ingest, so force sync afterward (or at least wait for ingest to finish). UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", prefValue); await QuickSuggestTestUtils.forceSync(); await callback(); UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", originalPrefValue); await QuickSuggestTestUtils.forceSync(); } function makeExpectedResult({ title, url, telemetryType, suggestionType, isSponsored = false, isBestMatch = false, suggestedIndex = -1, isSuggestedIndexRelativeToGroup = true, isRichSuggestion = undefined, }) { return { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, isBestMatch, suggestedIndex, isRichSuggestion, isSuggestedIndexRelativeToGroup, payload: { title, url, isSponsored, telemetryType, suggestionType, source: "rust", provider: "Dynamic", isManageable: true, helpUrl: QuickSuggest.HELP_URL, }, }; }