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 });