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 }