summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/quicksuggest/unit/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/quicksuggest/unit/head.js')
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/head.js911
1 files changed, 911 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/head.js b/browser/components/urlbar/tests/quicksuggest/unit/head.js
new file mode 100644
index 0000000000..c468e4526f
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js
@@ -0,0 +1,911 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../unit/head.js */
+/* eslint-disable jsdoc/require-param */
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+add_setup(async function setUpQuickSuggestXpcshellTest() {
+ // Initializing TelemetryEnvironment in an xpcshell environment requires
+ // jumping through a bunch of hoops. Suggest's use of TelemetryEnvironment is
+ // tested in browser tests, and there's no other necessary reason to wait for
+ // TelemetryEnvironment initialization in xpcshell tests, so just skip it.
+ UrlbarPrefs._testSkipTelemetryEnvironmentInit = true;
+});
+
+/**
+ * Adds two tasks: One with the Rust backend disabled and one with it enabled.
+ * The names of the task functions will be the name of the passed-in task
+ * function appended with "_rustDisabled" and "_rustEnabled". If the passed-in
+ * task doesn't have a name, "anonymousTask" will be used. Call this with the
+ * usual `add_task()` arguments.
+ */
+function add_tasks_with_rust(...args) {
+ let taskFnIndex = args.findIndex(a => typeof a == "function");
+ let taskFn = args[taskFnIndex];
+
+ for (let rustEnabled of [false, true]) {
+ let newTaskFn = async (...taskFnArgs) => {
+ info("add_tasks_with_rust: Setting rustEnabled: " + rustEnabled);
+ UrlbarPrefs.set("quicksuggest.rustEnabled", rustEnabled);
+ info("add_tasks_with_rust: Done setting rustEnabled: " + rustEnabled);
+
+ // The current backend may now start syncing, so wait for it to finish.
+ info("add_tasks_with_rust: Forcing sync");
+ await QuickSuggestTestUtils.forceSync();
+ info("add_tasks_with_rust: Done forcing sync");
+
+ let rv;
+ try {
+ info(
+ "add_tasks_with_rust: Calling original task function: " + taskFn.name
+ );
+ rv = await taskFn(...taskFnArgs);
+ } catch (e) {
+ // Clearly report any unusual errors to make them easier to spot and to
+ // make the flow of the test clearer. The harness throws NS_ERROR_ABORT
+ // when a normal assertion fails, so don't report that.
+ if (e.result != Cr.NS_ERROR_ABORT) {
+ Assert.ok(
+ false,
+ "add_tasks_with_rust: The original task function threw an error: " +
+ e
+ );
+ }
+ throw e;
+ } finally {
+ info(
+ "add_tasks_with_rust: Done calling original task function: " +
+ taskFn.name
+ );
+ info("add_tasks_with_rust: Clearing rustEnabled");
+ UrlbarPrefs.clear("quicksuggest.rustEnabled");
+ info("add_tasks_with_rust: Done clearing rustEnabled");
+
+ // The current backend may now start syncing, so wait for it to finish.
+ info("add_tasks_with_rust: Forcing sync");
+ await QuickSuggestTestUtils.forceSync();
+ info("add_tasks_with_rust: Done forcing sync");
+ }
+ return rv;
+ };
+
+ Object.defineProperty(newTaskFn, "name", {
+ value:
+ (taskFn.name || "anonymousTask") +
+ (rustEnabled ? "_rustEnabled" : "_rustDisabled"),
+ });
+ let addTaskArgs = [...args];
+ addTaskArgs[taskFnIndex] = newTaskFn;
+ add_task(...addTaskArgs);
+ }
+}
+
+/**
+ * Returns an expected Wikipedia (non-sponsored) result that can be passed to
+ * `check_results()` regardless of whether the Rust backend is enabled.
+ *
+ * @returns {object}
+ * An object that can be passed to `check_results()`.
+ */
+function makeWikipediaResult({
+ source,
+ provider,
+ keyword = "wikipedia",
+ title = "Wikipedia Suggestion",
+ url = "http://example.com/wikipedia",
+ originalUrl = "http://example.com/wikipedia",
+ icon = null,
+ iconBlob = new Blob([new Uint8Array([])]),
+ impressionUrl = "http://example.com/wikipedia-impression",
+ clickUrl = "http://example.com/wikipedia-click",
+ blockId = 1,
+ advertiser = "Wikipedia",
+ iabCategory = "5 - Education",
+ suggestedIndex = -1,
+ isSuggestedIndexRelativeToGroup = true,
+}) {
+ let result = {
+ suggestedIndex,
+ isSuggestedIndexRelativeToGroup,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ title,
+ url,
+ originalUrl,
+ displayUrl: url.replace(/^https:\/\//, ""),
+ isSponsored: false,
+ qsSuggestion: keyword,
+ sponsoredAdvertiser: "Wikipedia",
+ sponsoredIabCategory: "5 - Education",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: "urlbar-result-menu-dismiss-firefox-suggest",
+ },
+ telemetryType: "adm_nonsponsored",
+ },
+ };
+
+ if (UrlbarPrefs.get("quickSuggestRustEnabled")) {
+ result.payload.source = source || "rust";
+ result.payload.provider = provider || "Wikipedia";
+ result.payload.iconBlob = iconBlob;
+ } else {
+ result.payload.source = source || "remote-settings";
+ result.payload.provider = provider || "AdmWikipedia";
+ result.payload.icon = icon;
+ result.payload.sponsoredImpressionUrl = impressionUrl;
+ result.payload.sponsoredClickUrl = clickUrl;
+ result.payload.sponsoredBlockId = blockId;
+ result.payload.sponsoredAdvertiser = advertiser;
+ result.payload.sponsoredIabCategory = iabCategory;
+ }
+
+ return result;
+}
+
+/**
+ * Returns an expected AMP (sponsored) result that can be passed to
+ * `check_results()` regardless of whether the Rust backend is enabled.
+ *
+ * @returns {object}
+ * An object that can be passed to `check_results()`.
+ */
+function makeAmpResult({
+ source,
+ provider,
+ keyword = "amp",
+ title = "Amp Suggestion",
+ url = "http://example.com/amp",
+ originalUrl = "http://example.com/amp",
+ icon = null,
+ iconBlob = new Blob([new Uint8Array([])]),
+ impressionUrl = "http://example.com/amp-impression",
+ clickUrl = "http://example.com/amp-click",
+ blockId = 1,
+ advertiser = "Amp",
+ iabCategory = "22 - Shopping",
+ suggestedIndex = -1,
+ isSuggestedIndexRelativeToGroup = true,
+ requestId = undefined,
+} = {}) {
+ let result = {
+ suggestedIndex,
+ isSuggestedIndexRelativeToGroup,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ title,
+ url,
+ originalUrl,
+ requestId,
+ displayUrl: url.replace(/^https:\/\//, ""),
+ isSponsored: true,
+ qsSuggestion: keyword,
+ sponsoredImpressionUrl: impressionUrl,
+ sponsoredClickUrl: clickUrl,
+ sponsoredBlockId: blockId,
+ sponsoredAdvertiser: advertiser,
+ sponsoredIabCategory: iabCategory,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: "urlbar-result-menu-dismiss-firefox-suggest",
+ },
+ telemetryType: "adm_sponsored",
+ descriptionL10n: { id: "urlbar-result-action-sponsored" },
+ },
+ };
+
+ if (UrlbarPrefs.get("quickSuggestRustEnabled")) {
+ result.payload.source = source || "rust";
+ result.payload.provider = provider || "Amp";
+ if (result.payload.source == "rust") {
+ result.payload.iconBlob = iconBlob;
+ } else {
+ result.payload.icon = icon;
+ }
+ } else {
+ result.payload.source = source || "remote-settings";
+ result.payload.provider = provider || "AdmWikipedia";
+ result.payload.icon = icon;
+ }
+
+ return result;
+}
+
+/**
+ * Returns an expected MDN result that can be passed to `check_results()`
+ * regardless of whether the Rust backend is enabled.
+ *
+ * @returns {object}
+ * An object that can be passed to `check_results()`.
+ */
+function makeMdnResult({ url, title, description }) {
+ let finalUrl = new URL(url);
+ finalUrl.searchParams.set("utm_medium", "firefox-desktop");
+ finalUrl.searchParams.set("utm_source", "firefox-suggest");
+ finalUrl.searchParams.set(
+ "utm_campaign",
+ "firefox-mdn-web-docs-suggestion-experiment"
+ );
+ finalUrl.searchParams.set("utm_content", "treatment");
+
+ let result = {
+ isBestMatch: true,
+ suggestedIndex: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ heuristic: false,
+ payload: {
+ telemetryType: "mdn",
+ title,
+ url: finalUrl.href,
+ originalUrl: url,
+ displayUrl: finalUrl.href.replace(/^https:\/\//, ""),
+ description,
+ icon: "chrome://global/skin/icons/mdn.svg",
+ shouldShowUrl: true,
+ bottomTextL10n: { id: "firefox-suggest-mdn-bottom-text" },
+ },
+ };
+
+ if (UrlbarPrefs.get("quickSuggestRustEnabled")) {
+ result.payload.source = "rust";
+ result.payload.provider = "Mdn";
+ } else {
+ result.payload.source = "remote-settings";
+ result.payload.provider = "MDNSuggestions";
+ }
+
+ return result;
+}
+
+/**
+ * Returns an expected AMO (addons) result that can be passed to
+ * `check_results()` regardless of whether the Rust backend is enabled.
+ *
+ * @returns {object}
+ * An object that can be passed to `check_results()`.
+ */
+function makeAmoResult({
+ source,
+ provider,
+ title = "Amo Suggestion",
+ description = "Amo description",
+ url = "http://example.com/amo",
+ originalUrl = "http://example.com/amo",
+ icon = null,
+ setUtmParams = true,
+}) {
+ if (setUtmParams) {
+ url = new URL(url);
+ url.searchParams.set("utm_medium", "firefox-desktop");
+ url.searchParams.set("utm_source", "firefox-suggest");
+ url = url.href;
+ }
+
+ let result = {
+ isBestMatch: true,
+ suggestedIndex: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ source,
+ provider,
+ title,
+ description,
+ url,
+ originalUrl,
+ icon,
+ displayUrl: url.replace(/^https:\/\//, ""),
+ shouldShowUrl: true,
+ bottomTextL10n: { id: "firefox-suggest-addons-recommended" },
+ helpUrl: QuickSuggest.HELP_URL,
+ telemetryType: "amo",
+ },
+ };
+
+ if (UrlbarPrefs.get("quickSuggestRustEnabled")) {
+ result.payload.source = source || "rust";
+ result.payload.provider = provider || "Amo";
+ } else {
+ result.payload.source = source || "remote-settings";
+ result.payload.provider = provider || "AddonSuggestions";
+ }
+
+ return result;
+}
+
+/**
+ * Returns an expected weather result that can be passed to `check_results()`
+ * regardless of whether the Rust backend is enabled.
+ *
+ * @returns {object}
+ * An object that can be passed to `check_results()`.
+ */
+function makeWeatherResult({
+ source,
+ provider,
+ telemetryType = undefined,
+ temperatureUnit = undefined,
+} = {}) {
+ if (!temperatureUnit) {
+ temperatureUnit =
+ Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
+ }
+
+ let result = {
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ suggestedIndex: 1,
+ payload: {
+ temperatureUnit,
+ url: MerinoTestUtils.WEATHER_SUGGESTION.url,
+ iconId: "6",
+ helpUrl: QuickSuggest.HELP_URL,
+ requestId: MerinoTestUtils.server.response.body.request_id,
+ source: "merino",
+ provider: "accuweather",
+ dynamicType: "weather",
+ city: MerinoTestUtils.WEATHER_SUGGESTION.city_name,
+ temperature:
+ MerinoTestUtils.WEATHER_SUGGESTION.current_conditions.temperature[
+ temperatureUnit
+ ],
+ currentConditions:
+ MerinoTestUtils.WEATHER_SUGGESTION.current_conditions.summary,
+ forecast: MerinoTestUtils.WEATHER_SUGGESTION.forecast.summary,
+ high: MerinoTestUtils.WEATHER_SUGGESTION.forecast.high[temperatureUnit],
+ low: MerinoTestUtils.WEATHER_SUGGESTION.forecast.low[temperatureUnit],
+ shouldNavigate: true,
+ },
+ };
+
+ if (UrlbarPrefs.get("quickSuggestRustEnabled")) {
+ result.payload.source = source || "rust";
+ result.payload.provider = provider || "Weather";
+ if (telemetryType !== null) {
+ result.payload.telemetryType = telemetryType || "weather";
+ }
+ } else {
+ result.payload.source = source || "merino";
+ result.payload.provider = provider || "accuweather";
+ }
+
+ return result;
+}
+
+/**
+ * Tests quick suggest prefs migrations.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {object} options.testOverrides
+ * An object that modifies how migration is performed. It has the following
+ * properties, and all are optional:
+ *
+ * {number} migrationVersion
+ * Migration will stop at this version, so for example you can test
+ * migration only up to version 1 even when the current actual version is
+ * larger than 1.
+ * {object} defaultPrefs
+ * An object that maps pref names (relative to `browser.urlbar`) to
+ * default-branch values. These should be the default prefs for the given
+ * `migrationVersion` and will be set as defaults before migration occurs.
+ *
+ * @param {string} options.scenario
+ * The scenario to set at the time migration occurs.
+ * @param {object} options.expectedPrefs
+ * The expected prefs after migration: `{ defaultBranch, userBranch }`
+ * Pref names should be relative to `browser.urlbar`.
+ * @param {object} [options.initialUserBranch]
+ * Prefs to set on the user branch before migration ocurs. Use these to
+ * simulate user actions like disabling prefs or opting in or out of the
+ * online modal. Pref names should be relative to `browser.urlbar`.
+ */
+async function doMigrateTest({
+ testOverrides,
+ scenario,
+ expectedPrefs,
+ initialUserBranch = {},
+}) {
+ info(
+ "Testing migration: " +
+ JSON.stringify({
+ testOverrides,
+ initialUserBranch,
+ scenario,
+ expectedPrefs,
+ })
+ );
+
+ function setPref(branch, name, value) {
+ switch (typeof value) {
+ case "boolean":
+ branch.setBoolPref(name, value);
+ break;
+ case "number":
+ branch.setIntPref(name, value);
+ break;
+ case "string":
+ branch.setCharPref(name, value);
+ break;
+ default:
+ Assert.ok(
+ false,
+ `Pref type not handled for setPref: ${name} = ${value}`
+ );
+ break;
+ }
+ }
+
+ function getPref(branch, name) {
+ let type = typeof UrlbarPrefs.get(name);
+ switch (type) {
+ case "boolean":
+ return branch.getBoolPref(name);
+ case "number":
+ return branch.getIntPref(name);
+ case "string":
+ return branch.getCharPref(name);
+ default:
+ Assert.ok(false, `Pref type not handled for getPref: ${name} ${type}`);
+ break;
+ }
+ return null;
+ }
+
+ let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+ let userBranch = Services.prefs.getBranch("browser.urlbar.");
+
+ // Set initial prefs. `initialDefaultBranch` are firefox.js values, i.e.,
+ // defaults immediately after startup and before any scenario update and
+ // migration happens.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ UrlbarPrefs.clear("quicksuggest.migrationVersion");
+ let initialDefaultBranch = {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ };
+ for (let name of Object.keys(initialDefaultBranch)) {
+ userBranch.clearUserPref(name);
+ }
+ for (let [branch, prefs] of [
+ [defaultBranch, initialDefaultBranch],
+ [userBranch, initialUserBranch],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ if (value !== undefined) {
+ setPref(branch, name, value);
+ }
+ }
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+
+ // Update the scenario and check prefs twice. The first time the migration
+ // should happen, and the second time the migration should not happen and
+ // all the prefs should stay the same.
+ for (let i = 0; i < 2; i++) {
+ info(`Calling updateFirefoxSuggestScenario, i=${i}`);
+
+ // Do the scenario update and set `isStartup` to simulate startup.
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ ...testOverrides,
+ scenario,
+ isStartup: true,
+ });
+
+ // Check expected pref values. Store expected effective values as we go so
+ // we can check them afterward. For a given pref, the expected effective
+ // value is the user value, or if there's not a user value, the default
+ // value.
+ let expectedEffectivePrefs = {};
+ let {
+ defaultBranch: expectedDefaultBranch,
+ userBranch: expectedUserBranch,
+ } = expectedPrefs;
+ expectedDefaultBranch = expectedDefaultBranch || {};
+ expectedUserBranch = expectedUserBranch || {};
+ for (let [branch, prefs, branchType] of [
+ [defaultBranch, expectedDefaultBranch, "default"],
+ [userBranch, expectedUserBranch, "user"],
+ ]) {
+ let entries = Object.entries(prefs);
+ if (!entries.length) {
+ continue;
+ }
+
+ info(
+ `Checking expected prefs on ${branchType} branch after updating scenario`
+ );
+ for (let [name, value] of entries) {
+ expectedEffectivePrefs[name] = value;
+ if (branch == userBranch) {
+ Assert.ok(
+ userBranch.prefHasUserValue(name),
+ `Pref ${name} is on user branch`
+ );
+ }
+ Assert.equal(
+ getPref(branch, name),
+ value,
+ `Pref ${name} value on ${branchType} branch`
+ );
+ }
+ }
+
+ info(
+ `Making sure prefs on the default branch without expected user-branch values are not on the user branch`
+ );
+ for (let name of Object.keys(initialDefaultBranch)) {
+ if (!expectedUserBranch.hasOwnProperty(name)) {
+ Assert.ok(
+ !userBranch.prefHasUserValue(name),
+ `Pref ${name} is not on user branch`
+ );
+ }
+ }
+
+ info(`Checking expected effective prefs`);
+ for (let [name, value] of Object.entries(expectedEffectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value`
+ );
+ }
+
+ let currentVersion =
+ testOverrides?.migrationVersion === undefined
+ ? UrlbarPrefs.FIREFOX_SUGGEST_MIGRATION_VERSION
+ : testOverrides.migrationVersion;
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.migrationVersion"),
+ currentVersion,
+ "quicksuggest.migrationVersion is correct after migration"
+ );
+ }
+
+ // Clean up.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ UrlbarPrefs.clear("quicksuggest.migrationVersion");
+ let userBranchNames = [
+ ...Object.keys(initialUserBranch),
+ ...Object.keys(expectedPrefs.userBranch || {}),
+ ];
+ for (let name of userBranchNames) {
+ userBranch.clearUserPref(name);
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+}
+
+/**
+ * Does some "show less frequently" tests where the cap is set in remote
+ * settings and Nimbus. See `doOneShowLessFrequentlyTest()`. This function
+ * assumes the matching behavior implemented by the given `BaseFeature` is based
+ * on matching prefixes of the given keyword starting at the first word. It
+ * also assumes the `BaseFeature` provides suggestions in remote settings.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {BaseFeature} options.feature
+ * The feature that provides the suggestion matched by the searches.
+ * @param {*} options.expectedResult
+ * The expected result that should be matched, for searches that are expected
+ * to match a result. Can also be a function; it's passed the current search
+ * string and it should return the expected result.
+ * @param {string} options.showLessFrequentlyCountPref
+ * The name of the pref that stores the "show less frequently" count being
+ * tested.
+ * @param {string} options.nimbusCapVariable
+ * The name of the Nimbus variable that stores the "show less frequently" cap
+ * being tested.
+ * @param {object} options.keyword
+ * The primary keyword to use during the test.
+ * @param {number} options.keywordBaseIndex
+ * The index in `keyword` to base substring checks around. This function will
+ * test substrings starting at the beginning of keyword and ending at the
+ * following indexes: one index before `keywordBaseIndex`,
+ * `keywordBaseIndex`, `keywordBaseIndex` + 1, `keywordBaseIndex` + 2, and
+ * `keywordBaseIndex` + 3.
+ */
+async function doShowLessFrequentlyTests({
+ feature,
+ expectedResult,
+ showLessFrequentlyCountPref,
+ nimbusCapVariable,
+ keyword,
+ keywordBaseIndex = keyword.indexOf(" "),
+}) {
+ // Do some sanity checks on the keyword. Any checks that fail are errors in
+ // the test.
+ if (keywordBaseIndex <= 0) {
+ throw new Error(
+ "keywordBaseIndex must be > 0, but it's " + keywordBaseIndex
+ );
+ }
+ if (keyword.length < keywordBaseIndex + 3) {
+ throw new Error(
+ "keyword must have at least two chars after keywordBaseIndex"
+ );
+ }
+
+ let tests = [
+ {
+ showLessFrequentlyCount: 0,
+ canShowLessFrequently: true,
+ newSearches: {
+ [keyword.substring(0, keywordBaseIndex - 1)]: false,
+ [keyword.substring(0, keywordBaseIndex)]: true,
+ [keyword.substring(0, keywordBaseIndex + 1)]: true,
+ [keyword.substring(0, keywordBaseIndex + 2)]: true,
+ [keyword.substring(0, keywordBaseIndex + 3)]: true,
+ },
+ },
+ {
+ showLessFrequentlyCount: 1,
+ canShowLessFrequently: true,
+ newSearches: {
+ [keyword.substring(0, keywordBaseIndex)]: false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 2,
+ canShowLessFrequently: true,
+ newSearches: {
+ [keyword.substring(0, keywordBaseIndex + 1)]: false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ newSearches: {
+ [keyword.substring(0, keywordBaseIndex + 2)]: false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ newSearches: {},
+ },
+ ];
+
+ info("Testing 'show less frequently' with cap in remote settings");
+ await doOneShowLessFrequentlyTest({
+ tests,
+ feature,
+ expectedResult,
+ showLessFrequentlyCountPref,
+ rs: {
+ show_less_frequently_cap: 3,
+ },
+ });
+
+ // Nimbus should override remote settings.
+ info("Testing 'show less frequently' with cap in Nimbus and remote settings");
+ await doOneShowLessFrequentlyTest({
+ tests,
+ feature,
+ expectedResult,
+ showLessFrequentlyCountPref,
+ rs: {
+ show_less_frequently_cap: 10,
+ },
+ nimbus: {
+ [nimbusCapVariable]: 3,
+ },
+ });
+}
+
+/**
+ * Does a group of searches, increments the "show less frequently" count, and
+ * repeats until all groups are done. The cap can be set by remote settings
+ * config and/or Nimbus.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {BaseFeature} options.feature
+ * The feature that provides the suggestion matched by the searches.
+ * @param {*} options.expectedResult
+ * The expected result that should be matched, for searches that are expected
+ * to match a result. Can also be a function; it's passed the current search
+ * string and it should return the expected result.
+ * @param {string} options.showLessFrequentlyCountPref
+ * The name of the pref that stores the "show less frequently" count being
+ * tested.
+ * @param {object} options.tests
+ * An array where each item describes a group of new searches to perform and
+ * expected state. Each item should look like this:
+ * `{ showLessFrequentlyCount, canShowLessFrequently, newSearches }`
+ *
+ * {number} showLessFrequentlyCount
+ * The expected value of `showLessFrequentlyCount` before the group of
+ * searches is performed.
+ * {boolean} canShowLessFrequently
+ * The expected value of `canShowLessFrequently` before the group of
+ * searches is performed.
+ * {object} newSearches
+ * An object that maps each search string to a boolean that indicates
+ * whether the first remote settings suggestion should be triggered by the
+ * search string. Searches are cumulative: The intended use is to pass a
+ * large initial group of searches in the first search group, and then each
+ * following `newSearches` is a diff against the previous.
+ * @param {object} options.rs
+ * The remote settings config to set.
+ * @param {object} options.nimbus
+ * The Nimbus variables to set.
+ */
+async function doOneShowLessFrequentlyTest({
+ feature,
+ expectedResult,
+ showLessFrequentlyCountPref,
+ tests,
+ rs = {},
+ nimbus = {},
+}) {
+ // Disable Merino so we trigger only remote settings suggestions. The
+ // `BaseFeature` is expected to add remote settings suggestions using keywords
+ // start starting with the first word in each full keyword, but the mock
+ // Merino server will always return whatever suggestion it's told to return
+ // regardless of the search string. That means Merino will return a suggestion
+ // for a keyword that's smaller than the first full word.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
+
+ // Set Nimbus variables and RS config.
+ let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus);
+ await QuickSuggestTestUtils.withConfig({
+ config: rs,
+ callback: async () => {
+ let cumulativeSearches = {};
+
+ for (let {
+ showLessFrequentlyCount,
+ canShowLessFrequently,
+ newSearches,
+ } of tests) {
+ info(
+ "Starting subtest: " +
+ JSON.stringify({
+ showLessFrequentlyCount,
+ canShowLessFrequently,
+ newSearches,
+ })
+ );
+
+ Assert.equal(
+ feature.showLessFrequentlyCount,
+ showLessFrequentlyCount,
+ "showLessFrequentlyCount should be correct initially"
+ );
+ Assert.equal(
+ UrlbarPrefs.get(showLessFrequentlyCountPref),
+ showLessFrequentlyCount,
+ "Pref should be correct initially"
+ );
+ Assert.equal(
+ feature.canShowLessFrequently,
+ canShowLessFrequently,
+ "canShowLessFrequently should be correct initially"
+ );
+
+ // Merge the current `newSearches` object into the cumulative object.
+ cumulativeSearches = {
+ ...cumulativeSearches,
+ ...newSearches,
+ };
+
+ for (let [searchString, isExpected] of Object.entries(
+ cumulativeSearches
+ )) {
+ info("Doing search: " + JSON.stringify({ searchString, isExpected }));
+
+ let results = [];
+ if (isExpected) {
+ results.push(
+ typeof expectedResult == "function"
+ ? expectedResult(searchString)
+ : expectedResult
+ );
+ }
+
+ await check_results({
+ context: createContext(searchString, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: results,
+ });
+ }
+
+ feature.incrementShowLessFrequentlyCount();
+ }
+ },
+ });
+
+ await cleanUpNimbus();
+ UrlbarPrefs.clear(showLessFrequentlyCountPref);
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+}
+
+/**
+ * Queries the Rust component directly and checks the returned suggestions. The
+ * point is to make sure the Rust backend passes the correct providers to the
+ * Rust component depending on the types of enabled suggestions. Assuming the
+ * Rust component isn't buggy, it should return suggestions only for the
+ * passed-in providers.
+ *
+ * @param {object} options
+ * Options object
+ * @param {string} options.searchString
+ * The search string.
+ * @param {Array} options.tests
+ * Array of test objects: `{ prefs, expectedUrls }`
+ *
+ * For each object, the given prefs are set, the Rust component is queried
+ * using the given search string, and the URLs of the returned suggestions are
+ * compared to the given expected URLs (order doesn't matter).
+ *
+ * {object} prefs
+ * An object mapping pref names (relative to `browser.urlbar`) to values.
+ * These prefs will be set before querying and should be used to enable or
+ * disable particular types of suggestions.
+ * {Array} expectedUrls
+ * An array of the URLs of the suggestions that are expected to be returned.
+ * The order doesn't matter.
+ */
+async function doRustProvidersTests({ searchString, tests }) {
+ UrlbarPrefs.set("quicksuggest.rustEnabled", true);
+
+ for (let { prefs, expectedUrls } of tests) {
+ info(
+ "Starting Rust providers test: " + JSON.stringify({ prefs, expectedUrls })
+ );
+
+ info("Setting prefs and forcing sync");
+ for (let [name, value] of Object.entries(prefs)) {
+ UrlbarPrefs.set(name, value);
+ }
+ await QuickSuggestTestUtils.forceSync();
+
+ info("Querying with search string: " + JSON.stringify(searchString));
+ let suggestions = await QuickSuggest.rustBackend.query(searchString);
+ info("Got suggestions: " + JSON.stringify(suggestions));
+
+ Assert.deepEqual(
+ suggestions.map(s => s.url).sort(),
+ expectedUrls.sort(),
+ "query() should return the expected suggestions (by URL)"
+ );
+
+ info("Clearing prefs and forcing sync");
+ for (let name of Object.keys(prefs)) {
+ UrlbarPrefs.clear(name);
+ }
+ await QuickSuggestTestUtils.forceSync();
+ }
+
+ info("Clearing rustEnabled pref and forcing sync");
+ UrlbarPrefs.clear("quicksuggest.rustEnabled");
+ await QuickSuggestTestUtils.forceSync();
+}