test_quicksuggest_importantDatesSuggestions.js (14371B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const REMOTE_SETTINGS_RECORDS = [ 7 // US region, en-US locale 8 { 9 type: "dynamic-suggestions", 10 suggestion_type: "important_dates", 11 filter_expression: "env.country == 'US' && env.locale == 'en-US'", 12 attachment: [ 13 { 14 keywords: [["event", [" 1"]]], 15 data: { 16 result: { 17 isImportantDate: true, 18 payload: { 19 dates: ["2025-03-05", "2026-02-18"], 20 name: "Event 1", 21 }, 22 }, 23 }, 24 }, 25 { 26 keywords: [["multi d", ["ay event"]]], 27 data: { 28 result: { 29 isImportantDate: true, 30 payload: { 31 dates: [ 32 ["2025-06-10", "2025-06-20"], 33 ["2026-06-10", "2026-06-20"], 34 ], 35 name: "Multi Day Event", 36 }, 37 }, 38 }, 39 }, 40 ], 41 }, 42 43 // DE region, de locale 44 { 45 type: "dynamic-suggestions", 46 suggestion_type: "important_dates", 47 filter_expression: "env.country == 'DE' && env.locale == 'de'", 48 attachment: [ 49 { 50 keywords: ["de de event"], 51 data: { 52 result: { 53 isImportantDate: true, 54 payload: { 55 dates: ["2026-01-01"], 56 name: "DE de Event", 57 }, 58 }, 59 }, 60 }, 61 ], 62 }, 63 64 // DE region, en-US locale 65 { 66 type: "dynamic-suggestions", 67 suggestion_type: "important_dates", 68 filter_expression: "env.country == 'DE' && env.locale == 'en-US'", 69 attachment: [ 70 { 71 keywords: ["de en-us event"], 72 data: { 73 result: { 74 isImportantDate: true, 75 payload: { 76 dates: ["2026-01-02"], 77 name: "DE en-US Event", 78 }, 79 }, 80 }, 81 }, 82 ], 83 }, 84 85 // other non-important-dates records 86 { 87 type: "dynamic-suggestions", 88 suggestion_type: "other_suggestions", 89 attachment: [ 90 { 91 keywords: [["event", [" 2"]]], 92 data: { 93 result: { 94 isBestMatch: true, 95 payload: { 96 title: "Top Pick Suggestion 1", 97 url: "https://foo.com/", 98 description: 99 "A suggestion that just so happens to have the same keyword", 100 }, 101 }, 102 }, 103 }, 104 ], 105 }, 106 { 107 type: "dynamic-suggestions", 108 suggestion_type: "other_suggestions", 109 attachment: [ 110 { 111 keywords: [["event", [" 3"]]], 112 data: { 113 result: { 114 isBestMatch: true, 115 payload: { 116 title: "Top Pick Suggestion 2", 117 url: "https://foo.com/", 118 description: 119 "Another suggestion that just so happens to have the same keyword", 120 }, 121 }, 122 }, 123 }, 124 ], 125 }, 126 ]; 127 128 let SystemDate; 129 130 add_setup(async function () { 131 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 132 remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, 133 prefs: [["quicksuggest.dynamicSuggestionTypes", "other_suggestions"]], 134 }); 135 136 // All tasks will assume US region, en-US locale by default. 137 await QuickSuggestTestUtils.setRegionAndLocale({ 138 region: "US", 139 locale: "en-US", 140 }); 141 142 await Services.search.init(); 143 144 SystemDate = Cu.getGlobalForObject(QuickSuggestTestUtils).Date; 145 }); 146 147 add_task(async function telemetryType() { 148 Assert.equal( 149 QuickSuggest.getFeature( 150 "ImportantDatesSuggestions" 151 ).getSuggestionTelemetryType({}), 152 "important_dates", 153 "Telemetry type should be 'important_dates'" 154 ); 155 }); 156 157 // Important dates should respect the `importantDates.featureGate` and 158 // `suggest.importantDates` prefs. 159 add_task(async function enablingPrefs() { 160 setTime("2025-03-01T00:00"); 161 162 let query = "event 1"; 163 let expected = makeExpectedResult({ 164 date: "Wednesday, March 5, 2025", 165 descriptionL10n: { 166 id: "urlbar-result-dates-countdown", 167 args: { name: "Event 1", daysUntilStart: 4 }, 168 }, 169 }); 170 171 for (let pref of ["importantDates.featureGate", "suggest.importantDates"]) { 172 info("Doing enabling-pref test: " + pref); 173 174 // First make sure the result is matched. 175 await checkDatesResults(query, expected); 176 177 // Now disable the pref and do another search. 178 UrlbarPrefs.set(pref, false); 179 await checkDatesResults(query, null); 180 181 // Clean up. 182 UrlbarPrefs.set(pref, true); 183 await QuickSuggestTestUtils.forceSync(); 184 } 185 }); 186 187 // Important dates are considered "utility" suggestions and not part of the 188 // Suggest brand, so they should be enabled regardless of the `all` and 189 // sponsored Suggest prefs. 190 add_task(async function neitherSponsoredNorNonsponsored() { 191 setTime("2025-03-01T00:00"); 192 193 let query = "event 1"; 194 let expected = makeExpectedResult({ 195 date: "Wednesday, March 5, 2025", 196 descriptionL10n: { 197 id: "urlbar-result-dates-countdown", 198 args: { name: "Event 1", daysUntilStart: 4 }, 199 }, 200 }); 201 202 for (let all of [true, false]) { 203 for (let sponsored of [true, false]) { 204 info( 205 "Doing all/sponsored pref test: " + JSON.stringify({ all, sponsored }) 206 ); 207 208 UrlbarPrefs.set("suggest.quicksuggest.all", all); 209 UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsored); 210 await QuickSuggestTestUtils.forceSync(); 211 212 await checkDatesResults(query, expected); 213 } 214 } 215 216 UrlbarPrefs.clear("suggest.quicksuggest.all"); 217 UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); 218 await QuickSuggestTestUtils.forceSync(); 219 }); 220 221 add_task(async function fourDaysBefore() { 222 setTime("2025-03-01T00:00"); 223 224 let query = "event 1"; 225 let expected = makeExpectedResult({ 226 date: "Wednesday, March 5, 2025", 227 descriptionL10n: { 228 id: "urlbar-result-dates-countdown", 229 args: { name: "Event 1", daysUntilStart: 4 }, 230 }, 231 }); 232 233 await checkDatesResults(query, expected); 234 }); 235 236 add_task(async function onDayOfEvent() { 237 setTime("2025-03-05T00:00"); 238 239 let query = "event 1"; 240 let expected = makeExpectedResult({ 241 date: "Wednesday, March 5, 2025", 242 descriptionL10n: { 243 id: "urlbar-result-dates-today", 244 args: { name: "Event 1" }, 245 }, 246 }); 247 248 await checkDatesResults(query, expected); 249 }); 250 251 add_task(async function oneDayAfter() { 252 setTime("2025-03-06T00:00"); 253 254 let query = "event 1"; 255 let expected = makeExpectedResult({ 256 // Should select the event in the next year. 257 date: "Wednesday, February 18, 2026", 258 // Since the event is over SHOW_COUNTDOWN_THRESHOLD_DAYS 259 // days away, it should not display the countdown. 260 description: "Event 1", 261 }); 262 263 await checkDatesResults(query, expected); 264 }); 265 266 add_task(async function afterAllInstances() { 267 setTime("2027-01-01T00:00"); 268 269 let query = "event 1"; 270 let expected = null; 271 272 await checkDatesResults(query, expected); 273 }); 274 275 add_task(async function beforeMultiDay() { 276 setTime("2025-06-09T23:59"); 277 278 let query = "multi day event"; 279 let expected = makeExpectedResult({ 280 date: "June 10 – 20, 2025", 281 descriptionL10n: { 282 id: "urlbar-result-dates-countdown-range", 283 args: { name: "Multi Day Event", daysUntilStart: 1 }, 284 }, 285 }); 286 287 await checkDatesResults(query, expected); 288 }); 289 290 add_task(async function duringMultiDay() { 291 setTime("2025-06-19T00:00"); 292 293 let query = "multi day event"; 294 let expected = makeExpectedResult({ 295 date: "June 10 – 20, 2025", 296 descriptionL10n: { 297 id: "urlbar-result-dates-ongoing", 298 args: { name: "Multi Day Event", daysUntilEnd: 1 }, 299 }, 300 }); 301 302 await checkDatesResults(query, expected); 303 }); 304 305 add_task(async function lastDayDuringMultiDay() { 306 setTime("2025-06-20T00:00"); 307 308 let query = "multi day event"; 309 let expected = makeExpectedResult({ 310 date: "June 10 – 20, 2025", 311 descriptionL10n: { 312 id: "urlbar-result-dates-ends-today", 313 args: { name: "Multi Day Event" }, 314 }, 315 }); 316 317 await checkDatesResults(query, expected); 318 }); 319 320 // Test whether the date suggestion is before the other 321 // isBestMatch suggestion. 322 add_task(async function testTwoSuggestions() { 323 // `other_suggestions` is nonsponsored so it depends on the `all` pref. 324 UrlbarPrefs.set("suggest.quicksuggest.all", true); 325 await QuickSuggestTestUtils.forceSync(); 326 327 setTime("2025-03-01T00:00"); 328 329 // 1 date suggestion and 2 other suggestions match this, but 330 // one of the two other suggestions should be deduped. 331 let query = "event"; 332 let expectedDateSuggestion = makeExpectedResult({ 333 date: "Wednesday, March 5, 2025", 334 descriptionL10n: { 335 id: "urlbar-result-dates-countdown", 336 args: { daysUntilStart: 4, name: "Event 1" }, 337 }, 338 }); 339 340 let expectedOtherSuggestion = { 341 type: UrlbarUtils.RESULT_TYPE.URL, 342 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 343 heuristic: false, 344 isBestMatch: true, 345 isRichSuggestion: true, 346 suggestedIndex: 1, 347 payload: { 348 source: "rust", 349 provider: "Dynamic", 350 suggestionType: "other_suggestions", 351 title: "Top Pick Suggestion 1", 352 url: "https://foo.com/", 353 telemetryType: "other_suggestions", 354 description: "A suggestion that just so happens to have the same keyword", 355 isManageable: true, 356 isSponsored: false, 357 helpUrl: QuickSuggest.HELP_URL, 358 }, 359 }; 360 361 await checkDatesResults(query, [ 362 expectedDateSuggestion, 363 expectedOtherSuggestion, 364 ]); 365 366 UrlbarPrefs.clear("suggest.quicksuggest.all"); 367 await QuickSuggestTestUtils.forceSync(); 368 }); 369 370 add_task(async function otherRegionsAndLocales() { 371 let tests = [ 372 // DE region, de locale (should match) 373 { 374 region: "DE", 375 locale: "de", 376 matchingQuery: "de de event", 377 expectedResultData: { 378 date: "Donnerstag, 1. Januar 2026", 379 description: "DE de Event", 380 }, 381 nonMatchingQueries: [ 382 "de en-US event", // DE region, en-US locale 383 "event 1", // US region, en-US locale 384 ], 385 }, 386 387 // DE region, en-US locale (should match) 388 { 389 region: "DE", 390 locale: "en-US", 391 matchingQuery: "de en-us event", 392 expectedResultData: { 393 date: "Friday, January 2, 2026", 394 description: "DE en-US Event", 395 }, 396 nonMatchingQueries: [ 397 "de de event", // DE region, de locale 398 "event 1", // US region, en-US locale 399 ], 400 }, 401 402 // XX region, en-US locale (should not match) 403 { 404 region: "XX", 405 locale: "en-US", 406 matchingQuery: null, 407 nonMatchingQueries: [ 408 "de de event", // DE region, de locale 409 "de en-us event", // DE region, en-US locale 410 "event 1", // US region, en-US locale 411 ], 412 }, 413 414 // US region, de locale (should not match) 415 { 416 region: "US", 417 locale: "de", 418 matchingQuery: null, 419 nonMatchingQueries: [ 420 "de de event", // DE region, de locale 421 "de en-us event", // DE region, en-US locale 422 "event 1", // US region, en-US locale 423 ], 424 }, 425 ]; 426 427 for (let { 428 region, 429 locale, 430 matchingQuery, 431 expectedResultData, 432 nonMatchingQueries, 433 } of tests) { 434 info("Doing subtest: " + JSON.stringify({ region, locale })); 435 436 await QuickSuggestTestUtils.withRegionAndLocale({ 437 region, 438 locale, 439 callback: async () => { 440 Assert.equal( 441 UrlbarPrefs.get("quickSuggestEnabled"), 442 !!matchingQuery, 443 "quickSuggestEnabled should be enabled as expected" 444 ); 445 Assert.equal( 446 UrlbarPrefs.get("importantDates.featureGate"), 447 !!matchingQuery, 448 "importantDates.featureGate should be enabled as expected" 449 ); 450 451 setTime("2025-03-01T00:00"); 452 453 if (matchingQuery) { 454 info("Checking matching query: " + matchingQuery); 455 await checkDatesResults( 456 matchingQuery, 457 makeExpectedResult(expectedResultData) 458 ); 459 } 460 461 for (let query of nonMatchingQueries) { 462 info("Checking non-matching query: " + query); 463 await checkDatesResults(query, null); 464 } 465 }, 466 }); 467 } 468 }); 469 470 /** 471 * Stubs the Date object of the system global to use timeStr 472 * when the constructor is called without arguments. 473 * 474 * @param {string} timeStr 475 * An ISO time string. 476 */ 477 function setTime(timeStr) { 478 let DateStub = function (...args) { 479 if (!args.length) { 480 return new SystemDate(timeStr); 481 } 482 return new SystemDate(...args); 483 }; 484 DateStub.prototype = SystemDate.prototype; 485 486 Object.getOwnPropertyNames(SystemDate).forEach(prop => { 487 const desc = Object.getOwnPropertyDescriptor(SystemDate, prop); 488 Object.defineProperty(DateStub, prop, desc); 489 }); 490 491 Cu.getGlobalForObject(QuickSuggestTestUtils).Date = DateStub; 492 } 493 494 async function checkDatesResults(query, expected) { 495 info( 496 "Doing query: " + 497 JSON.stringify({ 498 query, 499 expected, 500 }) 501 ); 502 503 let queryContext = createContext(query, { 504 providers: [UrlbarProviderQuickSuggest.name], 505 isPrivate: false, 506 }); 507 await check_results({ 508 context: queryContext, 509 matches: expected ? [expected].flat() : [], 510 }); 511 512 if (expected?.payload) { 513 info("Check the highligts"); 514 let { value, highlights } = 515 queryContext.results[0].getDisplayableValueAndHighlights("title", { 516 tokens: queryContext.tokens, 517 }); 518 Assert.equal(expected.payload.title, value); 519 Assert.deepEqual([[0, expected.payload.title.length]], highlights); 520 } 521 } 522 523 function makeExpectedResult({ 524 date, 525 description, 526 descriptionL10n, 527 isSponsored = false, 528 isBestMatch = true, 529 isRichSuggestion = undefined, 530 }) { 531 let name = description ?? descriptionL10n.args.name; 532 return { 533 type: UrlbarUtils.RESULT_TYPE.SEARCH, 534 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 535 heuristic: false, 536 isBestMatch, 537 suggestedIndex: 1, 538 isRichSuggestion, 539 payload: { 540 title: date, 541 engine: Services.search.defaultEngine.name, 542 query: name, 543 lowerCaseSuggestion: name.toLocaleLowerCase(), 544 description, 545 descriptionL10n, 546 isSponsored, 547 telemetryType: "important_dates", 548 source: "rust", 549 provider: "Dynamic", 550 suggestionType: "important_dates", 551 isManageable: true, 552 isBlockable: true, 553 helpUrl: QuickSuggest.HELP_URL, 554 icon: "chrome://browser/skin/calendar-24.svg", 555 }, 556 }; 557 }