tor-browser

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

test_quicksuggest_yelp_ml.js (16076B)


      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 for Yelp suggestions served by the Suggest ML backend.
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  MLSuggest: "moz-src:///browser/components/urlbar/private/MLSuggest.sys.mjs",
     11 });
     12 
     13 const REMOTE_SETTINGS_RECORDS = [
     14  {
     15    type: "yelp-suggestions",
     16    attachment: {
     17      // The "coffee" subject is important: It's how `YelpSuggestions` looks up
     18      // the Yelp icon and score right now.
     19      subjects: ["coffee"],
     20      preModifiers: [],
     21      postModifiers: [],
     22      locationSigns: ["in", "nearby"],
     23      yelpModifiers: [],
     24      icon: "1234",
     25      score: 0.5,
     26    },
     27  },
     28  ...QuickSuggestTestUtils.geonamesRecords(),
     29  ...QuickSuggestTestUtils.geonamesAlternatesRecords(),
     30 ];
     31 
     32 const WATERLOO_RESULT = {
     33  url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Waterloo%2C+IA",
     34  title: "burgers in Waterloo, IA",
     35 };
     36 
     37 const YOKOHAMA_RESULT = {
     38  url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Yokohama%2C+Kanagawa",
     39  title: "burgers in Yokohama, Kanagawa",
     40 };
     41 
     42 let gSandbox;
     43 let gMakeSuggestionsStub;
     44 
     45 add_setup(async function init() {
     46  // Stub `MLSuggest`.
     47  gSandbox = sinon.createSandbox();
     48  gSandbox.stub(MLSuggest, "initialize");
     49  gSandbox.stub(MLSuggest, "shutdown");
     50  gMakeSuggestionsStub = gSandbox.stub(MLSuggest, "makeSuggestions");
     51 
     52  // Set up Rust Yelp suggestions that can be matched on the keyword "coffee".
     53  await QuickSuggestTestUtils.ensureQuickSuggestInit({
     54    prefs: [
     55      ["browser.ml.enable", true],
     56      ["quicksuggest.mlEnabled", true],
     57      ["suggest.quicksuggest.all", true],
     58      ["suggest.quicksuggest.sponsored", true],
     59      ["yelp.mlEnabled", true],
     60      ["yelp.serviceResultDistinction", false],
     61    ],
     62    remoteSettingsRecords: REMOTE_SETTINGS_RECORDS,
     63  });
     64 
     65  await MerinoTestUtils.initGeolocation();
     66 });
     67 
     68 // Yelp ML should be disabled when the relevant prefs are disabled.
     69 add_task(async function yelpDisabled() {
     70  gMakeSuggestionsStub.returns({ intent: "yelp_intent", subject: "burgers" });
     71  let expectedResult = makeExpectedResult(YOKOHAMA_RESULT);
     72 
     73  let tests = [
     74    // These disable the Yelp feature itself, including Rust suggestions.
     75    "suggest.quicksuggest.sponsored",
     76    "suggest.yelp",
     77    "yelp.featureGate",
     78 
     79    // These disable Yelp ML suggestions but leave the Yelp feature enabled.
     80    // This test doesn't add any Yelp data to remote settings, but if it did,
     81    // Yelp Rust suggestions would still be triggered.
     82    "yelp.mlEnabled",
     83    "browser.ml.enable",
     84    "quicksuggest.mlEnabled",
     85 
     86    // pref combinations
     87    {
     88      prefs: {
     89        "suggest.quicksuggest.sponsored": true,
     90        "suggest.quicksuggest.all": true,
     91      },
     92      expected: true,
     93    },
     94    {
     95      prefs: {
     96        "suggest.quicksuggest.sponsored": true,
     97        "suggest.quicksuggest.all": false,
     98      },
     99      expected: false,
    100    },
    101    {
    102      prefs: {
    103        "suggest.quicksuggest.sponsored": false,
    104        "suggest.quicksuggest.all": true,
    105      },
    106      expected: false,
    107    },
    108    {
    109      prefs: {
    110        "suggest.quicksuggest.sponsored": false,
    111        "suggest.quicksuggest.all": false,
    112      },
    113      expected: false,
    114    },
    115  ];
    116  for (let test of tests) {
    117    info("Starting subtest: " + JSON.stringify(test));
    118 
    119    let prefs;
    120    let expected;
    121    if (typeof test == "string") {
    122      // A string value is a pref name, and we'll set it to false and expect no
    123      // suggestions.
    124      prefs = { [test]: false };
    125      expected = false;
    126    } else {
    127      ({ prefs, expected } = test);
    128    }
    129 
    130    // Before setting the prefs, first make sure the suggestion is added.
    131    info("Doing search 1");
    132    await check_results({
    133      context: createContext("burgers", {
    134        providers: [UrlbarProviderQuickSuggest.name],
    135        isPrivate: false,
    136      }),
    137      matches: [expectedResult],
    138    });
    139 
    140    // Also get the original pref values.
    141    let originalPrefs = Object.fromEntries(
    142      Object.keys(prefs).map(name => [name, UrlbarPrefs.get(name)])
    143    );
    144 
    145    // Now set the prefs.
    146    info("Setting prefs and doing search 2");
    147    for (let [name, value] of Object.entries(prefs)) {
    148      UrlbarPrefs.set(name, value);
    149    }
    150    await check_results({
    151      context: createContext("burgers", {
    152        providers: [UrlbarProviderQuickSuggest.name],
    153        isPrivate: false,
    154      }),
    155      matches: expected ? [expectedResult] : [],
    156    });
    157 
    158    // Revert.
    159    for (let [name, value] of Object.entries(originalPrefs)) {
    160      UrlbarPrefs.set(name, value);
    161    }
    162    await QuickSuggestTestUtils.forceSync();
    163 
    164    // Make sure Yelp is added again.
    165    info("Doing search 3 after reverting the prefs");
    166    await check_results({
    167      context: createContext("burgers", {
    168        providers: [UrlbarProviderQuickSuggest.name],
    169        isPrivate: false,
    170      }),
    171      matches: [expectedResult],
    172    });
    173  }
    174 });
    175 
    176 // Runs through a variety of mock intents.
    177 add_task(async function intents() {
    178  let tests = [
    179    {
    180      desc: "subject with no location",
    181      ml: { intent: "yelp_intent", subject: "burgers" },
    182      expected: YOKOHAMA_RESULT,
    183    },
    184    {
    185      desc: "subject with null city and null state",
    186      ml: {
    187        intent: "yelp_intent",
    188        subject: "burgers",
    189        location: { city: null, state: null },
    190      },
    191      expected: YOKOHAMA_RESULT,
    192    },
    193    {
    194      desc: "subject with city",
    195      ml: {
    196        intent: "yelp_intent",
    197        subject: "burgers",
    198        location: { city: "Waterloo", state: null },
    199      },
    200      expected: WATERLOO_RESULT,
    201    },
    202    {
    203      desc: "subject with state abbreviation",
    204      ml: {
    205        intent: "yelp_intent",
    206        subject: "burgers",
    207        location: { city: null, state: "IA" },
    208      },
    209      expected: null,
    210    },
    211    {
    212      desc: "subject with state name",
    213      ml: {
    214        intent: "yelp_intent",
    215        subject: "burgers",
    216        location: { city: null, state: "Iowa" },
    217      },
    218      expected: {
    219        url: "https://www.yelp.com/search?find_desc=burgers&find_loc=Iowa",
    220        title: "burgers in Iowa",
    221      },
    222    },
    223    {
    224      desc: "subject with city and state",
    225      ml: {
    226        intent: "yelp_intent",
    227        subject: "burgers",
    228        location: { city: "Waterloo", state: "IA" },
    229      },
    230      expected: WATERLOO_RESULT,
    231    },
    232    {
    233      desc: "no subject with no location",
    234      ml: {
    235        intent: "yelp_intent",
    236        subject: "",
    237      },
    238      expected: null,
    239    },
    240    {
    241      desc: "no subject with null city and null state",
    242      ml: {
    243        intent: "yelp_intent",
    244        subject: "",
    245        location: { city: null, state: null },
    246      },
    247      expected: null,
    248    },
    249    {
    250      desc: "no subject with city",
    251      ml: {
    252        intent: "yelp_intent",
    253        subject: "",
    254        location: { city: "Waterloo", state: null },
    255      },
    256      expected: null,
    257    },
    258    {
    259      desc: "no subject with state",
    260      ml: {
    261        intent: "yelp_intent",
    262        subject: "",
    263        location: { city: null, state: "IA" },
    264      },
    265      expected: null,
    266    },
    267    {
    268      desc: "no subject with city and state",
    269      ml: {
    270        intent: "yelp_intent",
    271        subject: "",
    272        location: { city: "Waterloo", state: "IA" },
    273      },
    274      expected: null,
    275    },
    276    {
    277      desc: "unrecognized intent",
    278      ml: { intent: "unrecognized_intent" },
    279      expected: null,
    280    },
    281    {
    282      desc: "only Rust returns a suggestion",
    283      query: "coffee",
    284      ml: null,
    285      expected: {
    286        source: "rust",
    287        provider: "Yelp",
    288        url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Yokohama%2C+Kanagawa",
    289        title: "coffee in Yokohama, Kanagawa",
    290      },
    291    },
    292 
    293    {
    294      desc: "both ML and Rust return a suggestion",
    295      query: "coffee",
    296      ml: {
    297        intent: "yelp_intent",
    298        subject: "coffee",
    299        location: { city: "Waterloo", state: null },
    300      },
    301      expected: {
    302        url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Waterloo%2C+IA",
    303        title: "coffee in Waterloo, IA",
    304      },
    305    },
    306  ];
    307 
    308  for (let { desc, ml, expected, query = "test" } of tests) {
    309    info("Doing subtest: " + JSON.stringify({ desc, query, ml }));
    310 
    311    // Do a query with ML enabled. If the expected result is from Rust, the
    312    // query shouldn't return any results because *only* ML results are returned
    313    // when ML is enabled.
    314    gMakeSuggestionsStub.returns(ml);
    315    await check_results({
    316      context: createContext(query, {
    317        providers: [UrlbarProviderQuickSuggest.name],
    318        isPrivate: false,
    319      }),
    320      matches:
    321        expected && expected.source != "rust"
    322          ? [makeExpectedResult(expected)]
    323          : [],
    324    });
    325 
    326    // If the expected result is from Rust, disable ML and query again to make
    327    // sure it matches.
    328    if (expected?.source == "rust") {
    329      UrlbarPrefs.set("yelp.mlEnabled", false);
    330      await check_results({
    331        context: createContext(query, {
    332          providers: [UrlbarProviderQuickSuggest.name],
    333          isPrivate: false,
    334        }),
    335        matches: [makeExpectedResult(expected)],
    336      });
    337      UrlbarPrefs.set("yelp.mlEnabled", true);
    338    }
    339  }
    340 });
    341 
    342 // The search string passed in to `MLSuggest.makeSuggestions()` should be
    343 // trimmed and lowercased.
    344 add_task(async function searchString() {
    345  let searchStrings = [];
    346  gMakeSuggestionsStub.callsFake(str => {
    347    searchStrings.push(str);
    348    return {
    349      intent: "yelp_intent",
    350      subject: "burgers",
    351    };
    352  });
    353 
    354  await check_results({
    355    context: createContext("  AaA   bBb     CcC   ", {
    356      providers: [UrlbarProviderQuickSuggest.name],
    357      isPrivate: false,
    358    }),
    359    matches: [makeExpectedResult(YOKOHAMA_RESULT)],
    360  });
    361 
    362  Assert.deepEqual(
    363    searchStrings,
    364    ["aaa   bbb     ccc"],
    365    "The search string passed in to MLSuggest should be trimmed and lowercased"
    366  );
    367 });
    368 
    369 // The metadata cache should be populated from the "coffee" Rust suggestion when
    370 // it's present in remote settings.
    371 add_task(async function cache_fromRust() {
    372  await doCacheTest({
    373    expectedScore: REMOTE_SETTINGS_RECORDS[0].attachment.score,
    374    expectedRust: {
    375      source: "rust",
    376      provider: "Yelp",
    377      url: "https://www.yelp.com/search?find_desc=coffee&find_loc=Yokohama%2C+Kanagawa",
    378      title: "coffee in Yokohama, Kanagawa",
    379    },
    380  });
    381 });
    382 
    383 // The metadata cache should be populated with default values when the "coffee"
    384 // Rust suggestion is not present in remote settings.
    385 add_task(async function cache_defaultValues() {
    386  await QuickSuggestTestUtils.setRemoteSettingsRecords([
    387    ...QuickSuggestTestUtils.geonamesRecords(),
    388    ...QuickSuggestTestUtils.geonamesAlternatesRecords(),
    389  ]);
    390  await doCacheTest({
    391    // This value is hardcoded in `YelpSuggestions` as the default.
    392    expectedScore: 0.25,
    393    expectedRust: null,
    394  });
    395  await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS);
    396 });
    397 
    398 async function doCacheTest({ expectedScore, expectedRust }) {
    399  // Do a search with ML disabled to verify the Rust suggestion is matched as
    400  // expected.
    401  info("Doing search with ML disabled");
    402  UrlbarPrefs.set("yelp.mlEnabled", false);
    403  await check_results({
    404    context: createContext("coffee", {
    405      providers: [UrlbarProviderQuickSuggest.name],
    406      isPrivate: false,
    407    }),
    408    matches: expectedRust ? [makeExpectedResult(expectedRust)] : [],
    409  });
    410  UrlbarPrefs.set("yelp.mlEnabled", true);
    411 
    412  gMakeSuggestionsStub.returns({
    413    intent: "yelp_intent",
    414    subject: "burgers",
    415    location: { city: "Waterloo", state: null },
    416  });
    417 
    418  // Stub `YelpSuggestions.makeResult()` so we can get the suggestion object
    419  // passed into it.
    420  let passedSuggestion;
    421  let feature = QuickSuggest.getFeature("YelpSuggestions");
    422  let stub = gSandbox
    423    .stub(feature, "makeResult")
    424    .callsFake((queryContext, suggestion, searchString) => {
    425      passedSuggestion = suggestion;
    426      return stub.wrappedMethod.call(
    427        feature,
    428        queryContext,
    429        suggestion,
    430        searchString
    431      );
    432    });
    433 
    434  // Do a search with ML enabled.
    435  info("Doing search with ML enabled");
    436  feature._test_invalidateMetadataCache();
    437  await check_results({
    438    context: createContext("test", {
    439      providers: [UrlbarProviderQuickSuggest.name],
    440      isPrivate: false,
    441    }),
    442    matches: [makeExpectedResult(WATERLOO_RESULT)],
    443  });
    444 
    445  stub.restore();
    446 
    447  // The score of the ML suggestion passed into `makeResult()` should have been
    448  // taken from the metadata cache.
    449  Assert.ok(
    450    passedSuggestion,
    451    "makeResult should have been called and passed the suggestion"
    452  );
    453  Assert.equal(
    454    passedSuggestion.score,
    455    expectedScore,
    456    "The suggestion should have borrowed its score from the Rust 'coffee' suggestion"
    457  );
    458 }
    459 
    460 // Tests the "Not relevant" command: a dismissed suggestion shouldn't be added.
    461 add_task(async function notRelevant() {
    462  let burgersIntent = { intent: "yelp_intent", subject: "burgers" };
    463  let waterlooIntent = {
    464    intent: "yelp_intent",
    465    subject: "burgers",
    466    location: { city: "Waterloo" },
    467  };
    468 
    469  gMakeSuggestionsStub.returns(burgersIntent);
    470  let result = makeExpectedResult(YOKOHAMA_RESULT);
    471 
    472  info("Doing initial search to verify the suggestion is matched");
    473  await check_results({
    474    context: createContext("burgers", {
    475      providers: [UrlbarProviderQuickSuggest.name],
    476      isPrivate: false,
    477    }),
    478    matches: [result],
    479  });
    480 
    481  let dismissalPromise = TestUtils.topicObserved(
    482    "quicksuggest-dismissals-changed"
    483  );
    484  triggerCommand({
    485    result,
    486    command: "not_relevant",
    487    feature: QuickSuggest.getFeature("YelpSuggestions"),
    488    expectedCountsByCall: {
    489      removeResult: 1,
    490    },
    491  });
    492  await dismissalPromise;
    493 
    494  Assert.ok(
    495    await QuickSuggest.isResultDismissed(result),
    496    "The result should be dismissed"
    497  );
    498 
    499  info("Doing search for dismissed suggestion");
    500  await check_results({
    501    context: createContext("burgers", {
    502      providers: [UrlbarProviderQuickSuggest.name],
    503      isPrivate: false,
    504    }),
    505    matches: [],
    506  });
    507 
    508  // Yelp suggestions are dismissed by URL excluding location, so all
    509  // "ramen in <valid location>" results should be dismissed.
    510  gMakeSuggestionsStub.returns(waterlooIntent);
    511  await check_results({
    512    context: createContext("burgers in waterloo", {
    513      providers: [UrlbarProviderQuickSuggest.name],
    514      isPrivate: false,
    515    }),
    516    matches: [],
    517  });
    518 
    519  info("Doing search for a suggestion that wasn't dismissed");
    520  gMakeSuggestionsStub.returns({ intent: "yelp_intent", subject: "ramen" });
    521  await check_results({
    522    context: createContext("ramen", {
    523      providers: [UrlbarProviderQuickSuggest.name],
    524      isPrivate: false,
    525    }),
    526    matches: [
    527      makeExpectedResult({
    528        url: "https://www.yelp.com/search?find_desc=ramen&find_loc=Yokohama%2C+Kanagawa",
    529        title: "ramen in Yokohama, Kanagawa",
    530      }),
    531    ],
    532  });
    533 
    534  info("Clearing dismissed suggestions");
    535  await QuickSuggest.clearDismissedSuggestions();
    536 
    537  info("Doing search for un-dismissed suggestion");
    538  gMakeSuggestionsStub.returns(burgersIntent);
    539  await check_results({
    540    context: createContext("burgers", {
    541      providers: [UrlbarProviderQuickSuggest.name],
    542      isPrivate: false,
    543    }),
    544    matches: [result],
    545  });
    546  gMakeSuggestionsStub.returns(waterlooIntent);
    547  await check_results({
    548    context: createContext("burgers in waterloo", {
    549      providers: [UrlbarProviderQuickSuggest.name],
    550      isPrivate: false,
    551    }),
    552    matches: [makeExpectedResult(WATERLOO_RESULT)],
    553  });
    554 });
    555 
    556 function makeExpectedResult({
    557  url,
    558  title,
    559  source = "ml",
    560  provider = "yelp_intent",
    561  originalUrl = undefined,
    562  // Expect index -1 for amp results because we test
    563  // without the search suggestions provider.
    564  suggestedIndex = -1,
    565 }) {
    566  return QuickSuggestTestUtils.yelpResult({
    567    url,
    568    title,
    569    source,
    570    provider,
    571    originalUrl,
    572    suggestedIndex,
    573  });
    574 }