test_weather.js (31410B)
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 the quick suggest weather feature. 6 // 7 // w/r/t weather queries with cities, note that the Suggest Rust component 8 // handles city/region parsing and has extensive tests for that. Here we need to 9 // test our geolocation logic, make sure Merino is called with the correct 10 // city/region, and make sure the urlbar result has the correct city. 11 12 "use strict"; 13 14 ChromeUtils.defineESModuleGetters(this, { 15 MerinoClient: "moz-src:///browser/components/urlbar/MerinoClient.sys.mjs", 16 Region: "resource://gre/modules/Region.sys.mjs", 17 UrlbarProviderPlaces: 18 "moz-src:///browser/components/urlbar/UrlbarProviderPlaces.sys.mjs", 19 }); 20 21 const { WEATHER_SUGGESTION } = MerinoTestUtils; 22 23 const EXPECTED_MERINO_PARAMS_WATERLOO_IA = { 24 city: "Waterloo", 25 region: "IA,013,94597", 26 country: "US", 27 }; 28 29 const EXPECTED_MERINO_PARAMS_WATERLOO_AL = { 30 city: "Waterloo", 31 region: "AL,077", 32 country: "US", 33 }; 34 35 let gWeather; 36 37 add_setup(async () => { 38 // Weather suggestion titles depend on the current home region, and this test 39 // assumes it's the US. 40 Region._setHomeRegion("US", true); 41 42 await QuickSuggestTestUtils.ensureQuickSuggestInit({ 43 prefs: [ 44 ["suggest.quicksuggest.sponsored", true], 45 ["weather.featureGate", true], 46 ], 47 remoteSettingsRecords: [ 48 QuickSuggestTestUtils.weatherRecord(), 49 ...QuickSuggestTestUtils.geonamesRecords(), 50 ...QuickSuggestTestUtils.geonamesAlternatesRecords(), 51 ], 52 }); 53 54 await MerinoTestUtils.initWeather(); 55 56 gWeather = QuickSuggest.getFeature("WeatherSuggestions"); 57 }); 58 59 // The feature should be properly enabled according to relavant prefs. 60 add_task(async function disableAndEnable() { 61 let prefs = [ 62 "weather.featureGate", 63 "suggest.weather", 64 "suggest.quicksuggest.all", 65 "suggest.quicksuggest.sponsored", 66 ]; 67 for (let pref of prefs) { 68 info("Testing pref: " + pref); 69 await doBasicDisableAndEnableTest(pref); 70 } 71 }); 72 73 async function doBasicDisableAndEnableTest(pref) { 74 let cleanup = GeolocationTestUtils.stubGeolocation( 75 GeolocationTestUtils.SAN_FRANCISCO 76 ); 77 78 // Disable the feature. It should be immediately uninitialized. 79 UrlbarPrefs.set(pref, false); 80 assertDisabled({ 81 message: "After disabling", 82 }); 83 84 // No suggestion should be returned for a search. 85 let context = createContext("weather", { 86 providers: [UrlbarProviderQuickSuggest.name], 87 isPrivate: false, 88 }); 89 await check_results({ 90 context, 91 matches: [], 92 }); 93 94 // Re-enable the feature. 95 info("Re-enable the feature"); 96 UrlbarPrefs.set(pref, true); 97 98 // The suggestion should be returned for a search. 99 context = createContext("weather", { 100 providers: [UrlbarProviderQuickSuggest.name], 101 isPrivate: false, 102 }); 103 await check_results({ 104 context, 105 matches: [QuickSuggestTestUtils.weatherResult()], 106 }); 107 108 await cleanup(); 109 } 110 111 // Tests a Merino fetch that doesn't return a suggestion. 112 add_task(async function noSuggestion() { 113 let { suggestions } = MerinoTestUtils.server.response.body; 114 MerinoTestUtils.server.response.body.suggestions = []; 115 116 let context = createContext("weather", { 117 providers: [UrlbarProviderQuickSuggest.name], 118 isPrivate: false, 119 }); 120 await check_results({ 121 context, 122 matches: [], 123 }); 124 125 MerinoTestUtils.server.response.body.suggestions = suggestions; 126 }); 127 128 // When the Merino response doesn't include a `region_code` for the geolocated 129 // version of the suggestion, the suggestion title should only contain a city. 130 add_task(async function geolocationSuggestionNoRegion() { 131 let cleanup = GeolocationTestUtils.stubGeolocation( 132 GeolocationTestUtils.SAN_FRANCISCO 133 ); 134 135 let { suggestions } = MerinoTestUtils.server.response.body; 136 let s = { ...MerinoTestUtils.WEATHER_SUGGESTION }; 137 delete s.region_code; 138 MerinoTestUtils.server.response.body.suggestions = [s]; 139 140 let context = createContext("weather", { 141 providers: [UrlbarProviderQuickSuggest.name], 142 isPrivate: false, 143 }); 144 await check_results({ 145 context, 146 matches: [ 147 QuickSuggestTestUtils.weatherResult({ 148 titleL10n: { 149 id: "urlbar-result-weather-title-city-only", 150 args: { 151 city: s.city_name, 152 }, 153 }, 154 }), 155 ], 156 }); 157 158 MerinoTestUtils.server.response.body.suggestions = suggestions; 159 await cleanup(); 160 }); 161 162 // When the query matches both the weather suggestion and a previous visit to 163 // the suggestion's URL, the suggestion should be shown and the history visit 164 // should not be shown. 165 add_task(async function urlAlreadyInHistory() { 166 let cleanup = GeolocationTestUtils.stubGeolocation( 167 GeolocationTestUtils.SAN_FRANCISCO 168 ); 169 170 // A visit to the weather suggestion's exact URL. 171 let suggestionVisit = { 172 uri: MerinoTestUtils.WEATHER_SUGGESTION.url, 173 title: MerinoTestUtils.WEATHER_SUGGESTION.title, 174 }; 175 176 // A visit to a totally unrelated URL that also matches "weather" just to make 177 // sure the Places provider is enabled and returning matches as expected. 178 let otherVisit = { 179 uri: "https://example.com/some-other-weather-page", 180 title: "Some other weather page", 181 }; 182 183 await PlacesTestUtils.addVisits([suggestionVisit, otherVisit]); 184 185 // First make sure both visit results are matched by doing a search with only 186 // the Places provider. 187 info("Doing first search"); 188 let context = createContext("weather", { 189 providers: [UrlbarProviderPlaces.name], 190 isPrivate: false, 191 }); 192 await check_results({ 193 context, 194 matches: [ 195 makeVisitResult(context, otherVisit), 196 makeVisitResult(context, suggestionVisit), 197 ], 198 }); 199 200 // Now do a search with both the Suggest and Places providers. 201 info("Doing second search"); 202 context = createContext("weather", { 203 providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderPlaces.name], 204 isPrivate: false, 205 }); 206 await check_results({ 207 context, 208 matches: [ 209 // The visit result to the unrelated URL will be first since the weather 210 // suggestion has a `suggestedIndex` of 1. 211 makeVisitResult(context, otherVisit), 212 QuickSuggestTestUtils.weatherResult(), 213 ], 214 }); 215 216 await PlacesUtils.history.clear(); 217 await cleanup(); 218 }); 219 220 // Locale task for when this test runs on an en-US OS. 221 add_task(async function locale_enUS() { 222 await doLocaleTest({ 223 shouldRunTask: osLocale => osLocale == "en-US", 224 osUnit: "f", 225 unitsByLocale: { 226 "en-US": "f", 227 // When the app's locale is set to any en-* locale, F will be used because 228 // `regionalPrefsLocales` will prefer the en-US OS locale. 229 "en-CA": "f", 230 "en-GB": "f", 231 de: "c", 232 }, 233 }); 234 }); 235 236 // Locale task for when this test runs on a non-US English OS. 237 add_task(async function locale_nonUSEnglish() { 238 await doLocaleTest({ 239 shouldRunTask: osLocale => osLocale.startsWith("en") && osLocale != "en-US", 240 osUnit: "c", 241 unitsByLocale: { 242 // When the app's locale is set to en-US, C will be used because 243 // `regionalPrefsLocales` will prefer the non-US English OS locale. 244 "en-US": "c", 245 "en-CA": "c", 246 "en-GB": "c", 247 de: "c", 248 }, 249 }); 250 }); 251 252 // Locale task for when this test runs on a non-English OS. 253 add_task(async function locale_nonEnglish() { 254 await doLocaleTest({ 255 shouldRunTask: osLocale => !osLocale.startsWith("en"), 256 osUnit: "c", 257 unitsByLocale: { 258 "en-US": "f", 259 "en-CA": "c", 260 "en-GB": "c", 261 de: "c", 262 }, 263 }); 264 }); 265 266 /** 267 * Testing locales is tricky due to the weather feature's use of 268 * `Services.locale.regionalPrefsLocales`. By default `regionalPrefsLocales` 269 * prefers the OS locale if its language is the same as the app locale's 270 * language; otherwise it prefers the app locale. For example, assuming the OS 271 * locale is en-CA, then if the app locale is en-US it will prefer en-CA since 272 * both are English, but if the app locale is de it will prefer de. If the pref 273 * `intl.regional_prefs.use_os_locales` is set, then the OS locale is always 274 * preferred. 275 * 276 * This function tests a given set of locales with and without 277 * `intl.regional_prefs.use_os_locales` set. 278 * 279 * @param {object} options 280 * Options 281 * @param {Function} options.shouldRunTask 282 * Called with the OS locale. Should return true if the function should run. 283 * Use this to skip tasks that don't target a desired OS locale. 284 * @param {string} options.osUnit 285 * The expected "c" or "f" unit for the OS locale. 286 * @param {object} options.unitsByLocale 287 * The expected "c" or "f" unit when the app's locale is set to particular 288 * locales. This should be an object that maps locales to expected units. For 289 * each locale in the object, the app's locale is set to that locale and the 290 * actual unit is expected to be the unit in the object. 291 */ 292 async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) { 293 Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true); 294 let osLocale = Services.locale.regionalPrefsLocales[0]; 295 Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales"); 296 297 if (!shouldRunTask(osLocale)) { 298 info("Skipping task, should not run for this OS locale"); 299 return; 300 } 301 302 // Sanity check initial locale info. 303 Assert.equal( 304 Services.locale.appLocaleAsBCP47, 305 "en-US", 306 "Initial app locale should be en-US" 307 ); 308 Assert.ok( 309 !Services.prefs.getBoolPref("intl.regional_prefs.use_os_locales"), 310 "intl.regional_prefs.use_os_locales should be false initially" 311 ); 312 313 // Check locales. 314 for (let [locale, temperatureUnit] of Object.entries(unitsByLocale)) { 315 await QuickSuggestTestUtils.withRegionAndLocale({ 316 locale, 317 // Weather suggestions are not enabled by default for all regions/locale 318 // combinations in this test, so don't reset Suggest so that they remain 319 // enabled rather than being set according to region/locale. 320 skipSuggestReset: true, 321 callback: async () => { 322 let cleanup = GeolocationTestUtils.stubGeolocation( 323 GeolocationTestUtils.SAN_FRANCISCO 324 ); 325 326 info("Checking locale: " + locale); 327 await check_results({ 328 context: createContext("weather", { 329 providers: [UrlbarProviderQuickSuggest.name], 330 isPrivate: false, 331 }), 332 matches: [QuickSuggestTestUtils.weatherResult({ temperatureUnit })], 333 }); 334 335 info( 336 "Checking locale with intl.regional_prefs.use_os_locales: " + locale 337 ); 338 Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true); 339 await check_results({ 340 context: createContext("weather", { 341 providers: [UrlbarProviderQuickSuggest.name], 342 isPrivate: false, 343 }), 344 matches: [ 345 QuickSuggestTestUtils.weatherResult({ temperatureUnit: osUnit }), 346 ], 347 }); 348 Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales"); 349 350 await cleanup(); 351 }, 352 }); 353 } 354 } 355 356 // Query for country in North America (US), client in same country 357 // 358 // Suggestion title should be: "{city}, {region}" 359 add_task(async function queryForNorthAmerica_clientInSameCountry() { 360 await doRegionTest({ 361 homeRegion: "US", 362 locale: "en-US", 363 query: "waterloo ia", 364 expectedTitleL10n: { 365 id: "urlbar-result-weather-title", 366 args: { 367 city: "Waterloo", 368 region: "IA", 369 }, 370 }, 371 }); 372 }); 373 374 // Query for country in North America (US), client in different North American 375 // country (CA) 376 // 377 // Suggestion title should be: "{city}, {region}, {country}" 378 add_task(async function queryForNorthAmerica_clientInNorthAmerica() { 379 await doRegionTest({ 380 homeRegion: "CA", 381 locale: "en-CA", 382 query: "waterloo ia", 383 expectedTitleL10n: { 384 id: "urlbar-result-weather-title-with-country", 385 args: { 386 city: "Waterloo", 387 region: "IA", 388 country: "United States", 389 }, 390 }, 391 }); 392 }); 393 394 // Query for country in North America (US), client in different country outside 395 // North America (GB) 396 // 397 // Suggestion title should be: "{city}, {region}, {country}" 398 add_task(async function queryForNorthAmerica_clientOutsideNorthAmerica() { 399 await doRegionTest({ 400 homeRegion: "GB", 401 locale: "en-GB", 402 query: "waterloo ia", 403 expectedTitleL10n: { 404 id: "urlbar-result-weather-title-with-country", 405 args: { 406 city: "Waterloo", 407 region: "IA", 408 country: "United States", 409 }, 410 }, 411 }); 412 }); 413 414 // Query for country outside North America (GB), client in same country 415 // 416 // Suggestion title should be: "{city}" 417 add_task(async function queryOutsideNorthAmerica_clientInSameCountry() { 418 await doRegionTest({ 419 homeRegion: "GB", 420 locale: "en-GB", 421 query: "liverpool uk", 422 expectedTitleL10n: { 423 id: "urlbar-result-weather-title-city-only", 424 args: { 425 city: "Liverpool", 426 }, 427 }, 428 }); 429 }); 430 431 // Query for country outside North America (GB), client in North American 432 // country (US) 433 // 434 // Suggestion title should be: "{city}, {region}" 435 // * `region` should be the country name (GB) 436 add_task(async function queryOutsideNorthAmerica_clientInNorthAmerica() { 437 await doRegionTest({ 438 homeRegion: "US", 439 locale: "en-US", 440 query: "liverpool uk", 441 expectedTitleL10n: { 442 id: "urlbar-result-weather-title", 443 args: { 444 city: "Liverpool", 445 region: "United Kingdom", 446 }, 447 }, 448 }); 449 }); 450 451 // Query for country outside North America (GB), client different country 452 // outside North America (DE) 453 // 454 // Suggestion title should be: "{city}, {region}" 455 // * `region` should be the country name (GB) 456 add_task(async function queryOutsideNorthAmerica_clientOutsideNorthAmerica() { 457 await doRegionTest({ 458 homeRegion: "DE", 459 locale: "de", 460 query: "liverpool uk", 461 expectedTitleL10n: { 462 id: "urlbar-result-weather-title", 463 args: { 464 city: "Liverpool", 465 region: "United Kingdom", 466 }, 467 }, 468 }); 469 }); 470 471 async function doRegionTest({ homeRegion, locale, query, expectedTitleL10n }) { 472 await QuickSuggestTestUtils.withRegionAndLocale({ 473 locale, 474 region: homeRegion, 475 // Weather suggestions are not enabled by default for all regions/locale 476 // combinations in this test, so don't reset Suggest so that they remain 477 // enabled rather than being set according to region/locale. 478 skipSuggestReset: true, 479 callback: async () => { 480 info( 481 "Doing region test: " + JSON.stringify({ homeRegion, locale, query }) 482 ); 483 await check_results({ 484 context: createContext(query, { 485 providers: [UrlbarProviderQuickSuggest.name], 486 isPrivate: false, 487 }), 488 matches: [ 489 QuickSuggestTestUtils.weatherResult({ 490 titleL10n: expectedTitleL10n, 491 }), 492 ], 493 }); 494 }, 495 }); 496 } 497 498 // Tests dismissal. 499 add_task(async function dismissal() { 500 let cleanup = GeolocationTestUtils.stubGeolocation( 501 GeolocationTestUtils.SAN_FRANCISCO 502 ); 503 504 await doDismissAllTest({ 505 result: QuickSuggestTestUtils.weatherResult(), 506 command: "dismiss", 507 feature: QuickSuggest.getFeature("WeatherSuggestions"), 508 pref: "suggest.weather", 509 queries: [ 510 { 511 query: "weather", 512 }, 513 ], 514 }); 515 516 await cleanup(); 517 }); 518 519 // When a Nimbus experiment is installed, it should override the remote settings 520 // weather record. 521 add_task(async function nimbusOverride() { 522 let cleanup = GeolocationTestUtils.stubGeolocation( 523 GeolocationTestUtils.SAN_FRANCISCO 524 ); 525 let defaultResult = QuickSuggestTestUtils.weatherResult(); 526 527 // Verify a search works as expected with the default remote settings weather 528 // record (which was added in the init task). 529 await check_results({ 530 context: createContext("weather", { 531 providers: [UrlbarProviderQuickSuggest.name], 532 isPrivate: false, 533 }), 534 matches: [defaultResult], 535 }); 536 537 // Install an experiment with a different min keyword length. 538 let nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({ 539 weatherKeywordsMinimumLength: 999, 540 }); 541 542 // The suggestion shouldn't be returned anymore. 543 await check_results({ 544 context: createContext("weather", { 545 providers: [UrlbarProviderQuickSuggest.name], 546 isPrivate: false, 547 }), 548 matches: [], 549 }); 550 551 // Uninstall the experiment. 552 await nimbusCleanup(); 553 554 // The suggestion should be returned again. 555 await check_results({ 556 context: createContext("weather", { 557 providers: [UrlbarProviderQuickSuggest.name], 558 isPrivate: false, 559 }), 560 matches: [defaultResult], 561 }); 562 563 await cleanup(); 564 }); 565 566 // Tests queries that include a city without a region and where Merino does not 567 // return a geolocation. 568 add_task(async function cityQueries_noGeo() { 569 await doCityTest({ 570 desc: "Should match most populous Waterloo (Waterloo IA)", 571 query: "waterloo", 572 geolocation: null, 573 expected: { 574 geolocationCalled: true, 575 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 576 titleL10n: { 577 id: "urlbar-result-weather-title", 578 args: { 579 city: "Waterloo", 580 region: "IA", 581 }, 582 }, 583 }, 584 }); 585 }); 586 587 // Tests queries that include a city without a region and where Merino returns a 588 // geolocation with geographic coordinates. 589 add_task(async function cityQueries_geoCoords() { 590 await doCityTest({ 591 desc: "Coordinates closer to Waterloo IA, so should match it", 592 query: "waterloo", 593 geolocation: { 594 location: { 595 latitude: 41.0, 596 longitude: -93.0, 597 }, 598 }, 599 expected: { 600 geolocationCalled: true, 601 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 602 titleL10n: { 603 id: "urlbar-result-weather-title", 604 args: { 605 city: "Waterloo", 606 region: "IA", 607 }, 608 }, 609 }, 610 }); 611 612 await doCityTest({ 613 desc: "Coordinates closer to Waterloo AL, so should match it", 614 query: "waterloo", 615 geolocation: { 616 location: { 617 latitude: 33.0, 618 longitude: -87.0, 619 }, 620 }, 621 expected: { 622 geolocationCalled: true, 623 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_AL, 624 titleL10n: { 625 id: "urlbar-result-weather-title", 626 args: { 627 city: "Waterloo", 628 region: "AL", 629 }, 630 }, 631 }, 632 }); 633 634 // This assumes the mock GeoNames data includes "Twin City A" and 635 // "Twin City B" and they're <= 5 km apart. 636 await doCityTest({ 637 desc: "When multiple cities are tied for nearest (within the accuracy radius), the most populous one should match", 638 query: "weather twin city", 639 geolocation: { 640 location: { 641 latitude: 0.0, 642 longitude: 0.0, 643 // 5 km radius 644 accuracy: 5, 645 }, 646 }, 647 expected: { 648 geolocationCalled: true, 649 weatherParams: { 650 city: "Twin City B", 651 region: "GA", 652 country: "US", 653 }, 654 titleL10n: { 655 id: "urlbar-result-weather-title-city-only", 656 args: { 657 city: "Twin City B", 658 }, 659 }, 660 }, 661 }); 662 }); 663 664 // Tests queries that include a city without a region and where Merino returns a 665 // geolocation with only region and country codes, no geographic coordinates. 666 add_task(async function cityQueries_geoRegion() { 667 await doCityTest({ 668 desc: "Should match Waterloo IA", 669 query: "waterloo", 670 geolocation: { 671 region_code: "IA", 672 country_code: "US", 673 }, 674 expected: { 675 geolocationCalled: true, 676 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 677 titleL10n: { 678 id: "urlbar-result-weather-title", 679 args: { 680 city: "Waterloo", 681 region: "IA", 682 }, 683 }, 684 }, 685 }); 686 687 await doCityTest({ 688 desc: "Should match Waterloo AL", 689 query: "waterloo", 690 geolocation: { 691 region_code: "AL", 692 country_code: "US", 693 }, 694 expected: { 695 geolocationCalled: true, 696 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_AL, 697 titleL10n: { 698 id: "urlbar-result-weather-title", 699 args: { 700 city: "Waterloo", 701 region: "AL", 702 }, 703 }, 704 }, 705 }); 706 707 await doCityTest({ 708 desc: "Rust did not return Waterloo NY, so should match most populous Waterloo (Waterloo IA)", 709 query: "waterloo", 710 geolocation: { 711 region_code: "NY", 712 country_code: "US", 713 }, 714 expected: { 715 geolocationCalled: true, 716 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 717 titleL10n: { 718 id: "urlbar-result-weather-title", 719 args: { 720 city: "Waterloo", 721 region: "IA", 722 }, 723 }, 724 }, 725 }); 726 727 await doCityTest({ 728 desc: "Rust did not return Waterloo ON CA, so should match most populous Waterloo (Waterloo IA)", 729 query: "waterloo", 730 geolocation: { 731 region_code: "08", 732 country_code: "CA", 733 }, 734 expected: { 735 geolocationCalled: true, 736 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 737 titleL10n: { 738 id: "urlbar-result-weather-title", 739 args: { 740 city: "Waterloo", 741 region: "IA", 742 }, 743 }, 744 }, 745 }); 746 747 await doCityTest({ 748 desc: "Query matches a US and CA city, geolocation is US, so should match US city", 749 query: "us ca city", 750 geolocation: { 751 region_code: "HI", 752 country_code: "US", 753 }, 754 expected: { 755 geolocationCalled: true, 756 weatherParams: { 757 city: "US CA City", 758 region: "IA", 759 country: "US", 760 }, 761 titleL10n: { 762 id: "urlbar-result-weather-title", 763 args: { 764 city: "US CA City", 765 region: "IA", 766 }, 767 }, 768 }, 769 }); 770 771 await doCityTest({ 772 desc: "Query matches a US and CA city, geolocation is CA, so should match CA city", 773 query: "us ca city", 774 geolocation: { 775 region_code: "01", 776 country_code: "CA", 777 }, 778 expected: { 779 geolocationCalled: true, 780 weatherParams: { 781 city: "US CA City", 782 region: "08", 783 country: "CA", 784 }, 785 // There isn't a geoname in the data for the region of the CA version of 786 // this city, so the city-only title should be used. 787 titleL10n: { 788 id: "urlbar-result-weather-title-city-only", 789 args: { 790 city: "US CA City", 791 }, 792 }, 793 }, 794 }); 795 }); 796 797 // Tests queries that include both a city and a region. 798 add_task(async function cityRegionQueries() { 799 await doCityTest({ 800 desc: "Waterloo IA directly queried", 801 query: "waterloo ia", 802 geolocation: null, 803 expected: { 804 geolocationCalled: false, 805 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_IA, 806 titleL10n: { 807 id: "urlbar-result-weather-title", 808 args: { 809 city: "Waterloo", 810 region: "IA", 811 }, 812 }, 813 }, 814 }); 815 816 await doCityTest({ 817 desc: "Waterloo AL directly queried", 818 query: "waterloo al", 819 geolocation: null, 820 expected: { 821 geolocationCalled: false, 822 weatherParams: EXPECTED_MERINO_PARAMS_WATERLOO_AL, 823 titleL10n: { 824 id: "urlbar-result-weather-title", 825 args: { 826 city: "Waterloo", 827 region: "AL", 828 }, 829 }, 830 }, 831 }); 832 833 await doCityTest({ 834 desc: "Waterloo NY directly queried, but Rust didn't return Waterloo NY, so no match", 835 query: "waterloo ny", 836 geolocation: null, 837 expected: null, 838 }); 839 }); 840 841 // Tests weather queries that don't include a city. 842 add_task(async function noCityQuery() { 843 let cleanup = GeolocationTestUtils.stubGeolocation( 844 GeolocationTestUtils.SAN_FRANCISCO 845 ); 846 847 await doCityTest({ 848 desc: "No city in query, so only one call to Merino should be made and Merino does the geolocation internally", 849 query: "weather", 850 geolocation: null, 851 expected: { 852 geolocationCalled: false, 853 weatherParams: {}, 854 titleL10n: { 855 id: "urlbar-result-weather-title", 856 args: { 857 city: MerinoTestUtils.WEATHER_SUGGESTION.city_name, 858 region: MerinoTestUtils.WEATHER_SUGGESTION.region_code, 859 }, 860 }, 861 }, 862 }); 863 864 await cleanup(); 865 }); 866 867 async function doCityTest({ 868 desc, 869 query, 870 geolocation, 871 expected, 872 merinoSuggestion = null, 873 }) { 874 info("Doing city test: " + JSON.stringify({ desc, query })); 875 876 if (expected) { 877 expected.weatherParams.q ??= ""; 878 } 879 880 let callsByProvider = await doSearch({ 881 query, 882 geolocation, 883 merinoSuggestion, 884 expectedTitleL10n: expected?.titleL10n, 885 }); 886 887 // Check the Merino calls. 888 Assert.equal( 889 callsByProvider.geolocation?.length || 0, 890 expected?.geolocationCalled ? 1 : 0, 891 "geolocation provider should have been called the correct number of times" 892 ); 893 Assert.equal( 894 callsByProvider.accuweather?.length || 0, 895 expected ? 1 : 0, 896 "accuweather provider should have been called the correct number of times" 897 ); 898 if (expected) { 899 expected.weatherParams.source = "urlbar"; 900 901 for (let [key, value] of Object.entries(expected.weatherParams)) { 902 Assert.strictEqual( 903 callsByProvider.accuweather[0].get(key), 904 value, 905 "Weather param should be correct: " + key 906 ); 907 } 908 } 909 } 910 911 // `MerinoClient` should cache Merino responses for geolocation and weather. 912 add_task(async function merinoCache() { 913 let query = "waterloo"; 914 let geolocation = { 915 location: { 916 latitude: 41.0, 917 longitude: -93.0, 918 }, 919 }; 920 921 MerinoTestUtils.enableClientCache(true); 922 923 let startDateMs = Date.now(); 924 let sandbox = sinon.createSandbox(); 925 let dateNowStub = sandbox.stub( 926 Cu.getGlobalForObject(MerinoClient).Date, 927 "now" 928 ); 929 dateNowStub.returns(startDateMs); 930 931 // Search 1: Firefox should call Merino for both geolocation and weather and 932 // cache the responses. 933 info("Doing search 1"); 934 let callsByProvider = await doSearch({ 935 query, 936 geolocation, 937 expectedTitleL10n: { 938 id: "urlbar-result-weather-title", 939 args: { 940 city: "Waterloo", 941 region: "IA", 942 }, 943 }, 944 }); 945 info("search 1 callsByProvider: " + JSON.stringify(callsByProvider)); 946 Assert.equal( 947 callsByProvider.geolocation.length, 948 1, 949 "geolocation provider should have been called on search 1" 950 ); 951 Assert.equal( 952 callsByProvider.accuweather.length, 953 1, 954 "accuweather provider should have been called on search 1" 955 ); 956 957 // Set the date forward 0.5 minutes, which is shorter than the geolocation 958 // cache period of 2 hours and the weather cache period of 1 minute. 959 dateNowStub.returns(startDateMs + 0.5 * 60 * 1000); 960 961 // Search 2: Firefox should use the cached responses, so it should not call 962 // Merino. 963 info("Doing search 2"); 964 callsByProvider = await doSearch({ 965 query, 966 expectedTitleL10n: { 967 id: "urlbar-result-weather-title", 968 args: { 969 city: "Waterloo", 970 region: "IA", 971 }, 972 }, 973 }); 974 info("search 2 callsByProvider: " + JSON.stringify(callsByProvider)); 975 Assert.ok( 976 !callsByProvider.geolocation, 977 "geolocation provider should not have been called on search 2" 978 ); 979 Assert.ok( 980 !callsByProvider.accuweather, 981 "accuweather provider should not have been called on search 2" 982 ); 983 984 // Set the date forward 1.5 minutes, which is shorter than the geolocation 985 // cache period but longer than the weather cache period. 986 dateNowStub.returns(startDateMs + 1.5 * 60 * 1000); 987 988 // Search 3: Firefox should call Merino for the weather suggestion but not for 989 // geolocation. 990 info("Doing search 3"); 991 callsByProvider = await doSearch({ 992 query, 993 expectedTitleL10n: { 994 id: "urlbar-result-weather-title", 995 args: { 996 city: "Waterloo", 997 region: "IA", 998 }, 999 }, 1000 }); 1001 info("search 3 callsByProvider: " + JSON.stringify(callsByProvider)); 1002 Assert.ok( 1003 !callsByProvider.geolocation, 1004 "geolocation provider should not have been called on search 3" 1005 ); 1006 Assert.equal( 1007 callsByProvider.accuweather.length, 1008 1, 1009 "accuweather provider should have been called on search 3" 1010 ); 1011 1012 // Set the date forward 1.5 hours that is still shorter than the geolocation 1013 // period. 1014 dateNowStub.returns(startDateMs + 1.5 * 60 * 60 * 1000); 1015 1016 // Search 4: Firefox should still call Merino for the weather suggestion but 1017 // not for geolocation. 1018 info("Doing search 4"); 1019 callsByProvider = await doSearch({ 1020 query, 1021 expectedTitleL10n: { 1022 id: "urlbar-result-weather-title", 1023 args: { 1024 city: "Waterloo", 1025 region: "IA", 1026 }, 1027 }, 1028 }); 1029 info("search 4 callsByProvider: " + JSON.stringify(callsByProvider)); 1030 Assert.ok( 1031 !callsByProvider.geolocation, 1032 "geolocation provider should not have been called on search 4" 1033 ); 1034 Assert.equal( 1035 callsByProvider.accuweather.length, 1036 1, 1037 "accuweather provider should have been called on search 4" 1038 ); 1039 1040 // Set the date forward 3 hours. 1041 dateNowStub.returns(startDateMs + 3 * 60 * 60 * 1000); 1042 1043 // Search 5: Firefox should call Merino for both weather and geolocation. 1044 info("Doing search 5"); 1045 callsByProvider = await doSearch({ 1046 query, 1047 expectedTitleL10n: { 1048 id: "urlbar-result-weather-title", 1049 args: { 1050 city: "Waterloo", 1051 region: "IA", 1052 }, 1053 }, 1054 }); 1055 info("search 5 callsByProvider: " + JSON.stringify(callsByProvider)); 1056 Assert.equal( 1057 callsByProvider.geolocation.length, 1058 1, 1059 "geolocation provider should have been called on search 5" 1060 ); 1061 Assert.equal( 1062 callsByProvider.accuweather.length, 1063 1, 1064 "accuweather provider should have been called on search 5" 1065 ); 1066 1067 sandbox.restore(); 1068 MerinoTestUtils.enableClientCache(false); 1069 }); 1070 1071 async function doSearch({ 1072 query, 1073 geolocation, 1074 merinoSuggestion, 1075 expectedTitleL10n, 1076 }) { 1077 let callsByProvider = {}; 1078 1079 // Set up the Merino request handler. 1080 MerinoTestUtils.server.requestHandler = req => { 1081 let params = new URLSearchParams(req.queryString); 1082 let provider = params.get("providers"); 1083 callsByProvider[provider] ||= []; 1084 callsByProvider[provider].push(params); 1085 1086 // Handle geolocation requests. 1087 if (provider == "geolocation") { 1088 return { 1089 body: { 1090 request_id: "request_id", 1091 suggestions: !geolocation 1092 ? [] 1093 : [ 1094 { 1095 custom_details: { geolocation }, 1096 }, 1097 ], 1098 }, 1099 }; 1100 } 1101 1102 // Handle accuweather requests. 1103 Assert.equal( 1104 provider, 1105 "accuweather", 1106 "Sanity check: If the request isn't geolocation, it should be accuweather" 1107 ); 1108 let suggestion = { 1109 ...WEATHER_SUGGESTION, 1110 ...(merinoSuggestion ?? {}), 1111 }; 1112 return { 1113 body: { 1114 request_id: "request_id", 1115 suggestions: [suggestion], 1116 }, 1117 }; 1118 }; 1119 1120 // Do a search. 1121 await check_results({ 1122 context: createContext(query, { 1123 providers: [UrlbarProviderQuickSuggest.name], 1124 isPrivate: false, 1125 }), 1126 matches: !expectedTitleL10n 1127 ? [] 1128 : [ 1129 QuickSuggestTestUtils.weatherResult({ 1130 titleL10n: expectedTitleL10n, 1131 }), 1132 ], 1133 }); 1134 1135 MerinoTestUtils.server.requestHandler = null; 1136 return callsByProvider; 1137 } 1138 1139 function assertDisabled({ message }) { 1140 info("Asserting feature is disabled"); 1141 if (message) { 1142 info(message); 1143 } 1144 Assert.strictEqual(gWeather._test_merino, null, "Merino client is null"); 1145 }