tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head.js (26779B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* import-globals-from ../../unit/head.js */
      5 /* eslint-disable jsdoc/require-param */
      6 
      7 ChromeUtils.defineESModuleGetters(this, {
      8  Preferences: "resource://gre/modules/Preferences.sys.mjs",
      9  QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs",
     10  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
     11  TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
     12  UrlbarProviderAutofill:
     13    "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs",
     14  UrlbarProviderQuickSuggest:
     15    "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs",
     16  UrlbarSearchUtils:
     17    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     18 });
     19 
     20 add_setup(async function setUpQuickSuggestXpcshellTest() {
     21  // Initializing TelemetryEnvironment in an xpcshell environment requires
     22  // jumping through a bunch of hoops. Suggest's use of TelemetryEnvironment is
     23  // tested in browser tests, and there's no other necessary reason to wait for
     24  // TelemetryEnvironment initialization in xpcshell tests, so just skip it.
     25  QuickSuggest._testSkipTelemetryEnvironmentInit = true;
     26 });
     27 
     28 /**
     29 * Sets up a test so it can use `doMigrateTest`. The app's region and locale
     30 * will be set to US and en-US. Use `QuickSuggestTestUtils.withRegionAndLocale`
     31 * or `setRegionAndLocale` if you need to test migration in a different region
     32 * or locale.
     33 */
     34 async function setUpMigrateTest() {
     35  await UrlbarTestUtils.initNimbusFeature();
     36  await QuickSuggestTestUtils.setRegionAndLocale({
     37    region: "US",
     38    locale: "en-US",
     39  });
     40 }
     41 
     42 /**
     43 * Tests a single Suggest prefs migration, from one version to the next. Call
     44 * `setUpMigrateTest` in your setup task before using this. To test migration in
     45 * a region and locale other than US and en-US, wrap your `doMigrateTest` call
     46 * in `QuickSuggestTestUtils.withRegionAndLocale`.
     47 *
     48 * @param {object} options
     49 *   The options object.
     50 * @param {number} options.toVersion
     51 *   The version to test. Migration from `toVersion - 1` to `toVersion` will be
     52 *   performed.
     53 * @param {object} [options.preMigrationUserPrefs]
     54 *   Prefs to set on the user branch before migration. An object that maps pref
     55 *   names relative to `browser.urlbar.` to values.
     56 * @param {object} [options.expectedPostMigrationUserPrefs]
     57 *   Prefs that are expected to be set on the user branch after migration. An
     58 *   object that maps pref names relative to `browser.urlbar.` to values. If a
     59 *   pref is expected to be set on the user branch before migration but cleared
     60 *   after migration, set its value to `null`.
     61 */
     62 async function doMigrateTest({
     63  toVersion,
     64  preMigrationUserPrefs = {},
     65  expectedPostMigrationUserPrefs = {},
     66 }) {
     67  info(
     68    "Testing migration: " +
     69      JSON.stringify({
     70        toVersion,
     71        preMigrationUserPrefs,
     72        expectedPostMigrationUserPrefs,
     73      })
     74  );
     75 
     76  // Prefs whose user-branch values we should always make sure to check.
     77  // Includes obsolete prefs since they're relevant to some older migrations.
     78  let userPrefsToAlwaysCheck = [
     79    "quicksuggest.dataCollection.enabled",
     80    "quicksuggest.enabled",
     81    "suggest.quicksuggest",
     82    "suggest.quicksuggest.nonsponsored",
     83    "suggest.quicksuggest.sponsored",
     84  ];
     85 
     86  let userBranch = new Preferences({
     87    branch: "browser.urlbar.",
     88    defaultBranch: false,
     89  });
     90 
     91  // Set the last-seen migration version to `toVersion - 1`.
     92  if (toVersion == 1) {
     93    userBranch.reset("quicksuggest.migrationVersion");
     94  } else {
     95    userBranch.set("quicksuggest.migrationVersion", toVersion - 1);
     96  }
     97 
     98  // Set pre-migration user prefs.
     99  for (let [name, value] of Object.entries(preMigrationUserPrefs)) {
    100    userBranch.set(name, value);
    101  }
    102 
    103  // Record values for prefs in `userPrefsToAlwaysCheck` that weren't just set
    104  // above, so that we can use them later.
    105  for (let name of userPrefsToAlwaysCheck) {
    106    if (!preMigrationUserPrefs.hasOwnProperty(name)) {
    107      preMigrationUserPrefs[name] = userBranch.isSet(name)
    108        ? userBranch.get(name)
    109        : null;
    110    }
    111  }
    112 
    113  // The entire set of prefs that should be checked after migration.
    114  let userPrefsToCheckPostMigration = new Set([
    115    ...Object.keys(preMigrationUserPrefs),
    116    ...Object.keys(expectedPostMigrationUserPrefs),
    117  ]);
    118 
    119  // Reinitialize Suggest and check prefs twice. The first time the migration
    120  // should happen, and the second time the migration should not happen and
    121  // all the prefs should stay the same.
    122  for (let i = 0; i < 2; i++) {
    123    info(`Reinitializing Suggest, i=${i}`);
    124 
    125    // Reinitialize Suggest, which includes migration.
    126    await QuickSuggest._test_reset({
    127      migrationVersion: toVersion,
    128    });
    129 
    130    for (let name of userPrefsToCheckPostMigration) {
    131      // The expected value is the expected post-migration value, if any;
    132      // otherwise it's the pre-migration value.
    133      let expectedValue = expectedPostMigrationUserPrefs.hasOwnProperty(name)
    134        ? expectedPostMigrationUserPrefs[name]
    135        : preMigrationUserPrefs[name];
    136      if (expectedValue === null) {
    137        Assert.ok(
    138          !userBranch.isSet(name),
    139          "Pref should not have a user value after migration: " + name
    140        );
    141      } else {
    142        Assert.ok(
    143          userBranch.isSet(name),
    144          "Pref should have a user value after migration: " + name
    145        );
    146        Assert.equal(
    147          userBranch.get(name),
    148          expectedValue,
    149          "Pref should have been set to the expected value after migration: " +
    150            name
    151        );
    152      }
    153    }
    154 
    155    Assert.equal(
    156      userBranch.get("quicksuggest.migrationVersion"),
    157      toVersion,
    158      "quicksuggest.migrationVersion should be updated after migration"
    159    );
    160  }
    161 
    162  // Clean up.
    163  userBranch.reset("quicksuggest.migrationVersion");
    164  for (let name of userPrefsToCheckPostMigration) {
    165    userBranch.reset(name);
    166  }
    167 }
    168 
    169 /**
    170 * Does a test that dismisses a single result by triggering a command on it.
    171 *
    172 * @param {object} options
    173 *   Options object.
    174 * @param {SuggestFeature} options.feature
    175 *   The feature that provides the dismissed result.
    176 * @param {UrlbarResult} options.result
    177 *   The result to trigger the command on.
    178 * @param {string} options.command
    179 *   The name of the command to trigger. It should dismiss one result.
    180 * @param {Array} options.queriesForDismissals
    181 *   Array of objects: `{ query, expectedResults }`
    182 *   For each object, the test will perform a search with `query` as the search
    183 *   string. After dismissing the result, the query shouldn't match any results.
    184 *   After clearing dismissals, the query should match the results in
    185 *   `expectedResults`. If `expectedResults` is omitted, `[result]` will be
    186 *   used.
    187 * @param {Array} options.queriesForOthers
    188 *   Array of objects: `{ query, expectedResults }`
    189 *   For each object, the test will perform a search with `query` as the search
    190 *   string. The query should always match `expectedResults`.
    191 * @param {string[]} [options.providers]
    192 *   The providers to query.
    193 */
    194 async function doDismissOneTest({
    195  feature,
    196  result,
    197  command,
    198  queriesForDismissals,
    199  queriesForOthers,
    200  providers = [UrlbarProviderQuickSuggest.name],
    201 }) {
    202  await QuickSuggest.clearDismissedSuggestions();
    203  await QuickSuggestTestUtils.forceSync();
    204  Assert.ok(
    205    !(await QuickSuggest.canClearDismissedSuggestions()),
    206    "Sanity check: canClearDismissedSuggestions should return false initially"
    207  );
    208 
    209  let changedPromise = TestUtils.topicObserved(
    210    "quicksuggest-dismissals-changed"
    211  );
    212 
    213  let actualResult = await getActualResult({
    214    providers,
    215    query: queriesForDismissals[0].query,
    216    expectedResult: result,
    217  });
    218 
    219  triggerCommand({
    220    command,
    221    feature,
    222    result: actualResult,
    223    expectedCountsByCall: {
    224      removeResult: 1,
    225    },
    226  });
    227 
    228  info("Awaiting dismissals-changed promise");
    229  await changedPromise;
    230 
    231  Assert.ok(
    232    await QuickSuggest.canClearDismissedSuggestions(),
    233    "canClearDismissedSuggestions should return true after triggering command"
    234  );
    235  Assert.ok(
    236    await QuickSuggest.isResultDismissed(actualResult),
    237    "The result should be dismissed"
    238  );
    239 
    240  for (let { query } of queriesForDismissals) {
    241    info("Doing search for dismissed suggestions: " + JSON.stringify(query));
    242    await check_results({
    243      context: createContext(query, {
    244        providers,
    245        isPrivate: false,
    246      }),
    247      matches: [],
    248    });
    249  }
    250 
    251  for (let { query, expectedResults } of queriesForOthers) {
    252    info(
    253      "Doing search for non-dismissed suggestions: " + JSON.stringify(query)
    254    );
    255    await check_results({
    256      context: createContext(query, {
    257        providers,
    258        isPrivate: false,
    259      }),
    260      matches: expectedResults,
    261    });
    262  }
    263 
    264  let clearedPromise = TestUtils.topicObserved(
    265    "quicksuggest-dismissals-cleared"
    266  );
    267 
    268  info("Clearing dismissals");
    269  await QuickSuggest.clearDismissedSuggestions();
    270 
    271  // It's not necessary to await this -- awaiting `clearDismissedSuggestions()`
    272  // is sufficient -- but we do it to make sure the notification is sent.
    273  info("Awaiting dismissals-cleared promise");
    274  await clearedPromise;
    275 
    276  Assert.ok(
    277    !(await QuickSuggest.canClearDismissedSuggestions()),
    278    "canClearDismissedSuggestions should return false after clearing dismissals"
    279  );
    280 
    281  for (let { query, expectedResults = [result] } of queriesForDismissals) {
    282    info("Doing search after clearing dismissals: " + JSON.stringify(query));
    283    await check_results({
    284      context: createContext(query, {
    285        providers,
    286        isPrivate: false,
    287      }),
    288      matches: expectedResults,
    289    });
    290  }
    291 }
    292 
    293 /**
    294 * Does a test that dismisses a suggestion type (i.e., all suggestions of a
    295 * certain type) by triggering a command on a result.
    296 *
    297 * @param {object} options
    298 *   Options object.
    299 * @param {SuggestFeature} options.feature
    300 *   The feature that provides the suggestion type.
    301 * @param {UrlbarResult} options.result
    302 *   The result to trigger the command on.
    303 * @param {string} options.command
    304 *   The name of the command to trigger. It should dismiss all results of a
    305 *   suggestion type.
    306 * @param {string} options.pref
    307 *   The name of the user-controlled pref (relative to `browser.urlbar.`) that
    308 *   controls the suggestion type. Should be included in
    309 *   `feature.primaryUserControlledPreferences`.
    310 * @param {Array} options.queries
    311 *   Array of objects: `{ query, expectedResults }`
    312 *   For each object, the test will perform a search with `query` as the search
    313 *   string. After dismissing the suggestion type, the query shouldn't match any
    314 *   results. After clearing dismissals, the query should match the results in
    315 *   `expectedResults`. If `expectedResults` is omitted, `[result]` will be
    316 *   used.
    317 * @param {string[]} [options.providers]
    318 *   The providers to query.
    319 */
    320 async function doDismissAllTest({
    321  feature,
    322  result,
    323  command,
    324  pref,
    325  queries,
    326  providers = [UrlbarProviderQuickSuggest.name],
    327 }) {
    328  await QuickSuggest.clearDismissedSuggestions();
    329  await QuickSuggestTestUtils.forceSync();
    330  Assert.ok(
    331    !(await QuickSuggest.canClearDismissedSuggestions()),
    332    "Sanity check: canClearDismissedSuggestions should return false initially"
    333  );
    334 
    335  let changedPromise = TestUtils.topicObserved(
    336    "quicksuggest-dismissals-changed"
    337  );
    338 
    339  let actualResult = await getActualResult({
    340    providers,
    341    query: queries[0].query,
    342    expectedResult: result,
    343  });
    344 
    345  triggerCommand({
    346    command,
    347    feature,
    348    result: actualResult,
    349    expectedCountsByCall: {
    350      removeResult: 1,
    351    },
    352  });
    353 
    354  info("Awaiting dismissals-changed promise");
    355  await changedPromise;
    356 
    357  Assert.ok(
    358    await QuickSuggest.canClearDismissedSuggestions(),
    359    "canClearDismissedSuggestions should return true after triggering command"
    360  );
    361  Assert.ok(
    362    !UrlbarPrefs.get(pref),
    363    "Pref should be false after triggering command: " + pref
    364  );
    365 
    366  for (let { query } of queries) {
    367    info("Doing search after triggering command: " + JSON.stringify(query));
    368    await check_results({
    369      context: createContext(query, {
    370        providers,
    371        isPrivate: false,
    372      }),
    373      matches: [],
    374    });
    375  }
    376 
    377  let clearedPromise = TestUtils.topicObserved(
    378    "quicksuggest-dismissals-cleared"
    379  );
    380 
    381  info("Clearing dismissals");
    382  await QuickSuggest.clearDismissedSuggestions();
    383 
    384  // It's not necessary to await this -- awaiting `clearDismissedSuggestions()`
    385  // is sufficient -- but we do it to make sure the notification is sent.
    386  info("Awaiting dismissals-cleared promise");
    387  await clearedPromise;
    388 
    389  Assert.ok(
    390    !(await QuickSuggest.canClearDismissedSuggestions()),
    391    "canClearDismissedSuggestions should return false after clearing dismissals"
    392  );
    393  Assert.ok(
    394    UrlbarPrefs.get(pref),
    395    "Pref should be true after clearing it: " + pref
    396  );
    397 
    398  // Clearing the pref will trigger a sync, so wait for it.
    399  await QuickSuggestTestUtils.forceSync();
    400 
    401  for (let { query, expectedResults = [result] } of queries) {
    402    info("Doing search after clearing dismissals: " + JSON.stringify(query));
    403    await check_results({
    404      context: createContext(query, {
    405        providers,
    406        isPrivate: false,
    407      }),
    408      matches: expectedResults,
    409    });
    410  }
    411 }
    412 
    413 /**
    414 * Does a search, asserts an actual result exists that matches the given result,
    415 * and returns it.
    416 *
    417 * @param {object} options
    418 *   Options object.
    419 * @param {SuggestFeature} options.query
    420 *   The search string.
    421 * @param {UrlbarResult} options.expectedResult
    422 *   The expected result.
    423 * @param {string[]} [options.providers]
    424 *   The providers to query.
    425 */
    426 async function getActualResult({
    427  query,
    428  expectedResult,
    429  providers = [UrlbarProviderQuickSuggest.name],
    430 }) {
    431  info("Doing search to get an actual result: " + JSON.stringify(query));
    432  let context = createContext(query, {
    433    providers,
    434    isPrivate: false,
    435  });
    436  await check_results({
    437    context,
    438    matches: [expectedResult],
    439  });
    440 
    441  let actualResult = context.results.find(
    442    r =>
    443      r.providerName == UrlbarProviderQuickSuggest.name &&
    444      r.payload.provider == expectedResult.payload.provider
    445  );
    446  Assert.ok(actualResult, "Search should have returned a matching result");
    447 
    448  return actualResult;
    449 }
    450 
    451 /**
    452 * Does some "show less frequently" tests where the cap is set in remote
    453 * settings and Nimbus. See `doOneShowLessFrequentlyTest()`. This function
    454 * assumes the matching behavior implemented by the given `SuggestFeature` is
    455 * based on matching prefixes of the given keyword starting at the first word.
    456 * It also assumes the `SuggestFeature` provides suggestions in remote settings.
    457 *
    458 * @param {object} options
    459 *   Options object.
    460 * @param {SuggestFeature} options.feature
    461 *   The feature that provides the suggestion matched by the searches.
    462 * @param {*} options.expectedResult
    463 *   The expected result that should be matched, for searches that are expected
    464 *   to match a result. Can also be a function; it's passed the current search
    465 *   string and it should return the expected result.
    466 * @param {string} options.showLessFrequentlyCountPref
    467 *   The name of the pref that stores the "show less frequently" count being
    468 *   tested.
    469 * @param {string} options.nimbusCapVariable
    470 *   The name of the Nimbus variable that stores the "show less frequently" cap
    471 *   being tested.
    472 * @param {object} options.keyword
    473 *   The primary keyword to use during the test.
    474 * @param {number} options.keywordBaseIndex
    475 *   The index in `keyword` to base substring checks around. This function will
    476 *   test substrings starting at the beginning of keyword and ending at the
    477 *   following indexes: one index before `keywordBaseIndex`,
    478 *   `keywordBaseIndex`, `keywordBaseIndex` + 1, `keywordBaseIndex` + 2, and
    479 *   `keywordBaseIndex` + 3.
    480 */
    481 async function doShowLessFrequentlyTests({
    482  feature,
    483  expectedResult,
    484  showLessFrequentlyCountPref,
    485  nimbusCapVariable,
    486  keyword,
    487  keywordBaseIndex = keyword.indexOf(" "),
    488 }) {
    489  // Do some sanity checks on the keyword. Any checks that fail are errors in
    490  // the test.
    491  if (keywordBaseIndex <= 0) {
    492    throw new Error(
    493      "keywordBaseIndex must be > 0, but it's " + keywordBaseIndex
    494    );
    495  }
    496  if (keyword.length < keywordBaseIndex + 3) {
    497    throw new Error(
    498      "keyword must have at least two chars after keywordBaseIndex"
    499    );
    500  }
    501 
    502  let tests = [
    503    {
    504      showLessFrequentlyCount: 0,
    505      canShowLessFrequently: true,
    506      newSearches: {
    507        [keyword.substring(0, keywordBaseIndex - 1)]: false,
    508        [keyword.substring(0, keywordBaseIndex)]: true,
    509        [keyword.substring(0, keywordBaseIndex + 1)]: true,
    510        [keyword.substring(0, keywordBaseIndex + 2)]: true,
    511        [keyword.substring(0, keywordBaseIndex + 3)]: true,
    512      },
    513    },
    514    {
    515      showLessFrequentlyCount: 1,
    516      canShowLessFrequently: true,
    517      newSearches: {
    518        [keyword.substring(0, keywordBaseIndex)]: false,
    519      },
    520    },
    521    {
    522      showLessFrequentlyCount: 2,
    523      canShowLessFrequently: true,
    524      newSearches: {
    525        [keyword.substring(0, keywordBaseIndex + 1)]: false,
    526      },
    527    },
    528    {
    529      showLessFrequentlyCount: 3,
    530      canShowLessFrequently: false,
    531      newSearches: {
    532        [keyword.substring(0, keywordBaseIndex + 2)]: false,
    533      },
    534    },
    535    {
    536      showLessFrequentlyCount: 3,
    537      canShowLessFrequently: false,
    538      newSearches: {},
    539    },
    540  ];
    541 
    542  info("Testing 'show less frequently' with cap in remote settings");
    543  await doOneShowLessFrequentlyTest({
    544    tests,
    545    feature,
    546    expectedResult,
    547    showLessFrequentlyCountPref,
    548    rs: {
    549      show_less_frequently_cap: 3,
    550    },
    551  });
    552 
    553  // Nimbus should override remote settings.
    554  info("Testing 'show less frequently' with cap in Nimbus and remote settings");
    555  await doOneShowLessFrequentlyTest({
    556    tests,
    557    feature,
    558    expectedResult,
    559    showLessFrequentlyCountPref,
    560    rs: {
    561      show_less_frequently_cap: 10,
    562    },
    563    nimbus: {
    564      [nimbusCapVariable]: 3,
    565    },
    566  });
    567 }
    568 
    569 /**
    570 * Does a group of searches, increments the "show less frequently" count, and
    571 * repeats until all groups are done. The cap can be set by remote settings
    572 * config and/or Nimbus.
    573 *
    574 * @param {object} options
    575 *   Options object.
    576 * @param {SuggestFeature} options.feature
    577 *   The feature that provides the suggestion matched by the searches.
    578 * @param {*} options.expectedResult
    579 *   The expected result that should be matched, for searches that are expected
    580 *   to match a result. Can also be a function; it's passed the current search
    581 *   string and it should return the expected result.
    582 * @param {string} options.showLessFrequentlyCountPref
    583 *   The name of the pref that stores the "show less frequently" count being
    584 *   tested.
    585 * @param {object} options.tests
    586 *   An array where each item describes a group of new searches to perform and
    587 *   expected state. Each item should look like this:
    588 *   `{ showLessFrequentlyCount, canShowLessFrequently, newSearches }`
    589 *
    590 *   {number} showLessFrequentlyCount
    591 *     The expected value of `showLessFrequentlyCount` before the group of
    592 *     searches is performed.
    593 *   {boolean} canShowLessFrequently
    594 *     The expected value of `canShowLessFrequently` before the group of
    595 *     searches is performed.
    596 *   {object} newSearches
    597 *     An object that maps each search string to a boolean that indicates
    598 *     whether the first remote settings suggestion should be triggered by the
    599 *     search string. Searches are cumulative: The intended use is to pass a
    600 *     large initial group of searches in the first search group, and then each
    601 *     following `newSearches` is a diff against the previous.
    602 * @param {object} options.rs
    603 *   The remote settings config to set.
    604 * @param {object} options.nimbus
    605 *   The Nimbus variables to set.
    606 */
    607 async function doOneShowLessFrequentlyTest({
    608  feature,
    609  expectedResult,
    610  showLessFrequentlyCountPref,
    611  tests,
    612  rs = {},
    613  nimbus = {},
    614 }) {
    615  // Disable Merino so we trigger only remote settings suggestions. The
    616  // `SuggestFeature` is expected to add remote settings suggestions using
    617  // keywords start starting with the first word in each full keyword, but the
    618  // mock Merino server will always return whatever suggestion it's told to
    619  // return regardless of the search string. That means Merino will return a
    620  // suggestion for a keyword that's smaller than the first full word.
    621  UrlbarPrefs.set("quicksuggest.online.enabled", false);
    622 
    623  // Set Nimbus variables and RS config.
    624  let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus);
    625  await QuickSuggestTestUtils.withConfig({
    626    config: rs,
    627    callback: async () => {
    628      let cumulativeSearches = {};
    629 
    630      for (let {
    631        showLessFrequentlyCount,
    632        canShowLessFrequently,
    633        newSearches,
    634      } of tests) {
    635        info(
    636          "Starting subtest: " +
    637            JSON.stringify({
    638              showLessFrequentlyCount,
    639              canShowLessFrequently,
    640              newSearches,
    641            })
    642        );
    643 
    644        Assert.equal(
    645          feature.showLessFrequentlyCount,
    646          showLessFrequentlyCount,
    647          "showLessFrequentlyCount should be correct initially"
    648        );
    649        Assert.equal(
    650          UrlbarPrefs.get(showLessFrequentlyCountPref),
    651          showLessFrequentlyCount,
    652          "Pref should be correct initially"
    653        );
    654        Assert.equal(
    655          feature.canShowLessFrequently,
    656          canShowLessFrequently,
    657          "canShowLessFrequently should be correct initially"
    658        );
    659 
    660        // Merge the current `newSearches` object into the cumulative object.
    661        cumulativeSearches = {
    662          ...cumulativeSearches,
    663          ...newSearches,
    664        };
    665 
    666        for (let [searchString, isExpected] of Object.entries(
    667          cumulativeSearches
    668        )) {
    669          info("Doing search: " + JSON.stringify({ searchString, isExpected }));
    670 
    671          let results = [];
    672          if (isExpected) {
    673            results.push(
    674              typeof expectedResult == "function"
    675                ? expectedResult(searchString)
    676                : expectedResult
    677            );
    678          }
    679 
    680          await check_results({
    681            context: createContext(searchString, {
    682              providers: [UrlbarProviderQuickSuggest.name],
    683              isPrivate: false,
    684            }),
    685            matches: results,
    686          });
    687        }
    688 
    689        feature.incrementShowLessFrequentlyCount();
    690      }
    691    },
    692  });
    693 
    694  await cleanUpNimbus();
    695  UrlbarPrefs.clear(showLessFrequentlyCountPref);
    696  UrlbarPrefs.clear("quicksuggest.online.enabled");
    697 }
    698 
    699 /**
    700 * Queries the Rust component directly and checks the returned suggestions. The
    701 * point is to make sure the Rust backend passes the correct providers to the
    702 * Rust component depending on the types of enabled suggestions. Assuming the
    703 * Rust component isn't buggy, it should return suggestions only for the
    704 * passed-in providers.
    705 *
    706 * @param {object} options
    707 *   Options object
    708 * @param {string} options.searchString
    709 *   The search string.
    710 * @param {Array} options.tests
    711 *   Array of test objects: `{ prefs, expectedUrls }`
    712 *
    713 *   For each object, the given prefs are set, the Rust component is queried
    714 *   using the given search string, and the URLs of the returned suggestions are
    715 *   compared to the given expected URLs (order doesn't matter).
    716 *
    717 *   {object} prefs
    718 *     An object mapping pref names (relative to `browser.urlbar`) to values.
    719 *     These prefs will be set before querying and should be used to enable or
    720 *     disable particular types of suggestions.
    721 *   {Array} expectedUrls
    722 *     An array of the URLs of the suggestions that are expected to be returned.
    723 *     The order doesn't matter.
    724 */
    725 async function doRustProvidersTests({ searchString, tests }) {
    726  for (let { prefs, expectedUrls } of tests) {
    727    info(
    728      "Starting Rust providers test: " + JSON.stringify({ prefs, expectedUrls })
    729    );
    730 
    731    info("Setting prefs and forcing sync");
    732    for (let [name, value] of Object.entries(prefs)) {
    733      UrlbarPrefs.set(name, value);
    734    }
    735    await QuickSuggestTestUtils.forceSync();
    736 
    737    info("Querying with search string: " + JSON.stringify(searchString));
    738    let suggestions = await QuickSuggest.rustBackend.query(searchString);
    739    info("Got suggestions: " + JSON.stringify(suggestions));
    740 
    741    Assert.deepEqual(
    742      suggestions.map(s => s.url).sort(),
    743      expectedUrls.sort(),
    744      "query() should return the expected suggestions (by URL)"
    745    );
    746 
    747    info("Clearing prefs and forcing sync");
    748    for (let name of Object.keys(prefs)) {
    749      UrlbarPrefs.clear(name);
    750    }
    751    await QuickSuggestTestUtils.forceSync();
    752  }
    753 }
    754 
    755 /**
    756 * Simulates performing a command for a feature by calling its `onEngagement()`.
    757 *
    758 * @param {object} options
    759 *   Options object.
    760 * @param {SuggestFeature} options.feature
    761 *   The feature whose command will be triggered.
    762 * @param {string} options.command
    763 *   The name of the command to trigger.
    764 * @param {UrlbarResult} options.result
    765 *   The result that the command will act on.
    766 * @param {string} options.searchString
    767 *   The search string to pass to `onEngagement()`.
    768 * @param {object} options.expectedCountsByCall
    769 *   If non-null, this should map controller and view method names to the number
    770 *   of times they should be called in response to the command.
    771 * @returns {Map}
    772 *   A map from names of methods on the controller and view to the number of
    773 *   times they were called.
    774 */
    775 function triggerCommand({
    776  feature,
    777  command,
    778  result,
    779  searchString = "",
    780  expectedCountsByCall = null,
    781 }) {
    782  info(`Calling ${feature.name}.onEngagement() to trigger command: ${command}`);
    783 
    784  let countsByCall = new Map();
    785  let addCall = name => {
    786    if (!countsByCall.has(name)) {
    787      countsByCall.set(name, 0);
    788    }
    789    countsByCall.set(name, countsByCall.get(name) + 1);
    790  };
    791 
    792  feature.onEngagement(
    793    // query context
    794    {},
    795    // controller
    796    {
    797      removeResult() {
    798        addCall("removeResult");
    799      },
    800      input: {
    801        startQuery() {
    802          addCall("startQuery");
    803        },
    804      },
    805      view: {
    806        acknowledgeFeedback() {
    807          addCall("acknowledgeFeedback");
    808        },
    809        invalidateResultMenuCommands() {
    810          addCall("invalidateResultMenuCommands");
    811        },
    812      },
    813    },
    814    // details
    815    { result, selType: command },
    816    searchString
    817  );
    818 
    819  if (expectedCountsByCall) {
    820    for (let [name, expectedCount] of Object.entries(expectedCountsByCall)) {
    821      Assert.equal(
    822        countsByCall.get(name) ?? 0,
    823        expectedCount,
    824        "Function should have been called the expected number of times: " + name
    825      );
    826    }
    827  }
    828 
    829  return countsByCall;
    830 }