summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/quicksuggest/unit/test_weather.js')
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_weather.js1394
1 files changed, 1394 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
new file mode 100644
index 0000000000..04fbc0b9d8
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
@@ -0,0 +1,1394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the quick suggest weather feature.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_WEATHER_MS";
+const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE_WEATHER";
+
+const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils;
+
+add_task(async function init() {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: WEATHER_RS_DATA,
+ },
+ ],
+ });
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+
+ await MerinoTestUtils.initWeather();
+
+ // Give this a small value so it doesn't delay the test too long. Choose a
+ // value that's unlikely to be used anywhere else in the test so that when
+ // `lastFetchTimeMs` is expected to be `fetchDelayAfterComingOnlineMs`, we can
+ // be sure the value actually came from `fetchDelayAfterComingOnlineMs`.
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs = 53;
+});
+
+// The feature should be properly uninitialized when it's disabled and then
+// re-initialized when it's re-enabled. This task disables the feature using the
+// feature gate pref.
+add_task(async function disableAndEnable_featureGate() {
+ await doBasicDisableAndEnableTest("weather.featureGate");
+});
+
+// The feature should be properly uninitialized when it's disabled and then
+// re-initialized when it's re-enabled. This task disables the feature using the
+// suggest pref.
+add_task(async function disableAndEnable_suggestPref() {
+ await doBasicDisableAndEnableTest("suggest.weather");
+});
+
+async function doBasicDisableAndEnableTest(pref) {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set(pref, false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // No suggestion should be returned for a search.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ info("Re-enable the feature");
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set(pref, true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ await fetchPromise;
+ assertEnabled({
+ message: "After awaiting fetch",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ // The suggestion should be returned for a search.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+}
+
+add_task(async function keywordsNotDefined() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Set RS data without any keywords. Fetching should immediately stop.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {},
+ },
+ ]);
+ assertDisabled({
+ message: "After setting RS data without keywords",
+ pendingFetchCount: 0,
+ });
+
+ // No suggestion should be returned for a search.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Set keywords. Fetching should immediately start.
+ info("Setting keywords");
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ assertEnabled({
+ message: "Immediately after setting keywords",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ await fetchPromise;
+ assertEnabled({
+ message: "After awaiting fetch",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+
+ // The suggestion should be returned for a search.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+});
+
+// Disables and re-enables the feature without waiting for any intermediate
+// fetches to complete, using the following steps:
+//
+// 1. Disable
+// 2. Enable
+// 3. Disable again
+//
+// At this point, the fetch from step 2 will remain ongoing but once it finishes
+// it should be discarded since the feature is disabled.
+add_task(async function disableAndEnable_immediate1() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ // Disable it again. The fetch will remain ongoing since pending fetches
+ // aren't stopped when the feature is disabled.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling again",
+ pendingFetchCount: 1,
+ });
+
+ // Wait for the fetch to finish.
+ await fetchPromise;
+
+ // The fetched suggestion should be discarded and the feature should remain
+ // uninitialized.
+ assertDisabled({
+ message: "After awaiting fetch",
+ pendingFetchCount: 0,
+ });
+
+ // Clean up by re-enabling the feature for the remaining tasks.
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ await fetchPromise;
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Disables and re-enables the feature without waiting for any intermediate
+// fetches to complete, using the following steps:
+//
+// 1. Disable
+// 2. Enable
+// 3. Disable again
+// 4. Enable again
+//
+// At this point, the fetches from steps 2 and 4 will remain ongoing. The fetch
+// from step 2 should be discarded.
+add_task(async function disableAndEnable_immediate2() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ // Disable it again. The fetch will remain ongoing since pending fetches
+ // aren't stopped when the feature is disabled.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling again",
+ pendingFetchCount: 1,
+ });
+
+ // Re-enable it. A new fetch should start, so now there will be two pending
+ // fetches.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling again",
+ hasSuggestion: false,
+ pendingFetchCount: 2,
+ });
+
+ // Wait for both fetches to finish.
+ await fetchPromise;
+ assertEnabled({
+ message: "Immediately after re-enabling again",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A fetch that doesn't return a suggestion should cause the last-fetched
+// suggestion to be discarded.
+add_task(async function noSuggestion() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ let { suggestions } = MerinoTestUtils.server.response.body;
+ MerinoTestUtils.server.response.body.suggestions = [];
+
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "no_suggestion",
+ "The request successfully finished without suggestions"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "no_suggestion",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ MerinoTestUtils.server.response.body.suggestions = suggestions;
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A network error should cause the last-fetched suggestion to be discarded.
+add_task(async function networkError() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Set the weather fetch timeout high enough that the network error exception
+ // will happen first. See `MerinoTestUtils.withNetworkError()`.
+ QuickSuggest.weather._test_setTimeoutMs(10000);
+
+ await MerinoTestUtils.server.withNetworkError(async () => {
+ await QuickSuggest.weather._test_fetch();
+ });
+
+ QuickSuggest.weather._test_setTimeoutMs(-1);
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "network_error",
+ "The request failed with a network error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "network_error",
+ latencyRecorded: false,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// An HTTP error should cause the last-fetched suggestion to be discarded.
+add_task(async function httpError() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ MerinoTestUtils.server.response = { status: 500 };
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "http_error",
+ "The request failed with an HTTP error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "http_error",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ MerinoTestUtils.server.reset();
+ MerinoTestUtils.server.response.body.suggestions = [WEATHER_SUGGESTION];
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A fetch that doesn't return a suggestion due to a client timeout should cause
+// the last-fetched suggestion to be discarded.
+add_task(async function clientTimeout() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Make the server return a delayed response so the Merino client times out
+ // waiting for it.
+ MerinoTestUtils.server.response.delay = 400;
+
+ // Make the client time out immediately.
+ QuickSuggest.weather._test_setTimeoutMs(1);
+
+ // Set up a promise that will be resolved when the client finally receives the
+ // response.
+ let responsePromise = QuickSuggest.weather._test_merino.waitForNextResponse();
+
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "timeout",
+ "The request timed out"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "timeout",
+ latencyRecorded: false,
+ latencyStopwatchRunning: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Await the response.
+ await responsePromise;
+
+ // The `checkAndClearHistograms()` call above cleared the histograms. After
+ // that, nothing else should have been recorded for the response.
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: null,
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ QuickSuggest.weather._test_setTimeoutMs(-1);
+ delete MerinoTestUtils.server.response.delay;
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Locale task for when this test runs on an en-US OS.
+add_task(async function locale_enUS() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => osLocale == "en-US",
+ osUnit: "f",
+ unitsByLocale: {
+ "en-US": "f",
+ // When the app's locale is set to any en-* locale, F will be used because
+ // `regionalPrefsLocales` will prefer the en-US OS locale.
+ "en-CA": "f",
+ "en-GB": "f",
+ de: "c",
+ },
+ });
+});
+
+// Locale task for when this test runs on a non-US English OS.
+add_task(async function locale_nonUSEnglish() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => osLocale.startsWith("en") && osLocale != "en-US",
+ osUnit: "c",
+ unitsByLocale: {
+ // When the app's locale is set to en-US, C will be used because
+ // `regionalPrefsLocales` will prefer the non-US English OS locale.
+ "en-US": "c",
+ "en-CA": "c",
+ "en-GB": "c",
+ de: "c",
+ },
+ });
+});
+
+// Locale task for when this test runs on a non-English OS.
+add_task(async function locale_nonEnglish() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => !osLocale.startsWith("en"),
+ osUnit: "c",
+ unitsByLocale: {
+ "en-US": "f",
+ "en-CA": "c",
+ "en-GB": "c",
+ de: "c",
+ },
+ });
+});
+
+/**
+ * Testing locales is tricky due to the weather feature's use of
+ * `Services.locale.regionalPrefsLocales`. By default `regionalPrefsLocales`
+ * prefers the OS locale if its language is the same as the app locale's
+ * language; otherwise it prefers the app locale. For example, assuming the OS
+ * locale is en-CA, then if the app locale is en-US it will prefer en-CA since
+ * both are English, but if the app locale is de it will prefer de. If the pref
+ * `intl.regional_prefs.use_os_locales` is set, then the OS locale is always
+ * preferred.
+ *
+ * This function tests a given set of locales with and without
+ * `intl.regional_prefs.use_os_locales` set.
+ *
+ * @param {object} options
+ * Options
+ * @param {Function} options.shouldRunTask
+ * Called with the OS locale. Should return true if the function should run.
+ * Use this to skip tasks that don't target a desired OS locale.
+ * @param {string} options.osUnit
+ * The expected "c" or "f" unit for the OS locale.
+ * @param {object} options.unitsByLocale
+ * The expected "c" or "f" unit when the app's locale is set to particular
+ * locales. This should be an object that maps locales to expected units. For
+ * each locale in the object, the app's locale is set to that locale and the
+ * actual unit is expected to be the unit in the object.
+ */
+async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) {
+ Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true);
+ let osLocale = Services.locale.regionalPrefsLocales[0];
+ Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales");
+
+ if (!shouldRunTask(osLocale)) {
+ info("Skipping task, should not run for this OS locale");
+ return;
+ }
+
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Sanity check initial locale info.
+ Assert.equal(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "Initial app locale should be en-US"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref("intl.regional_prefs.use_os_locales"),
+ "intl.regional_prefs.use_os_locales should be false initially"
+ );
+
+ // Check locales.
+ for (let [locale, temperatureUnit] of Object.entries(unitsByLocale)) {
+ await QuickSuggestTestUtils.withLocales([locale], async () => {
+ info("Checking locale: " + locale);
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult({ temperatureUnit })],
+ });
+
+ info(
+ "Checking locale with intl.regional_prefs.use_os_locales: " + locale
+ );
+ Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true);
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult({ temperatureUnit: osUnit })],
+ });
+ Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales");
+ });
+ }
+}
+
+// Blocks a result and makes sure the weather pref is disabled.
+add_task(async function block() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+ Assert.ok(
+ UrlbarPrefs.get("suggest.weather"),
+ "Sanity check: suggest.weather is true initially"
+ );
+
+ // Do a search so we can get an actual result.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+
+ // Block the result.
+ UrlbarProviderWeather.onEngagement(false, "engagement", context, {
+ result: context.results[0],
+ selType: "dismiss",
+ selIndex: context.results[0].rowIndex,
+ });
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.weather"),
+ "suggest.weather is false after blocking the result"
+ );
+
+ // Do a second search. Nothing should be returned.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Re-enable the pref and clean up.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("suggest.weather", true);
+ await fetchPromise;
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Simulates wake 100ms before the start of the next fetch period. A new fetch
+// should not start.
+add_task(async function wakeBeforeNextFetchPeriod() {
+ await doWakeTest({
+ sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs - 100,
+ shouldFetchOnWake: false,
+ fetchTimerMsOnWake: 100,
+ });
+});
+
+// Simulates wake 100ms after the start of the next fetch period. A new fetch
+// should start.
+add_task(async function wakeAfterNextFetchPeriod() {
+ await doWakeTest({
+ sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs + 100,
+ shouldFetchOnWake: true,
+ });
+});
+
+// Simulates wake after many fetch periods + 100ms. A new fetch should start.
+add_task(async function wakeAfterManyFetchPeriods() {
+ await doWakeTest({
+ sleepIntervalMs: 100 * QuickSuggest.weather._test_fetchIntervalMs + 100,
+ shouldFetchOnWake: true,
+ });
+});
+
+async function doWakeTest({
+ sleepIntervalMs,
+ shouldFetchOnWake,
+ fetchTimerMsOnWake,
+}) {
+ // Make `Date.now()` return a value under our control, doesn't matter what it
+ // is. This is the time the first fetch period will start.
+ let nowOnStart = 100;
+ let sandbox = sinon.createSandbox();
+ let dateNowStub = sandbox.stub(
+ Cu.getGlobalForObject(QuickSuggest.weather).Date,
+ "now"
+ );
+ dateNowStub.returns(nowOnStart);
+
+ // Start the first fetch period.
+ info("Starting first fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "Last fetch time should be updated after fetch"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ // Advance the clock and simulate wake.
+ info("Sending wake notification");
+ let nowOnWake = nowOnStart + sleepIntervalMs;
+ dateNowStub.returns(nowOnWake);
+ QuickSuggest.weather.observe(null, "wake_notification", "");
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "After wake, next fetch should not have immediately started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "After wake, last fetch time should be unchanged"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "After wake, the timer should exist (be non-zero)"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "After wake, a new timer should have been created"
+ );
+
+ if (shouldFetchOnWake) {
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs,
+ "After wake, timer period should be fetchDelayAfterComingOnlineMs"
+ );
+ } else {
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ fetchTimerMsOnWake,
+ "After wake, timer period should be the remaining interval"
+ );
+ }
+
+ // Wait for the fetch. If the wake didn't trigger it, then the caller should
+ // have passed in a `sleepIntervalMs` that will make it start soon.
+ info("Waiting for fetch after wake");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "After post-wake fetch, timer period should remain full fetch interval"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "After post-wake fetch, no more fetches should be pending"
+ );
+
+ dateNowStub.restore();
+}
+
+// When network:link-status-changed is observed and the suggestion is non-null,
+// a fetch should not start.
+add_task(async function networkLinkStatusChanged_nonNull() {
+ // See nsINetworkLinkService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "network:link-status-changed",
+ dataValues: [
+ "down",
+ "up",
+ "changed",
+ "unknown",
+ "this is not a valid data value",
+ ],
+ });
+});
+
+// When network:offline-status-changed is observed and the suggestion is
+// non-null, a fetch should not start.
+add_task(async function networkOfflineStatusChanged_nonNull() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "network:offline-status-changed",
+ dataValues: ["offline", "online", "this is not a valid data value"],
+ });
+});
+
+// When captive-portal-login-success is observed and the suggestion is non-null,
+// a fetch should not start.
+add_task(async function captivePortalLoginSuccess_nonNull() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "captive-portal-login-success",
+ dataValues: [""],
+ });
+});
+
+async function doOnlineTestWithSuggestion({ topic, dataValues }) {
+ info("Starting fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(
+ QuickSuggest.weather.suggestion,
+ "Suggestion should have been fetched"
+ );
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ for (let data of dataValues) {
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "Timer should not have been recreated"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be the full fetch interval"
+ );
+ }
+}
+
+// When network:link-status-changed is observed and the suggestion is null, a
+// fetch should start unless the data indicates the status is offline.
+add_task(async function networkLinkStatusChanged_null() {
+ // See nsINetworkLinkService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "network:link-status-changed",
+ offlineData: "down",
+ otherDataValues: [
+ "up",
+ "changed",
+ "unknown",
+ "this is not a valid data value",
+ ],
+ });
+});
+
+// When network:offline-status-changed is observed and the suggestion is null, a
+// fetch should start unless the data indicates the status is offline.
+add_task(async function networkOfflineStatusChanged_null() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "network:offline-status-changed",
+ offlineData: "offline",
+ otherDataValues: ["online", "this is not a valid data value"],
+ });
+});
+
+// When captive-portal-login-success is observed and the suggestion is null, a
+// fetch should start.
+add_task(async function captivePortalLoginSuccess_null() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "captive-portal-login-success",
+ otherDataValues: [""],
+ });
+});
+
+async function doOnlineTestWithNullSuggestion({
+ topic,
+ otherDataValues,
+ offlineData = "",
+}) {
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ // First, send the notification with the offline data value. Nothing should
+ // happen.
+ if (offlineData) {
+ info("Sending notification: " + JSON.stringify({ topic, offlineData }));
+ QuickSuggest.weather.observe(null, topic, offlineData);
+
+ Assert.ok(
+ !QuickSuggest.weather.suggestion,
+ "Suggestion should remain null"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "Timer should not have been recreated"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be the full fetch interval"
+ );
+ }
+
+ // Now send it with all other data values. Fetches should be triggered.
+ for (let data of otherDataValues) {
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started yet"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "A new timer should have been created"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs,
+ "Timer ms should be fetchDelayAfterComingOnlineMs"
+ );
+
+ timer = QuickSuggest.weather._test_fetchTimer;
+
+ info("Waiting for fetch after notification");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not be pending"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "A new timer should have been created"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ timer = QuickSuggest.weather._test_fetchTimer;
+ }
+}
+
+// When many online notifications are received at once, only one fetch should
+// start.
+add_task(async function manyOnlineNotifications() {
+ await doManyNotificationsTest([
+ ["network:link-status-changed", "changed"],
+ ["network:link-status-changed", "up"],
+ ["network:offline-status-changed", "online"],
+ ]);
+});
+
+// When wake and online notifications are received at once, only one fetch
+// should start.
+add_task(async function wakeAndOnlineNotifications() {
+ await doManyNotificationsTest([
+ ["wake_notification", ""],
+ ["network:link-status-changed", "changed"],
+ ["network:link-status-changed", "up"],
+ ["network:offline-status-changed", "online"],
+ ]);
+});
+
+async function doManyNotificationsTest(notifications) {
+ // Make `Date.now()` return a value under our control, doesn't matter what it
+ // is. This is the time the first fetch period will start.
+ let nowOnStart = 100;
+ let sandbox = sinon.createSandbox();
+ let dateNowStub = sandbox.stub(
+ Cu.getGlobalForObject(QuickSuggest.weather).Date,
+ "now"
+ );
+ dateNowStub.returns(nowOnStart);
+
+ // Start a first fetch period so that after we send the notifications below
+ // the last fetch time will be in the past.
+ info("Starting first fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "Last fetch time should be updated after fetch"
+ );
+
+ // Now advance the clock by many fetch intervals.
+ let nowOnWake = nowOnStart + 100 * QuickSuggest.weather._test_fetchIntervalMs;
+ dateNowStub.returns(nowOnWake);
+
+ // Set the suggestion to null so online notifications will trigger a fetch.
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Clear the server's list of received requests.
+ MerinoTestUtils.server.reset();
+ MerinoTestUtils.server.response.body.suggestions = [
+ MerinoTestUtils.WEATHER_SUGGESTION,
+ ];
+
+ // Send the notifications.
+ for (let [topic, data] of notifications) {
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+ }
+
+ info("Waiting for fetch after notifications");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not be pending"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ Assert.equal(
+ MerinoTestUtils.server.requests.length,
+ 1,
+ "Merino should have received only one request"
+ );
+
+ dateNowStub.restore();
+}
+
+// Fetching when a VPN is detected should set the suggestion to null, and
+// turning off the VPN should trigger a re-fetch.
+add_task(async function vpn() {
+ // Register a mock object that implements nsINetworkLinkService.
+ let mockLinkService = {
+ isLinkUp: true,
+ linkStatusKnown: true,
+ linkType: Ci.nsINetworkLinkService.LINK_TYPE_WIFI,
+ networkID: "abcd",
+ dnsSuffixList: [],
+ platformDNSIndications: Ci.nsINetworkLinkService.NONE_DETECTED,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+ let networkLinkServiceCID = MockRegistrar.register(
+ "@mozilla.org/network/network-link-service;1",
+ mockLinkService
+ );
+ QuickSuggest.weather._test_linkService = mockLinkService;
+
+ // At this point no VPN is detected, so a fetch should complete successfully.
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should exist");
+
+ // Modify the mock link service to indicate a VPN is detected.
+ mockLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.VPN_DETECTED;
+
+ // Now a fetch should set the suggestion to null.
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Set `weather.ignoreVPN` and fetch again. It should complete successfully.
+ UrlbarPrefs.set("weather.ignoreVPN", true);
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched");
+
+ // Clear the pref and fetch again. It should set the suggestion back to null.
+ UrlbarPrefs.clear("weather.ignoreVPN");
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Simulate the link status changing. Since the mock link service still
+ // indicates a VPN is detected, the suggestion should remain null.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ QuickSuggest.weather.observe(null, "network:link-status-changed", "changed");
+ await fetchPromise;
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should remain null");
+
+ // Modify the mock link service to indicate a VPN is no longer detected.
+ mockLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.NONE_DETECTED;
+
+ // Simulate the link status changing again. The suggestion should be fetched.
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ QuickSuggest.weather.observe(null, "network:link-status-changed", "changed");
+ await fetchPromise;
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched");
+
+ MockRegistrar.unregister(networkLinkServiceCID);
+ delete QuickSuggest.weather._test_linkService;
+});
+
+// When a Nimbus experiment is installed, it should override the remote settings
+// config.
+add_task(async function nimbusOverride() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Verify a search works as expected with the default remote settings config.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // Install an experiment with a different keyword and min length.
+ let nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({
+ weatherKeywords: ["nimbusoverride"],
+ weatherKeywordsMinimumLength: "nimbus".length,
+ });
+
+ // The usual default keyword shouldn't match.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ // The new keyword from Nimbus should match.
+ await check_results({
+ context: createContext("nimbusoverride", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+ await check_results({
+ context: createContext("nimbus", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // Uninstall the experiment.
+ await nimbusCleanup();
+
+ // The usual default keyword should match again.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // The keywords from Nimbus shouldn't match anymore.
+ await check_results({
+ context: createContext("nimbusoverride", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+ await check_results({
+ context: createContext("nimbus", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+});
+
+function assertEnabled({ message, hasSuggestion, pendingFetchCount }) {
+ info("Asserting feature is enabled");
+ if (message) {
+ info(message);
+ }
+
+ Assert.equal(
+ !!QuickSuggest.weather.suggestion,
+ hasSuggestion,
+ "Suggestion is null or non-null as expected"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Fetch timer is non-zero"
+ );
+ Assert.ok(QuickSuggest.weather._test_merino, "Merino client is non-null");
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ pendingFetchCount,
+ "Expected pending fetch count"
+ );
+}
+
+function assertDisabled({ message, pendingFetchCount }) {
+ info("Asserting feature is disabled");
+ if (message) {
+ info(message);
+ }
+
+ Assert.strictEqual(
+ QuickSuggest.weather.suggestion,
+ null,
+ "Suggestion is null"
+ );
+ Assert.strictEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Fetch timer is zero"
+ );
+ Assert.strictEqual(
+ QuickSuggest.weather._test_merino,
+ null,
+ "Merino client is null"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ pendingFetchCount,
+ "Expected pending fetch count"
+ );
+}
+
+function makeExpectedResult({
+ suggestedIndex = 1,
+ temperatureUnit = undefined,
+} = {}) {
+ if (!temperatureUnit) {
+ temperatureUnit =
+ Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
+ }
+
+ return {
+ suggestedIndex,
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ temperatureUnit,
+ url: WEATHER_SUGGESTION.url,
+ iconId: "6",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ requestId: MerinoTestUtils.server.response.body.request_id,
+ source: "merino",
+ merinoProvider: "accuweather",
+ dynamicType: "weather",
+ city: WEATHER_SUGGESTION.city_name,
+ temperature:
+ WEATHER_SUGGESTION.current_conditions.temperature[temperatureUnit],
+ currentConditions: WEATHER_SUGGESTION.current_conditions.summary,
+ forecast: WEATHER_SUGGESTION.forecast.summary,
+ high: WEATHER_SUGGESTION.forecast.high[temperatureUnit],
+ low: WEATHER_SUGGESTION.forecast.low[temperatureUnit],
+ shouldNavigate: true,
+ },
+ };
+}