tor-browser

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

test_quicksuggest_merino.js (36105B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 // Tests Merino integration with UrlbarProviderQuickSuggest.
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  AmpSuggestions:
     11    "moz-src:///browser/components/urlbar/private/AmpSuggestions.sys.mjs",
     12 });
     13 
     14 const SEARCH_STRING = "frab";
     15 
     16 const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest;
     17 const { TIMESTAMP_TEMPLATE } = AmpSuggestions;
     18 
     19 const REMOTE_SETTINGS_RESULTS = [
     20  QuickSuggestTestUtils.ampRemoteSettings({
     21    keywords: [SEARCH_STRING],
     22  }),
     23 ];
     24 
     25 const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = QuickSuggestTestUtils.ampResult({
     26  keyword: SEARCH_STRING,
     27  suggestedIndex: -1,
     28 });
     29 
     30 const EXPECTED_MERINO_URLBAR_RESULT = QuickSuggestTestUtils.ampResult({
     31  source: "merino",
     32  provider: "adm",
     33  requestId: "request_id",
     34  suggestedIndex: -1,
     35 });
     36 
     37 add_setup(async () => {
     38  await MerinoTestUtils.server.start();
     39 
     40  // Set up the remote settings client with the test data.
     41  await QuickSuggestTestUtils.ensureQuickSuggestInit({
     42    prefs: [
     43      ["suggest.quicksuggest.all", true],
     44      ["suggest.quicksuggest.sponsored", true],
     45      ["quicksuggest.ampTopPickCharThreshold", 0],
     46    ],
     47  });
     48  await resetRemoteSettingsData();
     49 
     50  Assert.equal(
     51    typeof DEFAULT_SUGGESTION_SCORE,
     52    "number",
     53    "Sanity check: DEFAULT_SUGGESTION_SCORE is defined"
     54  );
     55 });
     56 
     57 // Tests with the Merino endpoint URL set to an empty string, which disables
     58 // fetching from Merino.
     59 add_task(async function merinoDisabled() {
     60  let mockEndpointUrl = UrlbarPrefs.get("merino.endpointURL");
     61  UrlbarPrefs.set("merino.endpointURL", "");
     62  UrlbarPrefs.set("quicksuggest.online.available", true);
     63  UrlbarPrefs.set("quicksuggest.online.enabled", true);
     64 
     65  // Clear the remote settings suggestions so that if Merino is actually queried
     66  // -- which would be a bug -- we don't accidentally mask the Merino suggestion
     67  // by also matching an RS suggestion with the same or higher score.
     68  await QuickSuggestTestUtils.setRemoteSettingsRecords([]);
     69 
     70  let context = createContext(SEARCH_STRING, {
     71    providers: [UrlbarProviderQuickSuggest.name],
     72    isPrivate: false,
     73  });
     74  await check_results({
     75    context,
     76    matches: [],
     77  });
     78 
     79  UrlbarPrefs.set("merino.endpointURL", mockEndpointUrl);
     80 
     81  await resetRemoteSettingsData();
     82 });
     83 
     84 // Results should be fetched from Merino only when online is both available and
     85 // enabled.
     86 add_task(async function onlineAvailableAndEnabled() {
     87  // Clear the remote settings suggestions so that if Merino is actually queried
     88  // -- which would be a bug -- we don't accidentally mask the Merino suggestion
     89  // by also matching an RS suggestion with the same or higher score.
     90  await QuickSuggestTestUtils.setRemoteSettingsRecords([]);
     91 
     92  for (let onlineAvailable of [false, true]) {
     93    for (let onlineEnabled of [false, true]) {
     94      UrlbarPrefs.set("quicksuggest.online.available", onlineAvailable);
     95      UrlbarPrefs.set("quicksuggest.online.enabled", onlineEnabled);
     96 
     97      await check_results({
     98        context: createContext(SEARCH_STRING, {
     99          providers: [UrlbarProviderQuickSuggest.name],
    100          isPrivate: false,
    101        }),
    102        matches:
    103          onlineAvailable && onlineEnabled
    104            ? [EXPECTED_MERINO_URLBAR_RESULT]
    105            : [],
    106      });
    107    }
    108  }
    109 
    110  await resetRemoteSettingsData();
    111 });
    112 
    113 // When the Merino suggestion has a higher score than the remote settings
    114 // suggestion, the Merino suggestion should be used.
    115 add_task(async function higherScore() {
    116  UrlbarPrefs.set("quicksuggest.online.available", true);
    117  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    118 
    119  MerinoTestUtils.server.response.body.suggestions[0].score =
    120    2 * DEFAULT_SUGGESTION_SCORE;
    121 
    122  let context = createContext(SEARCH_STRING, {
    123    providers: [UrlbarProviderQuickSuggest.name],
    124    isPrivate: false,
    125  });
    126  await check_results({
    127    context,
    128    matches: [EXPECTED_MERINO_URLBAR_RESULT],
    129  });
    130 
    131  MerinoTestUtils.server.reset();
    132  merinoClient().resetSession();
    133 });
    134 
    135 // When the Merino suggestion has a lower score than the remote settings
    136 // suggestion, the remote settings suggestion should be used.
    137 add_task(async function lowerScore() {
    138  UrlbarPrefs.set("quicksuggest.online.available", true);
    139  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    140 
    141  MerinoTestUtils.server.response.body.suggestions[0].score =
    142    DEFAULT_SUGGESTION_SCORE / 2;
    143 
    144  let context = createContext(SEARCH_STRING, {
    145    providers: [UrlbarProviderQuickSuggest.name],
    146    isPrivate: false,
    147  });
    148  await check_results({
    149    context,
    150    matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
    151  });
    152 
    153  MerinoTestUtils.server.reset();
    154  merinoClient().resetSession();
    155 });
    156 
    157 // When remote settings doesn't return a suggestion but Merino does, the Merino
    158 // suggestion should be used.
    159 add_task(async function noSuggestion_remoteSettings() {
    160  UrlbarPrefs.set("quicksuggest.online.available", true);
    161  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    162 
    163  let context = createContext("this doesn't match remote settings", {
    164    providers: [UrlbarProviderQuickSuggest.name],
    165    isPrivate: false,
    166  });
    167  await check_results({
    168    context,
    169    matches: [EXPECTED_MERINO_URLBAR_RESULT],
    170  });
    171 
    172  MerinoTestUtils.server.reset();
    173  merinoClient().resetSession();
    174 });
    175 
    176 // When Merino doesn't return a suggestion but remote settings does, the remote
    177 // settings suggestion should be used.
    178 add_task(async function noSuggestion_merino() {
    179  UrlbarPrefs.set("quicksuggest.online.available", true);
    180  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    181 
    182  MerinoTestUtils.server.response.body.suggestions = [];
    183 
    184  let context = createContext(SEARCH_STRING, {
    185    providers: [UrlbarProviderQuickSuggest.name],
    186    isPrivate: false,
    187  });
    188  await check_results({
    189    context,
    190    matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
    191  });
    192 
    193  MerinoTestUtils.server.reset();
    194  merinoClient().resetSession();
    195 });
    196 
    197 // When Merino returns multiple suggestions, the one with the largest score
    198 // should be used.
    199 add_task(async function multipleMerinoSuggestions() {
    200  UrlbarPrefs.set("quicksuggest.online.available", true);
    201  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    202 
    203  MerinoTestUtils.server.response.body.suggestions = [
    204    {
    205      provider: "adm",
    206      full_keyword: "multipleMerinoSuggestions 0 full_keyword",
    207      title: "multipleMerinoSuggestions 0 title",
    208      url: "multipleMerinoSuggestions 0 url",
    209      icon: "multipleMerinoSuggestions 0 icon",
    210      impression_url: "multipleMerinoSuggestions 0 impression_url",
    211      click_url: "multipleMerinoSuggestions 0 click_url",
    212      block_id: 0,
    213      advertiser: "multipleMerinoSuggestions 0 advertiser",
    214      iab_category: "22 - Shopping",
    215      is_sponsored: true,
    216      score: 0.1,
    217    },
    218    {
    219      provider: "adm",
    220      full_keyword: "multipleMerinoSuggestions 1 full_keyword",
    221      title: "multipleMerinoSuggestions 1 title",
    222      url: "multipleMerinoSuggestions 1 url",
    223      icon: "multipleMerinoSuggestions 1 icon",
    224      impression_url: "multipleMerinoSuggestions 1 impression_url",
    225      click_url: "multipleMerinoSuggestions 1 click_url",
    226      block_id: 1,
    227      advertiser: "multipleMerinoSuggestions 1 advertiser",
    228      iab_category: "22 - Shopping",
    229      is_sponsored: true,
    230      score: 1,
    231    },
    232    {
    233      provider: "adm",
    234      full_keyword: "multipleMerinoSuggestions 2 full_keyword",
    235      title: "multipleMerinoSuggestions 2 title",
    236      url: "multipleMerinoSuggestions 2 url",
    237      icon: "multipleMerinoSuggestions 2 icon",
    238      impression_url: "multipleMerinoSuggestions 2 impression_url",
    239      click_url: "multipleMerinoSuggestions 2 click_url",
    240      block_id: 2,
    241      advertiser: "multipleMerinoSuggestions 2 advertiser",
    242      iab_category: "22 - Shopping",
    243      is_sponsored: true,
    244      score: 0.2,
    245    },
    246  ];
    247 
    248  let context = createContext("test", {
    249    providers: [UrlbarProviderQuickSuggest.name],
    250    isPrivate: false,
    251  });
    252  await check_results({
    253    context,
    254    matches: [
    255      QuickSuggestTestUtils.ampResult({
    256        keyword: "multipleMerinoSuggestions 1 full_keyword",
    257        title: "multipleMerinoSuggestions 1 title",
    258        url: "multipleMerinoSuggestions 1 url",
    259        originalUrl: "multipleMerinoSuggestions 1 url",
    260        icon: "multipleMerinoSuggestions 1 icon",
    261        impressionUrl: "multipleMerinoSuggestions 1 impression_url",
    262        clickUrl: "multipleMerinoSuggestions 1 click_url",
    263        blockId: 1,
    264        advertiser: "multipleMerinoSuggestions 1 advertiser",
    265        requestId: "request_id",
    266        source: "merino",
    267        provider: "adm",
    268        suggestedIndex: -1,
    269      }),
    270    ],
    271  });
    272 
    273  MerinoTestUtils.server.reset();
    274  merinoClient().resetSession();
    275 });
    276 
    277 // Timestamp templates in URLs should be replaced with real timestamps.
    278 add_task(async function timestamps() {
    279  UrlbarPrefs.set("quicksuggest.online.available", true);
    280  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    281 
    282  // Set up the Merino response with template URLs.
    283  let suggestion = MerinoTestUtils.server.response.body.suggestions[0];
    284  suggestion.url = `http://example.com/time-${TIMESTAMP_TEMPLATE}`;
    285  suggestion.click_url = `http://example.com/time-${TIMESTAMP_TEMPLATE}-foo`;
    286 
    287  // Do a search.
    288  let context = createContext("test", {
    289    providers: [UrlbarProviderQuickSuggest.name],
    290    isPrivate: false,
    291  });
    292  let controller = UrlbarTestUtils.newMockController({
    293    input: {
    294      isPrivate: context.isPrivate,
    295      onFirstResult() {
    296        return false;
    297      },
    298      getSearchSource() {
    299        return "dummy-search-source";
    300      },
    301      window: {
    302        location: {
    303          href: AppConstants.BROWSER_CHROME_URL,
    304        },
    305      },
    306    },
    307  });
    308  await controller.startQuery(context);
    309 
    310  // Should be one quick suggest result.
    311  Assert.equal(context.results.length, 1, "One result returned");
    312  let result = context.results[0];
    313 
    314  QuickSuggestTestUtils.assertTimestampsReplaced(result, {
    315    url: suggestion.click_url,
    316    sponsoredClickUrl: suggestion.click_url,
    317  });
    318 
    319  MerinoTestUtils.server.reset();
    320  merinoClient().resetSession();
    321 });
    322 
    323 // Tests dismissals of managed Merino suggestions (suggestions that are managed
    324 // by a `SuggestFeature`).
    325 add_task(async function dismissals_managed() {
    326  UrlbarPrefs.set("quicksuggest.online.available", true);
    327  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    328 
    329  // Set up a single Merino AMP suggestion with a unique URL.
    330  let url = "https://example.com/merino-amp-url";
    331  MerinoTestUtils.server.response =
    332    MerinoTestUtils.server.makeDefaultResponse();
    333  MerinoTestUtils.server.response.body.suggestions[0].url = url;
    334 
    335  let expectedMerinoResult = QuickSuggestTestUtils.ampResult({
    336    url,
    337    source: "merino",
    338    provider: "adm",
    339    requestId: "request_id",
    340    suggestedIndex: -1,
    341  });
    342 
    343  // Do a search. The Merino suggestion should be matched.
    344  const context = createContext(SEARCH_STRING, {
    345    providers: [UrlbarProviderQuickSuggest.name],
    346    isPrivate: false,
    347  });
    348  await check_results({
    349    context,
    350    matches: [expectedMerinoResult],
    351  });
    352 
    353  let result = context.results[0];
    354  Assert.ok(
    355    QuickSuggest.getFeatureByResult(result),
    356    "Sanity check: The actual result should be managed by a feature"
    357  );
    358 
    359  // Dismiss the Merino result.
    360  await QuickSuggest.dismissResult(result);
    361  Assert.ok(
    362    await QuickSuggest.isResultDismissed(result),
    363    "isResultDismissed should return true after dismissing result"
    364  );
    365 
    366  // Do another search. The remote settings suggestion should now be matched.
    367  await check_results({
    368    context: createContext(SEARCH_STRING, {
    369      providers: [UrlbarProviderQuickSuggest.name],
    370      isPrivate: false,
    371    }),
    372    matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
    373  });
    374 
    375  // Clear dismissals.
    376  await QuickSuggest.clearDismissedSuggestions();
    377  Assert.ok(
    378    !(await QuickSuggest.isResultDismissed(result)),
    379    "isResultDismissed should return false after clearing dismissals"
    380  );
    381 
    382  // The Merino suggestion should be matched again.
    383  await check_results({
    384    context: createContext(SEARCH_STRING, {
    385      providers: [UrlbarProviderQuickSuggest.name],
    386      isPrivate: false,
    387    }),
    388    matches: [expectedMerinoResult],
    389  });
    390 
    391  MerinoTestUtils.server.reset();
    392  merinoClient().resetSession();
    393 });
    394 
    395 // Tests dismissals of Merino AMP suggestions, which have special handling
    396 // around their dismissal keys.
    397 add_task(async function dismissals_amp() {
    398  UrlbarPrefs.set("quicksuggest.online.available", true);
    399  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    400 
    401  UrlbarPrefs.set("suggest.quicksuggest.all", true);
    402  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
    403  await QuickSuggestTestUtils.forceSync();
    404 
    405  let tests = [
    406    {
    407      suggestion: {
    408        url: "https://example.com/0",
    409      },
    410      expected: {
    411        // dismissal key should be the `url` value
    412        dismissalKey: "https://example.com/0",
    413      },
    414    },
    415    {
    416      suggestion: {
    417        url: `https://example.com/1-${TIMESTAMP_TEMPLATE}`,
    418      },
    419      expected: {
    420        // dismissal key should be the original `url` value with the timestamp
    421        // template
    422        dismissalKey: `https://example.com/1-${TIMESTAMP_TEMPLATE}`,
    423      },
    424    },
    425    {
    426      suggestion: {
    427        url: "https://example.com/2",
    428        full_keyword: "full keyword 2",
    429      },
    430      expected: {
    431        // dismissal key should be the `url` value
    432        dismissalKey: "https://example.com/2",
    433        notDismissalKeys: ["full keyword 2"],
    434      },
    435    },
    436    {
    437      suggestion: {
    438        url: `https://example.com/3-${TIMESTAMP_TEMPLATE}`,
    439        full_keyword: "full keyword 3",
    440      },
    441      expected: {
    442        // dismissal key should be the `url` value
    443        dismissalKey: `https://example.com/3-${TIMESTAMP_TEMPLATE}`,
    444        notDismissalKeys: ["full keyword 3"],
    445      },
    446    },
    447    {
    448      suggestion: {
    449        url: "https://example.com/4",
    450        dismissal_key: "4-dismissal-key",
    451      },
    452      expected: {
    453        // dismissal key should be the `dismissal_key` value
    454        dismissalKey: "4-dismissal-key",
    455        notDismissalKeys: ["https://example.com/4"],
    456      },
    457    },
    458    {
    459      suggestion: {
    460        url: `https://example.com/5-${TIMESTAMP_TEMPLATE}`,
    461        dismissal_key: "5-dismissal-key",
    462      },
    463      expected: {
    464        // dismissal key should be the `dismissal_key` value
    465        dismissalKey: "5-dismissal-key",
    466        notDismissalKeys: [`https://example.com/5-${TIMESTAMP_TEMPLATE}`],
    467      },
    468    },
    469    {
    470      suggestion: {
    471        url: "https://example.com/6",
    472        full_keyword: "full keyword 6",
    473        dismissal_key: "6-dismissal-key",
    474      },
    475      expected: {
    476        // dismissal key should be the `dismissal_key` value
    477        dismissalKey: "6-dismissal-key",
    478        notDismissalKeys: ["full keyword 6", "https://example.com/6"],
    479      },
    480    },
    481    {
    482      suggestion: {
    483        url: `https://example.com/7-${TIMESTAMP_TEMPLATE}`,
    484        full_keyword: "full keyword 7",
    485        dismissal_key: "7-dismissal-key",
    486      },
    487      expected: {
    488        // dismissal key should be the `dismissal_key` value
    489        dismissalKey: "7-dismissal-key",
    490        notDismissalKeys: [
    491          "full keyword 7",
    492          `https://example.com/7-${TIMESTAMP_TEMPLATE}`,
    493        ],
    494      },
    495    },
    496  ];
    497 
    498  for (let test of tests) {
    499    info("Doing subtest: " + JSON.stringify(test));
    500 
    501    let { suggestion, expected } = test;
    502 
    503    suggestion = {
    504      provider: "adm",
    505      title: "title",
    506      icon: null,
    507      impression_url: "https://example.com/impression",
    508      click_url: "https://example.com/click",
    509      block_id: 1,
    510      advertiser: "advertiser",
    511      iab_category: "22 - Shopping",
    512      is_sponsored: true,
    513      request_id: "request_id",
    514      score: 1,
    515      ...suggestion,
    516    };
    517 
    518    MerinoTestUtils.server.response =
    519      MerinoTestUtils.server.makeDefaultResponse();
    520    MerinoTestUtils.server.response.body.suggestions = [suggestion];
    521 
    522    let expectedResult = {
    523      type: UrlbarUtils.RESULT_TYPE.URL,
    524      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    525      heuristic: false,
    526      payload: {
    527        provider: suggestion.provider,
    528        title: suggestion.full_keyword
    529          ? `${suggestion.full_keyword}${suggestion.title}`
    530          : suggestion.title,
    531        url: suggestion.url,
    532        originalUrl: suggestion.original_url || suggestion.url,
    533        dismissalKey: suggestion.dismissal_key,
    534        requestId: suggestion.request_id,
    535        sponsoredImpressionUrl: suggestion.impression_url,
    536        sponsoredClickUrl: suggestion.click_url,
    537        sponsoredBlockId: suggestion.block_id,
    538        sponsoredAdvertiser: suggestion.advertiser,
    539        sponsoredIabCategory: suggestion.iab_category,
    540        isBlockable: true,
    541        isManageable: true,
    542        isSponsored: true,
    543        source: "merino",
    544        telemetryType: "adm_sponsored",
    545        descriptionL10n: { id: "urlbar-result-action-sponsored" },
    546      },
    547    };
    548 
    549    // Do a search. The Merino suggestion should be matched.
    550    let context = createContext(SEARCH_STRING, {
    551      providers: [UrlbarProviderQuickSuggest.name],
    552      isPrivate: false,
    553    });
    554    await check_results({
    555      context,
    556      matches: [expectedResult],
    557      // Ignore values related to the timestamp template. They're not important
    558      // for this test.
    559      conditionalPayloadProperties: {
    560        url: { ignore: true },
    561        urlTimestampIndex: { ignore: true },
    562      },
    563    });
    564 
    565    let result = context.results[0];
    566    Assert.equal(
    567      QuickSuggest.getFeatureByResult(result)?.name,
    568      "AmpSuggestions",
    569      "Sanity check: The actual result should be managed by AmpSuggestions"
    570    );
    571 
    572    // Dismiss the Merino result.
    573    await QuickSuggest.dismissResult(result);
    574    Assert.ok(
    575      await QuickSuggest.isResultDismissed(result),
    576      "isResultDismissed should return true after dismissing result"
    577    );
    578 
    579    Assert.ok(
    580      await QuickSuggest.rustBackend.isDismissedByKey(expected.dismissalKey),
    581      "isDismissedByKey should return true after dismissing result"
    582    );
    583    if (expected.notDismissalKeys) {
    584      for (let value of expected.notDismissalKeys) {
    585        Assert.ok(
    586          !(await QuickSuggest.rustBackend.isDismissedByKey(value)),
    587          "isDismissedByKey should return false for notDismissalKey: " + value
    588        );
    589      }
    590    }
    591 
    592    // Do another search. The remote settings suggestion should now be matched.
    593    await check_results({
    594      context: createContext(SEARCH_STRING, {
    595        providers: [UrlbarProviderQuickSuggest.name],
    596        isPrivate: false,
    597      }),
    598      matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
    599    });
    600 
    601    // Clear dismissals.
    602    await QuickSuggest.clearDismissedSuggestions();
    603    Assert.ok(
    604      !(await QuickSuggest.isResultDismissed(result)),
    605      "isResultDismissed should return false after clearing dismissals"
    606    );
    607 
    608    // The Merino suggestion should be matched again.
    609    await check_results({
    610      context: createContext(SEARCH_STRING, {
    611        providers: [UrlbarProviderQuickSuggest.name],
    612        isPrivate: false,
    613      }),
    614      matches: [expectedResult],
    615      conditionalPayloadProperties: {
    616        url: { ignore: true },
    617        urlTimestampIndex: { ignore: true },
    618      },
    619    });
    620  }
    621 
    622  MerinoTestUtils.server.reset();
    623  merinoClient().resetSession();
    624 });
    625 
    626 // Tests dismissals of unmanaged Merino suggestions (suggestions that are not
    627 // managed by a `SuggestFeature`).
    628 add_task(async function dismissals_unmanaged_1() {
    629  UrlbarPrefs.set("quicksuggest.online.available", true);
    630  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    631 
    632  // The "top_picks" provider is the only supported unmanaged suggestion.
    633  let provider = "top_picks";
    634 
    635  let tests = [
    636    {
    637      suggestion: {
    638        provider,
    639        url: "https://example.com/0",
    640        score: 1,
    641      },
    642      expected: {
    643        // dismissal key should be the `url` value
    644        dismissalKey: "https://example.com/0",
    645      },
    646    },
    647    {
    648      suggestion: {
    649        provider,
    650        url: "https://example.com/1",
    651        original_url: "https://example.com/1-original-url",
    652        score: 1,
    653      },
    654      expected: {
    655        // dismissal key should be the `original_url` value
    656        dismissalKey: "https://example.com/1-original-url",
    657        notDismissalKeys: ["https://example.com/1"],
    658      },
    659    },
    660    {
    661      suggestion: {
    662        provider,
    663        url: "https://example.com/2",
    664        original_url: "https://example.com/2-original-url",
    665        dismissal_key: "2-dismissal-key",
    666        score: 1,
    667      },
    668      expected: {
    669        // dismissal key should be the `dismissal_key` value
    670        dismissalKey: "2-dismissal-key",
    671        notDismissalKeys: [
    672          "https://example.com/2",
    673          "https://example.com/2-original-url",
    674        ],
    675      },
    676    },
    677  ];
    678 
    679  for (let test of tests) {
    680    info("Doing subtest: " + JSON.stringify(test));
    681 
    682    let { suggestion, expected } = test;
    683 
    684    MerinoTestUtils.server.response =
    685      MerinoTestUtils.server.makeDefaultResponse();
    686    MerinoTestUtils.server.response.body.suggestions = [suggestion];
    687 
    688    let expectedResult = {
    689      type: UrlbarUtils.RESULT_TYPE.URL,
    690      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    691      heuristic: false,
    692      payload: {
    693        provider,
    694        url: suggestion.url,
    695        originalUrl: suggestion.original_url,
    696        dismissalKey: suggestion.dismissal_key,
    697        source: "merino",
    698        isSponsored: false,
    699        shouldShowUrl: true,
    700        isBlockable: true,
    701        isManageable: true,
    702        telemetryType: provider,
    703      },
    704    };
    705 
    706    // Do a search. The Merino suggestion should be matched.
    707    let context = createContext(SEARCH_STRING, {
    708      providers: [UrlbarProviderQuickSuggest.name],
    709      isPrivate: false,
    710    });
    711    await check_results({
    712      context,
    713      matches: [expectedResult],
    714    });
    715 
    716    let result = context.results[0];
    717    Assert.ok(
    718      !QuickSuggest.getFeatureByResult(result),
    719      "Sanity check: The actual result should not be managed by a feature"
    720    );
    721 
    722    // Dismiss the Merino result.
    723    await QuickSuggest.dismissResult(result);
    724    Assert.ok(
    725      await QuickSuggest.isResultDismissed(result),
    726      "isResultDismissed should return true after dismissing result"
    727    );
    728 
    729    Assert.ok(
    730      await QuickSuggest.rustBackend.isDismissedByKey(expected.dismissalKey),
    731      "isDismissedByKey should return true after dismissing result"
    732    );
    733    if (expected.notDismissalKeys) {
    734      for (let value of expected.notDismissalKeys) {
    735        Assert.ok(
    736          !(await QuickSuggest.rustBackend.isDismissedByKey(value)),
    737          "isDismissedByKey should return false for notDismissalKey: " + value
    738        );
    739      }
    740    }
    741 
    742    // Do another search. The remote settings suggestion should now be matched.
    743    await check_results({
    744      context: createContext(SEARCH_STRING, {
    745        providers: [UrlbarProviderQuickSuggest.name],
    746        isPrivate: false,
    747      }),
    748      matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
    749    });
    750 
    751    // Clear dismissals.
    752    await QuickSuggest.clearDismissedSuggestions();
    753    Assert.ok(
    754      !(await QuickSuggest.isResultDismissed(result)),
    755      "isResultDismissed should return false after clearing dismissals"
    756    );
    757 
    758    // The Merino suggestion should be matched again.
    759    await check_results({
    760      context: createContext(SEARCH_STRING, {
    761        providers: [UrlbarProviderQuickSuggest.name],
    762        isPrivate: false,
    763      }),
    764      matches: [expectedResult],
    765    });
    766  }
    767 
    768  MerinoTestUtils.server.reset();
    769  merinoClient().resetSession();
    770 });
    771 
    772 // Tests dismissals of unmanaged Merino suggestions (suggestions that are not
    773 // managed by a `SuggestFeature`) that all have the same URL but different
    774 // original URLs and dismissal keys.
    775 add_task(async function dismissals_unmanaged_2() {
    776  UrlbarPrefs.set("quicksuggest.online.available", true);
    777  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    778 
    779  // The "top_picks" provider is the only supported unmanaged suggestion.
    780  let provider = "top_picks";
    781 
    782  MerinoTestUtils.server.response =
    783    MerinoTestUtils.server.makeDefaultResponse();
    784  MerinoTestUtils.server.response.body.suggestions = [
    785    // all three: url, original_url, dismissal_key
    786    {
    787      provider,
    788      url: "https://example.com/url",
    789      original_url: "https://example.com/original_url",
    790      dismissal_key: "dismissal-key",
    791      score: 1.0,
    792    },
    793    // two: url, original_url
    794    {
    795      provider,
    796      url: "https://example.com/url",
    797      original_url: "https://example.com/original_url",
    798      score: 0.9,
    799    },
    800    // only one: url
    801    {
    802      provider,
    803      url: "https://example.com/url",
    804      score: 0.8,
    805    },
    806  ];
    807 
    808  let expectedBaseResult = {
    809    type: UrlbarUtils.RESULT_TYPE.URL,
    810    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    811    heuristic: false,
    812    payload: {
    813      provider,
    814      url: "https://example.com/url",
    815      source: "merino",
    816      isSponsored: false,
    817      shouldShowUrl: true,
    818      isBlockable: true,
    819      isManageable: true,
    820      telemetryType: provider,
    821    },
    822  };
    823 
    824  // Do a search. The first Merino suggestion should be matched.
    825  info("Doing search 1");
    826  let context = createContext(SEARCH_STRING, {
    827    providers: [UrlbarProviderQuickSuggest.name],
    828    isPrivate: false,
    829  });
    830  await check_results({
    831    context,
    832    matches: [
    833      {
    834        ...expectedBaseResult,
    835        payload: {
    836          ...expectedBaseResult.payload,
    837          originalUrl: "https://example.com/original_url",
    838          dismissalKey: "dismissal-key",
    839        },
    840      },
    841    ],
    842  });
    843 
    844  let result = context.results[0];
    845  Assert.ok(
    846    !QuickSuggest.getFeatureByResult(result),
    847    "Sanity check: The actual result should not be managed by a feature"
    848  );
    849 
    850  // Dismiss it.
    851  await QuickSuggest.dismissResult(result);
    852  Assert.ok(
    853    await QuickSuggest.isResultDismissed(result),
    854    "isResultDismissed should return true after dismissing result 1"
    855  );
    856 
    857  Assert.ok(
    858    await QuickSuggest.rustBackend.isDismissedByKey("dismissal-key"),
    859    "isDismissedByKey should return true after dismissing suggestion 1"
    860  );
    861 
    862  for (let value of [
    863    "https://example.com/url",
    864    "https://example.com/original_url",
    865  ]) {
    866    Assert.ok(
    867      !(await QuickSuggest.rustBackend.isDismissedByKey(value)),
    868      "isDismissedByKey should return false after dismissing suggestion 1: " +
    869        value
    870    );
    871  }
    872 
    873  // Do another search. The second suggestion should be matched.
    874  info("Doing search 2");
    875  context = createContext(SEARCH_STRING, {
    876    providers: [UrlbarProviderQuickSuggest.name],
    877    isPrivate: false,
    878  });
    879  await check_results({
    880    context,
    881    matches: [
    882      {
    883        ...expectedBaseResult,
    884        payload: {
    885          ...expectedBaseResult.payload,
    886          originalUrl: "https://example.com/original_url",
    887          // no dismissal_key
    888        },
    889      },
    890    ],
    891  });
    892 
    893  // Dismiss it.
    894  result = context.results[0];
    895  await QuickSuggest.dismissResult(result);
    896  Assert.ok(
    897    await QuickSuggest.isResultDismissed(result),
    898    "isResultDismissed should return true after dismissing result 2"
    899  );
    900 
    901  for (let value of ["dismissal-key", "https://example.com/original_url"]) {
    902    Assert.ok(
    903      await QuickSuggest.rustBackend.isDismissedByKey(value),
    904      "isDismissedByKey should return true after dismissing suggestion 2: " +
    905        value
    906    );
    907  }
    908 
    909  Assert.ok(
    910    !(await QuickSuggest.rustBackend.isDismissedByKey(
    911      "https://example.com/url"
    912    )),
    913    "isDismissedByKey should return false after dismissing suggestion 2"
    914  );
    915 
    916  // Do another search. The third suggestion should be matched.
    917  info("Doing search 3");
    918  context = createContext(SEARCH_STRING, {
    919    providers: [UrlbarProviderQuickSuggest.name],
    920    isPrivate: false,
    921  });
    922  await check_results({
    923    context,
    924    matches: [
    925      // no dismissal_key or original_url
    926      expectedBaseResult,
    927    ],
    928  });
    929 
    930  // Dismiss it.
    931  result = context.results[0];
    932  await QuickSuggest.dismissResult(result);
    933  Assert.ok(
    934    await QuickSuggest.isResultDismissed(result),
    935    "isResultDismissed should return true after dismissing result 3"
    936  );
    937 
    938  for (let value of [
    939    "dismissal-key",
    940    "https://example.com/original_url",
    941    "https://example.com/url",
    942  ]) {
    943    Assert.ok(
    944      await QuickSuggest.rustBackend.isDismissedByKey(value),
    945      "isDismissedByKey should return true after dismissing suggestion 3: " +
    946        value
    947    );
    948  }
    949 
    950  await QuickSuggest.clearDismissedSuggestions();
    951  MerinoTestUtils.server.reset();
    952  merinoClient().resetSession();
    953 });
    954 
    955 // Tests a Merino suggestion that is a top pick/best match.
    956 add_task(async function bestMatch() {
    957  UrlbarPrefs.set("quicksuggest.online.available", true);
    958  UrlbarPrefs.set("quicksuggest.online.enabled", true);
    959 
    960  // Set up a suggestion with `is_top_pick` and the "top_picks" provider so that
    961  // UrlbarProviderQuickSuggest will make a default result for it.
    962  let provider = "top_picks";
    963  MerinoTestUtils.server.response.body.suggestions = [
    964    {
    965      is_top_pick: true,
    966      provider,
    967      full_keyword: "full_keyword",
    968      title: "title",
    969      url: "url",
    970      icon: null,
    971      score: 1,
    972    },
    973  ];
    974 
    975  let context = createContext(SEARCH_STRING, {
    976    providers: [UrlbarProviderQuickSuggest.name],
    977    isPrivate: false,
    978  });
    979  await check_results({
    980    context,
    981    matches: [
    982      {
    983        isBestMatch: true,
    984        type: UrlbarUtils.RESULT_TYPE.URL,
    985        source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    986        heuristic: false,
    987        payload: {
    988          telemetryType: provider,
    989          title: "full_keyword — title",
    990          url: "url",
    991          icon: null,
    992          isSponsored: false,
    993          isBlockable: true,
    994          isManageable: true,
    995          source: "merino",
    996          provider,
    997        },
    998      },
    999    ],
   1000  });
   1001 
   1002  // This isn't necessary since `check_results()` checks `isBestMatch`, but
   1003  // check it here explicitly for good measure.
   1004  Assert.ok(context.results[0].isBestMatch, "Result is a best match");
   1005 
   1006  MerinoTestUtils.server.reset();
   1007  merinoClient().resetSession();
   1008 });
   1009 
   1010 // Tests a sponsored suggestion that isn't managed by a feature. When the `all`
   1011 // pref is disabled, a result for the suggestion should not be added.
   1012 add_task(async function unmanaged_sponsored_allDisabled() {
   1013  await doUnmanagedTest({
   1014    pref: "suggest.quicksuggest.all",
   1015    suggestion: {
   1016      title: "Sponsored without feature",
   1017      url: "https://example.com/sponsored-without-feature",
   1018      is_sponsored: true,
   1019    },
   1020    shouldBeAdded: false,
   1021  });
   1022 });
   1023 
   1024 // Tests a sponsored suggestion that isn't managed by a feature. When the
   1025 // sponsored pref is disabled, a result for the suggestion should not be added.
   1026 add_task(async function unmanaged_sponsored_sponsoredDisabled() {
   1027  await doUnmanagedTest({
   1028    pref: "suggest.quicksuggest.sponsored",
   1029    suggestion: {
   1030      title: "Sponsored without feature",
   1031      url: "https://example.com/sponsored-without-feature",
   1032      is_sponsored: true,
   1033    },
   1034    shouldBeAdded: false,
   1035  });
   1036 });
   1037 
   1038 // Tests a nonsponsored suggestion that isn't managed by a feature. When the
   1039 // `all` pref is disabled, a result for the suggestion should not be added.
   1040 add_task(async function unmanaged_nonsponsored_allDisabled() {
   1041  await doUnmanagedTest({
   1042    pref: "suggest.quicksuggest.all",
   1043    suggestion: {
   1044      title: "Nonsponsored without feature",
   1045      url: "https://example.com/nonsponsored-without-feature",
   1046      // no is_sponsored
   1047    },
   1048    shouldBeAdded: false,
   1049  });
   1050 });
   1051 
   1052 // Tests a nonsponsored suggestion that isn't managed by a feature. When the
   1053 // `all` pref is enabled and the sponsored pref is disabled, a result for the
   1054 // suggestion should be added.
   1055 add_task(async function unmanaged_nonsponsored_sponsoredDisabled() {
   1056  await doUnmanagedTest({
   1057    pref: "suggest.quicksuggest.sponsored",
   1058    suggestion: {
   1059      title: "Nonsponsored without feature",
   1060      url: "https://example.com/nonsponsored-without-feature",
   1061      // no is_sponsored
   1062    },
   1063    shouldBeAdded: true,
   1064  });
   1065 });
   1066 
   1067 async function doUnmanagedTest({ pref, suggestion, shouldBeAdded }) {
   1068  // The "top_picks" provider is the only supported unmanaged suggestion.
   1069  suggestion.provider = "top_picks";
   1070 
   1071  UrlbarPrefs.set("suggest.quicksuggest.all", true);
   1072  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   1073  await QuickSuggestTestUtils.forceSync();
   1074 
   1075  UrlbarPrefs.set("quicksuggest.online.available", true);
   1076  UrlbarPrefs.set("quicksuggest.online.enabled", true);
   1077  MerinoTestUtils.server.response.body.suggestions = [suggestion];
   1078 
   1079  let expectedResult = {
   1080    type: UrlbarUtils.RESULT_TYPE.URL,
   1081    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
   1082    heuristic: false,
   1083    payload: {
   1084      title: suggestion.title,
   1085      url: suggestion.url,
   1086      provider: suggestion.provider,
   1087      telemetryType: suggestion.provider,
   1088      isSponsored: !!suggestion.is_sponsored,
   1089      source: "merino",
   1090      isBlockable: true,
   1091      isManageable: true,
   1092      shouldShowUrl: true,
   1093    },
   1094  };
   1095 
   1096  // Do an initial search. The `all` pref and sponsored suggestions are both
   1097  // enabled, so the suggestion should be matched.
   1098  info("Doing search 1");
   1099  await check_results({
   1100    context: createContext("test", {
   1101      providers: [UrlbarProviderQuickSuggest.name],
   1102      isPrivate: false,
   1103    }),
   1104    matches: [expectedResult],
   1105  });
   1106 
   1107  // Set the passed-in pref to false and do another search. The suggestion
   1108  // should be matched as expected.
   1109  UrlbarPrefs.set(pref, false);
   1110  await QuickSuggestTestUtils.forceSync();
   1111 
   1112  info("Doing search 2");
   1113  await check_results({
   1114    context: createContext("test", {
   1115      providers: [UrlbarProviderQuickSuggest.name],
   1116      isPrivate: false,
   1117    }),
   1118    matches: shouldBeAdded ? [expectedResult] : [],
   1119  });
   1120 
   1121  // Flip the pref back to true and do a third search.
   1122  UrlbarPrefs.set(pref, true);
   1123  await QuickSuggestTestUtils.forceSync();
   1124 
   1125  info("Doing search 3");
   1126  let context = createContext("test", {
   1127    providers: [UrlbarProviderQuickSuggest.name],
   1128    isPrivate: false,
   1129  });
   1130  await check_results({
   1131    context,
   1132    matches: [expectedResult],
   1133  });
   1134 
   1135  // Trigger the dismiss command on the result.
   1136  let dismissalPromise = TestUtils.topicObserved(
   1137    "quicksuggest-dismissals-changed"
   1138  );
   1139  triggerCommand({
   1140    feature: UrlbarProvidersManager.getProvider(
   1141      UrlbarProviderQuickSuggest.name
   1142    ),
   1143    command: "dismiss",
   1144    result: context.results[0],
   1145    expectedCountsByCall: {
   1146      removeResult: 1,
   1147    },
   1148  });
   1149  await dismissalPromise;
   1150 
   1151  Assert.ok(
   1152    await QuickSuggest.isResultDismissed(context.results[0]),
   1153    "The result should be dismissed"
   1154  );
   1155 
   1156  await QuickSuggest.clearDismissedSuggestions();
   1157  MerinoTestUtils.server.reset();
   1158  merinoClient().resetSession();
   1159 }
   1160 
   1161 // An unmanaged suggestion with an unrecognized Merino provider (i.e., not
   1162 // "top_picks") should not be added.
   1163 add_task(async function unmanaged_unrecognized() {
   1164  UrlbarPrefs.set("suggest.quicksuggest.all", true);
   1165  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   1166  await QuickSuggestTestUtils.forceSync();
   1167 
   1168  UrlbarPrefs.set("quicksuggest.online.available", true);
   1169  UrlbarPrefs.set("quicksuggest.online.enabled", true);
   1170  MerinoTestUtils.server.response.body.suggestions = [
   1171    {
   1172      title: "Some unrecognized suggestion",
   1173      url: "https://example.com/unmanaged_unrecognized",
   1174      provider: "unmanaged-unrecognized-provider",
   1175    },
   1176  ];
   1177 
   1178  await check_results({
   1179    context: createContext("test", {
   1180      providers: [UrlbarProviderQuickSuggest.name],
   1181      isPrivate: false,
   1182    }),
   1183    matches: [],
   1184  });
   1185 });
   1186 
   1187 function merinoClient() {
   1188  return QuickSuggest.getFeature("SuggestBackendMerino")?.client;
   1189 }
   1190 
   1191 async function resetRemoteSettingsData() {
   1192  await QuickSuggestTestUtils.setRemoteSettingsRecords([
   1193    {
   1194      collection: QuickSuggestTestUtils.RS_COLLECTION.AMP,
   1195      type: QuickSuggestTestUtils.RS_TYPE.AMP,
   1196      attachment: REMOTE_SETTINGS_RESULTS,
   1197    },
   1198  ]);
   1199 }