tor-browser

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

test_quicksuggest_sports.js (14544B)


      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 sports suggestions and related code.
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  SportsSuggestions:
     11    "moz-src:///browser/components/urlbar/private/SportsSuggestions.sys.mjs",
     12 });
     13 
     14 // 2025-11-01 - game status is "scheduled", without icon
     15 const SUGGESTION_VALUE_SCHEDULED = {
     16  sport: "Sport 3",
     17  query: "query 3",
     18  date: "2025-11-01T17:00:00Z",
     19  home_team: {
     20    name: "Team 3 Home",
     21    score: null,
     22  },
     23  away_team: {
     24    name: "Team 3 Away",
     25    score: null,
     26  },
     27  status_type: "scheduled",
     28 };
     29 
     30 add_setup(async function init() {
     31  await Services.search.init();
     32 
     33  // Disable search suggestions so we don't hit the network.
     34  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
     35 
     36  // This test deals with `Intl` formating of dates and times, which depends on
     37  // the system locale, and assumes it's en-US. Make sure it's actually en-US.
     38  await QuickSuggestTestUtils.setRegionAndLocale({
     39    locale: "en-US",
     40    skipSuggestReset: true,
     41  });
     42 
     43  await QuickSuggestTestUtils.ensureQuickSuggestInit({
     44    merinoSuggestions: merinoSuggestions([SUGGESTION_VALUE_SCHEDULED]),
     45    prefs: [
     46      ["sports.featureGate", true],
     47      ["suggest.sports", true],
     48      ["suggest.quicksuggest.all", true],
     49    ],
     50  });
     51 });
     52 
     53 add_task(async function telemetryType() {
     54  Assert.equal(
     55    QuickSuggest.getFeature("SportsSuggestions").getSuggestionTelemetryType({}),
     56    "sports",
     57    "Telemetry type should be as expected"
     58  );
     59 });
     60 
     61 // The suggestions should be disabled when the relevant prefs are false.
     62 add_task(async function disabledPrefs() {
     63  setNow("2025-10-31T14:00:00-04:00[-04:00]");
     64 
     65  let prefs = [
     66    "quicksuggest.enabled",
     67    "sports.featureGate",
     68    "suggest.sports",
     69    "suggest.quicksuggest.all",
     70  ];
     71 
     72  for (let pref of prefs) {
     73    info("Testing pref: " + pref);
     74 
     75    // First make sure the suggestion is added.
     76    await check_results({
     77      context: createContext("test", {
     78        providers: [UrlbarProviderQuickSuggest.name],
     79        isPrivate: false,
     80      }),
     81      matches: [
     82        expectedResult([
     83          {
     84            query: "query 3",
     85            sport: "Sport 3",
     86            status_type: "scheduled",
     87            date: "2025-11-01T17:00:00Z",
     88            home_team: {
     89              name: "Team 3 Home",
     90              score: null,
     91            },
     92            away_team: {
     93              name: "Team 3 Away",
     94              score: null,
     95            },
     96          },
     97        ]),
     98      ],
     99    });
    100 
    101    // Now disable them.
    102    UrlbarPrefs.set(pref, false);
    103    await check_results({
    104      context: createContext("test", {
    105        providers: [UrlbarProviderQuickSuggest.name],
    106        isPrivate: false,
    107      }),
    108      matches: [],
    109    });
    110 
    111    // Revert.
    112    UrlbarPrefs.set(pref, true);
    113    await QuickSuggestTestUtils.forceSync();
    114  }
    115 });
    116 
    117 // Main test for `SportsSuggestions._parseDate`.
    118 add_task(async function datesAndTimes() {
    119  // For each test, we'll set `now`, call `_parseDate` with `date`, and check
    120  // the return value against `expected`.
    121  let tests = [
    122    // date is before this year
    123    {
    124      now: "2025-10-31T12:00:00-07:00[-07:00]",
    125      date: "2013-05-11T04:00:00-07:00",
    126      expected: {
    127        daysUntil: -Infinity,
    128        isFuture: false,
    129      },
    130    },
    131 
    132    // date is before yesterday
    133    {
    134      now: [
    135        "2025-10-31T00:00:00-07:00[-07:00]",
    136        "2025-10-31T23:59:59-07:00[-07:00]",
    137      ],
    138      date: ["2025-10-29T00:00:00-07:00", "2025-10-29T23:59:59-07:00"],
    139      expected: {
    140        daysUntil: -Infinity,
    141        isFuture: false,
    142      },
    143    },
    144 
    145    // date is yesterday
    146    {
    147      now: [
    148        "2025-10-31T00:00:00-07:00[-07:00]",
    149        "2025-10-31T23:59:59-07:00[-07:00]",
    150      ],
    151      date: ["2025-10-30T00:00:00-07:00", "2025-10-30T23:59:59-07:00"],
    152      expected: {
    153        daysUntil: -1,
    154        isFuture: false,
    155      },
    156    },
    157 
    158    // date is today (past)
    159    {
    160      now: [
    161        "2025-10-31T12:00:00-07:00[-07:00]",
    162        "2025-10-31T23:59:59-07:00[-07:00]",
    163      ],
    164      date: ["2025-10-31T00:00:00-07:00", "2025-10-31T11:59:59-07:00"],
    165      expected: {
    166        daysUntil: 0,
    167        isFuture: false,
    168      },
    169    },
    170 
    171    // date is today (now)
    172    {
    173      now: "2025-10-31T12:00:00-07:00[-07:00]",
    174      date: "2025-10-31T12:00:00-07:00",
    175      expected: {
    176        daysUntil: 0,
    177        isFuture: false,
    178      },
    179    },
    180 
    181    // date is today (future)
    182    {
    183      now: [
    184        "2025-10-31T00:00:00-07:00[-07:00]",
    185        "2025-10-31T12:00:00-07:00[-07:00]",
    186      ],
    187      date: ["2025-10-31T12:00:01-07:00", "2025-10-31T23:59:59-07:00"],
    188      expected: {
    189        daysUntil: 0,
    190        isFuture: true,
    191      },
    192    },
    193 
    194    // date is tomorrow
    195    {
    196      now: [
    197        "2025-10-31T00:00:00-07:00[-07:00]",
    198        "2025-10-31T23:59:59-07:00[-07:00]",
    199      ],
    200      date: ["2025-11-01T00:00:00-07:00", "2025-11-01T23:59:59-07:00"],
    201      expected: {
    202        daysUntil: 1,
    203        isFuture: true,
    204      },
    205    },
    206 
    207    // date is after tomorrow
    208    {
    209      now: [
    210        "2025-10-31T00:00:00-07:00[-07:00]",
    211        "2025-10-31T23:59:59-07:00[-07:00]",
    212      ],
    213      date: ["2025-11-02T00:00:00-07:00", "2025-11-02T23:59:59-07:00"],
    214      expected: {
    215        daysUntil: Infinity,
    216        isFuture: true,
    217      },
    218    },
    219 
    220    // date is after this year
    221    {
    222      now: "2025-10-31T00:00:00-07:00[-07:00]",
    223      date: "3013-05-11T04:00:00-07:00",
    224      expected: {
    225        daysUntil: Infinity,
    226        isFuture: true,
    227      },
    228    },
    229  ];
    230 
    231  for (let { now, date, expected } of tests) {
    232    let nows = typeof now == "string" ? [now] : now;
    233    let dates = typeof date == "string" ? [date] : date;
    234    for (let n of nows) {
    235      let zonedNow = setNow(n);
    236      for (let d of dates) {
    237        Assert.deepEqual(
    238          SportsSuggestions._parseDate(new Date(d)),
    239          {
    240            ...expected,
    241            zonedNow,
    242            zonedDate: new Date(d)
    243              .toTemporalInstant()
    244              .toZonedDateTimeISO(zonedNow),
    245          },
    246          "datesAndTimes test: " + JSON.stringify({ now: n, date: d })
    247        );
    248      }
    249    }
    250  }
    251 });
    252 
    253 // Tests `SportsSuggestions._parseDate` with dates across time zone changes.
    254 add_task(function timeZoneTransition() {
    255  // This task is based around 2025-11-02, when Daylight Saving Time ends in the
    256  // U.S. On 2025-11-02 at 2:00 am, the time changes to 1:00 am Standard Time.
    257 
    258  let tests = [
    259    // `now` and `date` both in PDT (daylight saving)
    260    {
    261      now: "2025-10-02T12:00:00-07:00[America/Los_Angeles]",
    262      date: "2025-10-01T00:00:00-07:00",
    263      expected: {
    264        daysUntil: -1,
    265        isFuture: false,
    266      },
    267    },
    268 
    269    // `now` in PST, `date` in PDT
    270    {
    271      now: "2025-11-03T00:00:00-08:00[America/Los_Angeles]",
    272      date: "2025-11-01T00:00:00-07:00",
    273      expected: {
    274        daysUntil: -Infinity,
    275        isFuture: false,
    276      },
    277    },
    278    {
    279      now: "2025-11-02T12:00:00-08:00[America/Los_Angeles]",
    280      date: "2025-11-01T00:00:00-07:00",
    281      expected: {
    282        daysUntil: -1,
    283        isFuture: false,
    284      },
    285    },
    286    {
    287      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
    288      date: "2025-11-01T00:00:00-07:00",
    289      expected: {
    290        daysUntil: -1,
    291        isFuture: false,
    292      },
    293    },
    294    {
    295      now: "2025-11-02T23:59:59-08:00[America/Los_Angeles]",
    296      date: "2025-11-01T00:00:00-07:00",
    297      expected: {
    298        daysUntil: -1,
    299        isFuture: false,
    300      },
    301    },
    302    {
    303      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
    304      date: "2025-11-02T00:00:00-07:00",
    305      expected: {
    306        daysUntil: 0,
    307        isFuture: false,
    308      },
    309    },
    310    {
    311      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
    312      date: "2025-11-02T01:00:00-07:00",
    313      expected: {
    314        daysUntil: 0,
    315        isFuture: false,
    316      },
    317    },
    318 
    319    // `now` in PDT, `date` in PST
    320    {
    321      now: "2025-11-02T01:00:00-07:00[America/Los_Angeles]",
    322      date: "2025-11-02T01:00:00-08:00",
    323      expected: {
    324        daysUntil: 0,
    325        isFuture: true,
    326      },
    327    },
    328    {
    329      now: "2025-11-02T00:00:00-07:00[America/Los_Angeles]",
    330      date: "2025-11-02T01:00:00-08:00",
    331      expected: {
    332        daysUntil: 0,
    333        isFuture: true,
    334      },
    335    },
    336    {
    337      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
    338      date: "2025-11-02T23:59:59-08:00",
    339      expected: {
    340        daysUntil: 1,
    341        isFuture: true,
    342      },
    343    },
    344    {
    345      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
    346      date: "2025-11-02T01:00:00-08:00",
    347      expected: {
    348        daysUntil: 1,
    349        isFuture: true,
    350      },
    351    },
    352    {
    353      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
    354      date: "2025-11-02T12:00:00-08:00",
    355      expected: {
    356        daysUntil: 1,
    357        isFuture: true,
    358      },
    359    },
    360    {
    361      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
    362      date: "2025-11-03T00:00:00-08:00",
    363      expected: {
    364        daysUntil: Infinity,
    365        isFuture: true,
    366      },
    367    },
    368 
    369    // `now` and `date` both in PST (standard time)
    370    {
    371      now: "2025-11-11T12:00:00-08:00[America/Los_Angeles]",
    372      date: "2025-11-10T00:00:00-08:00",
    373      expected: {
    374        daysUntil: -1,
    375        isFuture: false,
    376      },
    377    },
    378  ];
    379 
    380  for (let { now, date, expected } of tests) {
    381    let zonedNow = setNow(now);
    382    Assert.deepEqual(
    383      SportsSuggestions._parseDate(new Date(date)),
    384      {
    385        ...expected,
    386        zonedNow,
    387        zonedDate: new Date(date)
    388          .toTemporalInstant()
    389          .toZonedDateTimeISO(zonedNow),
    390      },
    391      "timeZoneTransition test: " + JSON.stringify({ now, date })
    392    );
    393  }
    394 });
    395 
    396 add_task(async function command_notInterested() {
    397  setNow("2025-10-31T14:00:00-04:00[-04:00]");
    398 
    399  await doDismissAllTest({
    400    result: expectedResult([
    401      {
    402        query: "query 3",
    403        sport: "Sport 3",
    404        status_type: "scheduled",
    405        date: "2025-11-01T17:00:00Z",
    406        home_team: {
    407          name: "Team 3 Home",
    408          score: null,
    409        },
    410        away_team: {
    411          name: "Team 3 Away",
    412          score: null,
    413        },
    414      },
    415    ]),
    416    command: "not_interested",
    417    feature: QuickSuggest.getFeature("SportsSuggestions"),
    418    pref: "suggest.sports",
    419    queries: [{ query: "test" }],
    420  });
    421 });
    422 
    423 add_task(async function command_showLessFrequently() {
    424  setNow("2025-10-31T14:00:00-04:00[-04:00]");
    425 
    426  UrlbarPrefs.clear("sports.showLessFrequentlyCount");
    427  UrlbarPrefs.clear("sports.minKeywordLength");
    428 
    429  let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
    430    realtimeMinKeywordLength: 0,
    431    realtimeShowLessFrequentlyCap: 3,
    432  });
    433 
    434  let result = expectedResult([
    435    {
    436      query: "query 3",
    437      sport: "Sport 3",
    438      status_type: "scheduled",
    439      date: "2025-11-01T17:00:00Z",
    440      home_team: {
    441        name: "Team 3 Home",
    442        score: null,
    443      },
    444      away_team: {
    445        name: "Team 3 Away",
    446        score: null,
    447      },
    448    },
    449  ]);
    450 
    451  const testData = [
    452    {
    453      input: "spo",
    454      before: {
    455        canShowLessFrequently: true,
    456        showLessFrequentlyCount: 0,
    457        minKeywordLength: 0,
    458      },
    459      after: {
    460        canShowLessFrequently: true,
    461        showLessFrequentlyCount: 1,
    462        minKeywordLength: 4,
    463      },
    464    },
    465    {
    466      input: "sport",
    467      before: {
    468        canShowLessFrequently: true,
    469        showLessFrequentlyCount: 1,
    470        minKeywordLength: 4,
    471      },
    472      after: {
    473        canShowLessFrequently: true,
    474        showLessFrequentlyCount: 2,
    475        minKeywordLength: 6,
    476      },
    477    },
    478    {
    479      input: "sports",
    480      before: {
    481        canShowLessFrequently: true,
    482        showLessFrequentlyCount: 2,
    483        minKeywordLength: 6,
    484      },
    485      after: {
    486        canShowLessFrequently: false,
    487        showLessFrequentlyCount: 3,
    488        minKeywordLength: 7,
    489      },
    490    },
    491  ];
    492 
    493  for (let { input, before, after } of testData) {
    494    let feature = QuickSuggest.getFeature("SportsSuggestions");
    495 
    496    await check_results({
    497      context: createContext(input, {
    498        providers: [UrlbarProviderQuickSuggest.name],
    499        isPrivate: false,
    500      }),
    501      matches: [result],
    502    });
    503 
    504    Assert.equal(
    505      UrlbarPrefs.get("sports.minKeywordLength"),
    506      before.minKeywordLength
    507    );
    508    Assert.equal(feature.canShowLessFrequently, before.canShowLessFrequently);
    509    Assert.equal(
    510      feature.showLessFrequentlyCount,
    511      before.showLessFrequentlyCount
    512    );
    513 
    514    triggerCommand({
    515      result,
    516      feature,
    517      command: "show_less_frequently",
    518      searchString: input,
    519    });
    520 
    521    Assert.equal(
    522      UrlbarPrefs.get("sports.minKeywordLength"),
    523      after.minKeywordLength
    524    );
    525    Assert.equal(feature.canShowLessFrequently, after.canShowLessFrequently);
    526    Assert.equal(
    527      feature.showLessFrequentlyCount,
    528      after.showLessFrequentlyCount
    529    );
    530 
    531    await check_results({
    532      context: createContext(input, {
    533        providers: [UrlbarProviderQuickSuggest.name],
    534        isPrivate: false,
    535      }),
    536      matches: [],
    537    });
    538  }
    539 
    540  await cleanUpNimbus();
    541  UrlbarPrefs.clear("sports.showLessFrequentlyCount");
    542  UrlbarPrefs.clear("sports.minKeywordLength");
    543 });
    544 
    545 let gSandbox;
    546 let gDateStub;
    547 
    548 function setNow(dateStr) {
    549  if (!dateStr) {
    550    gSandbox?.restore();
    551    return null;
    552  }
    553 
    554  let global = Cu.getGlobalForObject(SportsSuggestions);
    555  if (!gSandbox) {
    556    gSandbox = sinon.createSandbox();
    557    gDateStub = gSandbox.stub(SportsSuggestions, "_zonedDateTimeISO");
    558  }
    559 
    560  let zonedNow = global.Temporal.ZonedDateTime.from(dateStr);
    561  gDateStub.returns(zonedNow);
    562 
    563  return zonedNow;
    564 }
    565 
    566 function merinoSuggestions(values) {
    567  return [
    568    {
    569      provider: "sports",
    570      is_sponsored: false,
    571      score: 0.2,
    572      title: "",
    573      custom_details: {
    574        sports: {
    575          values,
    576        },
    577      },
    578    },
    579  ];
    580 }
    581 
    582 function expectedResult(expectedItems) {
    583  return {
    584    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    585    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    586    isBestMatch: true,
    587    hideRowLabel: true,
    588    rowIndex: -1,
    589    heuristic: false,
    590    exposureTelemetry: 0,
    591    payload: {
    592      items: expectedItems,
    593      source: "merino",
    594      provider: "sports",
    595      telemetryType: "sports",
    596      isSponsored: false,
    597      engine: Services.search.defaultEngine.name,
    598      dynamicType: "realtime-sports",
    599    },
    600  };
    601 }