tor-browser

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

test_quicksuggest_dynamicSuggestions.js (16720B)


      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 dynamic Rust suggestions.
      6 
      7 const REMOTE_SETTINGS_RECORDS = [
      8  // nonsponsored, no `bypassSuggestAll`
      9  {
     10    type: "dynamic-suggestions",
     11    suggestion_type: "aaa",
     12    score: 0.9,
     13    attachment: [
     14      {
     15        keywords: ["aaa keyword", "aaa bbb keyword", "wikipedia"],
     16        data: {
     17          result: {
     18            payload: {
     19              title: "aaa title",
     20              url: "https://example.com/aaa",
     21            },
     22          },
     23        },
     24      },
     25    ],
     26  },
     27 
     28  // sponsored, no `bypassSuggestAll`
     29  {
     30    type: "dynamic-suggestions",
     31    suggestion_type: "bbb",
     32    score: 0.1,
     33    attachment: [
     34      {
     35        keywords: ["bbb keyword", "aaa bbb keyword", "wikipedia"],
     36        dismissal_key: "bbb-dismissal-key",
     37        data: {
     38          result: {
     39            isBestMatch: true,
     40            suggestedIndex: 1,
     41            isSuggestedIndexRelativeToGroup: false,
     42            isRichSuggestion: true,
     43            payload: {
     44              title: "bbb title",
     45              url: "https://example.com/bbb",
     46              isSponsored: true,
     47              telemetryType: "bbb_telemetry_type",
     48            },
     49          },
     50        },
     51      },
     52    ],
     53  },
     54 
     55  // nonsponsored, `bypassSuggestAll: true`
     56  {
     57    type: "dynamic-suggestions",
     58    suggestion_type: "ccc",
     59    score: 0.9,
     60    attachment: [
     61      {
     62        keywords: ["ccc keyword", "ccc ddd keyword"],
     63        data: {
     64          result: {
     65            bypassSuggestAll: true,
     66            payload: {
     67              title: "ccc title",
     68              url: "https://example.com/ccc",
     69            },
     70          },
     71        },
     72      },
     73    ],
     74  },
     75 
     76  // sponsored, `bypassSuggestAll: true`
     77  {
     78    type: "dynamic-suggestions",
     79    suggestion_type: "ddd",
     80    score: 0.9,
     81    attachment: [
     82      {
     83        keywords: ["ddd keyword", "ccc ddd keyword"],
     84        data: {
     85          result: {
     86            bypassSuggestAll: true,
     87            payload: {
     88              title: "ddd title",
     89              url: "https://example.com/ddd",
     90              isSponsored: true,
     91            },
     92          },
     93        },
     94      },
     95    ],
     96  },
     97 
     98  {
     99    type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA,
    100    attachment: [QuickSuggestTestUtils.wikipediaRemoteSettings()],
    101  },
    102 ];
    103 
    104 const EXPECTED_AAA_RESULT = makeExpectedResult({
    105  title: "aaa title",
    106  url: "https://example.com/aaa",
    107  telemetryType: "aaa",
    108  suggestionType: "aaa",
    109 });
    110 
    111 const EXPECTED_BBB_RESULT = makeExpectedResult({
    112  title: "bbb title",
    113  url: "https://example.com/bbb",
    114  isSponsored: true,
    115  telemetryType: "bbb_telemetry_type",
    116  suggestionType: "bbb",
    117  isBestMatch: true,
    118  suggestedIndex: 1,
    119  isSuggestedIndexRelativeToGroup: false,
    120  isRichSuggestion: true,
    121 });
    122 
    123 const EXPECTED_CCC_RESULT = makeExpectedResult({
    124  title: "ccc title",
    125  url: "https://example.com/ccc",
    126  telemetryType: "ccc",
    127  suggestionType: "ccc",
    128 });
    129 
    130 const EXPECTED_DDD_RESULT = makeExpectedResult({
    131  title: "ddd title",
    132  url: "https://example.com/ddd",
    133  isSponsored: true,
    134  telemetryType: "ddd",
    135  suggestionType: "ddd",
    136 });
    137 
    138 add_setup(async function () {
    139  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    140    remoteSettingsRecords: REMOTE_SETTINGS_RECORDS,
    141    prefs: [
    142      ["quicksuggest.dynamicSuggestionTypes", "aaa,bbb,ccc,ddd"],
    143      ["suggest.quicksuggest.all", true],
    144      ["suggest.quicksuggest.sponsored", true],
    145      ["quicksuggest.ampTopPickCharThreshold", 0],
    146    ],
    147  });
    148 });
    149 
    150 // When a dynamic suggestion doesn't include `telemetryType`, its
    151 // `suggestionType` should be used as the telemetry type.
    152 add_task(async function telemetryType_default() {
    153  Assert.equal(
    154    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
    155      suggestionType: "abcdefg",
    156    }),
    157    "abcdefg",
    158    "Telemetry type should be correct when using default"
    159  );
    160 });
    161 
    162 // When a dynamic suggestion includes `telemetryType`, it should be used as the
    163 // telemetry type.
    164 add_task(async function telemetryType_override() {
    165  Assert.equal(
    166    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
    167      suggestionType: "abcdefg",
    168      data: {
    169        result: {
    170          payload: {
    171            telemetryType: "telemetry_type_override",
    172          },
    173        },
    174      },
    175    }),
    176    "telemetry_type_override",
    177    "Telemetry type should be correct when overridden"
    178  );
    179 });
    180 
    181 add_task(async function basic() {
    182  let queries = [
    183    {
    184      query: "no match",
    185      expected: [],
    186    },
    187    {
    188      query: "aaa keyword",
    189      expected: [EXPECTED_AAA_RESULT],
    190    },
    191    {
    192      query: "bbb keyword",
    193      expected: [EXPECTED_BBB_RESULT],
    194    },
    195    {
    196      query: "aaa bbb keyword",
    197      // The "aaa" suggestion has a higher score than "bbb".
    198      expected: [EXPECTED_AAA_RESULT],
    199    },
    200    {
    201      query: "ccc keyword",
    202      expected: [EXPECTED_CCC_RESULT],
    203    },
    204    {
    205      query: "ddd keyword",
    206      expected: [EXPECTED_DDD_RESULT],
    207    },
    208    {
    209      query: "ccc ddd keyword",
    210      // The "ccc" suggestion has a higher score than "ddd".
    211      expected: [EXPECTED_CCC_RESULT],
    212    },
    213  ];
    214 
    215  await doQueries(queries);
    216 });
    217 
    218 // When only one dynamic suggestion type is enabled, only its result should be
    219 // returned. This task assumes multiples types were added to remote settings in
    220 // the setup task.
    221 add_task(async function oneSuggestionType() {
    222  await withSuggestionTypesPref("bbb", async () => {
    223    await doQueries([
    224      {
    225        query: "aaa keyword",
    226        expected: [],
    227      },
    228      {
    229        query: "bbb keyword",
    230        expected: [EXPECTED_BBB_RESULT],
    231      },
    232      {
    233        query: "aaa bbb keyword",
    234        expected: [EXPECTED_BBB_RESULT],
    235      },
    236      {
    237        query: "doesn't match",
    238        expected: [],
    239      },
    240    ]);
    241  });
    242 });
    243 
    244 // When no dynamic suggestion types are enabled, no results should be added.
    245 add_task(async function disabled() {
    246  await withSuggestionTypesPref("", async () => {
    247    await doQueries(
    248      ["aaa keyword", "bbb keyword", "aaa bbb keyword"].map(query => ({
    249        query,
    250        expected: [],
    251      }))
    252    );
    253  });
    254 });
    255 
    256 // Dynamic suggestions shouldn't be added when `all` is disabled unless they
    257 // define `bypassSuggestAll`.
    258 add_task(async function allDisabled() {
    259  UrlbarPrefs.set("suggest.quicksuggest.all", false);
    260 
    261  // The sponsored pref shouldn't matter.
    262  for (let sponsoredEnabled of [true, false]) {
    263    UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsoredEnabled);
    264 
    265    await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => {
    266      await doQueries([
    267        {
    268          query: "aaa keyword",
    269          expected: [],
    270        },
    271        {
    272          query: "bbb keyword",
    273          expected: [],
    274        },
    275        {
    276          query: "aaa bbb keyword",
    277          expected: [],
    278        },
    279 
    280        {
    281          query: "ccc keyword",
    282          expected: [EXPECTED_CCC_RESULT],
    283        },
    284        {
    285          query: "ddd keyword",
    286          expected: [EXPECTED_DDD_RESULT],
    287        },
    288        {
    289          query: "ccc ddd keyword",
    290          // The "ccc" suggestion has a higher score than "ddd".
    291          expected: [EXPECTED_CCC_RESULT],
    292        },
    293      ]);
    294    });
    295  }
    296 
    297  UrlbarPrefs.set("suggest.quicksuggest.all", true);
    298  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
    299  await QuickSuggestTestUtils.forceSync();
    300 });
    301 
    302 // Dynamic suggestions that are sponsored shouldn't be added when sponsored
    303 // suggestions are disabled unless they define `bypassSuggestAll`.
    304 add_task(async function sponsoredDisabled() {
    305  UrlbarPrefs.set("suggest.quicksuggest.all", true);
    306  UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
    307 
    308  await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => {
    309    await doQueries([
    310      {
    311        query: "aaa keyword",
    312        expected: [EXPECTED_AAA_RESULT],
    313      },
    314      {
    315        query: "bbb keyword",
    316        expected: [],
    317      },
    318      {
    319        query: "aaa bbb keyword",
    320        expected: [EXPECTED_AAA_RESULT],
    321      },
    322 
    323      {
    324        query: "ccc keyword",
    325        expected: [EXPECTED_CCC_RESULT],
    326      },
    327      {
    328        query: "ddd keyword",
    329        expected: [EXPECTED_DDD_RESULT],
    330      },
    331      {
    332        query: "ccc ddd keyword",
    333        // The "ccc" suggestion has a higher score than "ddd".
    334        expected: [EXPECTED_CCC_RESULT],
    335      },
    336    ]);
    337  });
    338 
    339  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
    340  await QuickSuggestTestUtils.forceSync();
    341 });
    342 
    343 // Tests the `quickSuggestDynamicSuggestionTypes` Nimbus variable.
    344 add_task(async function nimbus() {
    345  // Clear `dynamicSuggestionTypes` to make sure the value comes from the Nimbus
    346  // variable and not the pref.
    347  await withSuggestionTypesPref("", async () => {
    348    let cleanup = await UrlbarTestUtils.initNimbusFeature({
    349      quickSuggestDynamicSuggestionTypes: "aaa,bbb",
    350    });
    351    await QuickSuggestTestUtils.forceSync();
    352    await doQueries([
    353      {
    354        query: "aaa keyword",
    355        expected: [EXPECTED_AAA_RESULT],
    356      },
    357      {
    358        query: "bbb keyword",
    359        expected: [EXPECTED_BBB_RESULT],
    360      },
    361      {
    362        query: "aaa bbb keyword",
    363        // The "aaa" suggestion has a higher score than "bbb".
    364        expected: [EXPECTED_AAA_RESULT],
    365      },
    366      {
    367        query: "doesn't match",
    368        expected: [],
    369      },
    370    ]);
    371 
    372    await cleanup();
    373  });
    374 });
    375 
    376 // Tests dismissals. Note that dynamic suggestions must define a `dismissal_key`
    377 // in order to be dismissable.
    378 add_task(async function dismissal() {
    379  // Do a search and get the actual result that's returned.
    380  let context = createContext("bbb keyword", {
    381    providers: [UrlbarProviderQuickSuggest.name],
    382    isPrivate: false,
    383  });
    384  await check_results({
    385    context,
    386    matches: [EXPECTED_BBB_RESULT],
    387  });
    388 
    389  let result = context.results[0];
    390  let { suggestionObject } = result.payload;
    391  let { dismissalKey } = suggestionObject;
    392  Assert.equal(
    393    dismissalKey,
    394    "bbb-dismissal-key",
    395    "The suggestion should have the expected dismissal key"
    396  );
    397 
    398  // It shouldn't be dismissed yet.
    399  Assert.ok(
    400    !(await QuickSuggest.isResultDismissed(result)),
    401    "The result should not be dismissed yet"
    402  );
    403  Assert.ok(
    404    !(await QuickSuggest.rustBackend.isRustSuggestionDismissed(
    405      suggestionObject
    406    )),
    407    "The suggestion should not be dismissed yet"
    408  );
    409  Assert.ok(
    410    !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)),
    411    "The dismissal key should not be registered yet"
    412  );
    413 
    414  // Dismiss it. It should be dismissed by its dismissal key.
    415  await QuickSuggest.dismissResult(result);
    416 
    417  Assert.ok(
    418    await QuickSuggest.isResultDismissed(result),
    419    "The result should be dismissed"
    420  );
    421  Assert.ok(
    422    await QuickSuggest.rustBackend.isRustSuggestionDismissed(suggestionObject),
    423    "The suggestion should be dismissed"
    424  );
    425 
    426  await check_results({
    427    context: createContext("bbb keyword", {
    428      providers: [UrlbarProviderQuickSuggest.name],
    429      isPrivate: false,
    430    }),
    431    matches: [],
    432  });
    433 
    434  // Clear dismissals and check again.
    435  await QuickSuggest.clearDismissedSuggestions();
    436 
    437  await check_results({
    438    context: createContext("bbb keyword", {
    439      providers: [UrlbarProviderQuickSuggest.name],
    440      isPrivate: false,
    441    }),
    442    matches: [EXPECTED_BBB_RESULT],
    443  });
    444 
    445  Assert.ok(
    446    !(await QuickSuggest.isResultDismissed(result)),
    447    "The result should not be dismissed after clearing dismissals"
    448  );
    449  Assert.ok(
    450    !(await QuickSuggest.rustBackend.isRustSuggestionDismissed(
    451      suggestionObject
    452    )),
    453    "The suggestion should not be dismissed after clearing dismissals"
    454  );
    455  Assert.ok(
    456    !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)),
    457    "The dismissal key should not be registered after clearing dismissals"
    458  );
    459 });
    460 
    461 // Tests whether the prefs DynamicSuggestion handles clears.
    462 add_task(async function clearDismissedSuggestions() {
    463  let feature = QuickSuggest.getFeature("DynamicSuggestions");
    464  let sandbox = sinon.createSandbox();
    465  sinon
    466    .stub(feature, "primaryUserControlledPreferences")
    467    .get(() => ["suggest.realtimeOptIn", "autoFill", "closeOtherPanelsOnOpen"]);
    468 
    469  UrlbarPrefs.set("suggest.realtimeOptIn", false);
    470  UrlbarPrefs.set("autoFill", false);
    471  UrlbarPrefs.set("closeOtherPanelsOnOpen", false);
    472 
    473  Assert.ok(await QuickSuggest.canClearDismissedSuggestions());
    474  await QuickSuggest.clearDismissedSuggestions();
    475 
    476  Assert.ok(UrlbarPrefs.get("suggest.realtimeOptIn"));
    477  Assert.ok(UrlbarPrefs.get("autoFill"));
    478  Assert.ok(UrlbarPrefs.get("closeOtherPanelsOnOpen"));
    479 
    480  sandbox.restore();
    481 });
    482 
    483 // Tests some suggestions with bad data that desktop ignores.
    484 add_task(async function badSuggestions() {
    485  await QuickSuggestTestUtils.setRemoteSettingsRecords([
    486    {
    487      type: "dynamic-suggestions",
    488      suggestion_type: "bad",
    489      attachment: [
    490        // Include a good suggestion so we can verify this record was actually
    491        // ingested. Change the keyword so we don't confuse ourselves by
    492        // searching for an "aaa" keyword and getting a urlbar result whose
    493        // telemetry type and dynamic suggestion type is "bad".
    494        {
    495          ...REMOTE_SETTINGS_RECORDS[0].attachment[0],
    496          keywords: ["good actually"],
    497        },
    498        // `data` is missing -- Rust actually allows this since `data` is
    499        // defined as `Option<serde_json::Value>`, but desktop doesn't.
    500        {
    501          keywords: ["bad"],
    502        },
    503        // `data` isn't an object
    504        {
    505          data: 123,
    506          keywords: ["bad"],
    507        },
    508        // `data.result` is missing
    509        {
    510          data: {},
    511          keywords: ["bad"],
    512        },
    513        // `data.result` isn't an object
    514        {
    515          data: {
    516            result: 123,
    517          },
    518          keywords: ["bad"],
    519        },
    520        // `data.result.payload` isn't an object
    521        {
    522          data: {
    523            result: {
    524              payload: 123,
    525            },
    526          },
    527          keywords: ["bad"],
    528        },
    529      ],
    530    },
    531  ]);
    532 
    533  await withSuggestionTypesPref("bad", async () => {
    534    // Verify the good suggestion was ingested.
    535    await check_results({
    536      context: createContext("good actually", {
    537        providers: [UrlbarProviderQuickSuggest.name],
    538        isPrivate: false,
    539      }),
    540      matches: [
    541        {
    542          ...EXPECTED_AAA_RESULT,
    543          payload: {
    544            ...EXPECTED_AAA_RESULT.payload,
    545            telemetryType: "bad",
    546            suggestionType: "bad",
    547          },
    548        },
    549      ],
    550    });
    551 
    552    // No "bad" suggestions should be matched.
    553    await check_results({
    554      context: createContext("bad", {
    555        providers: [UrlbarProviderQuickSuggest.name],
    556        isPrivate: false,
    557      }),
    558      matches: [],
    559    });
    560  });
    561 
    562  // Clean up.
    563  await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS);
    564 });
    565 
    566 async function doQueries(queries) {
    567  for (let { query, expected } of queries) {
    568    info(
    569      "Doing query: " +
    570        JSON.stringify({
    571          query,
    572          expected,
    573        })
    574    );
    575 
    576    await check_results({
    577      context: createContext(query, {
    578        providers: [UrlbarProviderQuickSuggest.name],
    579        isPrivate: false,
    580      }),
    581      matches: expected,
    582    });
    583  }
    584 }
    585 
    586 async function withSuggestionTypesPref(prefValue, callback) {
    587  // Use `Services` to get the original pref value since `UrlbarPrefs` will
    588  // parse the string value into a `Set`.
    589  let originalPrefValue = Services.prefs.getCharPref(
    590    "browser.urlbar.quicksuggest.dynamicSuggestionTypes"
    591  );
    592 
    593  // Changing the pref (or Nimbus variable) to a different value will trigger
    594  // ingest, so force sync afterward (or at least wait for ingest to finish).
    595  UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", prefValue);
    596  await QuickSuggestTestUtils.forceSync();
    597 
    598  await callback();
    599 
    600  UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", originalPrefValue);
    601  await QuickSuggestTestUtils.forceSync();
    602 }
    603 
    604 function makeExpectedResult({
    605  title,
    606  url,
    607  telemetryType,
    608  suggestionType,
    609  isSponsored = false,
    610  isBestMatch = false,
    611  suggestedIndex = -1,
    612  isSuggestedIndexRelativeToGroup = true,
    613  isRichSuggestion = undefined,
    614 }) {
    615  return {
    616    type: UrlbarUtils.RESULT_TYPE.URL,
    617    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    618    heuristic: false,
    619    isBestMatch,
    620    suggestedIndex,
    621    isRichSuggestion,
    622    isSuggestedIndexRelativeToGroup,
    623    payload: {
    624      title,
    625      url,
    626      isSponsored,
    627      telemetryType,
    628      suggestionType,
    629      source: "rust",
    630      provider: "Dynamic",
    631      isManageable: true,
    632      helpUrl: QuickSuggest.HELP_URL,
    633    },
    634  };
    635 }