/* 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/. */ import { CONTENT_MESSAGE_TYPE } from "common/Actions.mjs"; import { WIDGET_REGISTRY } from "common/WidgetsRegistry.mjs"; import { DEFAULT_SITES } from "lib/DefaultSites.sys.mjs"; import { ActivityStream, PREFS_CONFIG, csvPrefHasValue, } from "lib/ActivityStream.sys.mjs"; import { mockServices, stubGlobals } from "test/jest/test-utils"; // Stand-ins for the lazily loaded feed classes. The real feed modules cannot be // imported here (they statically import resource://gre/modules/*), and the // assertions below only care that the factory registered for each pref returns // a feed. function feedStubs() { return { AboutPreferences: class AboutPreferences {}, DiscoveryStreamFeed: class DiscoveryStreamFeed {}, FaviconFeed: class FaviconFeed {}, HighlightsFeed: class HighlightsFeed {}, NewTabInit: class NewTabInit {}, PlacesFeed: class PlacesFeed {}, PrefsFeed: class PrefsFeed {}, SectionsFeed: class SectionsFeed {}, SystemTickFeed: class SystemTickFeed {}, TelemetryFeed: class TelemetryFeed {}, TopSitesFeed: class TopSitesFeed {}, TopStoriesFeed: class TopStoriesFeed {}, }; } function FakeStore() { return { init: jest.fn(), uninit: jest.fn(), feeds: { get: () => {} } }; } // Enough of DefaultPrefs for the dynamic pref computation: it reads back // whatever default was last written for a pref. class FakeDefaultPrefs { constructor(config) { this._config = config; this._values = new Map(); this.init = jest.fn(); } get(name) { return this._values.get(name); } set(name, value) { this._values.set(name, value); } } /** * A jest.fn() with sinon's withArgs semantics: the first matching argument * prefix wins, and anything unmatched falls through to `fallback`. * * @param {Function} fallback Implementation for unmatched calls. * @returns {Function} jest.fn() with an extra whenCalledWith(...args) helper. */ function argsStub(fallback = () => undefined) { const behaviors = []; const fn = jest.fn((...args) => { const behavior = behaviors.find(({ expected }) => expected.every((value, i) => value === args[i]) ); return behavior ? behavior.value : fallback(...args); }); fn.whenCalledWith = (...expected) => ({ returns(value) { behaviors.unshift({ expected, value }); return fn; }, }); return fn; } const STORIES_REGION_LOCALE_PREF = "browser.newtabpage.activity-stream.discoverystream.stories-region-locale-config"; describe("ActivityStream", () => { let as; let restoreGlobals; let services; let region; let nimbusFeatures; let proxyService; beforeEach(() => { services = mockServices([ "appinfo", "locale", "obs", "prefs", "urlFormatter", ]); region = { home: "US", REGION_TOPIC: "browser-region-updated" }; nimbusFeatures = { pocketNewtab: { getVariable: argsStub() }, newtabTrainhop: { getAllEnrollments: jest.fn(() => []) }, }; proxyService = { registerChannelFilter: jest.fn(), unregisterChannelFilter: jest.fn(), newProxyInfo: jest.fn(), }; restoreGlobals = stubGlobals({ Services: services, Region: region, NimbusFeatures: nimbusFeatures, ProxyService: proxyService, AboutNewTabParent: { loadedTabs: new Set() }, Store: FakeStore, DefaultPrefs: FakeDefaultPrefs, DEFAULT_SITES, NewTabActorRegistry: { init: jest.fn(), uninit: jest.fn() }, ...feedStubs(), }); as = new ActivityStream(); PREFS_CONFIG.get("feeds.system.topstories").value = undefined; }); afterEach(() => { restoreGlobals(); }); it("should exist", () => { expect(ActivityStream).toBeTruthy(); }); it("should initialize with .initialized=false", () => { expect(as.initialized).toBe(false); }); it("should have a null createdInstant if not constructed with one", () => { const noCreatedInstantAS = new ActivityStream(); expect(noCreatedInstantAS.createdInstant).toBeNull(); }); it("should have a createdInstant value exposed if constructed with one", () => { // The Node environment does not know what Temporal is, but we can pretend // that a Date is a temporal, since ActivityStream isn't really doing any // type-checking here - it's just holding onto whatever it was constructed // with, and exposing it with a getter. const instant = new Date(); const createdInstantAS = new ActivityStream(instant); expect(createdInstantAS.createdInstant).toBe(instant); }); describe("#init", () => { beforeEach(() => { as.init(); }); it("should initialize default prefs", () => { expect(as._defaultPrefs.init).toHaveBeenCalledTimes(1); }); it("should set .initialized to true", () => { expect(as.initialized).toBe(true); }); it("should call .store.init", () => { expect(as.store.init).toHaveBeenCalledTimes(1); }); it("should pass to Store an INIT event for content", () => { const [[, action]] = as.store.init.mock.calls; expect(action.meta.to).toBe(CONTENT_MESSAGE_TYPE); }); it("should pass to Store an UNINIT event", () => { const [[, , action]] = as.store.init.mock.calls; expect(action.type).toBe("UNINIT"); }); it("should call addObserver for the app locales", () => { as.init(); expect(services.obs.addObserver).toHaveBeenCalledWith( as, "intl:app-locales-changed" ); }); }); describe("#uninit", () => { beforeEach(() => { as.init(); as.uninit(); }); it("should set .initialized to false", () => { expect(as.initialized).toBe(false); }); it("should call .store.uninit", () => { expect(as.store.uninit).toHaveBeenCalledTimes(1); }); it("should call removeObserver for the region", () => { as.geo = ""; as.uninit(); expect(services.obs.removeObserver).toHaveBeenCalledWith( as, region.REGION_TOPIC ); }); it("should call removeObserver for the app locales", () => { as.uninit(); expect(services.obs.removeObserver).toHaveBeenCalledWith( as, "intl:app-locales-changed" ); }); }); describe("#observe", () => { it("should call _updateDynamicPrefs from observe", () => { const updateStub = jest .spyOn(as, "_updateDynamicPrefs") .mockImplementation(() => {}); as.observe(undefined, region.REGION_TOPIC); expect(updateStub).toHaveBeenCalledTimes(1); }); }); describe("feeds", () => { it("should create a NewTabInit feed", () => { const feed = as.feeds.get("feeds.newtabinit")(); expect(feed).toBeTruthy(); }); it("should create a Places feed", () => { const feed = as.feeds.get("feeds.places")(); expect(feed).toBeTruthy(); }); it("should create a TopSites feed", () => { const feed = as.feeds.get("feeds.system.topsites")(); expect(feed).toBeTruthy(); }); it("should create a Telemetry feed", () => { const feed = as.feeds.get("feeds.telemetry")(); expect(feed).toBeTruthy(); }); it("should create a Prefs feed", () => { const feed = as.feeds.get("feeds.prefs")(); expect(feed).toBeTruthy(); }); it("should create a HighlightsFeed feed", () => { const feed = as.feeds.get("feeds.section.highlights")(); expect(feed).toBeTruthy(); }); it("should create a TopStoriesFeed feed", () => { const feed = as.feeds.get("feeds.system.topstories")(); expect(feed).toBeTruthy(); }); it("should create a AboutPreferences feed", () => { const feed = as.feeds.get("feeds.aboutpreferences")(); expect(feed).toBeTruthy(); }); it("should create a SectionsFeed", () => { const feed = as.feeds.get("feeds.sections")(); expect(feed).toBeTruthy(); }); it("should create a SystemTick feed", () => { const feed = as.feeds.get("feeds.systemtick")(); expect(feed).toBeTruthy(); }); it("should create a Favicon feed", () => { const feed = as.feeds.get("feeds.favicon")(); expect(feed).toBeTruthy(); }); it("should create a DiscoveryStreamFeed feed", () => { const feed = as.feeds.get("feeds.discoverystreamfeed")(); expect(feed).toBeTruthy(); }); }); describe("_migratePref", () => { it("should migrate a pref if the user has set a custom value", () => { services.prefs.prefHasUserValue.mockReturnValue(true); services.prefs.getPrefType.mockReturnValue(services.prefs.PREF_INT); services.prefs.getIntPref.mockReturnValue(10); const callback = jest.fn(); as._migratePref("oldPrefName", callback); expect(callback).toHaveBeenCalledWith(10); }); it("should not migrate a pref if the user has not set a custom value", () => { // we bailed out early so we don't check the pref type later services.prefs.prefHasUserValue.mockReturnValue(false); as._migratePref("oldPrefName"); expect(services.prefs.getPrefType).not.toHaveBeenCalled(); }); it("should use the proper pref getter for each type", () => { services.prefs.prefHasUserValue.mockReturnValue(true); // Integer services.prefs.getPrefType.mockReturnValue(services.prefs.PREF_INT); as._migratePref("oldPrefName", () => {}); expect(services.prefs.getIntPref).toHaveBeenCalledWith("oldPrefName"); // Boolean services.prefs.getPrefType.mockReturnValue(services.prefs.PREF_BOOL); as._migratePref("oldPrefName", () => {}); expect(services.prefs.getBoolPref).toHaveBeenCalledWith("oldPrefName"); // String services.prefs.getPrefType.mockReturnValue(services.prefs.PREF_STRING); as._migratePref("oldPrefName", () => {}); expect(services.prefs.getStringPref).toHaveBeenCalledWith("oldPrefName"); }); it("should clear the old pref after setting the new one", () => { services.prefs.prefHasUserValue.mockReturnValue(true); services.prefs.getPrefType.mockReturnValue(services.prefs.PREF_INT); as._migratePref("oldPrefName", () => {}); expect(services.prefs.clearUserPref).toHaveBeenCalledWith("oldPrefName"); }); }); describe("csvPrefHasValue", () => { let getStringPrefStub; beforeEach(() => { getStringPrefStub = argsStub(); services.prefs.getStringPref = getStringPrefStub; region.home = "CA"; services.locale.appLocaleAsBCP47 = "en-CA"; }); it("throws an error when the pref argument is not a string", () => { expect(() => csvPrefHasValue(0, "foo")).toThrow( new Error("The stringPrefName argument is not a string") ); }); it("returns true if pref contains test value", () => { getStringPrefStub.whenCalledWith("example.csvPref").returns("foo,bar"); const featureCheck = csvPrefHasValue("example.csvPref", "foo"); expect(featureCheck).toBe(true); }); it("returns false if pref contains test value", () => { getStringPrefStub.whenCalledWith("example.csvPref").returns("foo,bar"); const featureCheck = csvPrefHasValue("example.csvPref", "baz"); expect(featureCheck).toBe(false); }); it("returns false if pref value returns empty", () => { getStringPrefStub.whenCalledWith("example.csvPref").returns(""); const featureCheck = csvPrefHasValue("example.csvPref", "foo"); expect(featureCheck).toBe(false); }); it("returns false if test value is blank", () => { getStringPrefStub.whenCalledWith("example.csvPref").returns("foo,bar"); const featureCheck = csvPrefHasValue("example.csvPref", ""); expect(featureCheck).toBe(false); }); }); describe("showWeather", () => { let getStringPrefStub; const FEATURE_ENABLED_PREF = "system.showWeather"; const REGION_WEATHER_CONFIG = "browser.newtabpage.activity-stream.discoverystream.region-weather-config"; const LOCALE_WEATHER_CONFIG = "browser.newtabpage.activity-stream.discoverystream.locale-weather-config"; beforeEach(() => { services.locale.appLocaleAsBCP47 = "en-US"; getStringPrefStub = argsStub(); services.prefs.getStringPref = getStringPrefStub; // Set default regions getStringPrefStub.whenCalledWith(REGION_WEATHER_CONFIG).returns("US, CA"); // Set default locales getStringPrefStub .whenCalledWith(LOCALE_WEATHER_CONFIG) .returns("en-US,en-GB,en-CA"); }); it("should turn off when region and locale are not set", () => { region.home = ""; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region is not set", () => { region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when region is supported", () => { region.home = "US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when region is not supported", () => { region.home = "JP"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when locale is not set", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when locale is supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when locale is not supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region and locale are both not supported", () => { region.home = "FR"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); }); describe("getWeatherWidgetSize", () => { let getBoolPrefStub; let getStringPrefStub; const PREF = "widgets.weather.size"; const FORECAST_PREF = "browser.newtabpage.activity-stream.widgets.system.weatherForecast.enabled"; const MAXIMIZED_PREF = "browser.newtabpage.activity-stream.widgets.maximized"; const DISPLAY_PREF = "browser.newtabpage.activity-stream.weather.display"; beforeEach(() => { getBoolPrefStub = argsStub(); services.prefs.getBoolPref = getBoolPrefStub; getBoolPrefStub.whenCalledWith(FORECAST_PREF, false).returns(false); getBoolPrefStub.whenCalledWith(MAXIMIZED_PREF, true).returns(true); getStringPrefStub = argsStub((_pref, defaultVal) => defaultVal); services.prefs.getStringPref = getStringPrefStub; getStringPrefStub .whenCalledWith(DISPLAY_PREF, "detailed") .returns("detailed"); }); it("should return small when forecast system pref is disabled", () => { as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(PREF).value).toBe("small"); }); it("should return small when forecast is enabled but display is not detailed", () => { getBoolPrefStub.whenCalledWith(FORECAST_PREF, false).returns(true); getStringPrefStub .whenCalledWith(DISPLAY_PREF, "detailed") .returns("simple"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(PREF).value).toBe("small"); }); it("should return large when forecast is enabled and widgets are maximized", () => { getBoolPrefStub.whenCalledWith(FORECAST_PREF, false).returns(true); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(PREF).value).toBe("large"); }); it("should return medium when forecast is enabled but widgets are not maximized", () => { getBoolPrefStub.whenCalledWith(FORECAST_PREF, false).returns(true); getBoolPrefStub.whenCalledWith(MAXIMIZED_PREF, true).returns(false); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(PREF).value).toBe("medium"); }); }); describe("showTopicsSelection", () => { let getStringPrefStub; const FEATURE_ENABLED_PREF = "discoverystream.topicSelection.enabled"; const REGION_TOPICS_CONFIG = "browser.newtabpage.activity-stream.discoverystream.topicSelection.region-topics-config"; const LOCALE_TOPICS_CONFIG = "browser.newtabpage.activity-stream.discoverystream.topicSelection.locale-topics-config"; beforeEach(() => { services.locale.appLocaleAsBCP47 = "en-US"; getStringPrefStub = argsStub(); services.prefs.getStringPref = getStringPrefStub; // Set default regions getStringPrefStub.whenCalledWith(REGION_TOPICS_CONFIG).returns("US, CA"); // Set default locales getStringPrefStub .whenCalledWith(LOCALE_TOPICS_CONFIG) .returns("en-US,en-GB,en-CA"); }); it("should turn off when region and locale are not set", () => { region.home = ""; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region is not set", () => { region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when region is supported", () => { region.home = "US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when region is not supported", () => { region.home = "JP"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when locale is not set", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when locale is supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when locale is not supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region and locale are both not supported", () => { region.home = "FR"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); }); describe("showTopicLabels", () => { let getStringPrefStub; const FEATURE_ENABLED_PREF = "discoverystream.topicLabels.enabled"; const REGION_TOPIC_LABEL_CONFIG = "browser.newtabpage.activity-stream.discoverystream.topicLabels.region-topic-label-config"; const LOCALE_TOPIC_LABEL_CONFIG = "browser.newtabpage.activity-stream.discoverystream.topicLabels.locale-topic-label-config"; beforeEach(() => { services.locale.appLocaleAsBCP47 = "en-US"; getStringPrefStub = argsStub(); services.prefs.getStringPref = getStringPrefStub; // Set default regions getStringPrefStub .whenCalledWith(REGION_TOPIC_LABEL_CONFIG) .returns("US, CA"); // Set default locales getStringPrefStub .whenCalledWith(LOCALE_TOPIC_LABEL_CONFIG) .returns("en-US,en-GB,en-CA"); }); it("should turn off when region and locale are not set", () => { region.home = ""; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region is not set", () => { region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when region is supported", () => { region.home = "US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when region is not supported", () => { region.home = "JP"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when locale is not set", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn on when locale is supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(true); }); it("should turn off when locale is not supported", () => { region.home = "US"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); it("should turn off when region and locale are both not supported", () => { region.home = "FR"; services.locale.appLocaleAsBCP47 = "fr"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value).toBe(false); }); }); describe("discoverystream.region-basic-layout config", () => { let getStringPrefStub; beforeEach(() => { getStringPrefStub = argsStub(); services.prefs.getStringPref = getStringPrefStub; region.home = "CA"; services.locale.appLocaleAsBCP47 = "en-CA"; }); it("should enable 7 row layout pref if no basic config is set and no geo is set", () => { getStringPrefStub .whenCalledWith( "browser.newtabpage.activity-stream.discoverystream.region-basic-config" ) .returns(""); region.home = ""; as._updateDynamicPrefs(); expect( PREFS_CONFIG.get("discoverystream.region-basic-layout").value ).toBe(false); }); it("should enable 1 row layout pref based on region layout pref", () => { getStringPrefStub .whenCalledWith( "browser.newtabpage.activity-stream.discoverystream.region-basic-config" ) .returns("CA"); as._updateDynamicPrefs(); expect( PREFS_CONFIG.get("discoverystream.region-basic-layout").value ).toBe(true); }); it("should enable 7 row layout pref based on region layout pref", () => { getStringPrefStub .whenCalledWith( "browser.newtabpage.activity-stream.discoverystream.region-basic-config" ) .returns(""); as._updateDynamicPrefs(); expect( PREFS_CONFIG.get("discoverystream.region-basic-layout").value ).toBe(false); }); }); describe("_updateDynamicPrefs topstories default value", () => { let getVariableStub; let getBoolPrefStub; let getStringPrefStub; beforeEach(() => { getVariableStub = argsStub(); nimbusFeatures.pocketNewtab.getVariable = getVariableStub; getStringPrefStub = argsStub((_pref, defaultValue) => defaultValue); services.prefs.getStringPref = getStringPrefStub; getBoolPrefStub = argsStub(); services.prefs.getBoolPref = getBoolPrefStub; getBoolPrefStub .whenCalledWith( "browser.newtabpage.activity-stream.feeds.section.topstories" ) .returns(true); services.locale.appLocaleAsBCP47 = "en-US"; region.home = "US"; getStringPrefStub.whenCalledWith(STORIES_REGION_LOCALE_PREF).returns( JSON.stringify([ ["US", ["en-*"]], ["CA", ["en-*"]], ]) ); }); it("should be false with no geo/locale", () => { services.locale.appLocaleAsBCP47 = ""; region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false with no geo but an allowed locale", () => { services.locale.appLocaleAsBCP47 = "en-US"; region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false with unexpected geo", () => { region.home = "NOGEO"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false with expected geo and unexpected locale", () => { services.locale.appLocaleAsBCP47 = "no-LOCALE"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be true with expected geo and locale", () => { as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be false after expected geo and locale then unexpected", () => { region.home = "US"; as._updateDynamicPrefs(); region.home = "NOGEO"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be true with updated pref change", () => { services.locale.appLocaleAsBCP47 = "en-GB"; region.home = "GB"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["GB", ["en-*"]]])); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be true with a locale matched by a pattern", () => { services.locale.appLocaleAsBCP47 = "en-GB"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be true in any region with a wildcard region entry", () => { services.locale.appLocaleAsBCP47 = "en-US"; region.home = "MX"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["*", ["en-*"]]])); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be false for a locale the wildcard region does not list", () => { services.locale.appLocaleAsBCP47 = "es-MX"; region.home = "MX"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["*", ["en-*"]]])); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be true for a region added alongside a wildcard entry", () => { services.locale.appLocaleAsBCP47 = "es-MX"; region.home = "MX"; getStringPrefStub.whenCalledWith(STORIES_REGION_LOCALE_PREF).returns( JSON.stringify([ ["*", ["en-*"]], ["MX", ["es-*"]], ]) ); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be false with a blocked region despite a wildcard entry", () => { services.locale.appLocaleAsBCP47 = "en-US"; region.home = "MX"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["*", ["en-*"]]])); getVariableStub.whenCalledWith("regionStoriesBlock").returns("MX"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should not match a variant against an exact pattern", () => { services.locale.appLocaleAsBCP47 = "es-MX"; region.home = "ES"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["ES", ["es-ES"]]])); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should ignore casing in regions and locales", () => { services.locale.appLocaleAsBCP47 = "EN-us"; region.home = "us"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["us", ["EN-*"]]])); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should keep only the last entry for a region", () => { services.locale.appLocaleAsBCP47 = "de"; region.home = "BE"; getStringPrefStub.whenCalledWith(STORIES_REGION_LOCALE_PREF).returns( JSON.stringify([ ["BE", ["de"]], ["BE", ["fr"]], ]) ); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false with a geo but no locale", () => { services.locale.appLocaleAsBCP47 = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should fall back when the config parses to something else", () => { services.locale.appLocaleAsBCP47 = "es-MX"; region.home = "MX"; getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify({ MX: ["es-*"] })); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false everywhere with an emptied config", () => { getStringPrefStub.whenCalledWith(STORIES_REGION_LOCALE_PREF).returns(""); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should fall back to the built-in list with an invalid config", () => { getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns("not json"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be true for a trainhop region/locale pair", () => { services.locale.appLocaleAsBCP47 = "es-MX"; region.home = "MX"; nimbusFeatures.newtabTrainhop.getAllEnrollments = jest.fn(() => [ { value: { type: "multi-payload", payload: [ { type: "storiesRegionLocale", payload: { config: [["MX", ["es-*"]]] }, }, ], }, meta: { isRollout: true }, }, ]); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should let the trainhop payload replace the shipped pairs", () => { services.locale.appLocaleAsBCP47 = "en-US"; nimbusFeatures.newtabTrainhop.getAllEnrollments = jest.fn(() => [ { value: { type: "storiesRegionLocale", payload: { config: [["MX", ["es-*"]]] }, }, meta: { isRollout: true }, }, ]); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should prefer an experiment payload over a rollout payload", () => { services.locale.appLocaleAsBCP47 = "en-US"; nimbusFeatures.newtabTrainhop.getAllEnrollments = jest.fn(() => [ { value: { type: "storiesRegionLocale", payload: { config: [["MX", ["es-*"]]] }, }, meta: { isRollout: true }, }, { value: { type: "storiesRegionLocale", payload: { config: [["US", ["en-*"]]] }, }, meta: { isRollout: false }, }, ]); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be true for a legacy region/locale pair", () => { services.locale.appLocaleAsBCP47 = "pl"; region.home = "PL"; getVariableStub .whenCalledWith("regionStoriesConfig") .returns("US,DE,CA,GB,IE,CH,AT,BE,IN,FR,IT,ES,PL"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should be false for a legacy region without its locale", () => { services.locale.appLocaleAsBCP47 = "en-US"; region.home = "PL"; getVariableStub .whenCalledWith("regionStoriesConfig") .returns("US,DE,CA,GB,IE,CH,AT,BE,IN,FR,IT,ES,PL"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false for a legacy region left out of the config", () => { services.locale.appLocaleAsBCP47 = "pl"; region.home = "PL"; getVariableStub .whenCalledWith("regionStoriesConfig") .returns("US,DE,CA,GB,IE,CH,AT,BE,IN,FR,IT,ES"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should be false with a blocked region in the legacy config", () => { services.locale.appLocaleAsBCP47 = "pl"; region.home = "PL"; getVariableStub.whenCalledWith("regionStoriesConfig").returns("PL"); getVariableStub.whenCalledWith("regionStoriesBlock").returns("PL"); as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should use the built-in list with no config set", () => { getStringPrefStub .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(undefined); services.locale.appLocaleAsBCP47 = "de"; region.home = "DE"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); }); describe("_updateDynamicPrefs topstories delayed default value", () => { const notifyRegionUpdated = () => services.obs.notifyObservers("US", "browser-region-updated"); beforeEach(() => { jest.useFakeTimers(); // Have addObserver cause prefHasUserValue to now return true then observe services.obs.addObserver.mockImplementation(() => { setTimeout(notifyRegionUpdated); }); }); afterEach(() => jest.useRealTimers()); it("should set false with unexpected geo", () => { region.home = "NOGEO"; as._updateDynamicPrefs(); jest.advanceTimersByTime(1); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should set true with expected geo and locale", () => { services.prefs.getStringPref = argsStub(); services.prefs.getStringPref .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["US", ["en-*"]]])); services.prefs.getBoolPref = jest.fn(() => true); services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); jest.advanceTimersByTime(1); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(true); }); it("should not change default even with expected geo and locale", () => { as._defaultPrefs.set("feeds.system.topstories", false); services.prefs.getStringPref = argsStub(); services.prefs.getStringPref .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["US", ["en-*"]]])); services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); jest.advanceTimersByTime(1); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); it("should set false with geo blocked", () => { const getVariableStub = argsStub(); nimbusFeatures.pocketNewtab.getVariable = getVariableStub; services.prefs.getStringPref = argsStub(); services.prefs.getStringPref .whenCalledWith(STORIES_REGION_LOCALE_PREF) .returns(JSON.stringify([["US", ["en-*"]]])); getVariableStub.whenCalledWith("regionStoriesBlock").returns("US"); services.prefs.getBoolPref = jest.fn(() => true); services.locale.appLocaleAsBCP47 = "en-US"; as._updateDynamicPrefs(); jest.advanceTimersByTime(1); expect(PREFS_CONFIG.get("feeds.system.topstories").value).toBe(false); }); }); describe("searchs shortcuts shouldPin pref", () => { const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF = "improvesearch.topSiteSearchShortcuts.searchEngines"; it("should be an empty string when no geo is available", () => { region.home = ""; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value).toBe( "" ); }); it("should be 'baidu' in China", () => { region.home = "CN"; as._updateDynamicPrefs(); expect(PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value).toBe( "baidu" ); }); it("should be 'yandex' in Russia, Belarus, Kazakhstan, and Turkey", () => { const geos = ["BY", "KZ", "RU", "TR"]; for (const geo of geos) { region.home = geo; as._updateDynamicPrefs(); expect( PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value ).toBe("yandex"); } }); it("should be 'google,amazon' in Germany, France, the UK, Japan, Italy, and the US", () => { const geos = ["DE", "FR", "GB", "IT", "JP", "US"]; for (const geo of geos) { region.home = geo; as._updateDynamicPrefs(); expect( PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value ).toBe("google,amazon"); } }); it("should be 'google' elsewhere", () => { // A selection of other geos const geos = ["BR", "CA", "ES", "ID", "IN"]; for (const geo of geos) { region.home = geo; as._updateDynamicPrefs(); expect( PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value ).toBe("google"); } }); }); describe("proxying images", () => { let registerStub; let unregisterStub; beforeEach(() => { registerStub = jest .spyOn(as, "registerNetworkProxy") .mockImplementation(() => {}); unregisterStub = jest .spyOn(as, "unregisterNetworkProxy") .mockImplementation(() => {}); }); describe("#init", () => { it("should call registerNetworkProxy during init", () => { as.init(); expect(registerStub).toHaveBeenCalledTimes(1); }); }); describe("#uninit", () => { it("should call unregisterNetworkProxy during uninit", () => { as.init(); as.uninit(); expect(unregisterStub).toHaveBeenCalledTimes(1); }); }); describe("#getImageProxyConfig", () => { beforeEach(() => { as.initialized = true; }); it("should return null when proxy config is missing", () => { as.store = { getState: () => ({ Prefs: { values: {}, }, }), }; const config = as.getImageProxyConfig(); expect(config).toBeNull(); }); it("should return null when proxy is disabled", () => { as.store = { getState: () => ({ Prefs: { values: { trainhopConfig: { imageProxy: { enabled: false, proxyHost: "proxy.example.com", proxyPort: 443, proxyAuthHeader: "auth", }, }, "discoverystream.sections.personalization.inferred.enabled": true, }, }, }), }; const config = as.getImageProxyConfig(); expect(config).toBeNull(); }); it("should return null when required fields are missing", () => { as.store = { getState: () => ({ Prefs: { values: { trainhopConfig: { imageProxy: { enabled: true, proxyHost: "proxy.example.com", }, }, "discoverystream.sections.personalization.inferred.enabled": true, }, }, }), }; const config = as.getImageProxyConfig(); expect(config).toBeNull(); }); it("should return null when inferred personalization is disabled", () => { as.store = { getState: () => ({ Prefs: { values: { trainhopConfig: { imageProxy: { enabled: true, proxyHost: "proxy.example.com", proxyPort: 443, proxyAuthHeader: "auth", }, }, "discoverystream.sections.personalization.inferred.enabled": false, "discoverystream.imageProxy.enabled": true, }, }, }), }; const config = as.getImageProxyConfig(); expect(config).toBeNull(); }); it("should return valid config when properly configured", () => { as.store = { getState: () => ({ Prefs: { values: { trainhopConfig: { imageProxy: { enabled: true, proxyHost: "host", proxyPort: 1124, proxyAuthHeader: "123", connectionIsolationKey: "isolation-key", failoverProxy: "failover.example.com", imageProxyHosts: "host1.com,host2.com,host3.com", }, }, "discoverystream.sections.personalization.inferred.enabled": true, "discoverystream.imageProxy.enabled": true, }, }, }), }; const config = as.getImageProxyConfig(); expect(config).toBeTruthy(); }); }); describe("#applyFilter", () => { let mockChannel; let mockCallback; let mockProxyInfo; let mockBrowser; let mockCustomProxyInfo; let getConfigStub; let restoreFilterGlobals; beforeEach(() => { mockCallback = { onProxyFilterResult: jest.fn() }; mockProxyInfo = {}; mockCustomProxyInfo = {}; mockBrowser = {}; mockChannel = { URI: { host: "example.com", scheme: "https" }, loadInfo: { browsingContext: { top: { embedderElement: mockBrowser } }, }, }; getConfigStub = jest .spyOn(as, "getImageProxyConfig") .mockImplementation(() => {}); proxyService = { newProxyInfo: jest.fn(() => mockCustomProxyInfo), }; restoreFilterGlobals = stubGlobals({ AboutNewTabParent: { loadedTabs: new Set([mockBrowser]) }, ProxyService: proxyService, }); }); afterEach(() => { restoreFilterGlobals(); }); it("should pass through original proxy when config is null", () => { getConfigStub.mockReturnValue(null); as.applyFilter(mockChannel, mockProxyInfo, mockCallback); expect(mockCallback.onProxyFilterResult).toHaveBeenCalledTimes(1); expect(mockCallback.onProxyFilterResult).toHaveBeenCalledWith( mockProxyInfo ); }); it("should apply proxy for matching HTTPS host from newtab", () => { const config = { imageProxyHosts: ["example.com", "other.com"], proxyHost: "proxy.example.com", proxyPort: 443, proxyAuthHeader: "Bearer token", connectionIsolationKey: "key", failoverProxy: "failover.example.com", }; getConfigStub.mockReturnValue(config); as.applyFilter(mockChannel, mockProxyInfo, mockCallback); expect(proxyService.newProxyInfo).toHaveBeenCalledTimes(1); expect(proxyService.newProxyInfo).toHaveBeenCalledWith( "https", config.proxyHost, config.proxyPort, config.proxyAuthHeader, config.connectionIsolationKey, 0, 5000, config.failoverProxy ); expect(mockCallback.onProxyFilterResult).toHaveBeenCalledTimes(1); expect(mockCallback.onProxyFilterResult).toHaveBeenCalledWith( mockCustomProxyInfo ); }); }); }); }); describe("WIDGET_REGISTRY pref coverage", () => { for (const widget of WIDGET_REGISTRY) { it(`should have enabledPref registered for widget "${widget.id}"`, () => { expect(PREFS_CONFIG.has(widget.enabledPref)).toBe(true); }); it(`should have sizePref registered for widget "${widget.id}"`, () => { expect(PREFS_CONFIG.has(widget.sizePref)).toBe(true); }); } });