summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/asrouter/ASRouter.test.js')
-rw-r--r--browser/components/newtab/test/unit/asrouter/ASRouter.test.js3040
1 files changed, 3040 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
new file mode 100644
index 0000000000..732200b408
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -0,0 +1,3040 @@
+import { _ASRouter, MessageLoaderUtils } from "lib/ASRouter.jsm";
+import { QueryCache } from "lib/ASRouterTargeting.jsm";
+import {
+ FAKE_LOCAL_MESSAGES,
+ FAKE_LOCAL_PROVIDER,
+ FAKE_LOCAL_PROVIDERS,
+ FAKE_REMOTE_MESSAGES,
+ FAKE_REMOTE_PROVIDER,
+ FAKE_REMOTE_SETTINGS_PROVIDER,
+} from "./constants";
+import {
+ ASRouterPreferences,
+ TARGETING_PREFERENCES,
+} from "lib/ASRouterPreferences.jsm";
+import { ASRouterTriggerListeners } from "lib/ASRouterTriggerListeners.jsm";
+import { CFRPageActions } from "lib/CFRPageActions.jsm";
+import { GlobalOverrider } from "test/unit/utils";
+import { PanelTestProvider } from "lib/PanelTestProvider.sys.mjs";
+import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
+import { SnippetsTestMessageProvider } from "lib/SnippetsTestMessageProvider.sys.mjs";
+
+const MESSAGE_PROVIDER_PREF_NAME =
+ "browser.newtabpage.activity-stream.asrouter.providers.snippets";
+const FAKE_PROVIDERS = [
+ FAKE_LOCAL_PROVIDER,
+ FAKE_REMOTE_PROVIDER,
+ FAKE_REMOTE_SETTINGS_PROVIDER,
+];
+const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
+const FAKE_RESPONSE_HEADERS = { get() {} };
+const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
+
+const USE_REMOTE_L10N_PREF =
+ "browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
+
+// eslint-disable-next-line max-statements
+describe("ASRouter", () => {
+ let Router;
+ let globals;
+ let sandbox;
+ let initParams;
+ let messageBlockList;
+ let providerBlockList;
+ let messageImpressions;
+ let groupImpressions;
+ let previousSessionEnd;
+ let fetchStub;
+ let clock;
+ let getStringPrefStub;
+ let fakeAttributionCode;
+ let fakeTargetingContext;
+ let FakeToolbarBadgeHub;
+ let FakeToolbarPanelHub;
+ let FakeMomentsPageHub;
+ let ASRouterTargeting;
+ let screenImpressions;
+
+ function setMessageProviderPref(value) {
+ sandbox.stub(ASRouterPreferences, "providers").get(() => value);
+ }
+
+ function initASRouter(router) {
+ const getStub = sandbox.stub();
+ getStub.returns(Promise.resolve());
+ getStub
+ .withArgs("messageBlockList")
+ .returns(Promise.resolve(messageBlockList));
+ getStub
+ .withArgs("providerBlockList")
+ .returns(Promise.resolve(providerBlockList));
+ getStub
+ .withArgs("messageImpressions")
+ .returns(Promise.resolve(messageImpressions));
+ getStub.withArgs("groupImpressions").resolves(groupImpressions);
+ getStub
+ .withArgs("previousSessionEnd")
+ .returns(Promise.resolve(previousSessionEnd));
+ getStub
+ .withArgs("screenImpressions")
+ .returns(Promise.resolve(screenImpressions));
+ initParams = {
+ storage: {
+ get: getStub,
+ set: sandbox.stub().returns(Promise.resolve()),
+ },
+ sendTelemetry: sandbox.stub().resolves(),
+ clearChildMessages: sandbox.stub().resolves(),
+ clearChildProviders: sandbox.stub().resolves(),
+ updateAdminState: sandbox.stub().resolves(),
+ dispatchCFRAction: sandbox.stub().resolves(),
+ };
+ sandbox.stub(router, "loadMessagesFromAllProviders").callThrough();
+ return router.init(initParams);
+ }
+
+ async function createRouterAndInit(providers = FAKE_PROVIDERS) {
+ setMessageProviderPref(providers);
+ // `.freeze` to catch any attempts at modifying the object
+ Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
+ await initASRouter(Router);
+ }
+
+ beforeEach(async () => {
+ globals = new GlobalOverrider();
+ messageBlockList = [];
+ providerBlockList = [];
+ messageImpressions = {};
+ groupImpressions = {};
+ previousSessionEnd = 100;
+ screenImpressions = {};
+ sandbox = sinon.createSandbox();
+ ASRouterTargeting = {
+ isMatch: sandbox.stub(),
+ findMatchingMessage: sandbox.stub(),
+ Environment: {
+ locale: "en-US",
+ localeLanguageCode: "en",
+ browserSettings: {
+ update: {
+ channel: "default",
+ enabled: true,
+ autoDownload: true,
+ },
+ },
+ attributionData: {},
+ currentDate: "2000-01-01T10:00:0.001Z",
+ profileAgeCreated: {},
+ profileAgeReset: {},
+ usesFirefoxSync: false,
+ isFxAEnabled: true,
+ isFxASignedIn: false,
+ sync: {
+ desktopDevices: 0,
+ mobileDevices: 0,
+ totalDevices: 0,
+ },
+ xpinstallEnabled: true,
+ addonsInfo: {},
+ searchEngines: {},
+ isDefaultBrowser: false,
+ devToolsOpenedCount: 5,
+ topFrecentSites: {},
+ recentBookmarks: {},
+ pinnedSites: [
+ {
+ url: "https://amazon.com",
+ host: "amazon.com",
+ searchTopSite: true,
+ },
+ ],
+ providerCohorts: {
+ onboarding: "",
+ cfr: "",
+ "message-groups": "",
+ "messaging-experiments": "",
+ snippets: "",
+ "whats-new-panel": "",
+ },
+ totalBookmarksCount: {},
+ firefoxVersion: 80,
+ region: "US",
+ needsUpdate: {},
+ hasPinnedTabs: false,
+ hasAccessedFxAPanel: false,
+ isWhatsNewPanelEnabled: true,
+ userPrefs: {
+ cfrFeatures: true,
+ cfrAddons: true,
+ snippets: true,
+ },
+ totalBlockedCount: {},
+ blockedCountByType: {},
+ attachedFxAOAuthClients: [],
+ platformName: "macosx",
+ scores: {},
+ scoreThreshold: 5000,
+ isChinaRepack: false,
+ userId: "adsf",
+ },
+ };
+
+ ASRouterPreferences.specialConditions = {
+ someCondition: true,
+ };
+ sandbox.spy(ASRouterPreferences, "init");
+ sandbox.spy(ASRouterPreferences, "uninit");
+ sandbox.spy(ASRouterPreferences, "addListener");
+ sandbox.spy(ASRouterPreferences, "removeListener");
+
+ clock = sandbox.useFakeTimers();
+ fetchStub = sandbox
+ .stub(global, "fetch")
+ .withArgs("http://fake.com/endpoint")
+ .resolves({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve({ messages: FAKE_REMOTE_MESSAGES }),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
+
+ fakeAttributionCode = {
+ allowedCodeKeys: ["foo", "bar", "baz"],
+ _clearCache: () => sinon.stub(),
+ getAttrDataAsync: () => Promise.resolve({ content: "addonID" }),
+ deleteFileAsync: () => Promise.resolve(),
+ writeAttributionFile: () => Promise.resolve(),
+ getCachedAttributionData: sinon.stub(),
+ };
+ FakeToolbarPanelHub = {
+ init: sandbox.stub(),
+ uninit: sandbox.stub(),
+ forceShowMessage: sandbox.stub(),
+ enableToolbarButton: sandbox.stub(),
+ };
+ FakeToolbarBadgeHub = {
+ init: sandbox.stub(),
+ uninit: sandbox.stub(),
+ registerBadgeNotificationListener: sandbox.stub(),
+ };
+ FakeMomentsPageHub = {
+ init: sandbox.stub(),
+ uninit: sandbox.stub(),
+ executeAction: sandbox.stub(),
+ };
+ fakeTargetingContext = {
+ combineContexts: sandbox.stub(),
+ evalWithDefault: sandbox.stub().resolves(),
+ };
+ let fakeNimbusFeatures = [
+ "cfr",
+ "infobar",
+ "spotlight",
+ "moments-page",
+ "pbNewtab",
+ ].reduce((features, featureId) => {
+ features[featureId] = {
+ getAllVariables: sandbox.stub().returns(null),
+ recordExposureEvent: sandbox.stub(),
+ };
+ return features;
+ }, {});
+ globals.set({
+ // Testing framework doesn't know how to `defineLazyModuleGetter` so we're
+ // importing these modules into the global scope ourselves.
+ GroupsConfigurationProvider: { getMessages: () => Promise.resolve([]) },
+ ASRouterPreferences,
+ TARGETING_PREFERENCES,
+ ASRouterTargeting,
+ ASRouterTriggerListeners,
+ QueryCache,
+ gBrowser: { selectedBrowser: {} },
+ gURLBar: {},
+ isSeparateAboutWelcome: true,
+ AttributionCode: fakeAttributionCode,
+ SnippetsTestMessageProvider,
+ PanelTestProvider,
+ MacAttribution: { applicationPath: "" },
+ ToolbarBadgeHub: FakeToolbarBadgeHub,
+ ToolbarPanelHub: FakeToolbarPanelHub,
+ MomentsPageHub: FakeMomentsPageHub,
+ KintoHttpClient: class {
+ bucket() {
+ return this;
+ }
+ collection() {
+ return this;
+ }
+ getRecord() {
+ return Promise.resolve({ data: {} });
+ }
+ },
+ Downloader: class {
+ download() {
+ return Promise.resolve("/path/to/download");
+ }
+ },
+ NimbusFeatures: fakeNimbusFeatures,
+ ExperimentAPI: {
+ getExperimentMetaData: sandbox.stub().returns({
+ slug: "experiment-slug",
+ active: true,
+ branch: { slug: "experiment-branch-slug" },
+ }),
+ getExperiment: sandbox.stub().returns({
+ branch: {
+ slug: "unit-slug",
+ feature: {
+ featureId: "foo",
+ value: { id: "test-message" },
+ },
+ },
+ }),
+ getAllBranches: sandbox.stub().resolves([]),
+ ready: sandbox.stub().resolves(),
+ },
+ SpecialMessageActions: {
+ handleAction: sandbox.stub(),
+ },
+ TargetingContext: class {
+ static combineContexts(...args) {
+ return fakeTargetingContext.combineContexts.apply(sandbox, args);
+ }
+
+ evalWithDefault(expr) {
+ return fakeTargetingContext.evalWithDefault(expr);
+ }
+ },
+ RemoteL10n: {
+ // This is just a subset of supported locales that happen to be used in
+ // the test.
+ isLocaleSupported: locale => ["en-US", "ja-JP-mac"].includes(locale),
+ },
+ });
+ await createRouterAndInit();
+ });
+ afterEach(() => {
+ Router.uninit();
+ ASRouterPreferences.uninit();
+ sandbox.restore();
+ globals.restore();
+ });
+
+ describe(".state", () => {
+ it("should throw if an attempt to set .state was made", () => {
+ assert.throws(() => {
+ Router.state = {};
+ });
+ });
+ });
+
+ describe("#init", () => {
+ it("should only be called once", async () => {
+ Router = new _ASRouter();
+ let state = await initASRouter(Router);
+
+ assert.equal(state, Router.state);
+
+ assert.isNull(await Router.init({}));
+ });
+ it("should only be called once", async () => {
+ Router = new _ASRouter();
+ initASRouter(Router);
+ let secondCall = await Router.init({});
+
+ assert.isNull(
+ secondCall,
+ "Should not init twice, it should exit early with null"
+ );
+ });
+ it("should set state.messageBlockList to the block list in persistent storage", async () => {
+ messageBlockList = ["foo"];
+ Router = new _ASRouter();
+ await initASRouter(Router);
+
+ assert.deepEqual(Router.state.messageBlockList, ["foo"]);
+ });
+ it("should initialize all the hub providers", async () => {
+ // ASRouter init called in `beforeEach` block above
+
+ assert.calledOnce(FakeToolbarBadgeHub.init);
+ assert.calledOnce(FakeToolbarPanelHub.init);
+ assert.calledOnce(FakeMomentsPageHub.init);
+
+ assert.calledWithExactly(
+ FakeToolbarBadgeHub.init,
+ Router.waitForInitialized,
+ {
+ handleMessageRequest: Router.handleMessageRequest,
+ addImpression: Router.addImpression,
+ blockMessageById: Router.blockMessageById,
+ sendTelemetry: Router.sendTelemetry,
+ unblockMessageById: Router.unblockMessageById,
+ }
+ );
+
+ assert.calledWithExactly(
+ FakeToolbarPanelHub.init,
+ Router.waitForInitialized,
+ {
+ getMessages: Router.handleMessageRequest,
+ sendTelemetry: Router.sendTelemetry,
+ }
+ );
+
+ assert.calledWithExactly(
+ FakeMomentsPageHub.init,
+ Router.waitForInitialized,
+ {
+ handleMessageRequest: Router.handleMessageRequest,
+ addImpression: Router.addImpression,
+ blockMessageById: Router.blockMessageById,
+ sendTelemetry: Router.sendTelemetry,
+ }
+ );
+ });
+ it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
+ // Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
+ // otherwise they will be cleaned up by .cleanupImpressions()
+ const testMessage = { id: "foo", frequency: { lifetimeCap: 10 } };
+ messageImpressions = { foo: [0, 1, 2] };
+ setMessageProviderPref([
+ { id: "onboarding", type: "local", messages: [testMessage] },
+ ]);
+ Router = new _ASRouter();
+ await initASRouter(Router);
+
+ assert.deepEqual(Router.state.messageImpressions, messageImpressions);
+ });
+ it("should set state.screenImpressions to the screenImpressions object in persistent storage", async () => {
+ screenImpressions = { test: 123 };
+
+ Router = new _ASRouter();
+ await initASRouter(Router);
+
+ assert.deepEqual(Router.state.screenImpressions, screenImpressions);
+ });
+ it("should clear impressions for groups that are not active", async () => {
+ groupImpressions = { foo: [0, 1, 2] };
+ Router = new _ASRouter();
+ await initASRouter(Router);
+
+ assert.notProperty(Router.state.groupImpressions, "foo");
+ });
+ it("should keep impressions for groups that are active", async () => {
+ Router = new _ASRouter();
+ await initASRouter(Router);
+ await Router.setState(() => {
+ return {
+ groups: [
+ {
+ id: "foo",
+ enabled: true,
+ frequency: {
+ custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
+ lifetime: Infinity,
+ },
+ },
+ ],
+ groupImpressions: { foo: [Date.now()] },
+ };
+ });
+ Router.cleanupImpressions();
+
+ assert.property(Router.state.groupImpressions, "foo");
+ assert.lengthOf(Router.state.groupImpressions.foo, 1);
+ });
+ it("should remove old impressions for a group", async () => {
+ Router = new _ASRouter();
+ await initASRouter(Router);
+ await Router.setState(() => {
+ return {
+ groups: [
+ {
+ id: "foo",
+ enabled: true,
+ frequency: {
+ custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
+ },
+ },
+ ],
+ groupImpressions: {
+ foo: [Date.now() - ONE_DAY_IN_MS - 1, Date.now()],
+ },
+ };
+ });
+ Router.cleanupImpressions();
+
+ assert.property(Router.state.groupImpressions, "foo");
+ assert.lengthOf(Router.state.groupImpressions.foo, 1);
+ });
+ it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
+ Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
+
+ await initASRouter(Router);
+
+ assert.calledOnce(Router.loadMessagesFromAllProviders);
+ assert.isArray(Router.state.messages);
+ assert.lengthOf(
+ Router.state.messages,
+ FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length
+ );
+ });
+ it("should load additional allowed hosts", async () => {
+ getStringPrefStub.returns('["allow.com"]');
+ await createRouterAndInit();
+
+ assert.propertyVal(Router.ALLOWLIST_HOSTS, "allow.com", "preview");
+ // Should still include the defaults
+ assert.lengthOf(Object.keys(Router.ALLOWLIST_HOSTS), 3);
+ });
+ it("should fallback to defaults if pref parsing fails", async () => {
+ getStringPrefStub.returns("err");
+ await createRouterAndInit();
+
+ assert.lengthOf(Object.keys(Router.ALLOWLIST_HOSTS), 2);
+ assert.propertyVal(
+ Router.ALLOWLIST_HOSTS,
+ "snippets-admin.mozilla.org",
+ "preview"
+ );
+ assert.propertyVal(
+ Router.ALLOWLIST_HOSTS,
+ "activity-stream-icons.services.mozilla.com",
+ "production"
+ );
+ });
+ it("should set state.previousSessionEnd from IndexedDB", async () => {
+ previousSessionEnd = 200;
+ await createRouterAndInit();
+
+ assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
+ });
+ it("should assign ASRouterPreferences.specialConditions to state", async () => {
+ assert.isTrue(ASRouterPreferences.specialConditions.someCondition);
+ assert.isTrue(Router.state.someCondition);
+ });
+ it("should add observer for `intl:app-locales-changed`", async () => {
+ sandbox.spy(global.Services.obs, "addObserver");
+ await createRouterAndInit();
+
+ assert.calledWithExactly(
+ global.Services.obs.addObserver,
+ Router._onLocaleChanged,
+ "intl:app-locales-changed"
+ );
+ });
+ it("should add a pref observer", async () => {
+ sandbox.spy(global.Services.prefs, "addObserver");
+ await createRouterAndInit();
+
+ assert.calledOnce(global.Services.prefs.addObserver);
+ assert.calledWithExactly(
+ global.Services.prefs.addObserver,
+ USE_REMOTE_L10N_PREF,
+ Router
+ );
+ });
+ describe("lazily loading local test providers", () => {
+ afterEach(() => {
+ Router.uninit();
+ });
+ it("should add the local test providers on init if devtools are enabled", async () => {
+ sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
+
+ await createRouterAndInit();
+
+ assert.property(Router._localProviders, "SnippetsTestMessageProvider");
+ assert.property(Router._localProviders, "PanelTestProvider");
+ });
+ it("should not add the local test providers on init if devtools are disabled", async () => {
+ sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
+
+ await createRouterAndInit();
+
+ assert.notProperty(
+ Router._localProviders,
+ "SnippetsTestMessageProvider"
+ );
+ assert.notProperty(Router._localProviders, "PanelTestProvider");
+ });
+ });
+ });
+
+ describe("preference changes", () => {
+ it("should call ASRouterPreferences.init and add a listener on init", () => {
+ assert.calledOnce(ASRouterPreferences.init);
+ assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);
+ });
+ it("should call ASRouterPreferences.uninit and remove the listener on uninit", () => {
+ Router.uninit();
+ assert.calledOnce(ASRouterPreferences.uninit);
+ assert.calledWith(
+ ASRouterPreferences.removeListener,
+ Router.onPrefChange
+ );
+ });
+ it("should send a AS_ROUTER_TARGETING_UPDATE message", async () => {
+ const messageTargeted = {
+ id: "1",
+ campaign: "foocampaign",
+ targeting: "true",
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ const messageNotTargeted = {
+ id: "2",
+ campaign: "foocampaign",
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ await Router.setState({
+ messages: [messageTargeted, messageNotTargeted],
+ providers: [{ id: "snippets" }],
+ });
+ fakeTargetingContext.evalWithDefault.resolves(false);
+
+ await Router.onPrefChange("services.sync.username");
+
+ assert.calledOnce(initParams.clearChildMessages);
+ assert.calledWith(initParams.clearChildMessages, [messageTargeted.id]);
+ });
+ it("should call loadMessagesFromAllProviders on pref change", () => {
+ ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
+ assert.calledOnce(Router.loadMessagesFromAllProviders);
+ });
+ it("should update groups state if a user pref changes", async () => {
+ await Router.setState({
+ groups: [{ id: "foo", userPreferences: ["bar"], enabled: true }],
+ });
+ sandbox.stub(ASRouterPreferences, "getUserPreference");
+
+ await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
+
+ assert.calledWithExactly(ASRouterPreferences.getUserPreference, "bar");
+ });
+ it("should update the list of providers on pref change", async () => {
+ const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
+ url: "baz.com",
+ });
+ setMessageProviderPref([
+ FAKE_LOCAL_PROVIDER,
+ modifiedRemoteProvider,
+ FAKE_REMOTE_SETTINGS_PROVIDER,
+ ]);
+
+ const { length } = Router.state.providers;
+
+ ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
+ await Router._updateMessageProviders();
+
+ const provider = Router.state.providers.find(p => p.url === "baz.com");
+ assert.lengthOf(Router.state.providers, length);
+ assert.isDefined(provider);
+ });
+ it("should clear disabled providers on pref change", async () => {
+ const TEST_PROVIDER_ID = "some_provider_id";
+ await Router.setState({
+ providers: [{ id: TEST_PROVIDER_ID }],
+ });
+ const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
+ id: TEST_PROVIDER_ID,
+ enabled: false,
+ });
+ setMessageProviderPref([
+ FAKE_LOCAL_PROVIDER,
+ modifiedRemoteProvider,
+ FAKE_REMOTE_SETTINGS_PROVIDER,
+ ]);
+ await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
+
+ assert.calledOnce(initParams.clearChildProviders);
+ assert.calledWith(initParams.clearChildProviders, [TEST_PROVIDER_ID]);
+ });
+ });
+
+ describe("setState", () => {
+ it("should broadcast a message to update the admin tool on a state change if the asrouter.devtoolsEnabled pref is", async () => {
+ sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
+ sandbox.stub(Router, "getTargetingParameters").resolves({});
+ const state = await Router.setState({ foo: 123 });
+
+ assert.calledOnce(initParams.updateAdminState);
+ assert.deepEqual(state.providerPrefs, ASRouterPreferences.providers);
+ assert.deepEqual(
+ state.userPrefs,
+ ASRouterPreferences.getAllUserPreferences()
+ );
+ assert.deepEqual(state.targetingParameters, {});
+ assert.deepEqual(state.errors, Router.errors);
+ });
+ it("should not send a message on a state change asrouter.devtoolsEnabled pref is on", async () => {
+ sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
+ await Router.setState({ foo: 123 });
+
+ assert.notCalled(initParams.updateAdminState);
+ });
+ });
+
+ describe("getTargetingParameters", () => {
+ it("should return the targeting parameters", async () => {
+ const stub = sandbox.stub().resolves("foo");
+ const obj = { foo: 1 };
+ sandbox.stub(obj, "foo").get(stub);
+ const result = await Router.getTargetingParameters(obj, obj);
+
+ assert.calledTwice(stub);
+ assert.propertyVal(result, "foo", "foo");
+ });
+ });
+
+ describe("evaluateExpression", () => {
+ it("should call ASRouterTargeting to evaluate", async () => {
+ fakeTargetingContext.evalWithDefault.resolves("foo");
+ const response = await Router.evaluateExpression({});
+ assert.equal(response.evaluationStatus.result, "foo");
+ assert.isTrue(response.evaluationStatus.success);
+ });
+ it("should catch evaluation errors", async () => {
+ fakeTargetingContext.evalWithDefault.returns(
+ Promise.reject(new Error("fake error"))
+ );
+ const response = await Router.evaluateExpression({});
+ assert.isFalse(response.evaluationStatus.success);
+ });
+ });
+
+ describe("#routeCFRMessage", () => {
+ let browser;
+ beforeEach(() => {
+ sandbox.stub(CFRPageActions, "forceRecommendation");
+ sandbox.stub(CFRPageActions, "addRecommendation");
+ browser = {};
+ });
+ it("should route whatsnew_panel_message message to the right hub", () => {
+ Router.routeCFRMessage(
+ { template: "whatsnew_panel_message" },
+ browser,
+ "",
+ true
+ );
+
+ assert.calledOnce(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route moments messages to the right hub", () => {
+ Router.routeCFRMessage({ template: "update_action" }, browser, "", true);
+
+ assert.calledOnce(FakeMomentsPageHub.executeAction);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ });
+ it("should route toolbar_badge message to the right hub", () => {
+ Router.routeCFRMessage({ template: "toolbar_badge" }, browser);
+
+ assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route milestone_message to the right hub", () => {
+ Router.routeCFRMessage(
+ { template: "milestone_message" },
+ browser,
+ "",
+ false
+ );
+
+ assert.calledOnce(CFRPageActions.addRecommendation);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route cfr_doorhanger message to the right hub force = false", () => {
+ Router.routeCFRMessage(
+ { template: "cfr_doorhanger" },
+ browser,
+ { param: {} },
+ false
+ );
+
+ assert.calledOnce(CFRPageActions.addRecommendation);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route cfr_doorhanger message to the right hub force = true", () => {
+ Router.routeCFRMessage({ template: "cfr_doorhanger" }, browser, {}, true);
+
+ assert.calledOnce(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route cfr_urlbar_chiclet message to the right hub force = false", () => {
+ Router.routeCFRMessage(
+ { template: "cfr_urlbar_chiclet" },
+ browser,
+ { param: {} },
+ false
+ );
+
+ assert.calledOnce(CFRPageActions.addRecommendation);
+ const { args } = CFRPageActions.addRecommendation.firstCall;
+ // Host should be null
+ assert.isNull(args[1]);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route cfr_urlbar_chiclet message to the right hub force = true", () => {
+ Router.routeCFRMessage(
+ { template: "cfr_urlbar_chiclet" },
+ browser,
+ {},
+ true
+ );
+
+ assert.calledOnce(CFRPageActions.forceRecommendation);
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ it("should route default to sending to content", () => {
+ Router.routeCFRMessage({ template: "snippets" }, browser, {}, true);
+
+ assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
+ assert.notCalled(CFRPageActions.forceRecommendation);
+ assert.notCalled(CFRPageActions.addRecommendation);
+ assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
+ assert.notCalled(FakeMomentsPageHub.executeAction);
+ });
+ });
+
+ describe("#loadMessagesFromAllProviders", () => {
+ function assertRouterContainsMessages(messages) {
+ const messageIdsInRouter = Router.state.messages.map(m => m.id);
+ for (const message of messages) {
+ assert.include(messageIdsInRouter, message.id);
+ }
+ }
+
+ it("should not trigger an update if not enough time has passed for a provider", async () => {
+ await createRouterAndInit([
+ {
+ id: "remotey",
+ type: "remote",
+ enabled: true,
+ url: "http://fake.com/endpoint",
+ updateCycleInMs: 300,
+ },
+ ]);
+
+ const previousState = Router.state;
+
+ // Since we've previously gotten messages during init and we haven't advanced our fake timer,
+ // no updates should be triggered.
+ await Router.loadMessagesFromAllProviders();
+ assert.deepEqual(Router.state, previousState);
+ });
+ it("should not trigger an update if we only have local providers", async () => {
+ await createRouterAndInit([
+ {
+ id: "foo",
+ type: "local",
+ enabled: true,
+ messages: FAKE_LOCAL_MESSAGES,
+ },
+ ]);
+
+ const previousState = Router.state;
+ const stub = sandbox.stub(MessageLoaderUtils, "loadMessagesForProvider");
+
+ clock.tick(300);
+
+ await Router.loadMessagesFromAllProviders();
+
+ assert.deepEqual(Router.state, previousState);
+ assert.notCalled(stub);
+ });
+ it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
+ const NEW_MESSAGES = [{ id: "new_123" }];
+ await createRouterAndInit([
+ {
+ id: "remotey",
+ type: "remote",
+ url: "http://fake.com/endpoint",
+ enabled: true,
+ updateCycleInMs: 300,
+ },
+ {
+ id: "alocalprovider",
+ type: "local",
+ enabled: true,
+ messages: FAKE_LOCAL_MESSAGES,
+ },
+ ]);
+ fetchStub.withArgs("http://fake.com/endpoint").resolves({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve({ messages: NEW_MESSAGES }),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+
+ clock.tick(301);
+ await Router.loadMessagesFromAllProviders();
+
+ // These are the new messages
+ assertRouterContainsMessages(NEW_MESSAGES);
+ // These are the local messages that should not have been deleted
+ assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);
+ });
+ it("should parse the triggers in the messages and register the trigger listeners", async () => {
+ sandbox.spy(
+ ASRouterTriggerListeners.get("openURL"),
+ "init"
+ ); /* eslint-disable object-property-newline */
+
+ /* eslint-disable object-curly-newline */ await createRouterAndInit([
+ {
+ id: "foo",
+ type: "local",
+ enabled: true,
+ messages: [
+ {
+ id: "foo",
+ template: "simple_template",
+ trigger: { id: "firstRun" },
+ content: { title: "Foo", body: "Foo123" },
+ },
+ {
+ id: "bar1",
+ template: "simple_template",
+ trigger: {
+ id: "openURL",
+ params: ["www.mozilla.org", "www.mozilla.com"],
+ },
+ content: { title: "Bar1", body: "Bar123" },
+ },
+ {
+ id: "bar2",
+ template: "simple_template",
+ trigger: { id: "openURL", params: ["www.example.com"] },
+ content: { title: "Bar2", body: "Bar123" },
+ },
+ ],
+ },
+ ]); /* eslint-enable object-property-newline */
+ /* eslint-enable object-curly-newline */ assert.calledTwice(
+ ASRouterTriggerListeners.get("openURL").init
+ );
+ assert.calledWithExactly(
+ ASRouterTriggerListeners.get("openURL").init,
+ Router._triggerHandler,
+ ["www.mozilla.org", "www.mozilla.com"],
+ undefined
+ );
+ assert.calledWithExactly(
+ ASRouterTriggerListeners.get("openURL").init,
+ Router._triggerHandler,
+ ["www.example.com"],
+ undefined
+ );
+ });
+ it("should parse the message's messagesLoaded trigger and immediately fire trigger", async () => {
+ setMessageProviderPref([
+ {
+ id: "foo",
+ type: "local",
+ enabled: true,
+ messages: [
+ {
+ id: "bar3",
+ template: "simple_template",
+ trigger: { id: "messagesLoaded" },
+ content: { title: "Bar3", body: "Bar123" },
+ },
+ ],
+ },
+ ]);
+ Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
+ sandbox.spy(Router, "sendTriggerMessage");
+ await initASRouter(Router);
+ assert.calledOnce(Router.sendTriggerMessage);
+ assert.calledWith(
+ Router.sendTriggerMessage,
+ sandbox.match({ id: "messagesLoaded" }),
+ true
+ );
+ });
+ it("should gracefully handle messages loading before a window or browser exists", async () => {
+ sandbox.stub(global, "gBrowser").value(undefined);
+ sandbox
+ .stub(global.Services.wm, "getMostRecentBrowserWindow")
+ .returns(undefined);
+ setMessageProviderPref([
+ {
+ id: "foo",
+ type: "local",
+ enabled: true,
+ messages: [
+ "whatsnew_panel_message",
+ "cfr_doorhanger",
+ "toolbar_badge",
+ "update_action",
+ "infobar",
+ "spotlight",
+ "toast_notification",
+ ].map((template, i) => {
+ return {
+ id: `foo${i}`,
+ template,
+ trigger: { id: "messagesLoaded" },
+ content: { title: `Foo${i}`, body: "Bar123" },
+ };
+ }),
+ },
+ ]);
+ Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
+ sandbox.spy(Router, "sendTriggerMessage");
+ await initASRouter(Router);
+ assert.calledWith(
+ Router.sendTriggerMessage,
+ sandbox.match({ id: "messagesLoaded" }),
+ true
+ );
+ });
+ it("should gracefully handle RemoteSettings blowing up and dispatch undesired event", async () => {
+ sandbox
+ .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
+ .rejects("fake error");
+ await createRouterAndInit();
+ assert.calledWith(initParams.dispatchCFRAction, {
+ data: {
+ action: "asrouter_undesired_event",
+ event: "ASR_RS_ERROR",
+ event_context: "remotey-settingsy",
+ message_id: "n/a",
+ },
+ meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
+ type: "AS_ROUTER_TELEMETRY_USER_EVENT",
+ });
+ });
+ it("should dispatch undesired event if RemoteSettings returns no messages", async () => {
+ sandbox
+ .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
+ .resolves([]);
+ assert.calledWith(initParams.dispatchCFRAction, {
+ data: {
+ action: "asrouter_undesired_event",
+ event: "ASR_RS_NO_MESSAGES",
+ event_context: "remotey-settingsy",
+ message_id: "n/a",
+ },
+ meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
+ type: "AS_ROUTER_TELEMETRY_USER_EVENT",
+ });
+ });
+ it("should download the attachment if RemoteSettings returns some messages", async () => {
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "en-US");
+ sandbox
+ .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
+ .resolves([{ id: "message_1" }]);
+ const spy = sandbox.spy();
+ global.Downloader.prototype.downloadToDisk = spy;
+ const provider = {
+ id: "cfr",
+ enabled: true,
+ type: "remote-settings",
+ collection: "cfr",
+ };
+ await createRouterAndInit([provider]);
+
+ assert.calledOnce(spy);
+ });
+ it("should dispatch undesired event if the ms-language-packs returns no messages", async () => {
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "en-US");
+ sandbox
+ .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
+ .resolves([{ id: "message_1" }]);
+ sandbox
+ .stub(global.KintoHttpClient.prototype, "getRecord")
+ .resolves(null);
+ const provider = {
+ id: "cfr",
+ enabled: true,
+ type: "remote-settings",
+ collection: "cfr",
+ };
+ await createRouterAndInit([provider]);
+
+ assert.calledWith(initParams.dispatchCFRAction, {
+ data: {
+ action: "asrouter_undesired_event",
+ event: "ASR_RS_NO_MESSAGES",
+ event_context: "ms-language-packs",
+ message_id: "n/a",
+ },
+ meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
+ type: "AS_ROUTER_TELEMETRY_USER_EVENT",
+ });
+ });
+ });
+
+ describe("#_updateMessageProviders", () => {
+ it("should correctly replace %STARTPAGE_VERSION% in remote provider urls", async () => {
+ // If this test fails, you need to update the constant STARTPAGE_VERSION in
+ // ASRouter.jsm to match the `version` property of provider-response-schema.json
+ const expectedStartpageVersion = ProviderResponseSchema.version;
+ const provider = {
+ id: "foo",
+ enabled: true,
+ type: "remote",
+ url: "https://www.mozilla.org/%STARTPAGE_VERSION%/",
+ };
+ setMessageProviderPref([provider]);
+ await Router._updateMessageProviders();
+ assert.equal(
+ Router.state.providers[0].url,
+ `https://www.mozilla.org/${parseInt(expectedStartpageVersion, 10)}/`
+ );
+ });
+ it("should replace other params in remote provider urls by calling Services.urlFormater.formatURL", async () => {
+ const url = "https://www.example.com/";
+ const replacedUrl = "https://www.foo.bar/";
+ const stub = sandbox
+ .stub(global.Services.urlFormatter, "formatURL")
+ .withArgs(url)
+ .returns(replacedUrl);
+ const provider = { id: "foo", enabled: true, type: "remote", url };
+ setMessageProviderPref([provider]);
+ await Router._updateMessageProviders();
+ assert.calledOnce(stub);
+ assert.calledWithExactly(stub, url);
+ assert.equal(Router.state.providers[0].url, replacedUrl);
+ });
+ it("should only add the providers that are enabled", async () => {
+ const providers = [
+ {
+ id: "foo",
+ enabled: false,
+ type: "remote",
+ url: "https://www.foo.com/",
+ },
+ {
+ id: "bar",
+ enabled: true,
+ type: "remote",
+ url: "https://www.bar.com/",
+ },
+ ];
+ setMessageProviderPref(providers);
+ await Router._updateMessageProviders();
+ assert.equal(Router.state.providers.length, 1);
+ assert.equal(Router.state.providers[0].id, providers[1].id);
+ });
+ });
+
+ describe("#handleMessageRequest", () => {
+ beforeEach(async () => {
+ await Router.setState(() => ({
+ providers: [{ id: "snippets" }, { id: "badge" }],
+ }));
+ });
+ it("should not return a blocked message", async () => {
+ // Block all messages except the first
+ await Router.setState(() => ({
+ messages: [
+ { id: "foo", provider: "snippets", groups: ["snippets"] },
+ { id: "bar", provider: "snippets", groups: ["snippets"] },
+ ],
+ messageBlockList: ["foo"],
+ }));
+ await Router.handleMessageRequest({
+ provider: "snippets",
+ });
+ assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
+ messages: [{ id: "bar", provider: "snippets", groups: ["snippets"] }],
+ });
+ });
+ it("should not return a message from a disabled group", async () => {
+ ASRouterTargeting.findMatchingMessage.callsFake(
+ ({ messages }) => messages[0]
+ );
+ // Block all messages except the first
+ await Router.setState(() => ({
+ messages: [
+ { id: "foo", provider: "snippets", groups: ["snippets"] },
+ { id: "bar", provider: "snippets", groups: ["snippets"] },
+ ],
+ groups: [{ id: "snippets", enabled: false }],
+ }));
+ const result = await Router.handleMessageRequest({
+ provider: "snippets",
+ });
+ assert.isNull(result);
+ });
+ it("should not return a message from a blocked campaign", async () => {
+ // Block all messages except the first
+ await Router.setState(() => ({
+ messages: [
+ {
+ id: "foo",
+ provider: "snippets",
+ campaign: "foocampaign",
+ groups: ["snippets"],
+ },
+ { id: "bar", provider: "snippets", groups: ["snippets"] },
+ ],
+ messageBlockList: ["foocampaign"],
+ }));
+
+ await Router.handleMessageRequest({
+ provider: "snippets",
+ });
+ assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
+ messages: [{ id: "bar", provider: "snippets", groups: ["snippets"] }],
+ });
+ });
+ it("should not return a message excluded by the provider", async () => {
+ // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
+ // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
+ await Router.setState(() => ({
+ providers: [{ id: "snippets", exclude: ["foo"] }],
+ }));
+
+ await Router.setState(() => ({
+ messages: [{ id: "foo", provider: "snippets" }],
+ messageBlockList: ["foocampaign"],
+ }));
+
+ const result = await Router.handleMessageRequest({
+ provider: "snippets",
+ });
+ assert.isNull(result);
+ });
+ it("should not return a message if the frequency cap has been hit", async () => {
+ sandbox.stub(Router, "isBelowFrequencyCaps").returns(false);
+ await Router.setState(() => ({
+ messages: [{ id: "foo", provider: "snippets" }],
+ }));
+ const result = await Router.handleMessageRequest({
+ provider: "snippets",
+ });
+ assert.isNull(result);
+ });
+ it("should get unblocked messages that match the trigger", async () => {
+ const message1 = {
+ id: "1",
+ campaign: "foocampaign",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ trigger: { id: "bar" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ await Router.setState({ messages: [message2, message1] });
+ // Just return the first message provided as arg
+ ASRouterTargeting.findMatchingMessage.callsFake(
+ ({ messages }) => messages[0]
+ );
+
+ const result = Router.handleMessageRequest({ triggerId: "foo" });
+
+ assert.deepEqual(result, message1);
+ });
+ it("should get unblocked messages that match trigger and template", async () => {
+ const message1 = {
+ id: "1",
+ campaign: "foocampaign",
+ template: "badge",
+ trigger: { id: "foo" },
+ groups: ["badge"],
+ provider: "badge",
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ template: "snippet",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ await Router.setState({ messages: [message2, message1] });
+ // Just return the first message provided as arg
+ ASRouterTargeting.findMatchingMessage.callsFake(
+ ({ messages }) => messages[0]
+ );
+
+ const result = Router.handleMessageRequest({
+ triggerId: "foo",
+ template: "badge",
+ });
+
+ assert.deepEqual(result, message1);
+ });
+ it("should have messageImpressions in the message context", () => {
+ assert.propertyVal(
+ Router._getMessagesContext(),
+ "messageImpressions",
+ Router.state.messageImpressions
+ );
+ });
+ it("should return all unblocked messages that match the template, trigger if returnAll=true", async () => {
+ const message1 = {
+ provider: "whats_new",
+ id: "1",
+ template: "whatsnew_panel_message",
+ trigger: { id: "whatsNewPanelOpened" },
+ groups: ["whats_new"],
+ };
+ const message2 = {
+ provider: "whats_new",
+ id: "2",
+ template: "whatsnew_panel_message",
+ trigger: { id: "whatsNewPanelOpened" },
+ groups: ["whats_new"],
+ };
+ const message3 = {
+ provider: "whats_new",
+ id: "3",
+ template: "badge",
+ groups: ["whats_new"],
+ };
+ ASRouterTargeting.findMatchingMessage.callsFake(() => [
+ message2,
+ message1,
+ ]);
+ await Router.setState({
+ messages: [message3, message2, message1],
+ providers: [{ id: "whats_new" }],
+ });
+ const result = await Router.handleMessageRequest({
+ template: "whatsnew_panel_message",
+ triggerId: "whatsNewPanelOpened",
+ returnAll: true,
+ });
+
+ assert.deepEqual(result, [message2, message1]);
+ });
+ it("should forward trigger param info", async () => {
+ const trigger = {
+ triggerId: "foo",
+ triggerParam: "bar",
+ triggerContext: "context",
+ };
+ const message1 = {
+ id: "1",
+ campaign: "foocampaign",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ trigger: { id: "bar" },
+ groups: ["badge"],
+ provider: "badge",
+ };
+ await Router.setState({ messages: [message2, message1] });
+ // Just return the first message provided as arg
+
+ Router.handleMessageRequest(trigger);
+
+ assert.calledOnce(ASRouterTargeting.findMatchingMessage);
+
+ const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
+ assert.propertyVal(options.trigger, "id", trigger.triggerId);
+ assert.propertyVal(options.trigger, "param", trigger.triggerParam);
+ assert.propertyVal(options.trigger, "context", trigger.triggerContext);
+ });
+ it("should cache snippets messages", async () => {
+ const trigger = {
+ triggerId: "foo",
+ triggerParam: "bar",
+ triggerContext: "context",
+ };
+ const message1 = {
+ id: "1",
+ provider: "snippets",
+ campaign: "foocampaign",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ trigger: { id: "bar" },
+ groups: ["snippets"],
+ };
+ await Router.setState({ messages: [message2, message1] });
+
+ Router.handleMessageRequest(trigger);
+
+ assert.calledOnce(ASRouterTargeting.findMatchingMessage);
+
+ const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
+ assert.propertyVal(options, "shouldCache", true);
+ });
+ it("should not cache badge messages", async () => {
+ const trigger = {
+ triggerId: "bar",
+ triggerParam: "bar",
+ triggerContext: "context",
+ };
+ const message1 = {
+ id: "1",
+ provider: "snippets",
+ campaign: "foocampaign",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ trigger: { id: "bar" },
+ groups: ["badge"],
+ provider: "badge",
+ };
+ await Router.setState({ messages: [message2, message1] });
+ // Just return the first message provided as arg
+
+ Router.handleMessageRequest(trigger);
+
+ assert.calledOnce(ASRouterTargeting.findMatchingMessage);
+
+ const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
+ assert.propertyVal(options, "shouldCache", false);
+ });
+ it("should filter out messages without a trigger (or different) when a triggerId is defined", async () => {
+ const trigger = { triggerId: "foo" };
+ const message1 = {
+ id: "1",
+ campaign: "foocampaign",
+ trigger: { id: "foo" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ const message2 = {
+ id: "2",
+ campaign: "foocampaign",
+ trigger: { id: "bar" },
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ const message3 = {
+ id: "3",
+ campaign: "bazcampaign",
+ groups: ["snippets"],
+ provider: "snippets",
+ };
+ await Router.setState({
+ messages: [message2, message1, message3],
+ groups: [{ id: "snippets", enabled: true }],
+ });
+ // Just return the first message provided as arg
+ ASRouterTargeting.findMatchingMessage.callsFake(args => args.messages);
+
+ const result = Router.handleMessageRequest(trigger);
+
+ assert.lengthOf(result, 1);
+ assert.deepEqual(result[0], message1);
+ });
+ });
+
+ describe("#uninit", () => {
+ it("should unregister the trigger listeners", () => {
+ for (const listener of ASRouterTriggerListeners.values()) {
+ sandbox.spy(listener, "uninit");
+ }
+
+ Router.uninit();
+
+ for (const listener of ASRouterTriggerListeners.values()) {
+ assert.calledOnce(listener.uninit);
+ }
+ });
+ it("should set .dispatchCFRAction to null", () => {
+ Router.uninit();
+ assert.isNull(Router.dispatchCFRAction);
+ assert.isNull(Router.clearChildMessages);
+ assert.isNull(Router.sendTelemetry);
+ });
+ it("should save previousSessionEnd", () => {
+ Router.uninit();
+
+ assert.calledOnce(Router._storage.set);
+ assert.calledWithExactly(
+ Router._storage.set,
+ "previousSessionEnd",
+ sinon.match.number
+ );
+ });
+ it("should remove the observer for `intl:app-locales-changed`", () => {
+ sandbox.spy(global.Services.obs, "removeObserver");
+ Router.uninit();
+
+ assert.calledWithExactly(
+ global.Services.obs.removeObserver,
+ Router._onLocaleChanged,
+ "intl:app-locales-changed"
+ );
+ });
+ it("should remove the pref observer for `USE_REMOTE_L10N_PREF`", async () => {
+ sandbox.spy(global.Services.prefs, "removeObserver");
+ Router.uninit();
+
+ // Grab the last call as #uninit() also involves multiple calls of `Services.prefs.removeObserver`.
+ const call = global.Services.prefs.removeObserver.lastCall;
+ assert.calledWithExactly(call, USE_REMOTE_L10N_PREF, Router);
+ });
+ });
+
+ describe("sendNewTabMessage", () => {
+ it("should construct an appropriate response message", async () => {
+ Router.loadMessagesFromAllProviders.resetHistory();
+ Router.loadMessagesFromAllProviders.onFirstCall().resolves();
+
+ let message = {
+ id: "foo",
+ provider: "snippets",
+ groups: ["snippets"],
+ };
+
+ await Router.setState({
+ messages: [message],
+ providers: [{ id: "snippets" }],
+ });
+
+ ASRouterTargeting.findMatchingMessage.callsFake(
+ ({ messages }) => messages[0]
+ );
+
+ let response = await Router.sendNewTabMessage({
+ tabId: 0,
+ browser: {},
+ });
+
+ assert.deepEqual(response.message, message);
+ });
+ it("should send an empty object message if no messages are available", async () => {
+ await Router.setState({ messages: [] });
+ let response = await Router.sendNewTabMessage({
+ tabId: 0,
+ browser: {},
+ });
+
+ assert.deepEqual(response.message, {});
+ });
+
+ describe("#addPreviewEndpoint", () => {
+ it("should make a request to the provided endpoint", async () => {
+ const url = "https://snippets-admin.mozilla.org/foo";
+ const browser = {};
+ browser.sendMessageToActor = sandbox.stub();
+
+ await Router.sendNewTabMessage({
+ endpoint: { url },
+ tabId: 0,
+ browser,
+ });
+
+ assert.calledWith(global.fetch, url);
+ assert.lengthOf(
+ Router.state.providers.filter(p => p.url === url),
+ 0
+ );
+ });
+ it("should send EnterSnippetPreviewMode when adding a preview endpoint", async () => {
+ const url = "https://snippets-admin.mozilla.org/foo";
+ const browser = {};
+ browser.sendMessageToActor = sandbox.stub();
+
+ await Router.addPreviewEndpoint(url, browser);
+
+ assert.calledWithExactly(
+ browser.sendMessageToActor,
+ "EnterSnippetsPreviewMode",
+ {},
+ "ASRouter"
+ );
+ });
+ it("should not add a url that is not from an allowed host", async () => {
+ const url = "https://mozilla.org";
+ const browser = {};
+ browser.sendMessageToActor = sandbox.stub();
+
+ await Router.addPreviewEndpoint(url, browser);
+
+ assert.lengthOf(
+ Router.state.providers.filter(p => p.url === url),
+ 0
+ );
+ });
+ it("should reject bad urls", async () => {
+ const url = "foo";
+ const browser = {};
+ browser.sendMessageToActor = sandbox.stub();
+
+ await Router.addPreviewEndpoint(url, browser);
+
+ assert.lengthOf(
+ Router.state.providers.filter(p => p.url === url),
+ 0
+ );
+ });
+ });
+
+ it("should record telemetry for message request duration", async () => {
+ const startTelemetryStopwatch = sandbox.stub(
+ global.TelemetryStopwatch,
+ "start"
+ );
+ const finishTelemetryStopwatch = sandbox.stub(
+ global.TelemetryStopwatch,
+ "finish"
+ );
+ sandbox.stub(Router, "handleMessageRequest");
+ const tabId = 123;
+ await Router.sendNewTabMessage({
+ tabId,
+ browser: {},
+ });
+
+ // Called once for the messagesLoaded trigger and once for the above call.
+ assert.calledTwice(startTelemetryStopwatch);
+ assert.calledWithExactly(
+ startTelemetryStopwatch,
+ "MS_MESSAGE_REQUEST_TIME_MS",
+ { tabId }
+ );
+ assert.calledTwice(finishTelemetryStopwatch);
+ assert.calledWithExactly(
+ finishTelemetryStopwatch,
+ "MS_MESSAGE_REQUEST_TIME_MS",
+ { tabId }
+ );
+ });
+ it("should return the preview message if that's available and remove it from Router.state", async () => {
+ const expectedObj = {
+ id: "foo",
+ groups: ["preview"],
+ provider: "preview",
+ };
+ await Router.setState({
+ messages: [expectedObj],
+ providers: [{ id: "preview" }],
+ });
+
+ ASRouterTargeting.findMatchingMessage.callsFake(
+ ({ messages }) => expectedObj
+ );
+
+ Router.loadMessagesFromAllProviders.resetHistory();
+ Router.loadMessagesFromAllProviders.onFirstCall().resolves();
+
+ let response = await Router.sendNewTabMessage({
+ endpoint: { url: "foo.com" },
+ tabId: 0,
+ browser: {},
+ });
+
+ assert.deepEqual(response.message, expectedObj);
+
+ assert.isUndefined(
+ Router.state.messages.find(m => m.provider === "preview")
+ );
+ });
+ });
+
+ describe("#setMessageById", async () => {
+ it("should send an empty message if provided id did not resolve to a message", async () => {
+ let response = await Router.setMessageById({ id: -1 }, true, {});
+ assert.deepEqual(response.message, {});
+ });
+ });
+
+ describe("#isUnblockedMessage", () => {
+ it("should block a message if the group is blocked", async () => {
+ const msg = { id: "msg1", groups: ["foo"], provider: "unit-test" };
+ await Router.setState({
+ groups: [{ id: "foo", enabled: false }],
+ messages: [msg],
+ providers: [{ id: "unit-test" }],
+ });
+ assert.isFalse(Router.isUnblockedMessage(msg));
+
+ await Router.setState({ groups: [{ id: "foo", enabled: true }] });
+
+ assert.isTrue(Router.isUnblockedMessage(msg));
+ });
+ it("should block a message if at least one group is blocked", async () => {
+ const msg = {
+ id: "msg1",
+ groups: ["foo", "bar"],
+ provider: "unit-test",
+ };
+ await Router.setState({
+ groups: [
+ { id: "foo", enabled: false },
+ { id: "bar", enabled: false },
+ ],
+ messages: [msg],
+ providers: [{ id: "unit-test" }],
+ });
+ assert.isFalse(Router.isUnblockedMessage(msg));
+
+ await Router.setState({
+ groups: [
+ { id: "foo", enabled: true },
+ { id: "bar", enabled: false },
+ ],
+ });
+
+ assert.isFalse(Router.isUnblockedMessage(msg));
+ });
+ });
+
+ describe("#blockMessageById", () => {
+ it("should add the id to the messageBlockList", async () => {
+ await Router.blockMessageById("foo");
+ assert.isTrue(Router.state.messageBlockList.includes("foo"));
+ });
+ it("should add the campaign to the messageBlockList instead of id if .campaign is specified and not select messages of that campaign again", async () => {
+ await Router.setState({
+ messages: [
+ { id: "1", campaign: "foocampaign" },
+ { id: "2", campaign: "foocampaign" },
+ ],
+ });
+ await Router.blockMessageById("1");
+
+ assert.isTrue(Router.state.messageBlockList.includes("foocampaign"));
+ assert.isEmpty(Router.state.messages.filter(Router.isUnblockedMessage));
+ });
+ it("should be able to add multiple items to the messageBlockList", async () => {
+ await await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
+ assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
+ assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
+ });
+ it("should save the messageBlockList", async () => {
+ await await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
+ assert.calledWithExactly(Router._storage.set, "messageBlockList", [
+ FAKE_BUNDLE[0].id,
+ FAKE_BUNDLE[1].id,
+ ]);
+ });
+ });
+
+ describe("#unblockMessageById", () => {
+ it("should remove the id from the messageBlockList", async () => {
+ await Router.blockMessageById("foo");
+ assert.isTrue(Router.state.messageBlockList.includes("foo"));
+ await Router.unblockMessageById("foo");
+ assert.isFalse(Router.state.messageBlockList.includes("foo"));
+ });
+ it("should remove the campaign from the messageBlockList if it is defined", async () => {
+ await Router.setState({ messages: [{ id: "1", campaign: "foo" }] });
+ await Router.blockMessageById("1");
+ assert.isTrue(
+ Router.state.messageBlockList.includes("foo"),
+ "blocklist has campaign id"
+ );
+ await Router.unblockMessageById("1");
+ assert.isFalse(
+ Router.state.messageBlockList.includes("foo"),
+ "campaign id removed from blocklist"
+ );
+ });
+ it("should save the messageBlockList", async () => {
+ await Router.unblockMessageById("foo");
+ assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
+ });
+ });
+
+ describe("#routeCFRMessage", () => {
+ it("should allow for echoing back message modifications", () => {
+ const message = { somekey: "some value" };
+ const data = { content: message };
+ const browser = {};
+ let msg = Router.routeCFRMessage(data.content, browser, data, false);
+ assert.deepEqual(msg.message, message);
+ });
+ it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
+ sandbox.stub(CFRPageActions, "forceRecommendation");
+ const testMessage = { id: "foo", template: "cfr_doorhanger" };
+ await Router.setState({ messages: [testMessage] });
+ Router.routeCFRMessage(testMessage, {}, null, true);
+
+ assert.calledOnce(CFRPageActions.forceRecommendation);
+ });
+ it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
+ sandbox.stub(CFRPageActions, "addRecommendation");
+ const testMessage = { id: "foo", template: "cfr_doorhanger" };
+ await Router.setState({ messages: [testMessage] });
+ Router.routeCFRMessage(testMessage, {}, {}, false);
+ assert.calledOnce(CFRPageActions.addRecommendation);
+ });
+ });
+
+ describe("#updateTargetingParameters", () => {
+ it("should return an object containing the whole state", async () => {
+ sandbox.stub(Router, "getTargetingParameters").resolves({});
+ let msg = await Router.updateTargetingParameters();
+ let expected = Object.assign({}, Router.state, {
+ providerPrefs: ASRouterPreferences.providers,
+ userPrefs: ASRouterPreferences.getAllUserPreferences(),
+ targetingParameters: {},
+ errors: Router.errors,
+ });
+
+ assert.deepEqual(msg, expected);
+ });
+ });
+
+ describe("#reachEvent", () => {
+ let experimentAPIStub;
+ let featureIds = ["cfr", "moments-page", "infobar", "spotlight"];
+ beforeEach(() => {
+ let getExperimentMetaDataStub = sandbox.stub();
+ let getAllBranchesStub = sandbox.stub();
+ featureIds.forEach(feature => {
+ global.NimbusFeatures[feature].getAllVariables.returns({
+ id: `message-${feature}`,
+ });
+ getExperimentMetaDataStub.withArgs({ featureId: feature }).returns({
+ slug: `slug-${feature}`,
+ branch: {
+ slug: `branch-${feature}`,
+ },
+ });
+ getAllBranchesStub.withArgs(`slug-${feature}`).resolves([
+ {
+ slug: `other-branch-${feature}`,
+ [feature]: { value: { trigger: "unit-test" } },
+ },
+ ]);
+ });
+ experimentAPIStub = {
+ getExperimentMetaData: getExperimentMetaDataStub,
+ getAllBranches: getAllBranchesStub,
+ };
+ globals.set("ExperimentAPI", experimentAPIStub);
+ });
+ afterEach(() => {
+ sandbox.restore();
+ });
+ it("should tag `forReachEvent` for all the expected message types", async () => {
+ // This should match the `providers.messaging-experiments`
+ let response = await MessageLoaderUtils.loadMessagesForProvider({
+ type: "remote-experiments",
+ featureIds,
+ });
+
+ // 1 message for reach 1 for expose
+ assert.property(response, "messages");
+ assert.lengthOf(response.messages, featureIds.length * 2);
+ assert.lengthOf(
+ response.messages.filter(m => m.forReachEvent),
+ featureIds.length
+ );
+ });
+ });
+
+ describe("#sendTriggerMessage", () => {
+ it("should pass the trigger to ASRouterTargeting when sending trigger message", async () => {
+ await Router.setState({
+ messages: [
+ {
+ id: "foo1",
+ provider: "onboarding",
+ template: "onboarding",
+ trigger: { id: "firstRun" },
+ content: { title: "Foo1", body: "Foo123-1" },
+ groups: ["onboarding"],
+ },
+ ],
+ providers: [{ id: "onboarding" }],
+ });
+
+ Router.loadMessagesFromAllProviders.resetHistory();
+ Router.loadMessagesFromAllProviders.onFirstCall().resolves();
+
+ await Router.sendTriggerMessage({
+ tabId: 0,
+ browser: {},
+ id: "firstRun",
+ });
+
+ assert.calledOnce(ASRouterTargeting.findMatchingMessage);
+ assert.deepEqual(
+ ASRouterTargeting.findMatchingMessage.firstCall.args[0].trigger,
+ {
+ id: "firstRun",
+ param: undefined,
+ context: undefined,
+ }
+ );
+ });
+ it("should record telemetry information", async () => {
+ const startTelemetryStopwatch = sandbox.stub(
+ global.TelemetryStopwatch,
+ "start"
+ );
+ const finishTelemetryStopwatch = sandbox.stub(
+ global.TelemetryStopwatch,
+ "finish"
+ );
+
+ const tabId = 123;
+
+ await Router.sendTriggerMessage({
+ tabId,
+ browser: {},
+ id: "firstRun",
+ });
+
+ assert.calledTwice(startTelemetryStopwatch);
+ assert.calledWithExactly(
+ startTelemetryStopwatch,
+ "MS_MESSAGE_REQUEST_TIME_MS",
+ { tabId }
+ );
+ assert.calledTwice(finishTelemetryStopwatch);
+ assert.calledWithExactly(
+ finishTelemetryStopwatch,
+ "MS_MESSAGE_REQUEST_TIME_MS",
+ { tabId }
+ );
+ });
+ it("should have previousSessionEnd in the message context", () => {
+ assert.propertyVal(
+ Router._getMessagesContext(),
+ "previousSessionEnd",
+ 100
+ );
+ });
+ it("should record the Reach event if found any", async () => {
+ let messages = [
+ {
+ id: "foo1",
+ forReachEvent: { sent: false, group: "cfr" },
+ experimentSlug: "exp01",
+ branchSlug: "branch01",
+ template: "simple_template",
+ trigger: { id: "foo" },
+ content: { title: "Foo1", body: "Foo123-1" },
+ },
+ {
+ id: "foo2",
+ template: "simple_template",
+ trigger: { id: "bar" },
+ content: { title: "Foo2", body: "Foo123-2" },
+ provider: "onboarding",
+ },
+ {
+ id: "foo3",
+ forReachEvent: { sent: false, group: "cfr" },
+ experimentSlug: "exp02",
+ branchSlug: "branch02",
+ template: "simple_template",
+ trigger: { id: "foo" },
+ content: { title: "Foo1", body: "Foo123-1" },
+ },
+ ];
+ sandbox.stub(Router, "handleMessageRequest").resolves(messages);
+ sandbox.spy(Services.telemetry, "recordEvent");
+
+ await Router.sendTriggerMessage({
+ tabId: 0,
+ browser: {},
+ id: "foo",
+ });
+
+ assert.calledTwice(Services.telemetry.recordEvent);
+ });
+ it("should not record the Reach event if it's already sent", async () => {
+ let messages = [
+ {
+ id: "foo1",
+ forReachEvent: { sent: true, group: "cfr" },
+ experimentSlug: "exp01",
+ branchSlug: "branch01",
+ template: "simple_template",
+ trigger: { id: "foo" },
+ content: { title: "Foo1", body: "Foo123-1" },
+ },
+ ];
+ sandbox.stub(Router, "handleMessageRequest").resolves(messages);
+ sandbox.spy(Services.telemetry, "recordEvent");
+
+ await Router.sendTriggerMessage({
+ tabId: 0,
+ browser: {},
+ id: "foo",
+ });
+ assert.notCalled(Services.telemetry.recordEvent);
+ });
+ it("should record the Exposure event for each valid feature", async () => {
+ ["cfr_doorhanger", "update_action", "infobar", "spotlight"].forEach(
+ async template => {
+ let featureMap = {
+ cfr_doorhanger: "cfr",
+ spotlight: "spotlight",
+ infobar: "infobar",
+ update_action: "moments-page",
+ };
+ assert.notCalled(
+ global.NimbusFeatures[featureMap[template]].recordExposureEvent
+ );
+
+ let messages = [
+ {
+ id: "foo1",
+ template,
+ trigger: { id: "foo" },
+ content: { title: "Foo1", body: "Foo123-1" },
+ },
+ ];
+ sandbox.stub(Router, "handleMessageRequest").resolves(messages);
+
+ await Router.sendTriggerMessage({
+ tabId: 0,
+ browser: {},
+ id: "foo",
+ });
+
+ assert.calledOnce(
+ global.NimbusFeatures[featureMap[template]].recordExposureEvent
+ );
+ }
+ );
+ });
+ });
+
+ describe("forceAttribution", () => {
+ let setReferrerUrl;
+ beforeEach(() => {
+ setReferrerUrl = sinon.spy();
+ global.Cc["@mozilla.org/mac-attribution;1"] = {
+ getService: () => ({ setReferrerUrl }),
+ };
+
+ sandbox.stub(global.Services.env, "set");
+ });
+ it("should double encode on windows", async () => {
+ sandbox.stub(fakeAttributionCode, "writeAttributionFile");
+
+ Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
+
+ assert.notCalled(setReferrerUrl);
+ assert.calledWithMatch(
+ fakeAttributionCode.writeAttributionFile,
+ "foo%3DFOO!%26bar%3DBAR%253F"
+ );
+ });
+ it("should set referrer on mac", async () => {
+ sandbox.stub(global.AppConstants, "platform").value("macosx");
+
+ Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
+
+ assert.calledOnce(setReferrerUrl);
+ assert.calledWithMatch(setReferrerUrl, "", "?foo=FOO!&bar=BAR%3F");
+ });
+ });
+
+ describe("#forceWNPanel", () => {
+ let browser = {
+ ownerGlobal: {
+ document: new Document(),
+ PanelUI: {
+ showSubView: sinon.stub(),
+ panel: {
+ setAttribute: sinon.stub(),
+ },
+ },
+ },
+ };
+ let fakePanel = {
+ setAttribute: sinon.stub(),
+ };
+ sinon
+ .stub(browser.ownerGlobal.document, "getElementById")
+ .returns(fakePanel);
+
+ it("should call enableToolbarButton", async () => {
+ await Router.forceWNPanel(browser);
+ assert.calledOnce(FakeToolbarPanelHub.enableToolbarButton);
+ assert.calledOnce(browser.ownerGlobal.PanelUI.showSubView);
+ assert.calledWith(fakePanel.setAttribute, "noautohide", true);
+ });
+ });
+
+ describe("_triggerHandler", () => {
+ it("should call #sendTriggerMessage with the correct trigger", () => {
+ const getter = sandbox.stub();
+ getter.returns(false);
+ sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
+ sinon.spy(Router, "sendTriggerMessage");
+ const browser = {};
+ const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
+ Router._triggerHandler(browser, trigger);
+ assert.calledOnce(Router.sendTriggerMessage);
+ assert.calledWith(
+ Router.sendTriggerMessage,
+ sandbox.match({
+ id: "FAKE_TRIGGER",
+ param: "some fake param",
+ })
+ );
+ });
+ });
+
+ describe("_triggerHandler_kiosk", () => {
+ it("should not call #sendTriggerMessage", () => {
+ const getter = sandbox.stub();
+ getter.returns(true);
+ sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
+ sinon.spy(Router, "sendTriggerMessage");
+ const browser = {};
+ const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
+ Router._triggerHandler(browser, trigger);
+ assert.notCalled(Router.sendTriggerMessage);
+ });
+ });
+
+ describe("valid preview endpoint", () => {
+ it("should report an error if url protocol is not https", () => {
+ sandbox.stub(console, "error");
+
+ assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
+ assert.calledTwice(console.error);
+ });
+ });
+
+ describe("impressions", () => {
+ describe("#addImpression for groups", () => {
+ it("should save an impression in each group-with-frequency in a message", async () => {
+ const fooMessageImpressions = [0];
+ const aGroupImpressions = [0, 1, 2];
+ const bGroupImpressions = [3, 4, 5];
+ const cGroupImpressions = [6, 7, 8];
+
+ const message = {
+ id: "foo",
+ provider: "bar",
+ groups: ["a", "b", "c"],
+ };
+ const groups = [
+ { id: "a", frequency: { lifetime: 3 } },
+ { id: "b", frequency: { lifetime: 4 } },
+ { id: "c", frequency: { lifetime: 5 } },
+ ];
+ await Router.setState(state => {
+ // Add provider
+ const providers = [...state.providers];
+ // Add fooMessageImpressions
+ // eslint-disable-next-line no-shadow
+ const messageImpressions = Object.assign(
+ {},
+ state.messageImpressions
+ );
+ let gImpressions = {};
+ gImpressions.a = aGroupImpressions;
+ gImpressions.b = bGroupImpressions;
+ gImpressions.c = cGroupImpressions;
+ messageImpressions.foo = fooMessageImpressions;
+ return {
+ providers,
+ messageImpressions,
+ groups,
+ groupImpressions: gImpressions,
+ };
+ });
+
+ await Router.addImpression(message);
+
+ assert.deepEqual(
+ Router.state.groupImpressions.a,
+ [0, 1, 2, 0],
+ "a impressions"
+ );
+ assert.deepEqual(
+ Router.state.groupImpressions.b,
+ [3, 4, 5, 0],
+ "b impressions"
+ );
+ assert.deepEqual(
+ Router.state.groupImpressions.c,
+ [6, 7, 8, 0],
+ "c impressions"
+ );
+ });
+ });
+
+ describe("#isBelowFrequencyCaps", () => {
+ it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
+ sinon.spy(Router, "_isBelowItemFrequencyCap");
+
+ const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
+ const fooMessageImpressions = [0, 1];
+ const barGroupImpressions = [0, 1, 2];
+
+ const message = {
+ id: "foo",
+ provider: "bar",
+ groups: ["bar"],
+ frequency: { lifetime: 3 },
+ };
+ const groups = [{ id: "bar", frequency: { lifetime: 5 } }];
+
+ await Router.setState(state => {
+ // Add provider
+ const providers = [...state.providers];
+ // Add fooMessageImpressions
+ // eslint-disable-next-line no-shadow
+ const messageImpressions = Object.assign(
+ {},
+ state.messageImpressions
+ );
+ let gImpressions = {};
+ gImpressions.bar = barGroupImpressions;
+ messageImpressions.foo = fooMessageImpressions;
+ return {
+ providers,
+ messageImpressions,
+ groups,
+ groupImpressions: gImpressions,
+ };
+ });
+
+ await Router.isBelowFrequencyCaps(message);
+
+ assert.calledTwice(Router._isBelowItemFrequencyCap);
+ assert.calledWithExactly(
+ Router._isBelowItemFrequencyCap,
+ message,
+ fooMessageImpressions,
+ MAX_MESSAGE_LIFETIME_CAP
+ );
+ assert.calledWithExactly(
+ Router._isBelowItemFrequencyCap,
+ groups[0],
+ barGroupImpressions
+ );
+ });
+ });
+
+ describe("#_isBelowItemFrequencyCap", () => {
+ it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
+ const item = { id: "foo", frequency: { lifetime: 5 } };
+ const impressions = [0, 1];
+ const maxLifetimeCap = 1;
+ const result = Router._isBelowItemFrequencyCap(
+ item,
+ impressions,
+ maxLifetimeCap
+ );
+ assert.isFalse(result);
+ });
+
+ describe("lifetime frequency caps", () => {
+ it("should return true if .frequency is not defined on the item", () => {
+ const item = { id: "foo" };
+ const impressions = [0, 1];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isTrue(result);
+ });
+ it("should return true if there are no impressions", () => {
+ const item = {
+ id: "foo",
+ frequency: {
+ lifetime: 10,
+ custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
+ },
+ };
+ const impressions = [];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isTrue(result);
+ });
+ it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
+ const item = { id: "foo", frequency: { lifetime: 3 } };
+ const impressions = [0, 1];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isTrue(result);
+ });
+ it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
+ const item = { id: "foo", frequency: { lifetime: 3 } };
+ const impressions = [0, 1, 2];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isFalse(result);
+ });
+ it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
+ const item = { id: "foo", frequency: { lifetime: 3 } };
+ const impressions = [0, 1, 2, 3];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isFalse(result);
+ });
+ });
+
+ describe("custom frequency caps", () => {
+ it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
+ clock.tick(ONE_DAY_IN_MS + 10);
+ const item = {
+ id: "foo",
+ frequency: {
+ custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
+ lifetime: 3,
+ },
+ };
+ const impressions = [0, ONE_DAY_IN_MS + 1];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isTrue(result);
+ });
+ it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
+ clock.tick(200);
+ const item = {
+ id: "msg1",
+ frequency: { custom: [{ period: 100, cap: 2 }], lifetime: 3 },
+ };
+ const impressions = [0, 160, 161];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isFalse(result);
+ });
+ it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
+ clock.tick(ONE_DAY_IN_MS + 200);
+ const itemTrue = {
+ id: "msg2",
+ frequency: { custom: [{ period: 100, cap: 2 }] },
+ };
+ const itemFalse = {
+ id: "msg1",
+ frequency: {
+ custom: [
+ { period: 100, cap: 2 },
+ { period: ONE_DAY_IN_MS, cap: 3 },
+ ],
+ },
+ };
+ const impressions = [
+ 0,
+ ONE_DAY_IN_MS + 160,
+ ONE_DAY_IN_MS - 100,
+ ONE_DAY_IN_MS - 200,
+ ];
+ assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
+ assert.isFalse(
+ Router._isBelowItemFrequencyCap(itemFalse, impressions)
+ );
+ });
+ it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
+ clock.tick(ONE_DAY_IN_MS + 10);
+ const item = {
+ id: "msg1",
+ frequency: {
+ custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
+ lifetime: 3,
+ },
+ };
+ const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isFalse(result);
+ });
+ it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
+ clock.tick(ONE_DAY_IN_MS + 10);
+ const item = {
+ id: "msg1",
+ frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
+ };
+ const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isTrue(result);
+ });
+ it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
+ clock.tick(ONE_DAY_IN_MS + 10);
+ const item = {
+ id: "msg1",
+ frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
+ };
+ const impressions = [
+ 0,
+ 1,
+ 2,
+ 3,
+ ONE_DAY_IN_MS + 1,
+ ONE_DAY_IN_MS + 2,
+ ONE_DAY_IN_MS + 3,
+ ];
+ const result = Router._isBelowItemFrequencyCap(item, impressions);
+ assert.isFalse(result);
+ });
+ });
+ });
+
+ describe("#getLongestPeriod", () => {
+ it("should return the period if there is only one definition", () => {
+ const message = {
+ id: "foo",
+ frequency: { custom: [{ period: 200, cap: 2 }] },
+ };
+ assert.equal(Router.getLongestPeriod(message), 200);
+ });
+ it("should return the longest period if there are more than one definitions", () => {
+ const message = {
+ id: "foo",
+ frequency: {
+ custom: [
+ { period: 1000, cap: 3 },
+ { period: ONE_DAY_IN_MS, cap: 5 },
+ { period: 100, cap: 2 },
+ ],
+ },
+ };
+ assert.equal(Router.getLongestPeriod(message), ONE_DAY_IN_MS);
+ });
+ it("should return null if there are is no .frequency", () => {
+ const message = { id: "foo" };
+ assert.isNull(Router.getLongestPeriod(message));
+ });
+ it("should return null if there are is no .frequency.custom", () => {
+ const message = { id: "foo", frequency: { lifetime: 10 } };
+ assert.isNull(Router.getLongestPeriod(message));
+ });
+ });
+
+ describe("cleanup on init", () => {
+ it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
+ const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
+ messageImpressions = { foo: [0], bar: [0, 1] };
+ // Impressions for "bar" should be removed since that id does not exist in messages
+ const result = { foo: [0] };
+
+ await createRouterAndInit([
+ { id: "onboarding", type: "local", messages, enabled: true },
+ ]);
+ assert.calledWith(Router._storage.set, "messageImpressions", result);
+ assert.deepEqual(Router.state.messageImpressions, result);
+ });
+ it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
+ const CURRENT_TIME = ONE_DAY_IN_MS * 2;
+ clock.tick(CURRENT_TIME);
+ const messages = [
+ {
+ id: "foo",
+ frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 5 }] },
+ },
+ ];
+ messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
+ // Only 0 and 1 are more than 24 hours before CURRENT_TIME
+ const result = { foo: [CURRENT_TIME - 10] };
+
+ await createRouterAndInit([
+ { id: "onboarding", type: "local", messages, enabled: true },
+ ]);
+ assert.calledWith(Router._storage.set, "messageImpressions", result);
+ assert.deepEqual(Router.state.messageImpressions, result);
+ });
+ it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
+ const CURRENT_TIME = ONE_DAY_IN_MS * 2;
+ clock.tick(CURRENT_TIME);
+ const messages = [
+ {
+ id: "foo",
+ frequency: {
+ custom: [
+ { period: ONE_DAY_IN_MS, cap: 5 },
+ { period: 100, cap: 2 },
+ ],
+ },
+ },
+ ];
+ messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
+ // Only 0 and 1 are more than 24 hours before CURRENT_TIME
+ const result = { foo: [CURRENT_TIME - 10] };
+
+ await createRouterAndInit([
+ { id: "onboarding", type: "local", messages, enabled: true },
+ ]);
+ assert.calledWith(Router._storage.set, "messageImpressions", result);
+ assert.deepEqual(Router.state.messageImpressions, result);
+ });
+ it("should clear messageImpressions if they are not properly formatted", async () => {
+ const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
+ // this is impromperly formatted since messageImpressions are supposed to be an array
+ messageImpressions = { foo: 0 };
+ const result = {};
+
+ await createRouterAndInit([
+ { id: "onboarding", type: "local", messages, enabled: true },
+ ]);
+ assert.calledWith(Router._storage.set, "messageImpressions", result);
+ assert.deepEqual(Router.state.messageImpressions, result);
+ });
+ it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
+ const messages = [
+ { id: "foo", frequency: { lifetime: 10 } },
+ { id: "bar", frequency: { lifetime: 10 } },
+ ];
+ messageImpressions = { foo: [0], bar: [] };
+
+ await createRouterAndInit([
+ { id: "onboarding", type: "local", messages, enabled: true },
+ ]);
+ assert.notCalled(Router._storage.set);
+ assert.deepEqual(Router.state.messageImpressions, messageImpressions);
+ });
+ });
+ });
+
+ describe("#_onLocaleChanged", () => {
+ it("should call _maybeUpdateL10nAttachment in the handler", async () => {
+ sandbox.spy(Router, "_maybeUpdateL10nAttachment");
+ await Router._onLocaleChanged();
+
+ assert.calledOnce(Router._maybeUpdateL10nAttachment);
+ });
+ });
+
+ describe("#_maybeUpdateL10nAttachment", () => {
+ it("should update the l10n attachment if the locale was changed", async () => {
+ const getter = sandbox.stub();
+ getter.onFirstCall().returns("en-US");
+ getter.onSecondCall().returns("fr");
+ sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
+ const provider = {
+ id: "cfr",
+ enabled: true,
+ type: "remote-settings",
+ collection: "cfr",
+ };
+ await createRouterAndInit([provider]);
+ sandbox.spy(Router, "setState");
+ Router.loadMessagesFromAllProviders.resetHistory();
+
+ await Router._maybeUpdateL10nAttachment();
+
+ assert.calledWith(Router.setState, {
+ localeInUse: "fr",
+ providers: [
+ {
+ id: "cfr",
+ enabled: true,
+ type: "remote-settings",
+ collection: "cfr",
+ lastUpdated: undefined,
+ errors: [],
+ },
+ ],
+ });
+ assert.calledOnce(Router.loadMessagesFromAllProviders);
+ });
+ it("should not update the l10n attachment if the provider doesn't need l10n attachment", async () => {
+ const getter = sandbox.stub();
+ getter.onFirstCall().returns("en-US");
+ getter.onSecondCall().returns("fr");
+ sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
+ const provider = {
+ id: "localProvider",
+ enabled: true,
+ type: "local",
+ };
+ await createRouterAndInit([provider]);
+ Router.loadMessagesFromAllProviders.resetHistory();
+ sandbox.spy(Router, "setState");
+
+ await Router._maybeUpdateL10nAttachment();
+
+ assert.notCalled(Router.setState);
+ assert.notCalled(Router.loadMessagesFromAllProviders);
+ });
+ });
+ describe("#observe", () => {
+ it("should reload l10n for CFRPageActions when the `USE_REMOTE_L10N_PREF` pref is changed", () => {
+ sandbox.spy(CFRPageActions, "reloadL10n");
+
+ Router.observe("", "", USE_REMOTE_L10N_PREF);
+
+ assert.calledOnce(CFRPageActions.reloadL10n);
+ });
+ it("should not react to other pref changes", () => {
+ sandbox.spy(CFRPageActions, "reloadL10n");
+
+ Router.observe("", "", "foo");
+
+ assert.notCalled(CFRPageActions.reloadL10n);
+ });
+ });
+ describe("#loadAllMessageGroups", () => {
+ it("should disable the group if the pref is false", async () => {
+ sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
+ sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
+ {
+ id: "provider-group",
+ enabled: true,
+ type: "remote",
+ userPreferences: ["cfrAddons"],
+ },
+ ]);
+ await Router.setState({
+ providers: [
+ {
+ id: "message-groups",
+ enabled: true,
+ collection: "collection",
+ type: "remote-settings",
+ },
+ ],
+ });
+
+ await Router.loadAllMessageGroups();
+
+ const group = Router.state.groups.find(g => g.id === "provider-group");
+
+ assert.ok(group);
+ assert.propertyVal(group, "enabled", false);
+ });
+ it("should enable the group if at least one pref is true", async () => {
+ sandbox
+ .stub(ASRouterPreferences, "getUserPreference")
+ .withArgs("cfrAddons")
+ .returns(false)
+ .withArgs("cfrFeatures")
+ .returns(true);
+ sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
+ {
+ id: "provider-group",
+ enabled: true,
+ type: "remote",
+ userPreferences: ["cfrAddons", "cfrFeatures"],
+ },
+ ]);
+ await Router.setState({
+ providers: [
+ {
+ id: "message-groups",
+ enabled: true,
+ collection: "collection",
+ type: "remote-settings",
+ },
+ ],
+ });
+
+ await Router.loadAllMessageGroups();
+
+ const group = Router.state.groups.find(g => g.id === "provider-group");
+
+ assert.ok(group);
+ assert.propertyVal(group, "enabled", true);
+ });
+ it("should be keep the group disabled if disabled is true", async () => {
+ sandbox.stub(ASRouterPreferences, "getUserPreference").returns(true);
+ sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
+ {
+ id: "provider-group",
+ enabled: false,
+ type: "remote",
+ userPreferences: ["cfrAddons"],
+ },
+ ]);
+ await Router.setState({
+ providers: [
+ {
+ id: "message-groups",
+ enabled: true,
+ collection: "collection",
+ type: "remote-settings",
+ },
+ ],
+ });
+
+ await Router.loadAllMessageGroups();
+
+ const group = Router.state.groups.find(g => g.id === "provider-group");
+
+ assert.ok(group);
+ assert.propertyVal(group, "enabled", false);
+ });
+ it("should keep local groups unchanged if provider doesn't require an update", async () => {
+ sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
+ sandbox.stub(MessageLoaderUtils, "_loadDataForProvider");
+ await Router.setState({
+ groups: [
+ {
+ id: "cfr",
+ enabled: true,
+ collection: "collection",
+ type: "remote-settings",
+ },
+ ],
+ });
+
+ await Router.loadAllMessageGroups();
+
+ const group = Router.state.groups.find(g => g.id === "cfr");
+
+ assert.ok(group);
+ assert.propertyVal(group, "enabled", true);
+ // Because it should not have updated
+ assert.notCalled(MessageLoaderUtils._loadDataForProvider);
+ });
+ it("should update local groups on pref change (no RS update)", async () => {
+ sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
+ sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
+ await Router.setState({
+ groups: [
+ {
+ id: "cfr",
+ enabled: true,
+ collection: "collection",
+ type: "remote-settings",
+ userPreferences: ["cfrAddons"],
+ },
+ ],
+ });
+
+ await Router.loadAllMessageGroups();
+
+ const group = Router.state.groups.find(g => g.id === "cfr");
+
+ assert.ok(group);
+ // Pref changed, updated the group state
+ assert.propertyVal(group, "enabled", false);
+ });
+ });
+ describe("unblockAll", () => {
+ it("Clears the message block list and returns the state value", async () => {
+ await Router.setState({ messageBlockList: ["one", "two", "three"] });
+ assert.equal(Router.state.messageBlockList.length, 3);
+ const state = await Router.unblockAll();
+ assert.equal(Router.state.messageBlockList.length, 0);
+ assert.equal(state.messageBlockList.length, 0);
+ });
+ });
+ describe("#loadMessagesForProvider", () => {
+ it("should fetch messages from the ExperimentAPI", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["spotlight"],
+ };
+
+ await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.calledOnce(global.NimbusFeatures.spotlight.getAllVariables);
+ assert.calledOnce(global.ExperimentAPI.getExperimentMetaData);
+ assert.calledWithExactly(global.ExperimentAPI.getExperimentMetaData, {
+ featureId: "spotlight",
+ });
+ });
+ it("should handle the case of no experiments in the ExperimentAPI", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["infobar"],
+ };
+
+ global.ExperimentAPI.getExperiment.returns(null);
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.lengthOf(result.messages, 0);
+ });
+ it("should normally load ExperimentAPI messages", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["infobar"],
+ };
+ const enrollment = {
+ branch: {
+ slug: "branch01",
+ infobar: {
+ featureId: "infobar",
+ value: { id: "id01", trigger: { id: "openURL" } },
+ },
+ },
+ };
+
+ global.NimbusFeatures.infobar.getAllVariables.returns(
+ enrollment.branch.infobar.value
+ );
+ global.ExperimentAPI.getExperimentMetaData.returns({
+ branch: { slug: enrollment.branch.slug },
+ });
+ global.ExperimentAPI.getAllBranches.returns([
+ enrollment.branch,
+ {
+ slug: "control",
+ infobar: {
+ featureId: "infobar",
+ value: null,
+ },
+ },
+ ]);
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.lengthOf(result.messages, 1);
+ });
+ it("should skip disabled features and not load the messages", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["cfr"],
+ };
+
+ global.NimbusFeatures.cfr.getAllVariables.returns(null);
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.lengthOf(result.messages, 0);
+ });
+ it("should fetch branches with trigger", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["cfr"],
+ };
+ const enrollment = {
+ slug: "exp01",
+ branch: {
+ slug: "branch01",
+ cfr: {
+ featureId: "cfr",
+ value: { id: "id01", trigger: { id: "openURL" } },
+ },
+ },
+ };
+
+ global.NimbusFeatures.cfr.getAllVariables.returns(
+ enrollment.branch.cfr.value
+ );
+ global.ExperimentAPI.getExperimentMetaData.returns({
+ slug: enrollment.slug,
+ active: true,
+ branch: { slug: enrollment.branch.slug },
+ });
+ global.ExperimentAPI.getAllBranches.resolves([
+ enrollment.branch,
+ {
+ slug: "branch02",
+ cfr: {
+ featureId: "cfr",
+ value: { id: "id02", trigger: { id: "openURL" } },
+ },
+ },
+ {
+ // This branch should not be loaded as it doesn't have the trigger
+ slug: "branch03",
+ cfr: {
+ featureId: "cfr",
+ value: { id: "id03" },
+ },
+ },
+ ]);
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.equal(result.messages.length, 2);
+ assert.equal(result.messages[0].id, "id01");
+ assert.equal(result.messages[1].id, "id02");
+ assert.equal(result.messages[1].experimentSlug, "exp01");
+ assert.equal(result.messages[1].branchSlug, "branch02");
+ assert.deepEqual(result.messages[1].forReachEvent, {
+ sent: false,
+ group: "cfr",
+ });
+ });
+ it("should fetch branches with trigger even if enrolled branch is disabled", async () => {
+ const args = {
+ type: "remote-experiments",
+ featureIds: ["cfr"],
+ };
+ const enrollment = {
+ slug: "exp01",
+ branch: {
+ slug: "branch01",
+ cfr: {
+ featureId: "cfr",
+ value: {},
+ },
+ },
+ };
+
+ // Nedds to match the `featureIds` value to return an enrollment
+ // for that feature
+ global.NimbusFeatures.cfr.getAllVariables.returns(
+ enrollment.branch.cfr.value
+ );
+ global.ExperimentAPI.getExperimentMetaData.returns({
+ slug: enrollment.slug,
+ active: true,
+ branch: { slug: enrollment.branch.slug },
+ });
+ global.ExperimentAPI.getAllBranches.resolves([
+ enrollment.branch,
+ {
+ slug: "branch02",
+ cfr: {
+ featureId: "cfr",
+ value: { id: "id02", trigger: { id: "openURL" } },
+ },
+ },
+ {
+ // This branch should not be loaded as it doesn't have the trigger
+ slug: "branch03",
+ cfr: {
+ featureId: "cfr",
+ value: { id: "id03" },
+ },
+ },
+ ]);
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(args);
+
+ assert.equal(result.messages.length, 1);
+ assert.equal(result.messages[0].id, "id02");
+ assert.equal(result.messages[0].experimentSlug, "exp01");
+ assert.equal(result.messages[0].branchSlug, "branch02");
+ assert.deepEqual(result.messages[0].forReachEvent, {
+ sent: false,
+ group: "cfr",
+ });
+ });
+ });
+ describe("#_remoteSettingsLoader", () => {
+ let provider;
+ let spy;
+ beforeEach(() => {
+ provider = {
+ id: "cfr",
+ collection: "cfr",
+ };
+ sandbox
+ .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
+ .resolves([{ id: "message_1" }]);
+ spy = sandbox.spy();
+ global.Downloader.prototype.downloadToDisk = spy;
+ });
+ it("should be called with the expected dir path", async () => {
+ const dlSpy = sandbox.spy(global, "Downloader");
+
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "en-US");
+
+ await MessageLoaderUtils._remoteSettingsLoader(provider, {});
+
+ assert.calledWith(
+ dlSpy,
+ "main",
+ "ms-language-packs",
+ "browser",
+ "newtab"
+ );
+ });
+ it("should allow fetch for known locales", async () => {
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "en-US");
+
+ await MessageLoaderUtils._remoteSettingsLoader(provider, {});
+
+ assert.calledOnce(spy);
+ });
+ it("should fallback to 'en-US' for locale 'und' ", async () => {
+ sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => "und");
+ const getRecordSpy = sandbox.spy(
+ global.KintoHttpClient.prototype,
+ "getRecord"
+ );
+
+ await MessageLoaderUtils._remoteSettingsLoader(provider, {});
+
+ assert.ok(getRecordSpy.args[0][0].includes("en-US"));
+ assert.calledOnce(spy);
+ });
+ it("should fallback to 'ja-JP-mac' for locale 'ja-JP-macos'", async () => {
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "ja-JP-macos");
+ const getRecordSpy = sandbox.spy(
+ global.KintoHttpClient.prototype,
+ "getRecord"
+ );
+
+ await MessageLoaderUtils._remoteSettingsLoader(provider, {});
+
+ assert.ok(getRecordSpy.args[0][0].includes("ja-JP-mac"));
+ assert.calledOnce(spy);
+ });
+ it("should not allow fetch for unsupported locales", async () => {
+ sandbox
+ .stub(global.Services.locale, "appLocaleAsBCP47")
+ .get(() => "unkown");
+
+ await MessageLoaderUtils._remoteSettingsLoader(provider, {});
+
+ assert.notCalled(spy);
+ });
+ });
+ describe("#resetMessageState", () => {
+ it("should reset all message impressions", async () => {
+ await Router.setState({
+ messages: [{ id: "1" }, { id: "2" }],
+ });
+ await Router.setState({
+ messageImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
+ }); // Add impressions for test messages
+ let impressions = Object.values(Router.state.messageImpressions);
+ assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions
+
+ Router.resetMessageState();
+ impressions = Object.values(Router.state.messageImpressions);
+
+ assert.isEmpty(impressions.filter(i => i.length)); // Both messages now have zero impressions
+ assert.calledWithExactly(Router._storage.set, "messageImpressions", {
+ 1: [],
+ 2: [],
+ });
+ });
+ });
+ describe("#resetGroupsState", () => {
+ it("should reset all group impressions", async () => {
+ await Router.setState({
+ groups: [{ id: "1" }, { id: "2" }],
+ });
+ await Router.setState({
+ groupImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
+ }); // Add impressions for test groups
+ let impressions = Object.values(Router.state.groupImpressions);
+ assert.equal(impressions.filter(i => i.length).length, 2); // Both groups have impressions
+
+ Router.resetGroupsState();
+ impressions = Object.values(Router.state.groupImpressions);
+
+ assert.isEmpty(impressions.filter(i => i.length)); // Both groups now have zero impressions
+ assert.calledWithExactly(Router._storage.set, "groupImpressions", {
+ 1: [],
+ 2: [],
+ });
+ });
+ });
+});