tor-browser

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

ASRouter.test.js (115786B)


      1 import { _ASRouter, MessageLoaderUtils } from "modules/ASRouter.sys.mjs";
      2 import { QueryCache } from "modules/ASRouterTargeting.sys.mjs";
      3 import {
      4  FAKE_LOCAL_MESSAGES,
      5  FAKE_LOCAL_PROVIDER,
      6  FAKE_LOCAL_PROVIDERS,
      7  FAKE_REMOTE_MESSAGES,
      8  FAKE_REMOTE_PROVIDER,
      9  FAKE_REMOTE_SETTINGS_PROVIDER,
     10 } from "./constants";
     11 import {
     12  ASRouterPreferences,
     13  TARGETING_PREFERENCES,
     14 } from "modules/ASRouterPreferences.sys.mjs";
     15 import { ASRouterTriggerListeners } from "modules/ASRouterTriggerListeners.sys.mjs";
     16 import { CFRPageActions } from "modules/CFRPageActions.sys.mjs";
     17 import { GlobalOverrider } from "tests/unit/utils";
     18 import { PanelTestProvider } from "modules/PanelTestProvider.sys.mjs";
     19 import ProviderResponseSchema from "content-src/schemas/provider-response.schema.json";
     20 
     21 const MESSAGE_PROVIDER_PREF_NAME =
     22  "browser.newtabpage.activity-stream.asrouter.providers.cfr";
     23 const FAKE_PROVIDERS = [
     24  FAKE_LOCAL_PROVIDER,
     25  FAKE_REMOTE_PROVIDER,
     26  FAKE_REMOTE_SETTINGS_PROVIDER,
     27 ];
     28 const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
     29 const SIX_MONTHS_IN_MS = (24 * 60 * 60 * 365 * 1000) / 2;
     30 const FAKE_RESPONSE_HEADERS = { get() {} };
     31 const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
     32 
     33 const USE_REMOTE_L10N_PREF =
     34  "browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
     35 
     36 // eslint-disable-next-line max-statements
     37 describe("ASRouter", () => {
     38  let Router;
     39  let globals;
     40  let sandbox;
     41  let initParams;
     42  let messageBlockList;
     43  let providerBlockList;
     44  let messageImpressions;
     45  let groupImpressions;
     46  let previousSessionEnd;
     47  let fetchStub;
     48  let clock;
     49  let fakeAttributionCode;
     50  let fakeTargetingContext;
     51  let FakeToolbarBadgeHub;
     52  let FakeMomentsPageHub;
     53  let ASRouterTargeting;
     54  let gBrowser;
     55  let screenImpressions;
     56  let multiProfileMessageImpressions;
     57  let multiProfileMessageBlocklist;
     58 
     59  function setMessageProviderPref(value) {
     60    sandbox.stub(ASRouterPreferences, "providers").get(() => value);
     61  }
     62 
     63  function initASRouter(router) {
     64    const getStub = sandbox.stub();
     65    getStub.returns(Promise.resolve());
     66    getStub
     67      .withArgs("messageBlockList")
     68      .returns(Promise.resolve(messageBlockList));
     69    getStub
     70      .withArgs("multiProfileMessageBlocklist")
     71      .returns(Promise.resolve(multiProfileMessageBlocklist));
     72    getStub
     73      .withArgs("providerBlockList")
     74      .returns(Promise.resolve(providerBlockList));
     75    getStub
     76      .withArgs("messageImpressions")
     77      .returns(Promise.resolve(messageImpressions));
     78    getStub.withArgs("groupImpressions").resolves(groupImpressions);
     79    getStub
     80      .withArgs("previousSessionEnd")
     81      .returns(Promise.resolve(previousSessionEnd));
     82    getStub
     83      .withArgs("screenImpressions")
     84      .returns(Promise.resolve(screenImpressions));
     85    initParams = {
     86      storage: {
     87        get: getStub,
     88        set: sandbox.stub().returns(Promise.resolve()),
     89        setSharedMessageImpressions: sandbox.stub(),
     90        getSharedMessageImpressions: sandbox
     91          .stub()
     92          .resolves(multiProfileMessageImpressions),
     93        setSharedMessageBlocked: sandbox.stub(),
     94        getSharedMessageBlocklist: sandbox
     95          .stub()
     96          .resolves(multiProfileMessageBlocklist),
     97      },
     98      sendTelemetry: sandbox.stub().resolves(),
     99      clearChildMessages: sandbox.stub().resolves(),
    100      clearChildProviders: sandbox.stub().resolves(),
    101      updateAdminState: sandbox.stub().resolves(),
    102      dispatchCFRAction: sandbox.stub().resolves(),
    103    };
    104    sandbox.stub(router, "loadMessagesFromAllProviders").callThrough();
    105    return router.init(initParams);
    106  }
    107 
    108  async function createRouterAndInit(providers = FAKE_PROVIDERS) {
    109    setMessageProviderPref(providers);
    110    // `.freeze` to catch any attempts at modifying the object
    111    Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
    112    await initASRouter(Router);
    113  }
    114 
    115  beforeEach(async () => {
    116    globals = new GlobalOverrider();
    117    messageBlockList = [];
    118    providerBlockList = [];
    119    messageImpressions = {};
    120    groupImpressions = {};
    121    previousSessionEnd = 100;
    122    screenImpressions = {};
    123    multiProfileMessageImpressions = {};
    124    multiProfileMessageBlocklist = [];
    125    sandbox = sinon.createSandbox();
    126    ASRouterTargeting = {
    127      isMatch: sandbox.stub(),
    128      findMatchingMessage: sandbox.stub(),
    129      Environment: {
    130        locale: "en-US",
    131        localeLanguageCode: "en",
    132        browserSettings: {
    133          update: {
    134            channel: "default",
    135            enabled: true,
    136            autoDownload: true,
    137          },
    138        },
    139        attributionData: {},
    140        currentDate: "2000-01-01T10:00:0.001Z",
    141        profileAgeCreated: {},
    142        profileAgeReset: {},
    143        usesFirefoxSync: false,
    144        isFxAEnabled: true,
    145        isFxASignedIn: false,
    146        sync: {
    147          desktopDevices: 0,
    148          mobileDevices: 0,
    149          totalDevices: 0,
    150        },
    151        xpinstallEnabled: true,
    152        addonsInfo: {},
    153        searchEngines: {},
    154        isDefaultBrowser: false,
    155        devToolsOpenedCount: 5,
    156        topFrecentSites: {},
    157        recentBookmarks: {},
    158        pinnedSites: [
    159          {
    160            url: "https://amazon.com",
    161            host: "amazon.com",
    162            searchTopSite: true,
    163          },
    164        ],
    165        providerCohorts: {
    166          onboarding: "",
    167          cfr: "",
    168          "message-groups": "",
    169          "messaging-experiments": "",
    170        },
    171        totalBookmarksCount: {},
    172        firefoxVersion: 80,
    173        region: "US",
    174        needsUpdate: {},
    175        hasPinnedTabs: false,
    176        hasAccessedFxAPanel: false,
    177        userPrefs: {
    178          cfrFeatures: true,
    179          cfrAddons: true,
    180        },
    181        totalBlockedCount: {},
    182        blockedCountByType: {},
    183        attachedFxAOAuthClients: [],
    184        platformName: "macosx",
    185        scores: {},
    186        scoreThreshold: 5000,
    187        isChinaRepack: false,
    188        userId: "adsf",
    189        currentProfileId: "1",
    190        canCreateSelectableProfiles: false,
    191        hasSelectableProfiles: false,
    192      },
    193    };
    194    gBrowser = {
    195      selectedBrowser: {
    196        constructor: { name: "MozBrowser" },
    197        get ownerGlobal() {
    198          return { gBrowser };
    199        },
    200      },
    201    };
    202 
    203    ASRouterPreferences.specialConditions = {
    204      someCondition: true,
    205    };
    206    sandbox.stub(ASRouterPreferences, "_maybeSetMessagingProfileID").resolves();
    207    sandbox.spy(ASRouterPreferences, "init");
    208    sandbox.spy(ASRouterPreferences, "uninit");
    209    sandbox.spy(ASRouterPreferences, "addListener");
    210    sandbox.spy(ASRouterPreferences, "removeListener");
    211 
    212    clock = sandbox.useFakeTimers();
    213    fetchStub = sandbox
    214      .stub(global, "fetch")
    215      .withArgs("http://fake.com/endpoint")
    216      .resolves({
    217        ok: true,
    218        status: 200,
    219        json: () => Promise.resolve({ messages: FAKE_REMOTE_MESSAGES }),
    220        headers: FAKE_RESPONSE_HEADERS,
    221      });
    222    sandbox.stub(global.Services.prefs, "getStringPref");
    223 
    224    fakeAttributionCode = {
    225      allowedCodeKeys: ["foo", "bar", "baz"],
    226      _clearCache: () => sinon.stub(),
    227      getAttrDataAsync: () => Promise.resolve({ content: "addonID" }),
    228      deleteFileAsync: () => Promise.resolve(),
    229      writeAttributionFile: () => Promise.resolve(),
    230      getCachedAttributionData: sinon.stub(),
    231    };
    232    FakeToolbarBadgeHub = {
    233      init: sandbox.stub(),
    234      uninit: sandbox.stub(),
    235      registerBadgeNotificationListener: sandbox.stub(),
    236    };
    237    FakeMomentsPageHub = {
    238      init: sandbox.stub(),
    239      uninit: sandbox.stub(),
    240      executeAction: sandbox.stub(),
    241    };
    242    fakeTargetingContext = {
    243      combineContexts: sandbox.stub(),
    244      evalWithDefault: sandbox.stub().resolves(),
    245    };
    246    let fakeNimbusFeatures = [
    247      "cfr",
    248      "infobar",
    249      "spotlight",
    250      "moments-page",
    251      "pbNewtab",
    252      "fxms-message-15",
    253    ].reduce((features, featureId) => {
    254      features[featureId] = {
    255        getEnrollmentMetadata: sandbox.stub().returns({
    256          slug: "experiment-slug",
    257          branch: "experiment-branch-slug",
    258          isRollout: false,
    259        }),
    260        getAllVariables: sandbox.stub().returns(undefined),
    261        recordExposureEvent: sandbox.stub(),
    262      };
    263      return features;
    264    }, {});
    265    globals.set({
    266      // Testing framework doesn't know how to `defineESModuleGetters` so we're
    267      // importing these modules into the global scope ourselves.
    268      GroupsConfigurationProvider: { getMessages: () => Promise.resolve([]) },
    269      ASRouterPreferences,
    270      TARGETING_PREFERENCES,
    271      ASRouterTargeting,
    272      ASRouterTriggerListeners,
    273      QueryCache,
    274      gBrowser,
    275      gURLBar: {},
    276      isSeparateAboutWelcome: true,
    277      AttributionCode: fakeAttributionCode,
    278      PanelTestProvider,
    279      MacAttribution: { applicationPath: "" },
    280      ToolbarBadgeHub: FakeToolbarBadgeHub,
    281      MomentsPageHub: FakeMomentsPageHub,
    282      KintoHttpClient: class {
    283        bucket() {
    284          return this;
    285        }
    286        collection() {
    287          return this;
    288        }
    289        getRecord() {
    290          return Promise.resolve({ data: { attachment: { size: 42 } } });
    291        }
    292      },
    293      UnstoredDownloader: class {
    294        download() {
    295          return Promise.resolve({ buffer: "fake buffer" });
    296        }
    297      },
    298      NimbusFeatures: fakeNimbusFeatures,
    299      ExperimentAPI: {
    300        getAllBranches: sandbox.stub().resolves([]),
    301        ready: sandbox.stub().resolves(),
    302      },
    303      SpecialMessageActions: {
    304        handleAction: sandbox.stub(),
    305      },
    306      TargetingContext: class {
    307        static combineContexts(...args) {
    308          return fakeTargetingContext.combineContexts.apply(sandbox, args);
    309        }
    310 
    311        evalWithDefault(expr) {
    312          return fakeTargetingContext.evalWithDefault(expr);
    313        }
    314      },
    315      RemoteL10n: {
    316        // This is just a subset of supported locales that happen to be used in
    317        // the test.
    318        isLocaleSupported: locale => ["en-US", "ja-JP-mac"].includes(locale),
    319        // PathUtils.join() is mocked in `unit-entry.js`, only filenames count.
    320        cfrFluentFileDir: "ms-language-packs",
    321        cfrFluentFilePath: "asrouter.ftl",
    322      },
    323    });
    324    await createRouterAndInit();
    325  });
    326  afterEach(() => {
    327    Router.uninit();
    328    ASRouterPreferences.uninit();
    329    sandbox.restore();
    330    globals.restore();
    331  });
    332 
    333  describe(".state", () => {
    334    it("should throw if an attempt to set .state was made", () => {
    335      assert.throws(() => {
    336        Router.state = {};
    337      });
    338    });
    339  });
    340 
    341  describe("#init", () => {
    342    it("should only be called once", async () => {
    343      Router = new _ASRouter();
    344      let state = await initASRouter(Router);
    345 
    346      assert.equal(state, Router.state);
    347 
    348      assert.isNull(await Router.init({}));
    349    });
    350    it("should only be called once", async () => {
    351      Router = new _ASRouter();
    352      initASRouter(Router);
    353      let secondCall = await Router.init({});
    354 
    355      assert.isNull(
    356        secondCall,
    357        "Should not init twice, it should exit early with null"
    358      );
    359    });
    360    it("should set state.messageBlockList to the block list in persistent storage", async () => {
    361      messageBlockList = ["foo"];
    362      Router = new _ASRouter();
    363      await initASRouter(Router);
    364 
    365      assert.deepEqual(Router.state.messageBlockList, ["foo"]);
    366    });
    367    it("should initialize all the hub providers", async () => {
    368      // ASRouter init called in `beforeEach` block above
    369 
    370      assert.calledOnce(FakeToolbarBadgeHub.init);
    371      assert.calledOnce(FakeMomentsPageHub.init);
    372 
    373      assert.calledWithExactly(
    374        FakeToolbarBadgeHub.init,
    375        Router.waitForInitialized,
    376        {
    377          handleMessageRequest: Router.handleMessageRequest,
    378          addImpression: Router.addImpression,
    379          blockMessageById: Router.blockMessageById,
    380          sendTelemetry: Router.sendTelemetry,
    381          unblockMessageById: Router.unblockMessageById,
    382        }
    383      );
    384 
    385      assert.calledWithExactly(
    386        FakeMomentsPageHub.init,
    387        Router.waitForInitialized,
    388        {
    389          handleMessageRequest: Router.handleMessageRequest,
    390          addImpression: Router.addImpression,
    391          blockMessageById: Router.blockMessageById,
    392          sendTelemetry: Router.sendTelemetry,
    393        }
    394      );
    395    });
    396    it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
    397      // Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
    398      // otherwise they will be cleaned up by .cleanupImpressions()
    399      const testMessage = { id: "foo", frequency: { lifetimeCap: 10 } };
    400      messageImpressions = { foo: [0, 1, 2] };
    401      setMessageProviderPref([
    402        { id: "onboarding", type: "local", messages: [testMessage] },
    403      ]);
    404      Router = new _ASRouter();
    405      await initASRouter(Router);
    406 
    407      assert.deepEqual(Router.state.messageImpressions, messageImpressions);
    408    });
    409    it("should set state.screenImpressions to the screenImpressions object in persistent storage", async () => {
    410      screenImpressions = { test: 123 };
    411 
    412      Router = new _ASRouter();
    413      await initASRouter(Router);
    414 
    415      assert.deepEqual(Router.state.screenImpressions, screenImpressions);
    416    });
    417    it("should clear impressions for groups that are not active", async () => {
    418      groupImpressions = { foo: [0, 1, 2] };
    419      Router = new _ASRouter();
    420      await initASRouter(Router);
    421 
    422      assert.notProperty(Router.state.groupImpressions, "foo");
    423    });
    424    it("should keep impressions for groups that are active", async () => {
    425      Router = new _ASRouter();
    426      await initASRouter(Router);
    427      await Router.setState(() => {
    428        return {
    429          groups: [
    430            {
    431              id: "foo",
    432              enabled: true,
    433              frequency: {
    434                custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
    435                lifetime: Infinity,
    436              },
    437            },
    438          ],
    439          groupImpressions: { foo: [Date.now()] },
    440        };
    441      });
    442      Router.cleanupImpressions();
    443 
    444      assert.property(Router.state.groupImpressions, "foo");
    445      assert.lengthOf(Router.state.groupImpressions.foo, 1);
    446    });
    447    it("should remove old impressions for a group", async () => {
    448      Router = new _ASRouter();
    449      await initASRouter(Router);
    450      await Router.setState(() => {
    451        return {
    452          groups: [
    453            {
    454              id: "foo",
    455              enabled: true,
    456              frequency: {
    457                custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
    458              },
    459            },
    460          ],
    461          groupImpressions: {
    462            foo: [Date.now() - ONE_DAY_IN_MS - 1, Date.now()],
    463          },
    464        };
    465      });
    466      Router.cleanupImpressions();
    467 
    468      assert.property(Router.state.groupImpressions, "foo");
    469      assert.lengthOf(Router.state.groupImpressions.foo, 1);
    470    });
    471    it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
    472      Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
    473 
    474      await initASRouter(Router);
    475 
    476      assert.calledOnce(Router.loadMessagesFromAllProviders);
    477      assert.isArray(Router.state.messages);
    478      assert.lengthOf(
    479        Router.state.messages,
    480        FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length
    481      );
    482    });
    483    it("should set state.previousSessionEnd from IndexedDB", async () => {
    484      previousSessionEnd = 200;
    485      await createRouterAndInit();
    486 
    487      assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
    488    });
    489    it("should assign ASRouterPreferences.specialConditions to state", async () => {
    490      assert.isTrue(ASRouterPreferences.specialConditions.someCondition);
    491      assert.isTrue(Router.state.someCondition);
    492    });
    493    it("should add observer for `intl:app-locales-changed`", async () => {
    494      sandbox.spy(global.Services.obs, "addObserver");
    495      await createRouterAndInit();
    496 
    497      assert.calledWithExactly(
    498        global.Services.obs.addObserver,
    499        Router._onLocaleChanged,
    500        "intl:app-locales-changed"
    501      );
    502    });
    503    it("should add a pref observer", async () => {
    504      sandbox.spy(global.Services.prefs, "addObserver");
    505      await createRouterAndInit();
    506 
    507      assert.calledOnce(global.Services.prefs.addObserver);
    508      assert.calledWithExactly(
    509        global.Services.prefs.addObserver,
    510        USE_REMOTE_L10N_PREF,
    511        Router
    512      );
    513    });
    514    describe("lazily loading local test providers", () => {
    515      let justIdAndContent = ({ id, content }) => ({ id, content });
    516      afterEach(() => Router.uninit());
    517 
    518      it("should add the local test providers on init if devtools are enabled", async () => {
    519        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
    520 
    521        await createRouterAndInit();
    522 
    523        assert.property(Router._localProviders, "PanelTestProvider");
    524      });
    525      it("should not add the local test providers on init if devtools are disabled", async () => {
    526        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
    527 
    528        await createRouterAndInit();
    529 
    530        assert.notProperty(Router._localProviders, "PanelTestProvider");
    531      });
    532      it("should flatten experiment translated messages from local test providers if devtools are enabled...", async () => {
    533        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
    534 
    535        await createRouterAndInit();
    536 
    537        assert.property(Router._localProviders, "PanelTestProvider");
    538 
    539        expect(
    540          Router.state.messages.map(justIdAndContent)
    541        ).to.deep.include.members([
    542          { id: "experimentL10n", content: { text: "UniqueText" } },
    543        ]);
    544      });
    545      it("...but not if devtools are disabled", async () => {
    546        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
    547 
    548        await createRouterAndInit();
    549 
    550        assert.notProperty(Router._localProviders, "PanelTestProvider");
    551 
    552        let justIdAndContentMessages =
    553          Router.state.messages.map(justIdAndContent);
    554        expect(justIdAndContentMessages).not.to.deep.include.members([
    555          { id: "experimentL10n", content: { text: "UniqueText" } },
    556        ]);
    557        expect(justIdAndContentMessages).to.deep.include.members([
    558          {
    559            id: "experimentL10n",
    560            content: { text: { $l10n: { text: "UniqueText" } } },
    561          },
    562        ]);
    563      });
    564    });
    565 
    566    it("should load shared message impressions and blocklist when selectable profiles are enabled", async () => {
    567      const testMessage = { id: "msg1", frequency: { lifetimeCap: 10 } };
    568      setMessageProviderPref([
    569        { id: "onboarding", type: "local", messages: [testMessage] },
    570      ]);
    571 
    572      const testMultiProfileImpressions = { msg1: [123, 456] };
    573      const testMultiProfileBlocklist = ["blocked1", "blocked2"];
    574      const getSharedMessageImpressions = sandbox
    575        .stub()
    576        .resolves(testMultiProfileImpressions);
    577      const getSharedMessageBlocklist = sandbox
    578        .stub()
    579        .resolves(testMultiProfileBlocklist);
    580 
    581      sandbox
    582        .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
    583        .value(true);
    584      sandbox
    585        .stub(ASRouterTargeting.Environment, "hasSelectableProfiles")
    586        .value(true);
    587 
    588      Router = new _ASRouter();
    589      const getStub = sandbox.stub();
    590 
    591      const testInitParams = {
    592        storage: {
    593          get: getStub,
    594          set: sandbox.stub().returns(Promise.resolve()),
    595          getSharedMessageImpressions,
    596          getSharedMessageBlocklist,
    597        },
    598      };
    599 
    600      await Router.init(testInitParams);
    601 
    602      assert.calledOnce(getSharedMessageImpressions);
    603      assert.calledOnce(getSharedMessageBlocklist);
    604      assert.deepEqual(
    605        Router.state.multiProfileMessageImpressions,
    606        testMultiProfileImpressions
    607      );
    608      assert.deepEqual(
    609        Router.state.multiProfileMessageBlocklist,
    610        testMultiProfileBlocklist
    611      );
    612    });
    613 
    614    it("should not load shared data when selectable profiles are disabled", async () => {
    615      const getSharedMessageImpressions = sandbox.stub();
    616      const getSharedMessageBlocklist = sandbox.stub();
    617 
    618      sandbox
    619        .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
    620        .value(false);
    621      sandbox
    622        .stub(ASRouterTargeting.Environment, "hasSelectableProfiles")
    623        .value(false);
    624 
    625      Router = new _ASRouter();
    626      const getStub = sandbox.stub();
    627 
    628      const testInitParams = {
    629        storage: {
    630          get: getStub,
    631          set: sandbox.stub().returns(Promise.resolve()),
    632          getSharedMessageImpressions,
    633          getSharedMessageBlocklist,
    634        },
    635      };
    636 
    637      await Router.init(testInitParams);
    638 
    639      assert.notCalled(getSharedMessageImpressions);
    640      assert.notCalled(getSharedMessageBlocklist);
    641    });
    642 
    643    it("should add observer for multiprofile data updates", async () => {
    644      sandbox.spy(global.Services.obs, "addObserver");
    645      await createRouterAndInit();
    646 
    647      assert.calledWithExactly(
    648        global.Services.obs.addObserver,
    649        Router._updateMultiprofileData,
    650        "sps-profiles-updated"
    651      );
    652    });
    653  });
    654 
    655  describe("preference changes", () => {
    656    it("should call ASRouterPreferences.init and add a listener on init", () => {
    657      assert.calledOnce(ASRouterPreferences.init);
    658      assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);
    659    });
    660    it("should call ASRouterPreferences.uninit and remove the listener on uninit", () => {
    661      Router.uninit();
    662      assert.calledOnce(ASRouterPreferences.uninit);
    663      assert.calledWith(
    664        ASRouterPreferences.removeListener,
    665        Router.onPrefChange
    666      );
    667    });
    668    it("should call clearChildMessages (does nothing, see bug 1899028)", async () => {
    669      const messageTargeted = {
    670        id: "1",
    671        campaign: "foocampaign",
    672        targeting: "true",
    673        groups: ["cfr"],
    674        provider: "cfr",
    675      };
    676      const messageNotTargeted = {
    677        id: "2",
    678        campaign: "foocampaign",
    679        groups: ["cfr"],
    680        provider: "cfr",
    681      };
    682      await Router.setState({
    683        messages: [messageTargeted, messageNotTargeted],
    684        providers: [{ id: "cfr" }],
    685      });
    686      fakeTargetingContext.evalWithDefault.resolves(false);
    687 
    688      await Router.onPrefChange("services.sync.username");
    689 
    690      assert.calledOnce(initParams.clearChildMessages);
    691      assert.calledWith(initParams.clearChildMessages, [messageTargeted.id]);
    692    });
    693    it("should call loadMessagesFromAllProviders on pref change", () => {
    694      ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
    695      assert.calledOnce(Router.loadMessagesFromAllProviders);
    696    });
    697    it("should update groups state if a user pref changes", async () => {
    698      await Router.setState({
    699        groups: [{ id: "foo", userPreferences: ["bar"], enabled: true }],
    700      });
    701      sandbox.stub(ASRouterPreferences, "getUserPreference");
    702 
    703      await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
    704 
    705      assert.calledWithExactly(ASRouterPreferences.getUserPreference, "bar");
    706    });
    707    it("should update the list of providers on pref change", async () => {
    708      const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
    709        url: "baz.com",
    710      });
    711      setMessageProviderPref([
    712        FAKE_LOCAL_PROVIDER,
    713        modifiedRemoteProvider,
    714        FAKE_REMOTE_SETTINGS_PROVIDER,
    715      ]);
    716 
    717      const { length } = Router.state.providers;
    718 
    719      ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
    720      await Router._updateMessageProviders();
    721 
    722      const provider = Router.state.providers.find(p => p.url === "baz.com");
    723      assert.lengthOf(Router.state.providers, length);
    724      assert.isDefined(provider);
    725    });
    726    it("should clear disabled providers on pref change", async () => {
    727      const TEST_PROVIDER_ID = "some_provider_id";
    728      await Router.setState({
    729        providers: [{ id: TEST_PROVIDER_ID }],
    730      });
    731      const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
    732        id: TEST_PROVIDER_ID,
    733        enabled: false,
    734      });
    735      setMessageProviderPref([
    736        FAKE_LOCAL_PROVIDER,
    737        modifiedRemoteProvider,
    738        FAKE_REMOTE_SETTINGS_PROVIDER,
    739      ]);
    740      await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
    741 
    742      assert.calledOnce(initParams.clearChildProviders);
    743      assert.calledWith(initParams.clearChildProviders, [TEST_PROVIDER_ID]);
    744    });
    745  });
    746 
    747  describe("setState", () => {
    748    it("should broadcast a message to update the admin tool on a state change if the asrouter.devtoolsEnabled pref is", async () => {
    749      sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
    750      sandbox.stub(Router, "getTargetingParameters").resolves({});
    751      const state = await Router.setState({ foo: 123 });
    752 
    753      assert.calledOnce(initParams.updateAdminState);
    754      assert.deepEqual(state.providerPrefs, ASRouterPreferences.providers);
    755      assert.deepEqual(
    756        state.userPrefs,
    757        ASRouterPreferences.getAllUserPreferences()
    758      );
    759      assert.deepEqual(state.targetingParameters, {});
    760      assert.deepEqual(state.errors, Router.errors);
    761    });
    762    it("should not send a message on a state change asrouter.devtoolsEnabled pref is on", async () => {
    763      sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
    764      await Router.setState({ foo: 123 });
    765 
    766      assert.notCalled(initParams.updateAdminState);
    767    });
    768  });
    769 
    770  describe("getTargetingParameters", () => {
    771    it("should return the targeting parameters", async () => {
    772      const stub = sandbox.stub().resolves("foo");
    773      const obj = { foo: 1 };
    774      sandbox.stub(obj, "foo").get(stub);
    775      const result = await Router.getTargetingParameters(obj, obj);
    776 
    777      assert.calledTwice(stub);
    778      assert.propertyVal(result, "foo", "foo");
    779    });
    780  });
    781 
    782  describe("evaluateExpression", () => {
    783    it("should call ASRouterTargeting to evaluate", async () => {
    784      fakeTargetingContext.evalWithDefault.resolves("foo");
    785      const response = await Router.evaluateExpression({});
    786      assert.equal(response.evaluationStatus.result, "foo");
    787      assert.isTrue(response.evaluationStatus.success);
    788    });
    789    it("should catch evaluation errors", async () => {
    790      fakeTargetingContext.evalWithDefault.returns(
    791        Promise.reject(new Error("fake error"))
    792      );
    793      const response = await Router.evaluateExpression({});
    794      assert.isFalse(response.evaluationStatus.success);
    795    });
    796  });
    797 
    798  describe("#routeCFRMessage", () => {
    799    let browser;
    800    beforeEach(() => {
    801      sandbox.stub(CFRPageActions, "forceRecommendation");
    802      sandbox.stub(CFRPageActions, "addRecommendation");
    803      browser = {};
    804    });
    805    it("should route moments messages to the right hub", () => {
    806      Router.routeCFRMessage({ template: "update_action" }, browser, "", true);
    807 
    808      assert.calledOnce(FakeMomentsPageHub.executeAction);
    809      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    810      assert.notCalled(CFRPageActions.addRecommendation);
    811      assert.notCalled(CFRPageActions.forceRecommendation);
    812    });
    813    it("should route toolbar_badge message to the right hub", () => {
    814      Router.routeCFRMessage({ template: "toolbar_badge" }, browser);
    815 
    816      assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    817      assert.notCalled(CFRPageActions.addRecommendation);
    818      assert.notCalled(CFRPageActions.forceRecommendation);
    819      assert.notCalled(FakeMomentsPageHub.executeAction);
    820    });
    821    it("should route milestone_message to the right hub", () => {
    822      Router.routeCFRMessage(
    823        { template: "milestone_message" },
    824        browser,
    825        "",
    826        false
    827      );
    828 
    829      assert.calledOnce(CFRPageActions.addRecommendation);
    830      assert.notCalled(CFRPageActions.forceRecommendation);
    831      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    832      assert.notCalled(FakeMomentsPageHub.executeAction);
    833    });
    834    it("should route cfr_doorhanger message to the right hub force = false", () => {
    835      Router.routeCFRMessage(
    836        { template: "cfr_doorhanger" },
    837        browser,
    838        { param: {} },
    839        false
    840      );
    841 
    842      assert.calledOnce(CFRPageActions.addRecommendation);
    843      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    844      assert.notCalled(CFRPageActions.forceRecommendation);
    845      assert.notCalled(FakeMomentsPageHub.executeAction);
    846    });
    847    it("should route cfr_doorhanger message to the right hub force = true", () => {
    848      Router.routeCFRMessage({ template: "cfr_doorhanger" }, browser, {}, true);
    849 
    850      assert.calledOnce(CFRPageActions.forceRecommendation);
    851      assert.notCalled(CFRPageActions.addRecommendation);
    852      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    853      assert.notCalled(FakeMomentsPageHub.executeAction);
    854    });
    855    it("should route cfr_urlbar_chiclet message to the right hub force = false", () => {
    856      Router.routeCFRMessage(
    857        { template: "cfr_urlbar_chiclet" },
    858        browser,
    859        { param: {} },
    860        false
    861      );
    862 
    863      assert.calledOnce(CFRPageActions.addRecommendation);
    864      const { args } = CFRPageActions.addRecommendation.firstCall;
    865      // Host should be null
    866      assert.isNull(args[1]);
    867      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    868      assert.notCalled(CFRPageActions.forceRecommendation);
    869      assert.notCalled(FakeMomentsPageHub.executeAction);
    870    });
    871    it("should route cfr_urlbar_chiclet message to the right hub force = true", () => {
    872      Router.routeCFRMessage(
    873        { template: "cfr_urlbar_chiclet" },
    874        browser,
    875        {},
    876        true
    877      );
    878 
    879      assert.calledOnce(CFRPageActions.forceRecommendation);
    880      assert.notCalled(CFRPageActions.addRecommendation);
    881      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    882      assert.notCalled(FakeMomentsPageHub.executeAction);
    883    });
    884    it("should route default to sending to content", () => {
    885      Router.routeCFRMessage(
    886        { template: "some_other_template" },
    887        browser,
    888        {},
    889        true
    890      );
    891 
    892      assert.notCalled(CFRPageActions.forceRecommendation);
    893      assert.notCalled(CFRPageActions.addRecommendation);
    894      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
    895      assert.notCalled(FakeMomentsPageHub.executeAction);
    896    });
    897  });
    898 
    899  describe("#loadMessagesFromAllProviders", () => {
    900    function assertRouterContainsMessages(messages) {
    901      const messageIdsInRouter = Router.state.messages.map(m => m.id);
    902      for (const message of messages) {
    903        assert.include(messageIdsInRouter, message.id);
    904      }
    905    }
    906 
    907    it("should not trigger an update if not enough time has passed for a provider", async () => {
    908      await createRouterAndInit([
    909        {
    910          id: "remotey",
    911          type: "remote",
    912          enabled: true,
    913          url: "http://fake.com/endpoint",
    914          updateCycleInMs: 300,
    915        },
    916      ]);
    917 
    918      const previousState = Router.state;
    919 
    920      // Since we've previously gotten messages during init and we haven't advanced our fake timer,
    921      // no updates should be triggered.
    922      await Router.loadMessagesFromAllProviders();
    923      assert.deepEqual(Router.state, previousState);
    924    });
    925    it("should not trigger an update if we only have local providers", async () => {
    926      await createRouterAndInit([
    927        {
    928          id: "foo",
    929          type: "local",
    930          enabled: true,
    931          messages: FAKE_LOCAL_MESSAGES,
    932        },
    933      ]);
    934 
    935      const previousState = Router.state;
    936      const stub = sandbox.stub(MessageLoaderUtils, "loadMessagesForProvider");
    937 
    938      clock.tick(300);
    939 
    940      await Router.loadMessagesFromAllProviders();
    941 
    942      assert.deepEqual(Router.state, previousState);
    943      assert.notCalled(stub);
    944    });
    945    it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
    946      const NEW_MESSAGES = [{ id: "new_123" }];
    947      await createRouterAndInit([
    948        {
    949          id: "remotey",
    950          type: "remote",
    951          url: "http://fake.com/endpoint",
    952          enabled: true,
    953          updateCycleInMs: 300,
    954        },
    955        {
    956          id: "alocalprovider",
    957          type: "local",
    958          enabled: true,
    959          messages: FAKE_LOCAL_MESSAGES,
    960        },
    961      ]);
    962      fetchStub.withArgs("http://fake.com/endpoint").resolves({
    963        ok: true,
    964        status: 200,
    965        json: () => Promise.resolve({ messages: NEW_MESSAGES }),
    966        headers: FAKE_RESPONSE_HEADERS,
    967      });
    968 
    969      clock.tick(301);
    970      await Router.loadMessagesFromAllProviders();
    971 
    972      // These are the new messages
    973      assertRouterContainsMessages(NEW_MESSAGES);
    974      // These are the local messages that should not have been deleted
    975      assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);
    976    });
    977    it("should parse the triggers in the messages and register the trigger listeners", async () => {
    978      sandbox.spy(ASRouterTriggerListeners.get("openURL"), "init");
    979 
    980      await createRouterAndInit([
    981        {
    982          id: "foo",
    983          type: "local",
    984          enabled: true,
    985          messages: [
    986            {
    987              id: "foo",
    988              template: "simple_template",
    989              trigger: { id: "firstRun" },
    990              content: { title: "Foo", body: "Foo123" },
    991            },
    992            {
    993              id: "bar1",
    994              template: "simple_template",
    995              trigger: {
    996                id: "openURL",
    997                params: ["www.mozilla.org", "www.mozilla.com"],
    998              },
    999              content: { title: "Bar1", body: "Bar123" },
   1000            },
   1001            {
   1002              id: "bar2",
   1003              template: "simple_template",
   1004              trigger: { id: "openURL", params: ["www.example.com"] },
   1005              content: { title: "Bar2", body: "Bar123" },
   1006            },
   1007          ],
   1008        },
   1009      ]);
   1010      assert.calledTwice(ASRouterTriggerListeners.get("openURL").init);
   1011      assert.calledWithExactly(
   1012        ASRouterTriggerListeners.get("openURL").init,
   1013        Router._triggerHandler,
   1014        ["www.mozilla.org", "www.mozilla.com"],
   1015        undefined, // patterns
   1016        undefined // regexPatterns
   1017      );
   1018      assert.calledWithExactly(
   1019        ASRouterTriggerListeners.get("openURL").init,
   1020        Router._triggerHandler,
   1021        ["www.example.com"],
   1022        undefined, // patterns
   1023        undefined // regexPatterns
   1024      );
   1025    });
   1026    it("should parse the message's messagesLoaded trigger and immediately fire trigger", async () => {
   1027      setMessageProviderPref([
   1028        {
   1029          id: "foo",
   1030          type: "local",
   1031          enabled: true,
   1032          messages: [
   1033            {
   1034              id: "foo",
   1035              template: "simple_template",
   1036              trigger: { id: "messagesLoaded" },
   1037              content: { title: "Foo", body: "Bar123" },
   1038            },
   1039          ],
   1040        },
   1041      ]);
   1042      Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
   1043      sandbox.spy(Router, "sendTriggerMessage");
   1044      await initASRouter(Router);
   1045      assert.calledOnce(Router.sendTriggerMessage);
   1046      assert.calledWith(
   1047        Router.sendTriggerMessage,
   1048        sandbox.match({ id: "messagesLoaded" }),
   1049        true
   1050      );
   1051    });
   1052    it("should not register a trigger listener in automation for a message with skip_in_tests", async () => {
   1053      sandbox.spy(ASRouterTriggerListeners.get("openURL"), "init");
   1054      await createRouterAndInit([
   1055        {
   1056          id: "foo",
   1057          type: "local",
   1058          enabled: true,
   1059          messages: [
   1060            {
   1061              id: "foo",
   1062              template: "simple_template",
   1063              trigger: { id: "openURL" },
   1064              content: { title: "Foo", body: "Foo123" },
   1065              skip_in_tests: "testing",
   1066            },
   1067          ],
   1068        },
   1069      ]);
   1070      assert.notCalled(ASRouterTriggerListeners.get("openURL").init);
   1071    });
   1072    it("should gracefully handle messages loading before a window or browser exists", async () => {
   1073      sandbox.stub(global, "gBrowser").value(undefined);
   1074      sandbox
   1075        .stub(global.Services.wm, "getMostRecentBrowserWindow")
   1076        .returns(undefined);
   1077      setMessageProviderPref([
   1078        {
   1079          id: "foo",
   1080          type: "local",
   1081          enabled: true,
   1082          messages: [
   1083            "cfr_doorhanger",
   1084            "toolbar_badge",
   1085            "update_action",
   1086            "infobar",
   1087            "spotlight",
   1088            "toast_notification",
   1089          ].map((template, i) => {
   1090            return {
   1091              id: `foo${i}`,
   1092              template,
   1093              trigger: { id: "messagesLoaded" },
   1094              content: { title: `Foo${i}`, body: "Bar123" },
   1095            };
   1096          }),
   1097        },
   1098      ]);
   1099      Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
   1100      sandbox.spy(Router, "sendTriggerMessage");
   1101      await initASRouter(Router);
   1102      assert.calledWith(
   1103        Router.sendTriggerMessage,
   1104        sandbox.match({ id: "messagesLoaded" }),
   1105        true
   1106      );
   1107    });
   1108    it("should gracefully handle RemoteSettings blowing up and dispatch undesired event", async () => {
   1109      sandbox
   1110        .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
   1111        .rejects("fake error");
   1112      await createRouterAndInit();
   1113      assert.calledWith(initParams.dispatchCFRAction, {
   1114        type: "AS_ROUTER_TELEMETRY_USER_EVENT",
   1115        data: {
   1116          action: "asrouter_undesired_event",
   1117          message_id: "n/a",
   1118          event: "ASR_RS_ERROR",
   1119          event_context: "remotey-settingsy",
   1120        },
   1121      });
   1122    });
   1123    it("should dispatch undesired event if RemoteSettings returns no messages", async () => {
   1124      sandbox
   1125        .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
   1126        .resolves([]);
   1127      assert.calledWith(initParams.dispatchCFRAction, {
   1128        type: "AS_ROUTER_TELEMETRY_USER_EVENT",
   1129        data: {
   1130          action: "asrouter_undesired_event",
   1131          message_id: "n/a",
   1132          event: "ASR_RS_NO_MESSAGES",
   1133          event_context: "remotey-settingsy",
   1134        },
   1135      });
   1136    });
   1137    it("should download the attachment if RemoteSettings returns some messages", async () => {
   1138      sandbox
   1139        .stub(global.Services.locale, "appLocaleAsBCP47")
   1140        .get(() => "en-US");
   1141      sandbox
   1142        .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
   1143        .resolves([{ id: "message_1" }]);
   1144      sandbox.stub(global.IOUtils, "exists").resolves(false);
   1145      const spy = sandbox.spy();
   1146      global.UnstoredDownloader.prototype.download = spy;
   1147      const provider = {
   1148        id: "cfr",
   1149        enabled: true,
   1150        type: "remote-settings",
   1151        collection: "cfr",
   1152      };
   1153      await createRouterAndInit([provider]);
   1154 
   1155      assert.calledOnce(spy);
   1156    });
   1157    it("should dispatch undesired event if the ms-language-packs returns no messages", async () => {
   1158      sandbox
   1159        .stub(global.Services.locale, "appLocaleAsBCP47")
   1160        .get(() => "en-US");
   1161      sandbox
   1162        .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
   1163        .resolves([{ id: "message_1" }]);
   1164      sandbox
   1165        .stub(global.KintoHttpClient.prototype, "getRecord")
   1166        .resolves(null);
   1167      const provider = {
   1168        id: "cfr",
   1169        enabled: true,
   1170        type: "remote-settings",
   1171        collection: "cfr",
   1172      };
   1173      await createRouterAndInit([provider]);
   1174 
   1175      assert.calledWith(initParams.dispatchCFRAction, {
   1176        type: "AS_ROUTER_TELEMETRY_USER_EVENT",
   1177        data: {
   1178          action: "asrouter_undesired_event",
   1179          message_id: "n/a",
   1180          event: "ASR_RS_NO_MESSAGES",
   1181          event_context: "ms-language-packs",
   1182        },
   1183      });
   1184    });
   1185  });
   1186 
   1187  describe("#_updateMessageProviders", () => {
   1188    it("should correctly replace %STARTPAGE_VERSION% in remote provider urls", async () => {
   1189      // If this test fails, you need to update the constant STARTPAGE_VERSION in
   1190      // ASRouter.sys.mjs to match the `version` property of provider-response-schema.json
   1191      const expectedStartpageVersion = ProviderResponseSchema.version;
   1192      const provider = {
   1193        id: "foo",
   1194        enabled: true,
   1195        type: "remote",
   1196        url: "https://www.mozilla.org/%STARTPAGE_VERSION%/",
   1197      };
   1198      setMessageProviderPref([provider]);
   1199      await Router._updateMessageProviders();
   1200      assert.equal(
   1201        Router.state.providers[0].url,
   1202        `https://www.mozilla.org/${parseInt(expectedStartpageVersion, 10)}/`
   1203      );
   1204    });
   1205    it("should replace other params in remote provider urls by calling Services.urlFormater.formatURL", async () => {
   1206      const url = "https://www.example.com/";
   1207      const replacedUrl = "https://www.foo.bar/";
   1208      const stub = sandbox
   1209        .stub(global.Services.urlFormatter, "formatURL")
   1210        .withArgs(url)
   1211        .returns(replacedUrl);
   1212      const provider = { id: "foo", enabled: true, type: "remote", url };
   1213      setMessageProviderPref([provider]);
   1214      await Router._updateMessageProviders();
   1215      assert.calledOnce(stub);
   1216      assert.calledWithExactly(stub, url);
   1217      assert.equal(Router.state.providers[0].url, replacedUrl);
   1218    });
   1219    it("should only add the providers that are enabled", async () => {
   1220      const providers = [
   1221        {
   1222          id: "foo",
   1223          enabled: false,
   1224          type: "remote",
   1225          url: "https://www.foo.com/",
   1226        },
   1227        {
   1228          id: "bar",
   1229          enabled: true,
   1230          type: "remote",
   1231          url: "https://www.bar.com/",
   1232        },
   1233      ];
   1234      setMessageProviderPref(providers);
   1235      await Router._updateMessageProviders();
   1236      assert.equal(Router.state.providers.length, 1);
   1237      assert.equal(Router.state.providers[0].id, providers[1].id);
   1238    });
   1239  });
   1240 
   1241  describe("#handleMessageRequest", () => {
   1242    beforeEach(async () => {
   1243      await Router.setState(() => ({
   1244        providers: [{ id: "cfr" }, { id: "badge" }],
   1245      }));
   1246    });
   1247    it("should return no messages if shouldShowMessagesToProfile returns false", async () => {
   1248      sandbox.stub(Router, "shouldShowMessagesToProfile").returns(false);
   1249      await Router.setState(() => ({
   1250        messages: [
   1251          { id: "foo", provider: "cfr", groups: ["cfr"] },
   1252          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1253        ],
   1254      }));
   1255      const result = await Router.handleMessageRequest({
   1256        provider: "cfr",
   1257      });
   1258      assert.isNull(result);
   1259    });
   1260    it("should return messages if shouldShowMessagesToProfile returns true", async () => {
   1261      sandbox.stub(Router, "shouldShowMessagesToProfile").returns(true);
   1262      await Router.setState(() => ({
   1263        messages: [
   1264          { id: "foo", provider: "cfr", groups: ["cfr"] },
   1265          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1266        ],
   1267      }));
   1268      const result = await Router.handleMessageRequest({
   1269        provider: "cfr",
   1270      });
   1271      assert.isNotNull(result);
   1272      assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
   1273        messages: [
   1274          { id: "foo", provider: "cfr", groups: ["cfr"] },
   1275          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1276        ],
   1277      });
   1278    });
   1279    it("should not return a blocked message", async () => {
   1280      // Block all messages except the first
   1281      await Router.setState(() => ({
   1282        messages: [
   1283          { id: "foo", provider: "cfr", groups: ["cfr"] },
   1284          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1285        ],
   1286        messageBlockList: ["foo"],
   1287      }));
   1288      await Router.handleMessageRequest({
   1289        provider: "cfr",
   1290      });
   1291      assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
   1292        messages: [{ id: "bar", provider: "cfr", groups: ["cfr"] }],
   1293      });
   1294    });
   1295    it("should not return a message from a disabled group", async () => {
   1296      ASRouterTargeting.findMatchingMessage.callsFake(
   1297        ({ messages }) => messages[0]
   1298      );
   1299      // Block all messages except the first
   1300      await Router.setState(() => ({
   1301        messages: [
   1302          { id: "foo", provider: "cfr", groups: ["cfr"] },
   1303          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1304        ],
   1305        groups: [{ id: "cfr", enabled: false }],
   1306      }));
   1307      const result = await Router.handleMessageRequest({
   1308        provider: "cfr",
   1309      });
   1310      assert.isNull(result);
   1311    });
   1312    it("should not return a message from a blocked campaign", async () => {
   1313      // Block all messages except the first
   1314      await Router.setState(() => ({
   1315        messages: [
   1316          {
   1317            id: "foo",
   1318            provider: "cfr",
   1319            campaign: "foocampaign",
   1320            groups: ["cfr"],
   1321          },
   1322          { id: "bar", provider: "cfr", groups: ["cfr"] },
   1323        ],
   1324        messageBlockList: ["foocampaign"],
   1325      }));
   1326 
   1327      await Router.handleMessageRequest({
   1328        provider: "cfr",
   1329      });
   1330      assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
   1331        messages: [{ id: "bar", provider: "cfr", groups: ["cfr"] }],
   1332      });
   1333    });
   1334    it("should not return a message excluded by the provider", async () => {
   1335      // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
   1336      // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
   1337      await Router.setState(() => ({
   1338        providers: [{ id: "cfr", exclude: ["foo"] }],
   1339      }));
   1340 
   1341      await Router.setState(() => ({
   1342        messages: [{ id: "foo", provider: "cfr" }],
   1343        messageBlockList: ["foocampaign"],
   1344      }));
   1345 
   1346      const result = await Router.handleMessageRequest({
   1347        provider: "cfr",
   1348      });
   1349      assert.isNull(result);
   1350    });
   1351    it("should not return a message if the frequency cap has been hit", async () => {
   1352      sandbox.stub(Router, "isBelowFrequencyCaps").returns(false);
   1353      await Router.setState(() => ({
   1354        messages: [{ id: "foo", provider: "cfr" }],
   1355      }));
   1356      const result = await Router.handleMessageRequest({
   1357        provider: "cfr",
   1358      });
   1359      assert.isNull(result);
   1360    });
   1361    it("should get unblocked messages that match the trigger", async () => {
   1362      const message1 = {
   1363        id: "1",
   1364        campaign: "foocampaign",
   1365        trigger: { id: "foo" },
   1366        groups: ["cfr"],
   1367        provider: "cfr",
   1368      };
   1369      const message2 = {
   1370        id: "2",
   1371        campaign: "foocampaign",
   1372        trigger: { id: "bar" },
   1373        groups: ["cfr"],
   1374        provider: "cfr",
   1375      };
   1376      await Router.setState({ messages: [message2, message1] });
   1377      // Just return the first message provided as arg
   1378      ASRouterTargeting.findMatchingMessage.callsFake(
   1379        ({ messages }) => messages[0]
   1380      );
   1381 
   1382      const result = Router.handleMessageRequest({ triggerId: "foo" });
   1383 
   1384      assert.deepEqual(result, message1);
   1385    });
   1386    it("should get unblocked messages that match trigger and template", async () => {
   1387      const message1 = {
   1388        id: "1",
   1389        campaign: "foocampaign",
   1390        template: "badge",
   1391        trigger: { id: "foo" },
   1392        groups: ["badge"],
   1393        provider: "badge",
   1394      };
   1395      const message2 = {
   1396        id: "2",
   1397        campaign: "foocampaign",
   1398        template: "test_template",
   1399        trigger: { id: "foo" },
   1400        groups: ["cfr"],
   1401        provider: "cfr",
   1402      };
   1403      await Router.setState({ messages: [message2, message1] });
   1404      // Just return the first message provided as arg
   1405      ASRouterTargeting.findMatchingMessage.callsFake(
   1406        ({ messages }) => messages[0]
   1407      );
   1408 
   1409      const result = Router.handleMessageRequest({
   1410        triggerId: "foo",
   1411        template: "badge",
   1412      });
   1413 
   1414      assert.deepEqual(result, message1);
   1415    });
   1416    it("should have messageImpressions in the message context", () => {
   1417      assert.propertyVal(
   1418        Router._getMessagesContext(),
   1419        "messageImpressions",
   1420        Router.state.messageImpressions
   1421      );
   1422    });
   1423    it("should forward trigger param info", async () => {
   1424      const trigger = {
   1425        triggerId: "foo",
   1426        triggerParam: "bar",
   1427        triggerContext: "context",
   1428      };
   1429      const message1 = {
   1430        id: "1",
   1431        campaign: "foocampaign",
   1432        trigger: { id: "foo" },
   1433        groups: ["cfr"],
   1434        provider: "cfr",
   1435      };
   1436      const message2 = {
   1437        id: "2",
   1438        campaign: "foocampaign",
   1439        trigger: { id: "bar" },
   1440        groups: ["badge"],
   1441        provider: "badge",
   1442      };
   1443      await Router.setState({ messages: [message2, message1] });
   1444      // Just return the first message provided as arg
   1445 
   1446      Router.handleMessageRequest(trigger);
   1447 
   1448      assert.calledOnce(ASRouterTargeting.findMatchingMessage);
   1449 
   1450      const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
   1451      assert.propertyVal(options.trigger, "id", trigger.triggerId);
   1452      assert.propertyVal(options.trigger, "param", trigger.triggerParam);
   1453      assert.propertyVal(options.trigger, "context", trigger.triggerContext);
   1454    });
   1455    it("should not cache badge messages", async () => {
   1456      const trigger = {
   1457        triggerId: "bar",
   1458        triggerParam: "bar",
   1459        triggerContext: "context",
   1460      };
   1461      const message1 = {
   1462        id: "1",
   1463        provider: "cfr",
   1464        campaign: "foocampaign",
   1465        trigger: { id: "foo" },
   1466        groups: ["cfr"],
   1467      };
   1468      const message2 = {
   1469        id: "2",
   1470        campaign: "foocampaign",
   1471        trigger: { id: "bar" },
   1472        groups: ["badge"],
   1473        provider: "badge",
   1474      };
   1475      await Router.setState({ messages: [message2, message1] });
   1476      // Just return the first message provided as arg
   1477 
   1478      Router.handleMessageRequest(trigger);
   1479 
   1480      assert.calledOnce(ASRouterTargeting.findMatchingMessage);
   1481 
   1482      const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
   1483      assert.propertyVal(options, "shouldCache", false);
   1484    });
   1485    it("should filter out messages without a trigger (or different) when a triggerId is defined", async () => {
   1486      const trigger = { triggerId: "foo" };
   1487      const message1 = {
   1488        id: "1",
   1489        campaign: "foocampaign",
   1490        trigger: { id: "foo" },
   1491        groups: ["cfr"],
   1492        provider: "cfr",
   1493      };
   1494      const message2 = {
   1495        id: "2",
   1496        campaign: "foocampaign",
   1497        trigger: { id: "bar" },
   1498        groups: ["cfr"],
   1499        provider: "cfr",
   1500      };
   1501      const message3 = {
   1502        id: "3",
   1503        campaign: "bazcampaign",
   1504        groups: ["cfr"],
   1505        provider: "cfr",
   1506      };
   1507      await Router.setState({
   1508        messages: [message2, message1, message3],
   1509        groups: [{ id: "cfr", enabled: true }],
   1510      });
   1511      // Just return the first message provided as arg
   1512      ASRouterTargeting.findMatchingMessage.callsFake(args => args.messages);
   1513 
   1514      const result = Router.handleMessageRequest(trigger);
   1515 
   1516      assert.lengthOf(result, 1);
   1517      assert.deepEqual(result[0], message1);
   1518    });
   1519    it("should filter out messages with skip_in_tests when in automation", async () => {
   1520      await Router.setState(() => ({
   1521        messages: [
   1522          { id: "foo", provider: "cfr", skip_in_tests: "testing", groups: [] },
   1523        ],
   1524      }));
   1525      const result = await Router.handleMessageRequest({ provider: "cfr" });
   1526      assert.isNull(result);
   1527    });
   1528  });
   1529 
   1530  describe("#uninit", () => {
   1531    it("should unregister the trigger listeners", () => {
   1532      for (const listener of ASRouterTriggerListeners.values()) {
   1533        sandbox.spy(listener, "uninit");
   1534      }
   1535 
   1536      Router.uninit();
   1537 
   1538      for (const listener of ASRouterTriggerListeners.values()) {
   1539        assert.calledOnce(listener.uninit);
   1540      }
   1541    });
   1542    it("should set .dispatchCFRAction to null", () => {
   1543      Router.uninit();
   1544      assert.isNull(Router.dispatchCFRAction);
   1545      assert.isNull(Router.clearChildMessages);
   1546      assert.isNull(Router.sendTelemetry);
   1547    });
   1548    it("should save previousSessionEnd", () => {
   1549      Router.uninit();
   1550 
   1551      assert.calledOnce(Router._storage.set);
   1552      assert.calledWithExactly(
   1553        Router._storage.set,
   1554        "previousSessionEnd",
   1555        sinon.match.number
   1556      );
   1557    });
   1558    it("should remove the observer for `intl:app-locales-changed`", () => {
   1559      sandbox.spy(global.Services.obs, "removeObserver");
   1560      Router.uninit();
   1561 
   1562      assert.calledWithExactly(
   1563        global.Services.obs.removeObserver,
   1564        Router._onLocaleChanged,
   1565        "intl:app-locales-changed"
   1566      );
   1567    });
   1568    it("should remove the pref observer for `USE_REMOTE_L10N_PREF`", async () => {
   1569      sandbox.spy(global.Services.prefs, "removeObserver");
   1570      Router.uninit();
   1571 
   1572      // Grab the last call as #uninit() also involves multiple calls of `Services.prefs.removeObserver`.
   1573      const call = global.Services.prefs.removeObserver.lastCall;
   1574      assert.calledWithExactly(call, USE_REMOTE_L10N_PREF, Router);
   1575    });
   1576    it("should remove observer for multiprofile data updates", () => {
   1577      sandbox.spy(global.Services.obs, "removeObserver");
   1578      Router.uninit();
   1579 
   1580      assert.calledWithExactly(
   1581        global.Services.obs.removeObserver,
   1582        Router._updateMultiprofileData,
   1583        "sps-profiles-updated"
   1584      );
   1585    });
   1586  });
   1587 
   1588  describe("#setMessageById", async () => {
   1589    it("should send an empty message if provided id did not resolve to a message", async () => {
   1590      let response = await Router.setMessageById({ id: -1 }, true, {});
   1591      assert.deepEqual(response.message, {});
   1592    });
   1593  });
   1594 
   1595  describe("#isUnblockedMessage", () => {
   1596    it("should block a message if the group is blocked", async () => {
   1597      const msg = { id: "msg1", groups: ["foo"], provider: "unit-test" };
   1598      await Router.setState({
   1599        groups: [{ id: "foo", enabled: false }],
   1600        messages: [msg],
   1601        providers: [{ id: "unit-test" }],
   1602      });
   1603      assert.isFalse(Router.isUnblockedMessage(msg));
   1604 
   1605      await Router.setState({ groups: [{ id: "foo", enabled: true }] });
   1606 
   1607      assert.isTrue(Router.isUnblockedMessage(msg));
   1608    });
   1609    it("should block a message if at least one group is blocked", async () => {
   1610      const msg = {
   1611        id: "msg1",
   1612        groups: ["foo", "bar"],
   1613        provider: "unit-test",
   1614      };
   1615      await Router.setState({
   1616        groups: [
   1617          { id: "foo", enabled: false },
   1618          { id: "bar", enabled: false },
   1619        ],
   1620        messages: [msg],
   1621        providers: [{ id: "unit-test" }],
   1622      });
   1623      assert.isFalse(Router.isUnblockedMessage(msg));
   1624 
   1625      await Router.setState({
   1626        groups: [
   1627          { id: "foo", enabled: true },
   1628          { id: "bar", enabled: false },
   1629        ],
   1630      });
   1631 
   1632      assert.isFalse(Router.isUnblockedMessage(msg));
   1633    });
   1634  });
   1635 
   1636  describe("#blockMessageById", () => {
   1637    it("should add the id to the messageBlockList", async () => {
   1638      await Router.blockMessageById("foo");
   1639      assert.isTrue(Router.state.messageBlockList.includes("foo"));
   1640    });
   1641    it("should add the campaign to the messageBlockList instead of id if .campaign is specified and not select messages of that campaign again", async () => {
   1642      await Router.setState({
   1643        messages: [
   1644          { id: "1", campaign: "foocampaign" },
   1645          { id: "2", campaign: "foocampaign" },
   1646        ],
   1647      });
   1648      await Router.blockMessageById("1");
   1649 
   1650      assert.isTrue(Router.state.messageBlockList.includes("foocampaign"));
   1651      assert.isEmpty(Router.state.messages.filter(Router.isUnblockedMessage));
   1652    });
   1653    it("should be able to add multiple items to the messageBlockList", async () => {
   1654      await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
   1655      assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
   1656      assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
   1657    });
   1658    it("should save the messageBlockList", async () => {
   1659      await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
   1660      assert.calledWithExactly(Router._storage.set, "messageBlockList", [
   1661        FAKE_BUNDLE[0].id,
   1662        FAKE_BUNDLE[1].id,
   1663      ]);
   1664    });
   1665  });
   1666 
   1667  describe("#unblockMessageById", () => {
   1668    it("should remove the id from the messageBlockList", async () => {
   1669      await Router.blockMessageById("foo");
   1670      assert.isTrue(Router.state.messageBlockList.includes("foo"));
   1671      await Router.unblockMessageById("foo");
   1672      assert.isFalse(Router.state.messageBlockList.includes("foo"));
   1673    });
   1674    it("should remove the campaign from the messageBlockList if it is defined", async () => {
   1675      await Router.setState({ messages: [{ id: "1", campaign: "foo" }] });
   1676      await Router.blockMessageById("1");
   1677      assert.isTrue(
   1678        Router.state.messageBlockList.includes("foo"),
   1679        "blocklist has campaign id"
   1680      );
   1681      await Router.unblockMessageById("1");
   1682      assert.isFalse(
   1683        Router.state.messageBlockList.includes("foo"),
   1684        "campaign id removed from blocklist"
   1685      );
   1686    });
   1687    it("should save the messageBlockList", async () => {
   1688      await Router.unblockMessageById("foo");
   1689      assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
   1690    });
   1691  });
   1692 
   1693  describe("#routeCFRMessage", () => {
   1694    it("should allow for echoing back message modifications", () => {
   1695      const message = { somekey: "some value" };
   1696      const data = { content: message };
   1697      const browser = {};
   1698      let msg = Router.routeCFRMessage(data.content, browser, data, false);
   1699      assert.deepEqual(msg.message, message);
   1700    });
   1701    it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
   1702      sandbox.stub(CFRPageActions, "forceRecommendation");
   1703      const testMessage = { id: "foo", template: "cfr_doorhanger" };
   1704      await Router.setState({ messages: [testMessage] });
   1705      Router.routeCFRMessage(testMessage, {}, null, true);
   1706 
   1707      assert.calledOnce(CFRPageActions.forceRecommendation);
   1708    });
   1709    it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
   1710      sandbox.stub(CFRPageActions, "addRecommendation");
   1711      const testMessage = { id: "foo", template: "cfr_doorhanger" };
   1712      await Router.setState({ messages: [testMessage] });
   1713      Router.routeCFRMessage(testMessage, {}, {}, false);
   1714      assert.calledOnce(CFRPageActions.addRecommendation);
   1715    });
   1716  });
   1717 
   1718  describe("#updateTargetingParameters", () => {
   1719    it("should return an object containing the whole state", async () => {
   1720      sandbox.stub(Router, "getTargetingParameters").resolves({});
   1721      let msg = await Router.updateTargetingParameters();
   1722      let expected = Object.assign({}, Router.state, {
   1723        providerPrefs: ASRouterPreferences.providers,
   1724        userPrefs: ASRouterPreferences.getAllUserPreferences(),
   1725        targetingParameters: {},
   1726        errors: Router.errors,
   1727        devtoolsEnabled: ASRouterPreferences.devtoolsEnabled,
   1728      });
   1729 
   1730      assert.deepEqual(msg, expected);
   1731    });
   1732  });
   1733 
   1734  describe("#reachEvent", () => {
   1735    let experimentAPIStub;
   1736    let featureIds = ["cfr", "moments-page", "infobar", "spotlight"];
   1737    beforeEach(() => {
   1738      let getAllBranchesStub = sandbox.stub();
   1739      featureIds.forEach(feature => {
   1740        global.NimbusFeatures[feature].getAllVariables.returns({
   1741          id: `message-${feature}`,
   1742        });
   1743        global.NimbusFeatures[feature].getEnrollmentMetadata.returns({
   1744          slug: `slug-${feature}`,
   1745          branch: `branch-${feature}`,
   1746          isRollout: false,
   1747        });
   1748        getAllBranchesStub.withArgs(`slug-${feature}`).resolves([
   1749          {
   1750            slug: `other-branch-${feature}`,
   1751            [feature]: { value: { trigger: "unit-test" } },
   1752          },
   1753        ]);
   1754      });
   1755      experimentAPIStub = {
   1756        getAllBranches: getAllBranchesStub,
   1757      };
   1758      globals.set("ExperimentAPI", experimentAPIStub);
   1759    });
   1760    afterEach(() => {
   1761      sandbox.restore();
   1762    });
   1763    it("should tag `forReachEvent` for all the expected message types", async () => {
   1764      // This should match the `providers.messaging-experiments`
   1765      let response = await MessageLoaderUtils.loadMessagesForProvider({
   1766        type: "remote-experiments",
   1767        featureIds,
   1768      });
   1769 
   1770      // 1 message for reach 1 for expose
   1771      assert.property(response, "messages");
   1772      assert.lengthOf(response.messages, featureIds.length * 2);
   1773      assert.lengthOf(
   1774        response.messages.filter(m => m.forReachEvent),
   1775        featureIds.length
   1776      );
   1777    });
   1778  });
   1779 
   1780  describe("#sendTriggerMessage", () => {
   1781    it("should pass the trigger to ASRouterTargeting when sending trigger message", async () => {
   1782      await Router.setState({
   1783        messages: [
   1784          {
   1785            id: "foo1",
   1786            provider: "onboarding",
   1787            template: "onboarding",
   1788            trigger: { id: "firstRun" },
   1789            content: { title: "Foo1", body: "Foo123-1" },
   1790            groups: ["onboarding"],
   1791          },
   1792        ],
   1793        providers: [{ id: "onboarding" }],
   1794      });
   1795 
   1796      Router.loadMessagesFromAllProviders.resetHistory();
   1797      Router.loadMessagesFromAllProviders.onFirstCall().resolves();
   1798 
   1799      await Router.sendTriggerMessage({
   1800        browser: gBrowser.selectedBrowser,
   1801        id: "firstRun",
   1802      });
   1803 
   1804      const [{ trigger }] =
   1805        ASRouterTargeting.findMatchingMessage.firstCall.args;
   1806 
   1807      assert.calledOnce(ASRouterTargeting.findMatchingMessage);
   1808      assert.strictEqual(trigger.id, "firstRun");
   1809      assert.strictEqual(trigger.param, undefined);
   1810      assert.isObject(trigger.context);
   1811      assert.strictEqual(trigger.context.browserIsSelected, true);
   1812    });
   1813    it("should record telemetry information", async () => {
   1814      const fakeTimerId = 42;
   1815      const start = sandbox
   1816        .stub(global.Glean.messagingSystem.messageRequestTime, "start")
   1817        .returns(fakeTimerId);
   1818      const stopAndAccumulate = sandbox.stub(
   1819        global.Glean.messagingSystem.messageRequestTime,
   1820        "stopAndAccumulate"
   1821      );
   1822 
   1823      await Router.sendTriggerMessage({
   1824        browser: {},
   1825        id: "firstRun",
   1826      });
   1827 
   1828      assert.calledTwice(start);
   1829      assert.calledWithExactly(start);
   1830      assert.calledTwice(stopAndAccumulate);
   1831      assert.calledWithExactly(stopAndAccumulate, fakeTimerId);
   1832    });
   1833    it("should have previousSessionEnd in the message context", () => {
   1834      assert.propertyVal(
   1835        Router._getMessagesContext(),
   1836        "previousSessionEnd",
   1837        100
   1838      );
   1839    });
   1840    it("should record the Reach event if found any", async () => {
   1841      let messages = [
   1842        {
   1843          id: "foo1",
   1844          forReachEvent: { sent: false, group: "cfr" },
   1845          experimentSlug: "exp01",
   1846          branchSlug: "branch01",
   1847          template: "simple_template",
   1848          trigger: { id: "foo" },
   1849          content: { title: "Foo1", body: "Foo123-1" },
   1850        },
   1851        {
   1852          id: "foo2",
   1853          template: "simple_template",
   1854          trigger: { id: "bar" },
   1855          content: { title: "Foo2", body: "Foo123-2" },
   1856          provider: "onboarding",
   1857        },
   1858        {
   1859          id: "foo3",
   1860          forReachEvent: { sent: false, group: "cfr" },
   1861          experimentSlug: "exp02",
   1862          branchSlug: "branch02",
   1863          template: "simple_template",
   1864          trigger: { id: "foo" },
   1865          content: { title: "Foo1", body: "Foo123-1" },
   1866        },
   1867      ];
   1868      sandbox.stub(Router, "handleMessageRequest").resolves(messages);
   1869      sandbox.spy(Glean.messagingExperiments.reachCfr, "record");
   1870 
   1871      await Router.sendTriggerMessage({
   1872        browser: {},
   1873        id: "foo",
   1874      });
   1875 
   1876      assert.calledTwice(Glean.messagingExperiments.reachCfr.record);
   1877    });
   1878    it("should not record the Reach event if it's already sent", async () => {
   1879      let messages = [
   1880        {
   1881          id: "foo1",
   1882          forReachEvent: { sent: true, group: "cfr" },
   1883          experimentSlug: "exp01",
   1884          branchSlug: "branch01",
   1885          template: "simple_template",
   1886          trigger: { id: "foo" },
   1887          content: { title: "Foo1", body: "Foo123-1" },
   1888        },
   1889      ];
   1890      sandbox.stub(Router, "handleMessageRequest").resolves(messages);
   1891      sandbox.spy(Glean.messagingExperiments.reachCfr, "record");
   1892 
   1893      await Router.sendTriggerMessage({
   1894        browser: {},
   1895        id: "foo",
   1896      });
   1897      assert.notCalled(Glean.messagingExperiments.reachCfr.record);
   1898    });
   1899    // XXX this next test set (ie the single `it` that tries to generate
   1900    // four tests with `forEach`) doesn't work, because it will always
   1901    // pass, so don't use it as a pattern to write other tests. Bug 1967593
   1902    it("should record the Exposure event for each valid feature", async () => {
   1903      ["cfr_doorhanger", "update_action", "infobar", "spotlight"].forEach(
   1904        async template => {
   1905          let featureMap = {
   1906            cfr_doorhanger: "cfr",
   1907            spotlight: "spotlight",
   1908            infobar: "infobar",
   1909            update_action: "moments-page",
   1910          };
   1911          assert.notCalled(
   1912            global.NimbusFeatures[featureMap[template]].recordExposureEvent
   1913          );
   1914 
   1915          let messages = [
   1916            {
   1917              id: "foo1",
   1918              template,
   1919              trigger: { id: "foo" },
   1920              content: { title: "Foo1", body: "Foo123-1" },
   1921            },
   1922          ];
   1923          sandbox.stub(Router, "handleMessageRequest").resolves(messages);
   1924 
   1925          await Router.sendTriggerMessage({
   1926            browser: {},
   1927            id: "foo",
   1928          });
   1929 
   1930          assert.calledOnce(
   1931            global.NimbusFeatures[featureMap[template]].recordExposureEvent
   1932          );
   1933        }
   1934      );
   1935    });
   1936 
   1937    it("should send Exposure and route messages if recording reach fails", async () => {
   1938      const template = "feature_callout";
   1939      const featureId = "fxms-message-15";
   1940      const featureIdReachGroup = "FxmsMessage15";
   1941      let messages = [
   1942        {
   1943          _nimbusFeature: [featureId], // from _experimentsAPILoader
   1944          forReachEvent: {
   1945            sent: false,
   1946            group: featureIdReachGroup,
   1947          },
   1948          id: "foo1",
   1949          template,
   1950          trigger: { id: "fakeTrigger" },
   1951          content: { title: "Foo1", body: "Foo123-1" },
   1952        },
   1953        {
   1954          _nimbusFeature: [featureId], // from _experimentsAPILoader
   1955          id: "foo2",
   1956          template,
   1957          trigger: { id: "fakeTrigger" },
   1958          content: { title: "Foo2", body: "Foo123-2" },
   1959        },
   1960      ];
   1961      sandbox.stub(Router, "handleMessageRequest").resolves(messages);
   1962      sandbox.spy(Router, "routeCFRMessage");
   1963      sandbox
   1964        .stub(
   1965          Glean.messagingExperiments[`reach${featureIdReachGroup}`],
   1966          "record"
   1967        )
   1968        .throws(new Error("stuff"));
   1969      assert.notCalled(global.NimbusFeatures[featureId].recordExposureEvent);
   1970 
   1971      await Router.sendTriggerMessage(
   1972        {
   1973          browser: {},
   1974          id: "foo",
   1975        },
   1976        true // skipMessagesLoaded to avoid irrelevant calls spy/stub calls
   1977      );
   1978 
   1979      assert.calledOnce(global.NimbusFeatures[featureId].recordExposureEvent);
   1980      assert.calledOnce(Router.routeCFRMessage);
   1981    });
   1982  });
   1983 
   1984  describe("forceAttribution", () => {
   1985    let setAttributionString;
   1986    beforeEach(() => {
   1987      setAttributionString = sandbox.spy(Router, "setAttributionString");
   1988      sandbox.stub(global.Services.env, "set");
   1989    });
   1990    afterEach(() => {
   1991      sandbox.reset();
   1992    });
   1993    it("should double encode on windows", async () => {
   1994      sandbox.stub(fakeAttributionCode, "writeAttributionFile");
   1995 
   1996      Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
   1997 
   1998      assert.notCalled(setAttributionString);
   1999      assert.calledWithMatch(
   2000        fakeAttributionCode.writeAttributionFile,
   2001        "foo%3DFOO!%26bar%3DBAR%253F"
   2002      );
   2003    });
   2004    it("should set attribution string on mac", async () => {
   2005      sandbox.stub(global.AppConstants, "platform").value("macosx");
   2006 
   2007      Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
   2008 
   2009      assert.calledOnce(setAttributionString);
   2010      assert.calledWithMatch(
   2011        setAttributionString,
   2012        "foo%3DFOO!%26bar%3DBAR%253F"
   2013      );
   2014    });
   2015  });
   2016 
   2017  describe("_triggerHandler", () => {
   2018    it("should call #sendTriggerMessage with the correct trigger", () => {
   2019      const getter = sandbox.stub();
   2020      getter.returns(false);
   2021      sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
   2022      sinon.spy(Router, "sendTriggerMessage");
   2023      const browser = {};
   2024      const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
   2025      Router._triggerHandler(browser, trigger);
   2026      assert.calledOnce(Router.sendTriggerMessage);
   2027      assert.calledWith(
   2028        Router.sendTriggerMessage,
   2029        sandbox.match({
   2030          id: "FAKE_TRIGGER",
   2031          param: "some fake param",
   2032        })
   2033      );
   2034    });
   2035  });
   2036 
   2037  describe("_triggerHandler_kiosk", () => {
   2038    it("should not call #sendTriggerMessage", () => {
   2039      const getter = sandbox.stub();
   2040      getter.returns(true);
   2041      sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
   2042      sinon.spy(Router, "sendTriggerMessage");
   2043      const browser = {};
   2044      const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
   2045      Router._triggerHandler(browser, trigger);
   2046      assert.notCalled(Router.sendTriggerMessage);
   2047    });
   2048  });
   2049 
   2050  describe("valid preview endpoint", () => {
   2051    it("should report an error if url protocol is not https", () => {
   2052      sandbox.stub(console, "error");
   2053 
   2054      assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
   2055      assert.calledTwice(console.error);
   2056    });
   2057  });
   2058 
   2059  describe("impressions", () => {
   2060    describe("#addImpression for groups", () => {
   2061      it("should save an impression in each group-with-frequency in a message", async () => {
   2062        const fooMessageImpressions = [0];
   2063        const aGroupImpressions = [0, 1, 2];
   2064        const bGroupImpressions = [3, 4, 5];
   2065        const cGroupImpressions = [6, 7, 8];
   2066 
   2067        const message = {
   2068          id: "foo",
   2069          provider: "bar",
   2070          groups: ["a", "b", "c"],
   2071        };
   2072        const groups = [
   2073          { id: "a", frequency: { lifetime: 3 } },
   2074          { id: "b", frequency: { lifetime: 4 } },
   2075          { id: "c", frequency: { lifetime: 5 } },
   2076        ];
   2077        await Router.setState(state => {
   2078          // Add provider
   2079          const providers = [...state.providers];
   2080          // Add fooMessageImpressions
   2081          // eslint-disable-next-line no-shadow
   2082          const messageImpressions = Object.assign(
   2083            {},
   2084            state.messageImpressions
   2085          );
   2086          let gImpressions = {};
   2087          gImpressions.a = aGroupImpressions;
   2088          gImpressions.b = bGroupImpressions;
   2089          gImpressions.c = cGroupImpressions;
   2090          messageImpressions.foo = fooMessageImpressions;
   2091          return {
   2092            providers,
   2093            messageImpressions,
   2094            groups,
   2095            groupImpressions: gImpressions,
   2096          };
   2097        });
   2098 
   2099        await Router.addImpression(message);
   2100 
   2101        assert.deepEqual(
   2102          Router.state.groupImpressions.a,
   2103          [0, 1, 2, 0],
   2104          "a impressions"
   2105        );
   2106        assert.deepEqual(
   2107          Router.state.groupImpressions.b,
   2108          [3, 4, 5, 0],
   2109          "b impressions"
   2110        );
   2111        assert.deepEqual(
   2112          Router.state.groupImpressions.c,
   2113          [6, 7, 8, 0],
   2114          "c impressions"
   2115        );
   2116      });
   2117    });
   2118 
   2119    describe("#isBelowFrequencyCaps", () => {
   2120      it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
   2121        sinon.spy(Router, "_isBelowItemFrequencyCap");
   2122 
   2123        const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
   2124        const fooMessageImpressions = [0, 1];
   2125        const barGroupImpressions = [0, 1, 2];
   2126 
   2127        const message = {
   2128          id: "foo",
   2129          provider: "bar",
   2130          groups: ["bar"],
   2131          frequency: { lifetime: 3 },
   2132        };
   2133        const groups = [{ id: "bar", frequency: { lifetime: 5 } }];
   2134 
   2135        await Router.setState(state => {
   2136          // Add provider
   2137          const providers = [...state.providers];
   2138          // Add fooMessageImpressions
   2139          // eslint-disable-next-line no-shadow
   2140          const messageImpressions = Object.assign(
   2141            {},
   2142            state.messageImpressions
   2143          );
   2144          let gImpressions = {};
   2145          gImpressions.bar = barGroupImpressions;
   2146          messageImpressions.foo = fooMessageImpressions;
   2147          return {
   2148            providers,
   2149            messageImpressions,
   2150            groups,
   2151            groupImpressions: gImpressions,
   2152          };
   2153        });
   2154 
   2155        await Router.isBelowFrequencyCaps(message);
   2156 
   2157        assert.calledTwice(Router._isBelowItemFrequencyCap);
   2158        assert.calledWithExactly(
   2159          Router._isBelowItemFrequencyCap,
   2160          message,
   2161          fooMessageImpressions,
   2162          MAX_MESSAGE_LIFETIME_CAP
   2163        );
   2164        assert.calledWithExactly(
   2165          Router._isBelowItemFrequencyCap,
   2166          groups[0],
   2167          barGroupImpressions
   2168        );
   2169      });
   2170    });
   2171 
   2172    describe("#_isBelowItemFrequencyCap", () => {
   2173      it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
   2174        const item = { id: "foo", frequency: { lifetime: 5 } };
   2175        const impressions = [0, 1];
   2176        const maxLifetimeCap = 1;
   2177        const result = Router._isBelowItemFrequencyCap(
   2178          item,
   2179          impressions,
   2180          maxLifetimeCap
   2181        );
   2182        assert.isFalse(result);
   2183      });
   2184 
   2185      describe("lifetime frequency caps", () => {
   2186        it("should return true if .frequency is not defined on the item", () => {
   2187          const item = { id: "foo" };
   2188          const impressions = [0, 1];
   2189          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2190          assert.isTrue(result);
   2191        });
   2192        it("should return true if there are no impressions", () => {
   2193          const item = {
   2194            id: "foo",
   2195            frequency: {
   2196              lifetime: 10,
   2197              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
   2198            },
   2199          };
   2200          const impressions = [];
   2201          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2202          assert.isTrue(result);
   2203        });
   2204        it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
   2205          const item = { id: "foo", frequency: { lifetime: 3 } };
   2206          const impressions = [0, 1];
   2207          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2208          assert.isTrue(result);
   2209        });
   2210        it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
   2211          const item = { id: "foo", frequency: { lifetime: 3 } };
   2212          const impressions = [0, 1, 2];
   2213          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2214          assert.isFalse(result);
   2215        });
   2216        it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
   2217          const item = { id: "foo", frequency: { lifetime: 3 } };
   2218          const impressions = [0, 1, 2, 3];
   2219          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2220          assert.isFalse(result);
   2221        });
   2222      });
   2223 
   2224      describe("custom frequency caps", () => {
   2225        it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
   2226          clock.tick(ONE_DAY_IN_MS + 10);
   2227          const item = {
   2228            id: "foo",
   2229            frequency: {
   2230              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
   2231              lifetime: 3,
   2232            },
   2233          };
   2234          const impressions = [0, ONE_DAY_IN_MS + 1];
   2235          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2236          assert.isTrue(result);
   2237        });
   2238        it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
   2239          clock.tick(200);
   2240          const item = {
   2241            id: "msg1",
   2242            frequency: { custom: [{ period: 100, cap: 2 }], lifetime: 3 },
   2243          };
   2244          const impressions = [0, 160, 161];
   2245          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2246          assert.isFalse(result);
   2247        });
   2248        it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
   2249          clock.tick(ONE_DAY_IN_MS + 200);
   2250          const itemTrue = {
   2251            id: "msg2",
   2252            frequency: { custom: [{ period: 100, cap: 2 }] },
   2253          };
   2254          const itemFalse = {
   2255            id: "msg1",
   2256            frequency: {
   2257              custom: [
   2258                { period: 100, cap: 2 },
   2259                { period: ONE_DAY_IN_MS, cap: 3 },
   2260              ],
   2261            },
   2262          };
   2263          const impressions = [
   2264            0,
   2265            ONE_DAY_IN_MS + 160,
   2266            ONE_DAY_IN_MS - 100,
   2267            ONE_DAY_IN_MS - 200,
   2268          ];
   2269          assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
   2270          assert.isFalse(
   2271            Router._isBelowItemFrequencyCap(itemFalse, impressions)
   2272          );
   2273        });
   2274        it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
   2275          clock.tick(ONE_DAY_IN_MS + 10);
   2276          const item = {
   2277            id: "msg1",
   2278            frequency: {
   2279              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
   2280              lifetime: 3,
   2281            },
   2282          };
   2283          const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
   2284          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2285          assert.isFalse(result);
   2286        });
   2287        it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
   2288          clock.tick(ONE_DAY_IN_MS + 10);
   2289          const item = {
   2290            id: "msg1",
   2291            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
   2292          };
   2293          const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
   2294          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2295          assert.isTrue(result);
   2296        });
   2297        it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
   2298          clock.tick(ONE_DAY_IN_MS + 10);
   2299          const item = {
   2300            id: "msg1",
   2301            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
   2302          };
   2303          const impressions = [
   2304            0,
   2305            1,
   2306            2,
   2307            3,
   2308            ONE_DAY_IN_MS + 1,
   2309            ONE_DAY_IN_MS + 2,
   2310            ONE_DAY_IN_MS + 3,
   2311          ];
   2312          const result = Router._isBelowItemFrequencyCap(item, impressions);
   2313          assert.isFalse(result);
   2314        });
   2315      });
   2316    });
   2317 
   2318    describe("#getLongestPeriod", () => {
   2319      it("should return the period if there is only one definition", () => {
   2320        const message = {
   2321          id: "foo",
   2322          frequency: { custom: [{ period: 200, cap: 2 }] },
   2323        };
   2324        assert.equal(Router.getLongestPeriod(message), 200);
   2325      });
   2326      it("should return the longest period if there are more than one definitions", () => {
   2327        const message = {
   2328          id: "foo",
   2329          frequency: {
   2330            custom: [
   2331              { period: 1000, cap: 3 },
   2332              { period: ONE_DAY_IN_MS, cap: 5 },
   2333              { period: 100, cap: 2 },
   2334            ],
   2335          },
   2336        };
   2337        assert.equal(Router.getLongestPeriod(message), ONE_DAY_IN_MS);
   2338      });
   2339      it("should return null if there are is no .frequency", () => {
   2340        const message = { id: "foo" };
   2341        assert.isNull(Router.getLongestPeriod(message));
   2342      });
   2343      it("should return null if there are is no .frequency.custom", () => {
   2344        const message = { id: "foo", frequency: { lifetime: 10 } };
   2345        assert.isNull(Router.getLongestPeriod(message));
   2346      });
   2347    });
   2348 
   2349    describe("cleanup on init", () => {
   2350      it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
   2351        const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
   2352        messageImpressions = { foo: [0], bar: [0, 1] };
   2353        // Impressions for "bar" should be removed since that id does not exist in messages
   2354        const result = { foo: [0] };
   2355 
   2356        await createRouterAndInit([
   2357          { id: "onboarding", type: "local", messages, enabled: true },
   2358        ]);
   2359        assert.calledWith(Router._storage.set, "messageImpressions", result);
   2360        assert.deepEqual(Router.state.messageImpressions, result);
   2361      });
   2362      it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
   2363        const CURRENT_TIME = ONE_DAY_IN_MS * 2;
   2364        clock.tick(CURRENT_TIME);
   2365        const messages = [
   2366          {
   2367            id: "foo",
   2368            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 5 }] },
   2369          },
   2370        ];
   2371        messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
   2372        // Only 0 and 1 are more than 24 hours before CURRENT_TIME
   2373        const result = { foo: [CURRENT_TIME - 10] };
   2374 
   2375        await createRouterAndInit([
   2376          { id: "onboarding", type: "local", messages, enabled: true },
   2377        ]);
   2378        assert.calledWith(Router._storage.set, "messageImpressions", result);
   2379        assert.deepEqual(Router.state.messageImpressions, result);
   2380      });
   2381      it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
   2382        const CURRENT_TIME = ONE_DAY_IN_MS * 2;
   2383        clock.tick(CURRENT_TIME);
   2384        const messages = [
   2385          {
   2386            id: "foo",
   2387            frequency: {
   2388              custom: [
   2389                { period: ONE_DAY_IN_MS, cap: 5 },
   2390                { period: 100, cap: 2 },
   2391              ],
   2392            },
   2393          },
   2394        ];
   2395        messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
   2396        // Only 0 and 1 are more than 24 hours before CURRENT_TIME
   2397        const result = { foo: [CURRENT_TIME - 10] };
   2398 
   2399        await createRouterAndInit([
   2400          { id: "onboarding", type: "local", messages, enabled: true },
   2401        ]);
   2402        assert.calledWith(Router._storage.set, "messageImpressions", result);
   2403        assert.deepEqual(Router.state.messageImpressions, result);
   2404      });
   2405      it("should clear messageImpressions if they are not properly formatted", async () => {
   2406        const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
   2407        // this is impromperly formatted since messageImpressions are supposed to be an array
   2408        messageImpressions = { foo: 0 };
   2409        const result = {};
   2410 
   2411        await createRouterAndInit([
   2412          { id: "onboarding", type: "local", messages, enabled: true },
   2413        ]);
   2414        assert.calledWith(Router._storage.set, "messageImpressions", result);
   2415        assert.deepEqual(Router.state.messageImpressions, result);
   2416      });
   2417      it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
   2418        const messages = [
   2419          { id: "foo", frequency: { lifetime: 10 } },
   2420          { id: "bar", frequency: { lifetime: 10 } },
   2421        ];
   2422        messageImpressions = { foo: [0], bar: [] };
   2423 
   2424        await createRouterAndInit([
   2425          { id: "onboarding", type: "local", messages, enabled: true },
   2426        ]);
   2427        assert.notCalled(Router._storage.set);
   2428        assert.deepEqual(Router.state.messageImpressions, messageImpressions);
   2429      });
   2430    });
   2431  });
   2432 
   2433  describe("#_onLocaleChanged", () => {
   2434    it("should call _maybeUpdateL10nAttachment in the handler", async () => {
   2435      sandbox.spy(Router, "_maybeUpdateL10nAttachment");
   2436      await Router._onLocaleChanged();
   2437 
   2438      assert.calledOnce(Router._maybeUpdateL10nAttachment);
   2439    });
   2440  });
   2441 
   2442  describe("#_maybeUpdateL10nAttachment", () => {
   2443    it("should update the l10n attachment if the locale was changed", async () => {
   2444      const getter = sandbox.stub();
   2445      getter.onFirstCall().returns("en-US");
   2446      getter.onSecondCall().returns("fr");
   2447      sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
   2448      const provider = {
   2449        id: "cfr",
   2450        enabled: true,
   2451        type: "remote-settings",
   2452        collection: "cfr",
   2453      };
   2454      await createRouterAndInit([provider]);
   2455      sandbox.spy(Router, "setState");
   2456      Router.loadMessagesFromAllProviders.resetHistory();
   2457 
   2458      await Router._maybeUpdateL10nAttachment();
   2459 
   2460      assert.calledWith(Router.setState, {
   2461        localeInUse: "fr",
   2462        providers: [
   2463          {
   2464            id: "cfr",
   2465            enabled: true,
   2466            type: "remote-settings",
   2467            collection: "cfr",
   2468            lastUpdated: undefined,
   2469            errors: [],
   2470          },
   2471        ],
   2472      });
   2473      assert.calledOnce(Router.loadMessagesFromAllProviders);
   2474    });
   2475    it("should not update the l10n attachment if the provider doesn't need l10n attachment", async () => {
   2476      const getter = sandbox.stub();
   2477      getter.onFirstCall().returns("en-US");
   2478      getter.onSecondCall().returns("fr");
   2479      sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
   2480      const provider = {
   2481        id: "localProvider",
   2482        enabled: true,
   2483        type: "local",
   2484      };
   2485      await createRouterAndInit([provider]);
   2486      Router.loadMessagesFromAllProviders.resetHistory();
   2487      sandbox.spy(Router, "setState");
   2488 
   2489      await Router._maybeUpdateL10nAttachment();
   2490 
   2491      assert.notCalled(Router.setState);
   2492      assert.notCalled(Router.loadMessagesFromAllProviders);
   2493    });
   2494  });
   2495  describe("#observe", () => {
   2496    it("should reload l10n for CFRPageActions when the `USE_REMOTE_L10N_PREF` pref is changed", () => {
   2497      sandbox.spy(CFRPageActions, "reloadL10n");
   2498 
   2499      Router.observe("", "", USE_REMOTE_L10N_PREF);
   2500 
   2501      assert.calledOnce(CFRPageActions.reloadL10n);
   2502    });
   2503    it("should not react to other pref changes", () => {
   2504      sandbox.spy(CFRPageActions, "reloadL10n");
   2505 
   2506      Router.observe("", "", "foo");
   2507 
   2508      assert.notCalled(CFRPageActions.reloadL10n);
   2509    });
   2510  });
   2511  describe("#loadAllMessageGroups", () => {
   2512    it("should disable the group if the pref is false", async () => {
   2513      sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
   2514      sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
   2515        {
   2516          id: "provider-group",
   2517          enabled: true,
   2518          type: "remote",
   2519          userPreferences: ["cfrAddons"],
   2520        },
   2521      ]);
   2522      await Router.setState({
   2523        providers: [
   2524          {
   2525            id: "message-groups",
   2526            enabled: true,
   2527            collection: "collection",
   2528            type: "remote-settings",
   2529          },
   2530        ],
   2531      });
   2532 
   2533      await Router.loadAllMessageGroups();
   2534 
   2535      const group = Router.state.groups.find(g => g.id === "provider-group");
   2536 
   2537      assert.ok(group);
   2538      assert.propertyVal(group, "enabled", false);
   2539    });
   2540    it("should enable the group if at least one pref is true", async () => {
   2541      sandbox
   2542        .stub(ASRouterPreferences, "getUserPreference")
   2543        .withArgs("cfrAddons")
   2544        .returns(false)
   2545        .withArgs("cfrFeatures")
   2546        .returns(true);
   2547      sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
   2548        {
   2549          id: "provider-group",
   2550          enabled: true,
   2551          type: "remote",
   2552          userPreferences: ["cfrAddons", "cfrFeatures"],
   2553        },
   2554      ]);
   2555      await Router.setState({
   2556        providers: [
   2557          {
   2558            id: "message-groups",
   2559            enabled: true,
   2560            collection: "collection",
   2561            type: "remote-settings",
   2562          },
   2563        ],
   2564      });
   2565 
   2566      await Router.loadAllMessageGroups();
   2567 
   2568      const group = Router.state.groups.find(g => g.id === "provider-group");
   2569 
   2570      assert.ok(group);
   2571      assert.propertyVal(group, "enabled", true);
   2572    });
   2573    it("should be keep the group disabled if disabled is true", async () => {
   2574      sandbox.stub(ASRouterPreferences, "getUserPreference").returns(true);
   2575      sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
   2576        {
   2577          id: "provider-group",
   2578          enabled: false,
   2579          type: "remote",
   2580          userPreferences: ["cfrAddons"],
   2581        },
   2582      ]);
   2583      await Router.setState({
   2584        providers: [
   2585          {
   2586            id: "message-groups",
   2587            enabled: true,
   2588            collection: "collection",
   2589            type: "remote-settings",
   2590          },
   2591        ],
   2592      });
   2593 
   2594      await Router.loadAllMessageGroups();
   2595 
   2596      const group = Router.state.groups.find(g => g.id === "provider-group");
   2597 
   2598      assert.ok(group);
   2599      assert.propertyVal(group, "enabled", false);
   2600    });
   2601    it("should keep local groups unchanged if provider doesn't require an update", async () => {
   2602      sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
   2603      sandbox.stub(MessageLoaderUtils, "_loadDataForProvider");
   2604      await Router.setState({
   2605        groups: [
   2606          {
   2607            id: "cfr",
   2608            enabled: true,
   2609            collection: "collection",
   2610            type: "remote-settings",
   2611          },
   2612        ],
   2613      });
   2614 
   2615      await Router.loadAllMessageGroups();
   2616 
   2617      const group = Router.state.groups.find(g => g.id === "cfr");
   2618 
   2619      assert.ok(group);
   2620      assert.propertyVal(group, "enabled", true);
   2621      // Because it should not have updated
   2622      assert.notCalled(MessageLoaderUtils._loadDataForProvider);
   2623    });
   2624    it("should update local groups on pref change (no RS update)", async () => {
   2625      sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
   2626      sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
   2627      await Router.setState({
   2628        groups: [
   2629          {
   2630            id: "cfr",
   2631            enabled: true,
   2632            collection: "collection",
   2633            type: "remote-settings",
   2634            userPreferences: ["cfrAddons"],
   2635          },
   2636        ],
   2637      });
   2638 
   2639      await Router.loadAllMessageGroups();
   2640 
   2641      const group = Router.state.groups.find(g => g.id === "cfr");
   2642 
   2643      assert.ok(group);
   2644      // Pref changed, updated the group state
   2645      assert.propertyVal(group, "enabled", false);
   2646    });
   2647  });
   2648  describe("unblockAll", () => {
   2649    it("Clears the message block list and returns the state value", async () => {
   2650      await Router.setState({ messageBlockList: ["one", "two", "three"] });
   2651      assert.equal(Router.state.messageBlockList.length, 3);
   2652      const state = await Router.unblockAll();
   2653      assert.equal(Router.state.messageBlockList.length, 0);
   2654      assert.equal(state.messageBlockList.length, 0);
   2655    });
   2656  });
   2657  describe("#loadMessagesForProvider", () => {
   2658    it("should fetch messages from the ExperimentAPI", async () => {
   2659      const args = {
   2660        type: "remote-experiments",
   2661        featureIds: ["spotlight"],
   2662      };
   2663 
   2664      await MessageLoaderUtils.loadMessagesForProvider(args);
   2665 
   2666      assert.calledOnce(global.NimbusFeatures.spotlight.getEnrollmentMetadata);
   2667      assert.calledOnce(global.NimbusFeatures.spotlight.getAllVariables);
   2668    });
   2669    it("should handle the case of no experiments in the ExperimentAPI", async () => {
   2670      const args = {
   2671        type: "remote-experiments",
   2672        featureIds: ["infobar"],
   2673      };
   2674 
   2675      const result = await MessageLoaderUtils.loadMessagesForProvider(args);
   2676 
   2677      assert.lengthOf(result.messages, 0);
   2678    });
   2679    it("should normally load ExperimentAPI messages", async () => {
   2680      const args = {
   2681        type: "remote-experiments",
   2682        featureIds: ["infobar"],
   2683      };
   2684      const enrollment = {
   2685        slug: "enrollment01",
   2686        branch: {
   2687          slug: "branch01",
   2688          infobar: {
   2689            featureId: "infobar",
   2690            value: { id: "id01", trigger: { id: "openURL" } },
   2691          },
   2692        },
   2693      };
   2694 
   2695      global.NimbusFeatures.infobar.getAllVariables.returns(
   2696        enrollment.branch.infobar.value
   2697      );
   2698      global.NimbusFeatures.infobar.getEnrollmentMetadata.returns({
   2699        slug: enrollment.slug,
   2700        branch: enrollment.branch.slug,
   2701        isRollout: false,
   2702      });
   2703      global.ExperimentAPI.getAllBranches.returns([
   2704        enrollment.branch,
   2705        {
   2706          slug: "control",
   2707          infobar: {
   2708            featureId: "infobar",
   2709            value: null,
   2710          },
   2711        },
   2712      ]);
   2713 
   2714      const result = await MessageLoaderUtils.loadMessagesForProvider(args);
   2715 
   2716      assert.lengthOf(result.messages, 1);
   2717    });
   2718    it("should skip disabled features and not load the messages", async () => {
   2719      const args = {
   2720        type: "remote-experiments",
   2721        featureIds: ["cfr"],
   2722      };
   2723 
   2724      global.NimbusFeatures.cfr.getAllVariables.returns(null);
   2725 
   2726      const result = await MessageLoaderUtils.loadMessagesForProvider(args);
   2727 
   2728      assert.lengthOf(result.messages, 0);
   2729    });
   2730    it("should fetch branches with trigger", async () => {
   2731      const args = {
   2732        type: "remote-experiments",
   2733        featureIds: ["cfr"],
   2734      };
   2735      const enrollment = {
   2736        slug: "exp01",
   2737        branch: {
   2738          slug: "branch01",
   2739          cfr: {
   2740            featureId: "cfr",
   2741            value: { id: "id01", trigger: { id: "openURL" } },
   2742          },
   2743        },
   2744      };
   2745 
   2746      global.NimbusFeatures.cfr.getAllVariables.returns(
   2747        enrollment.branch.cfr.value
   2748      );
   2749      global.NimbusFeatures.cfr.getEnrollmentMetadata.returns({
   2750        slug: enrollment.slug,
   2751        branch: enrollment.branch.slug,
   2752        isRollout: false,
   2753      });
   2754      global.ExperimentAPI.getAllBranches.resolves([
   2755        enrollment.branch,
   2756        {
   2757          slug: "branch02",
   2758          cfr: {
   2759            featureId: "cfr",
   2760            value: { id: "id02", trigger: { id: "openURL" } },
   2761          },
   2762        },
   2763        {
   2764          // This branch should not be loaded as it doesn't have the trigger
   2765          slug: "branch03",
   2766          cfr: {
   2767            featureId: "cfr",
   2768            value: { id: "id03" },
   2769          },
   2770        },
   2771      ]);
   2772 
   2773      const result = await MessageLoaderUtils.loadMessagesForProvider(args);
   2774 
   2775      assert.equal(result.messages.length, 2);
   2776      assert.equal(result.messages[0].id, "id01");
   2777      assert.equal(result.messages[1].id, "id02");
   2778      assert.equal(result.messages[1].experimentSlug, "exp01");
   2779      assert.equal(result.messages[1].branchSlug, "branch02");
   2780      assert.deepEqual(result.messages[1].forReachEvent, {
   2781        sent: false,
   2782        group: "cfr",
   2783      });
   2784    });
   2785    it("should fetch branches with trigger even if enrolled branch is disabled", async () => {
   2786      const args = {
   2787        type: "remote-experiments",
   2788        featureIds: ["cfr"],
   2789      };
   2790      const enrollment = {
   2791        slug: "exp01",
   2792        branch: {
   2793          slug: "branch01",
   2794          cfr: {
   2795            featureId: "cfr",
   2796            value: {},
   2797          },
   2798        },
   2799      };
   2800 
   2801      // Needs to match the `featureIds` value to return an enrollment
   2802      // for that feature
   2803      global.NimbusFeatures.cfr.getAllVariables.returns(
   2804        enrollment.branch.cfr.value
   2805      );
   2806      global.NimbusFeatures.cfr.getEnrollmentMetadata.returns({
   2807        slug: enrollment.slug,
   2808        branch: enrollment.branch.slug,
   2809        isRollout: false,
   2810      });
   2811      global.ExperimentAPI.getAllBranches.resolves([
   2812        enrollment.branch,
   2813        {
   2814          slug: "branch02",
   2815          cfr: {
   2816            featureId: "cfr",
   2817            value: { id: "id02", trigger: { id: "openURL" } },
   2818          },
   2819        },
   2820        {
   2821          // This branch should not be loaded as it doesn't have the trigger
   2822          slug: "branch03",
   2823          cfr: {
   2824            featureId: "cfr",
   2825            value: { id: "id03" },
   2826          },
   2827        },
   2828      ]);
   2829 
   2830      const result = await MessageLoaderUtils.loadMessagesForProvider(args);
   2831 
   2832      assert.equal(result.messages.length, 1);
   2833      assert.equal(result.messages[0].id, "id02");
   2834      assert.equal(result.messages[0].experimentSlug, "exp01");
   2835      assert.equal(result.messages[0].branchSlug, "branch02");
   2836      assert.deepEqual(result.messages[0].forReachEvent, {
   2837        sent: false,
   2838        group: "cfr",
   2839      });
   2840    });
   2841  });
   2842  describe("#_remoteSettingsLoader", () => {
   2843    let provider;
   2844    let spy;
   2845    beforeEach(() => {
   2846      provider = {
   2847        id: "cfr",
   2848        collection: "cfr",
   2849      };
   2850      sandbox
   2851        .stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
   2852        .resolves([{ id: "message_1" }]);
   2853      sandbox.stub(global.IOUtils, "exists").resolves(false);
   2854      spy = sandbox.spy(global.UnstoredDownloader.prototype.download);
   2855      global.UnstoredDownloader.prototype.download = spy;
   2856    });
   2857    it("should be called with the expected dir path", async () => {
   2858      const writeSpy = sandbox.spy(global.IOUtils, "write");
   2859 
   2860      sandbox
   2861        .stub(global.Services.locale, "appLocaleAsBCP47")
   2862        .get(() => "en-US");
   2863 
   2864      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2865 
   2866      assert.calledOnce(spy);
   2867      assert.calledWithMatch(
   2868        writeSpy,
   2869        "asrouter.ftl", // PathUtils.join() is mocked in `unit-entry.js` and only returns the filename.
   2870        sinon.match.any,
   2871        { tmpPath: "asrouter.ftl.tmp" }
   2872      );
   2873    });
   2874    it("should download if local file has different size", async () => {
   2875      global.IOUtils.exists.resolves(true);
   2876      sandbox.stub(global.IOUtils, "stat").resolves({ size: 1337 });
   2877      sandbox
   2878        .stub(global.Services.locale, "appLocaleAsBCP47")
   2879        .get(() => "en-US");
   2880 
   2881      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2882 
   2883      assert.calledOnce(spy);
   2884    });
   2885    it("should not download if local file has same size", async () => {
   2886      global.IOUtils.exists.resolves(true);
   2887      sandbox.stub(global.IOUtils, "stat").resolves({ size: 42 });
   2888      sandbox
   2889        .stub(global.KintoHttpClient.prototype, "getRecord")
   2890        .resolves({ data: { attachment: { size: 42 } } });
   2891      sandbox
   2892        .stub(global.Services.locale, "appLocaleAsBCP47")
   2893        .get(() => "en-US");
   2894 
   2895      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2896 
   2897      assert.notCalled(spy);
   2898    });
   2899    it("should allow fetch for known locales", async () => {
   2900      sandbox
   2901        .stub(global.Services.locale, "appLocaleAsBCP47")
   2902        .get(() => "en-US");
   2903 
   2904      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2905 
   2906      assert.calledOnce(spy);
   2907    });
   2908    it("should fallback to 'en-US' for locale 'und' ", async () => {
   2909      sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => "und");
   2910      const getRecordSpy = sandbox.spy(
   2911        global.KintoHttpClient.prototype,
   2912        "getRecord"
   2913      );
   2914 
   2915      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2916 
   2917      assert.ok(getRecordSpy.args[0][0].includes("en-US"));
   2918      assert.calledOnce(spy);
   2919    });
   2920    it("should fallback to 'ja-JP-mac' for locale 'ja-JP-macos'", async () => {
   2921      sandbox
   2922        .stub(global.Services.locale, "appLocaleAsBCP47")
   2923        .get(() => "ja-JP-macos");
   2924      const getRecordSpy = sandbox.spy(
   2925        global.KintoHttpClient.prototype,
   2926        "getRecord"
   2927      );
   2928 
   2929      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2930 
   2931      assert.ok(getRecordSpy.args[0][0].includes("ja-JP-mac"));
   2932      assert.calledOnce(spy);
   2933    });
   2934    it("should not allow fetch for unsupported locales", async () => {
   2935      sandbox
   2936        .stub(global.Services.locale, "appLocaleAsBCP47")
   2937        .get(() => "unkown");
   2938 
   2939      await MessageLoaderUtils._remoteSettingsLoader(provider, {});
   2940 
   2941      assert.notCalled(spy);
   2942    });
   2943  });
   2944  describe("#resetMessageState", () => {
   2945    it("should reset all message impressions", async () => {
   2946      await Router.setState({
   2947        messages: [{ id: "1" }, { id: "2" }],
   2948      });
   2949      await Router.setState({
   2950        messageImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
   2951      }); // Add impressions for test messages
   2952      let impressions = Object.values(Router.state.messageImpressions);
   2953      assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions
   2954 
   2955      Router.resetMessageState();
   2956      impressions = Object.values(Router.state.messageImpressions);
   2957 
   2958      assert.isEmpty(impressions.filter(i => i.length)); // Both messages now have zero impressions
   2959      assert.calledWithExactly(Router._storage.set, "messageImpressions", {
   2960        1: [],
   2961        2: [],
   2962      });
   2963    });
   2964  });
   2965  describe("#resetGroupsState", () => {
   2966    it("should reset all group impressions", async () => {
   2967      await Router.setState({
   2968        groups: [{ id: "1" }, { id: "2" }],
   2969      });
   2970      await Router.setState({
   2971        groupImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
   2972      }); // Add impressions for test groups
   2973      let impressions = Object.values(Router.state.groupImpressions);
   2974      assert.equal(impressions.filter(i => i.length).length, 2); // Both groups have impressions
   2975 
   2976      Router.resetGroupsState();
   2977      impressions = Object.values(Router.state.groupImpressions);
   2978 
   2979      assert.isEmpty(impressions.filter(i => i.length)); // Both groups now have zero impressions
   2980      assert.calledWithExactly(Router._storage.set, "groupImpressions", {
   2981        1: [],
   2982        2: [],
   2983      });
   2984    });
   2985  });
   2986  describe("#resetScreenImpressions", () => {
   2987    it("should reset all screen impressions", async () => {
   2988      await Router.setState({ screenImpressions: { 1: 1, 2: 2 } });
   2989      let impressions = Object.values(Router.state.screenImpressions);
   2990      assert.equal(impressions.filter(i => i).length, 2); // Both screens have impressions
   2991 
   2992      Router.resetScreenImpressions();
   2993      impressions = Object.values(Router.state.screenImpressions);
   2994 
   2995      assert.isEmpty(impressions.filter(i => i)); // Both screens now have zero impressions
   2996      assert.calledWithExactly(Router._storage.set, "screenImpressions", {});
   2997    });
   2998  });
   2999  describe("#editState", () => {
   3000    it("should update message impressions", async () => {
   3001      sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
   3002      await Router.setState({ messages: [{ id: "1" }, { id: "2" }] });
   3003      await Router.setState({
   3004        messageImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
   3005      });
   3006      let impressions = Object.values(Router.state.messageImpressions);
   3007      assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions
   3008 
   3009      Router.editState("messageImpressions", {
   3010        1: [],
   3011        2: [],
   3012        3: [0, 1, 2],
   3013      });
   3014 
   3015      // The original messages now have zero impressions
   3016      assert.isEmpty(Router.state.messageImpressions["1"]);
   3017      assert.isEmpty(Router.state.messageImpressions["2"]);
   3018      // A new impression array was added for the new message
   3019      assert.equal(Router.state.messageImpressions["3"].length, 3);
   3020      assert.calledWithExactly(Router._storage.set, "messageImpressions", {
   3021        1: [],
   3022        2: [],
   3023        3: [0, 1, 2],
   3024      });
   3025    });
   3026  });
   3027 
   3028  describe("multiprofile messages", () => {
   3029    describe("#_updateMultiprofileData", () => {
   3030      it("should update multiprofile data state from storage with event source remote", async () => {
   3031        const testImpressions = { test_msg: [111, 222] };
   3032        const testBlocklist = ["blocked_msg"];
   3033 
   3034        Router._storage.getSharedMessageImpressions = sandbox
   3035          .stub()
   3036          .resolves(testImpressions);
   3037        Router._storage.getSharedMessageBlocklist = sandbox
   3038          .stub()
   3039          .resolves(testBlocklist);
   3040 
   3041        await Router._updateMultiprofileData(
   3042          null,
   3043          "sps-profiles-updated",
   3044          "remote"
   3045        );
   3046 
   3047        assert.calledOnce(Router._storage.getSharedMessageImpressions);
   3048        assert.calledOnce(Router._storage.getSharedMessageBlocklist);
   3049        assert.deepEqual(
   3050          Router.state.multiProfileMessageImpressions,
   3051          testImpressions
   3052        );
   3053        assert.deepEqual(
   3054          Router.state.multiProfileMessageBlocklist,
   3055          testBlocklist
   3056        );
   3057      });
   3058      it("should not update multiprofile data from storage with event source local", async () => {
   3059        const testImpressions = { test_msg: [111, 222] };
   3060        const testBlocklist = ["blocked_msg"];
   3061 
   3062        await Router.setState(() => {
   3063          return {
   3064            multiProfileMessageBlocklist: testBlocklist,
   3065            multiProfileMessageImpressions: testImpressions,
   3066          };
   3067        });
   3068 
   3069        Router._storage.getSharedMessageImpressions = sandbox.stub();
   3070        Router._storage.getSharedMessageBlocklist = sandbox.stub();
   3071 
   3072        await Router._updateMultiprofileData(
   3073          null,
   3074          "sps-profiles-updated",
   3075          "local"
   3076        );
   3077 
   3078        assert.notCalled(Router._storage.getSharedMessageImpressions);
   3079        assert.notCalled(Router._storage.getSharedMessageBlocklist);
   3080        assert.deepEqual(
   3081          Router.state.multiProfileMessageImpressions,
   3082          testImpressions
   3083        );
   3084        assert.deepEqual(
   3085          Router.state.multiProfileMessageBlocklist,
   3086          testBlocklist
   3087        );
   3088      });
   3089    });
   3090 
   3091    describe("multiprofile #addImpression", () => {
   3092      describe("addImpression when multiprofile is enabled", () => {
   3093        beforeEach(() => {
   3094          sandbox
   3095            .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
   3096            .value(true);
   3097        });
   3098        it("should add impression data when profileScope is set", async () => {
   3099          const message = {
   3100            id: "foo",
   3101            provider: "bar",
   3102            frequency: { lifetime: 3 },
   3103            profileScope: "single",
   3104          };
   3105          await Router.addImpression(message);
   3106          assert.deepEqual(
   3107            Router.state.multiProfileMessageImpressions.foo,
   3108            [0],
   3109            "foo message shared multiprofile impressions"
   3110          );
   3111          assert.deepEqual(
   3112            Router.state.messageImpressions.foo,
   3113            [0],
   3114            "foo message impressions"
   3115          );
   3116        });
   3117        it("should not add profileImpressions when profileScope is not set", async () => {
   3118          const message = {
   3119            id: "foo",
   3120            provider: "bar",
   3121            frequency: { lifetime: 3 },
   3122          };
   3123          await Router.addImpression(message);
   3124          assert.deepEqual(
   3125            Router.state.multiProfileMessageImpressions.foo,
   3126            undefined,
   3127            "foo message shared multiprofile impressions"
   3128          );
   3129          assert.deepEqual(
   3130            Router.state.messageImpressions.foo,
   3131            [0],
   3132            "foo message impressions"
   3133          );
   3134        });
   3135      });
   3136      describe("addImpression when multiprofile is not enabled", () => {
   3137        it("should not add shared multiprofile impression even when profileScope is set", async () => {
   3138          sandbox
   3139            .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
   3140            .value(false);
   3141 
   3142          const message = {
   3143            id: "foo",
   3144            provider: "bar",
   3145            frequency: { lifetime: 3 },
   3146            profileScope: "single",
   3147          };
   3148          await Router.addImpression(message);
   3149          assert.deepEqual(
   3150            Router.state.multiProfileMessageImpressions.foo,
   3151            undefined,
   3152            "foo message shared multiprofile impressions"
   3153          );
   3154          assert.deepEqual(
   3155            Router.state.messageImpressions.foo,
   3156            [0],
   3157            "foo message impressions"
   3158          );
   3159        });
   3160      });
   3161    });
   3162 
   3163    describe("multiprofile #cleanupImpressions", () => {
   3164      beforeEach(() => {
   3165        Router._storage.setSharedMessageImpressions = sandbox.stub();
   3166        sandbox
   3167          .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
   3168          .value(true);
   3169      });
   3170      it("should remove impressions from shared multiprofile impressions if the message is not in state & is older than six months", async () => {
   3171        await Router.setState(() => ({
   3172          multiProfileMessageImpressions: {
   3173            foo: [Date.now() - SIX_MONTHS_IN_MS - 1, Date.now()],
   3174          },
   3175          messageImpressions: {
   3176            foo: [Date.now() - SIX_MONTHS_IN_MS - 1, Date.now()],
   3177          },
   3178        }));
   3179 
   3180        Router.cleanupImpressions();
   3181 
   3182        assert.property(Router.state.multiProfileMessageImpressions, "foo");
   3183        assert.lengthOf(Router.state.multiProfileMessageImpressions.foo, 1);
   3184        assert.notProperty(Router.state.messageImpressions, "foo");
   3185      });
   3186      it("should remove impressions from shared multiprofile impressions if the frequency cap is exceeded", async () => {
   3187        const CURRENT_TIME = ONE_DAY_IN_MS * 2;
   3188        clock.tick(CURRENT_TIME);
   3189        const testMessages = [
   3190          {
   3191            id: "foo",
   3192            profileScope: "single",
   3193            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 5 }] },
   3194          },
   3195        ];
   3196        messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
   3197        // Only 0 and 1 are more than 24 hours before CURRENT_TIME
   3198        const result = { foo: [CURRENT_TIME - 10] };
   3199 
   3200        await Router.setState(() => ({
   3201          messages: testMessages,
   3202          multiProfileMessageImpressions: messageImpressions,
   3203        }));
   3204 
   3205        Router.cleanupImpressions();
   3206 
   3207        assert.deepEqual(
   3208          Router.state.multiProfileMessageImpressions,
   3209          result,
   3210          "foo message shared multiprofile impressions"
   3211        );
   3212      });
   3213    });
   3214 
   3215    describe("multiprofile #hasValidProfileScope", () => {
   3216      it("should not filter messages when profile scope not set", async () => {
   3217        const message1 = {
   3218          id: "foo",
   3219          provider: "cfr",
   3220          groups: [],
   3221        };
   3222        const result = await Router.hasValidProfileScope(message1);
   3223        assert.isTrue(result);
   3224      });
   3225      it("should not filter when profile scope set and has both message and shared profile impression", async () => {
   3226        const message1 = {
   3227          id: "foo",
   3228          provider: "cfr",
   3229          profileScope: "single",
   3230          groups: [],
   3231        };
   3232        await Router.setState(() => ({
   3233          messages: [message1],
   3234          multiProfileMessageImpressions: { foo: [111, 222] },
   3235          messageImpressions: { foo: [111, 222] },
   3236        }));
   3237 
   3238        const result = await Router.hasValidProfileScope(message1);
   3239        assert.isTrue(result);
   3240      });
   3241      it("should filter when profile scope set and has just shared profile impression", async () => {
   3242        const message1 = {
   3243          id: "foo",
   3244          provider: "cfr",
   3245          profileScope: "single",
   3246          groups: [],
   3247        };
   3248        await Router.setState(() => ({
   3249          messages: [message1],
   3250          multiProfileMessageImpressions: { foo: [111, 222] },
   3251        }));
   3252 
   3253        const result = await Router.hasValidProfileScope(message1);
   3254        assert.isFalse(result);
   3255      });
   3256    });
   3257 
   3258    describe("multiprofile #blockMessageById", () => {
   3259      beforeEach(() => {
   3260        sandbox
   3261          .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
   3262          .value(true);
   3263      });
   3264 
   3265      it("should add the id to the shared messageBlockList if the profile scope is single", async () => {
   3266        await Router.setState({
   3267          messages: [
   3268            { id: "foo", provider: "cfr", profileScope: "single", groups: [] },
   3269          ],
   3270        });
   3271 
   3272        await Router.blockMessageById("foo");
   3273        assert.isTrue(Router.state.messageBlockList.includes("foo"));
   3274        assert.isTrue(
   3275          Router.state.multiProfileMessageBlocklist.includes("foo")
   3276        );
   3277        assert.calledOnce(Router._storage.setSharedMessageBlocked);
   3278      });
   3279 
   3280      it("should not add the id to the shared messageBlockList if there is no profile scope", async () => {
   3281        await Router.setState({
   3282          messages: [{ id: "bar", provider: "cfr", groups: [] }],
   3283        });
   3284 
   3285        await Router.blockMessageById("bar");
   3286        assert.isTrue(Router.state.messageBlockList.includes("bar"));
   3287        assert.isFalse(
   3288          Router.state.multiProfileMessageBlocklist.includes("bar")
   3289        );
   3290        assert.notCalled(Router._storage.setSharedMessageBlocked);
   3291      });
   3292    });
   3293 
   3294    describe("multiprofile #unblockMessageById", () => {
   3295      beforeEach(() => {
   3296        sandbox
   3297          .stub(ASRouterTargeting.Environment, "canCreateSelectableProfiles")
   3298          .value(true);
   3299      });
   3300 
   3301      it("should remove the id from the messageBlockList", async () => {
   3302        await Router.setState({
   3303          messages: [
   3304            { id: "foo", provider: "cfr", profileScope: "single", groups: [] },
   3305          ],
   3306        });
   3307        await Router.blockMessageById("foo");
   3308        assert.isTrue(Router.state.messageBlockList.includes("foo"));
   3309        assert.isTrue(
   3310          Router.state.multiProfileMessageBlocklist.includes("foo")
   3311        );
   3312        assert.calledWithExactly(
   3313          Router._storage.setSharedMessageBlocked,
   3314          "foo"
   3315        );
   3316 
   3317        await Router.unblockMessageById("foo");
   3318        assert.isFalse(Router.state.messageBlockList.includes("foo"));
   3319        assert.isFalse(
   3320          Router.state.multiProfileMessageBlocklist.includes("foo")
   3321        );
   3322        // multiprofile uses the same function for block and unblock
   3323        assert.calledWithExactly(
   3324          Router._storage.setSharedMessageBlocked,
   3325          "foo",
   3326          false
   3327        );
   3328      });
   3329    });
   3330 
   3331    describe("multiprofile #handleMessageRequest", () => {
   3332      beforeEach(async () => {
   3333        await Router.setState(() => ({
   3334          providers: [{ id: "cfr" }],
   3335        }));
   3336 
   3337        sandbox.stub(Router, "shouldShowMessagesToProfile").returns(true);
   3338      });
   3339      it("should hide message when not a valid multi profile scope", async () => {
   3340        await Router.setState(() => ({
   3341          messages: [
   3342            { id: "foo", provider: "cfr", profileScope: "single", groups: [] },
   3343          ],
   3344          multiProfileMessageImpressions: { foo: [111, 222] },
   3345          messageImpressions: {},
   3346        }));
   3347        const result = await Router.handleMessageRequest({ provider: "cfr" });
   3348        assert.isNull(result);
   3349        assert.notCalled(ASRouterTargeting.findMatchingMessage);
   3350      });
   3351 
   3352      it("should show message for valid multi profile scope", async () => {
   3353        const message1 = {
   3354          id: "foo",
   3355          provider: "cfr",
   3356          profileScope: "single",
   3357          groups: [],
   3358        };
   3359        await Router.setState(() => ({
   3360          messages: [message1],
   3361          multiProfileMessageImpressions: { foo: [111, 222] },
   3362          messageImpressions: { foo: [111, 222] },
   3363        }));
   3364 
   3365        await Router.handleMessageRequest({ provider: "cfr" });
   3366        assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
   3367          messages: [
   3368            { id: "foo", provider: "cfr", groups: [], profileScope: "single" },
   3369          ],
   3370        });
   3371      });
   3372 
   3373      it("should show messages when profile scope is not set", async () => {
   3374        await Router.setState(() => ({
   3375          messages: [
   3376            { id: "foo", provider: "cfr", profileScope: "", groups: [] },
   3377          ],
   3378          messageImpressions: { foo: [111, 222] },
   3379        }));
   3380 
   3381        await Router.handleMessageRequest({ provider: "cfr" });
   3382        assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
   3383          messages: [
   3384            { id: "foo", provider: "cfr", groups: [], profileScope: "" },
   3385          ],
   3386        });
   3387      });
   3388    });
   3389  });
   3390 });