tor-browser

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

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 }