ASRouter.test.js 85 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778
  1. import {
  2. _ASRouter,
  3. chooseBranch,
  4. MessageLoaderUtils,
  5. TRAILHEAD_CONFIG,
  6. } from "lib/ASRouter.jsm";
  7. import {ASRouterTargeting, QueryCache} from "lib/ASRouterTargeting.jsm";
  8. import {
  9. CHILD_TO_PARENT_MESSAGE_NAME,
  10. FAKE_LOCAL_MESSAGES,
  11. FAKE_LOCAL_PROVIDER,
  12. FAKE_LOCAL_PROVIDERS,
  13. FAKE_RECOMMENDATION,
  14. FAKE_REMOTE_MESSAGES,
  15. FAKE_REMOTE_PROVIDER,
  16. FAKE_REMOTE_SETTINGS_PROVIDER,
  17. FakeRemotePageManager,
  18. PARENT_TO_CHILD_MESSAGE_NAME,
  19. } from "./constants";
  20. import {actionCreators as ac} from "common/Actions.jsm";
  21. import {ASRouterPreferences} from "lib/ASRouterPreferences.jsm";
  22. import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
  23. import {CFRPageActions} from "lib/CFRPageActions.jsm";
  24. import {GlobalOverrider} from "test/unit/utils";
  25. import {PanelTestProvider} from "lib/PanelTestProvider.jsm";
  26. import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
  27. import {SnippetsTestMessageProvider} from "lib/SnippetsTestMessageProvider.jsm";
  28. const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.providers.snippets";
  29. const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER, FAKE_REMOTE_SETTINGS_PROVIDER];
  30. const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
  31. const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
  32. const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
  33. const FAKE_RESPONSE_HEADERS = {get() {}};
  34. // Creates a message object that looks like messages returned by
  35. // RemotePageManager listeners
  36. function fakeAsyncMessage(action) {
  37. return {data: action, target: new FakeRemotePageManager()};
  38. }
  39. // Create a message that looks like a user action
  40. function fakeExecuteUserAction(action) {
  41. return fakeAsyncMessage({data: action, type: "USER_ACTION"});
  42. }
  43. describe("ASRouter", () => {
  44. let Router;
  45. let globals;
  46. let channel;
  47. let sandbox;
  48. let messageBlockList;
  49. let providerBlockList;
  50. let messageImpressions;
  51. let providerImpressions;
  52. let previousSessionEnd;
  53. let fetchStub;
  54. let clock;
  55. let getStringPrefStub;
  56. let dispatchStub;
  57. let fakeAttributionCode;
  58. let FakeBookmarkPanelHub;
  59. function createFakeStorage() {
  60. const getStub = sandbox.stub();
  61. getStub.returns(Promise.resolve());
  62. getStub.withArgs("messageBlockList").returns(Promise.resolve(messageBlockList));
  63. getStub.withArgs("providerBlockList").returns(Promise.resolve(providerBlockList));
  64. getStub.withArgs("messageImpressions").returns(Promise.resolve(messageImpressions));
  65. getStub.withArgs("providerImpressions").returns(Promise.resolve(providerImpressions));
  66. getStub.withArgs("previousSessionEnd").returns(Promise.resolve(previousSessionEnd));
  67. return {
  68. get: getStub,
  69. set: sandbox.stub().returns(Promise.resolve()),
  70. };
  71. }
  72. function setMessageProviderPref(value) {
  73. sandbox.stub(ASRouterPreferences, "providers").get(() => value);
  74. }
  75. async function createRouterAndInit(providers = FAKE_PROVIDERS) {
  76. setMessageProviderPref(providers);
  77. channel = new FakeRemotePageManager();
  78. dispatchStub = sandbox.stub();
  79. // `.freeze` to catch any attempts at modifying the object
  80. Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
  81. await Router.init(channel, createFakeStorage(), dispatchStub);
  82. }
  83. beforeEach(async () => {
  84. globals = new GlobalOverrider();
  85. messageBlockList = [];
  86. providerBlockList = [];
  87. messageImpressions = {};
  88. providerImpressions = {};
  89. previousSessionEnd = 100;
  90. sandbox = sinon.createSandbox();
  91. sandbox.spy(ASRouterPreferences, "init");
  92. sandbox.spy(ASRouterPreferences, "uninit");
  93. sandbox.spy(ASRouterPreferences, "addListener");
  94. sandbox.spy(ASRouterPreferences, "removeListener");
  95. clock = sandbox.useFakeTimers();
  96. fetchStub = sandbox.stub(global, "fetch")
  97. .withArgs("http://fake.com/endpoint")
  98. .resolves({ok: true, status: 200, json: () => Promise.resolve({messages: FAKE_REMOTE_MESSAGES}), headers: FAKE_RESPONSE_HEADERS});
  99. getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
  100. fakeAttributionCode = {
  101. _clearCache: () => sinon.stub(),
  102. getAttrDataAsync: () => (Promise.resolve({content: "addonID"})),
  103. };
  104. FakeBookmarkPanelHub = {
  105. init: sandbox.stub(),
  106. uninit: sandbox.stub(),
  107. _forceShowMessage: sandbox.stub(),
  108. };
  109. globals.set({
  110. AttributionCode: fakeAttributionCode,
  111. // Testing framework doesn't know how to `defineLazyModuleGetter` so we're
  112. // importing these modules into the global scope ourselves.
  113. SnippetsTestMessageProvider,
  114. PanelTestProvider,
  115. BookmarkPanelHub: FakeBookmarkPanelHub,
  116. });
  117. await createRouterAndInit();
  118. });
  119. afterEach(() => {
  120. ASRouterPreferences.uninit();
  121. sandbox.restore();
  122. globals.restore();
  123. });
  124. describe(".state", () => {
  125. it("should throw if an attempt to set .state was made", () => {
  126. assert.throws(() => {
  127. Router.state = {};
  128. });
  129. });
  130. });
  131. describe("#init", () => {
  132. it("should add a message listener to the RemotePageManager for incoming messages", () => {
  133. assert.calledWith(channel.addMessageListener, CHILD_TO_PARENT_MESSAGE_NAME);
  134. const [, listenerAdded] = channel.addMessageListener.firstCall.args;
  135. assert.isFunction(listenerAdded);
  136. });
  137. it("should set state.messageBlockList to the block list in persistent storage", async () => {
  138. messageBlockList = ["foo"];
  139. Router = new _ASRouter();
  140. await Router.init(channel, createFakeStorage(), dispatchStub);
  141. assert.deepEqual(Router.state.messageBlockList, ["foo"]);
  142. });
  143. it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
  144. // Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
  145. // otherwise they will be cleaned up by .cleanupImpressions()
  146. const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
  147. messageImpressions = {foo: [0, 1, 2]};
  148. setMessageProviderPref([{id: "onboarding", type: "local", messages: [testMessage]}]);
  149. Router = new _ASRouter();
  150. await Router.init(channel, createFakeStorage(), dispatchStub);
  151. assert.deepEqual(Router.state.messageImpressions, messageImpressions);
  152. });
  153. it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
  154. Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
  155. const loadMessagesSpy = sandbox.spy(Router, "loadMessagesFromAllProviders");
  156. await Router.init(channel, createFakeStorage(), dispatchStub);
  157. assert.calledOnce(loadMessagesSpy);
  158. assert.isArray(Router.state.messages);
  159. assert.lengthOf(Router.state.messages, FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length);
  160. });
  161. it("should load additional whitelisted hosts", async () => {
  162. getStringPrefStub.returns("[\"whitelist.com\"]");
  163. await createRouterAndInit();
  164. assert.propertyVal(Router.WHITELIST_HOSTS, "whitelist.com", "preview");
  165. // Should still include the defaults
  166. assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 3);
  167. });
  168. it("should fallback to defaults if pref parsing fails", async () => {
  169. getStringPrefStub.returns("err");
  170. await createRouterAndInit();
  171. assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 2);
  172. assert.propertyVal(Router.WHITELIST_HOSTS, "snippets-admin.mozilla.org", "preview");
  173. assert.propertyVal(Router.WHITELIST_HOSTS, "activity-stream-icons.services.mozilla.com", "production");
  174. });
  175. it("should set this.dispatchToAS to the third parameter passed to .init()", async () => {
  176. assert.equal(Router.dispatchToAS, dispatchStub);
  177. });
  178. it("should set state.previousSessionEnd from IndexedDB", async () => {
  179. previousSessionEnd = 200;
  180. await createRouterAndInit();
  181. assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
  182. });
  183. it("should dispatch a AS_ROUTER_INITIALIZED event to AS with ASRouterPreferences.specialConditions", async () => {
  184. assert.calledWith(Router.dispatchToAS, ac.BroadcastToContent({type: "AS_ROUTER_INITIALIZED", data: ASRouterPreferences.specialConditions}));
  185. });
  186. describe("lazily loading local test providers", () => {
  187. afterEach(() => {
  188. Router.uninit();
  189. });
  190. it("should add the local test providers on init if devtools are enabled", async () => {
  191. sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
  192. await createRouterAndInit();
  193. assert.property(Router._localProviders, "SnippetsTestMessageProvider");
  194. assert.property(Router._localProviders, "PanelTestProvider");
  195. });
  196. it("should not add the local test providers on init if devtools are disabled", async () => {
  197. sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
  198. await createRouterAndInit();
  199. assert.notProperty(Router._localProviders, "SnippetsTestMessageProvider");
  200. assert.notProperty(Router._localProviders, "PanelTestProvider");
  201. });
  202. });
  203. });
  204. describe("preference changes", () => {
  205. it("should call ASRouterPreferences.init and add a listener on init", () => {
  206. assert.calledOnce(ASRouterPreferences.init);
  207. assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);
  208. });
  209. it("should call ASRouterPreferences.uninit and remove the listener on uninit", () => {
  210. Router.uninit();
  211. assert.calledOnce(ASRouterPreferences.uninit);
  212. assert.calledWith(ASRouterPreferences.removeListener, Router.onPrefChange);
  213. });
  214. it("should send a AS_ROUTER_TARGETING_UPDATE message", async () => {
  215. const messageTargeted = {id: "1", campaign: "foocampaign", targeting: "true"};
  216. const messageNotTargeted = {id: "2", campaign: "foocampaign"};
  217. await Router.setState({messages: [messageTargeted, messageNotTargeted]});
  218. sandbox.stub(ASRouterTargeting, "isMatch").resolves(false);
  219. await Router.onPrefChange("services.sync.username");
  220. assert.calledOnce(channel.sendAsyncMessage);
  221. const [, {type, data}] = channel.sendAsyncMessage.firstCall.args;
  222. assert.equal(type, "AS_ROUTER_TARGETING_UPDATE");
  223. assert.equal(data[0], messageTargeted.id);
  224. assert.lengthOf(data, 1);
  225. });
  226. it("should call loadMessagesFromAllProviders on pref change", () => {
  227. sandbox.spy(Router, "loadMessagesFromAllProviders");
  228. ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
  229. assert.calledOnce(Router.loadMessagesFromAllProviders);
  230. });
  231. it("should update the list of providers on pref change", () => {
  232. const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {url: "baz.com"});
  233. setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider, FAKE_REMOTE_SETTINGS_PROVIDER]);
  234. const {length} = Router.state.providers;
  235. ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
  236. const provider = Router.state.providers.find(p => p.url === "baz.com");
  237. assert.lengthOf(Router.state.providers, length);
  238. assert.isDefined(provider);
  239. });
  240. });
  241. describe("setState", () => {
  242. it("should broadcast a message to update the admin tool on a state change if the asrouter.devtoolsEnabled pref is", async () => {
  243. sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
  244. sandbox.stub(Router, "getTargetingParameters").resolves({});
  245. await Router.setState({foo: 123});
  246. assert.calledOnce(channel.sendAsyncMessage);
  247. assert.deepEqual(channel.sendAsyncMessage.firstCall.args[1], {
  248. type: "ADMIN_SET_STATE",
  249. data: Object.assign({}, Router.state, {providerPrefs: ASRouterPreferences.providers, userPrefs: ASRouterPreferences.getAllUserPreferences(), targetingParameters: {}, errors: Router.errors}),
  250. });
  251. });
  252. it("should not send a message on a state change asrouter.devtoolsEnabled pref is on", async () => {
  253. sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
  254. await Router.setState({foo: 123});
  255. assert.notCalled(channel.sendAsyncMessage);
  256. });
  257. });
  258. describe("getTargetingParameters", () => {
  259. it("should return the targeting parameters", async () => {
  260. const stub = sandbox.stub().resolves("foo");
  261. const obj = {foo: 1};
  262. sandbox.stub(obj, "foo").get(stub);
  263. const result = await Router.getTargetingParameters(obj, obj);
  264. assert.calledTwice(stub);
  265. assert.propertyVal(result, "foo", "foo");
  266. });
  267. });
  268. describe("evaluateExpression", () => {
  269. let stub;
  270. beforeEach(async () => {
  271. stub = sandbox.stub();
  272. stub.resolves("foo");
  273. sandbox.stub(ASRouterTargeting, "isMatch").callsFake(stub);
  274. });
  275. afterEach(() => {
  276. sandbox.restore();
  277. });
  278. it("should call ASRouterTargeting to evaluate", async () => {
  279. const targetStub = {sendAsyncMessage: sandbox.stub()};
  280. await Router.evaluateExpression(targetStub, {});
  281. assert.calledOnce(targetStub.sendAsyncMessage);
  282. assert.equal(targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus.result, "foo");
  283. assert.isTrue(targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus.success);
  284. });
  285. it("should catch evaluation errors", async () => {
  286. stub.returns(Promise.reject(new Error("fake error")));
  287. const targetStub = {sendAsyncMessage: sandbox.stub()};
  288. await Router.evaluateExpression(targetStub, {});
  289. assert.isFalse(targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus.success);
  290. });
  291. });
  292. describe("#loadMessagesFromAllProviders", () => {
  293. function assertRouterContainsMessages(messages) {
  294. const messageIdsInRouter = Router.state.messages.map(m => m.id);
  295. for (const message of messages) {
  296. assert.include(messageIdsInRouter, message.id);
  297. }
  298. }
  299. it("should not trigger an update if not enough time has passed for a provider", async () => {
  300. await createRouterAndInit([
  301. {id: "remotey", type: "remote", enabled: true, url: "http://fake.com/endpoint", updateCycleInMs: 300},
  302. ]);
  303. const previousState = Router.state;
  304. // Since we've previously gotten messages during init and we haven't advanced our fake timer,
  305. // no updates should be triggered.
  306. await Router.loadMessagesFromAllProviders();
  307. assert.equal(Router.state, previousState);
  308. });
  309. it("should not trigger an update if we only have local providers", async () => {
  310. await createRouterAndInit([
  311. {id: "foo", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES},
  312. ]);
  313. const previousState = Router.state;
  314. clock.tick(300);
  315. await Router.loadMessagesFromAllProviders();
  316. assert.equal(Router.state, previousState);
  317. });
  318. it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
  319. const NEW_MESSAGES = [{id: "new_123"}];
  320. await createRouterAndInit([
  321. {id: "remotey", type: "remote", url: "http://fake.com/endpoint", enabled: true, updateCycleInMs: 300},
  322. {id: "alocalprovider", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES},
  323. ]);
  324. fetchStub
  325. .withArgs("http://fake.com/endpoint")
  326. .resolves({ok: true, status: 200, json: () => Promise.resolve({messages: NEW_MESSAGES}), headers: FAKE_RESPONSE_HEADERS});
  327. clock.tick(301);
  328. await Router.loadMessagesFromAllProviders();
  329. // These are the new messages
  330. assertRouterContainsMessages(NEW_MESSAGES);
  331. // These are the local messages that should not have been deleted
  332. assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);
  333. });
  334. it("should parse the triggers in the messages and register the trigger listeners", async () => {
  335. sandbox.spy(ASRouterTriggerListeners.get("openURL"), "init");
  336. /* eslint-disable object-curly-newline */ /* eslint-disable object-property-newline */
  337. await createRouterAndInit([
  338. {id: "foo", type: "local", enabled: true, messages: [
  339. {id: "foo", template: "simple_template", trigger: {id: "firstRun"}, content: {title: "Foo", body: "Foo123"}},
  340. {id: "bar1", template: "simple_template", trigger: {id: "openURL", params: ["www.mozilla.org", "www.mozilla.com"]}, content: {title: "Bar1", body: "Bar123"}},
  341. {id: "bar2", template: "simple_template", trigger: {id: "openURL", params: ["www.example.com"]}, content: {title: "Bar2", body: "Bar123"}},
  342. ]},
  343. ]);
  344. /* eslint-enable object-curly-newline */ /* eslint-enable object-property-newline */
  345. assert.calledTwice(ASRouterTriggerListeners.get("openURL").init);
  346. assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
  347. Router._triggerHandler, ["www.mozilla.org", "www.mozilla.com"], undefined);
  348. assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
  349. Router._triggerHandler, ["www.example.com"], undefined);
  350. });
  351. it("should gracefully handle RemoteSettings blowing up and dispatch undesired event", async () => {
  352. sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").rejects("fake error");
  353. await createRouterAndInit();
  354. assert.calledWith(Router.dispatchToAS, {
  355. data: {action: "asrouter_undesired_event", event: "ASR_RS_ERROR", value: "remotey-settingsy", message_id: "n/a"},
  356. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  357. type: "AS_ROUTER_TELEMETRY_USER_EVENT",
  358. });
  359. });
  360. it("should dispatch undesired event if RemoteSettings returns no messages", async () => {
  361. sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([]);
  362. await createRouterAndInit();
  363. assert.calledWith(Router.dispatchToAS, {
  364. data: {action: "asrouter_undesired_event", event: "ASR_RS_NO_MESSAGES", value: "remotey-settingsy", message_id: "n/a"},
  365. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  366. type: "AS_ROUTER_TELEMETRY_USER_EVENT",
  367. });
  368. });
  369. });
  370. describe("#_updateMessageProviders", () => {
  371. it("should correctly replace %STARTPAGE_VERSION% in remote provider urls", () => {
  372. // If this test fails, you need to update the constant STARTPAGE_VERSION in
  373. // ASRouter.jsm to match the `version` property of provider-response-schema.json
  374. const expectedStartpageVersion = ProviderResponseSchema.version;
  375. const provider = {id: "foo", enabled: true, type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
  376. setMessageProviderPref([provider]);
  377. Router._updateMessageProviders();
  378. assert.equal(Router.state.providers[0].url, `https://www.mozilla.org/${parseInt(expectedStartpageVersion, 10)}/`);
  379. });
  380. it("should replace other params in remote provider urls by calling Services.urlFormater.formatURL", () => {
  381. const url = "https://www.example.com/";
  382. const replacedUrl = "https://www.foo.bar/";
  383. const stub = sandbox.stub(global.Services.urlFormatter, "formatURL")
  384. .withArgs(url)
  385. .returns(replacedUrl);
  386. const provider = {id: "foo", enabled: true, type: "remote", url};
  387. setMessageProviderPref([provider]);
  388. Router._updateMessageProviders();
  389. assert.calledOnce(stub);
  390. assert.calledWithExactly(stub, url);
  391. assert.equal(Router.state.providers[0].url, replacedUrl);
  392. });
  393. it("should only add the providers that are enabled", () => {
  394. const providers = [
  395. {id: "foo", enabled: false, type: "remote", url: "https://www.foo.com/"},
  396. {id: "bar", enabled: true, type: "remote", url: "https://www.bar.com/"},
  397. ];
  398. setMessageProviderPref(providers);
  399. Router._updateMessageProviders();
  400. assert.equal(Router.state.providers.length, 1);
  401. assert.equal(Router.state.providers[0].id, providers[1].id);
  402. });
  403. it("should return provider `foo` because both categories are enabled", () => {
  404. const providers = [
  405. {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
  406. ];
  407. sandbox.stub(ASRouterPreferences, "providers").value(providers);
  408. sandbox.stub(ASRouterPreferences, "getUserPreference")
  409. .withArgs("cfrFeatures").returns(true)
  410. .withArgs("cfrAddons")
  411. .returns(true);
  412. Router._updateMessageProviders();
  413. assert.equal(Router.state.providers.length, 1);
  414. assert.equal(Router.state.providers[0].id, providers[0].id);
  415. });
  416. it("should return provider `foo` because at least 1 category is enabled", () => {
  417. const providers = [
  418. {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
  419. ];
  420. sandbox.stub(ASRouterPreferences, "providers").value(providers);
  421. sandbox.stub(ASRouterPreferences, "getUserPreference")
  422. .withArgs("cfrFeatures").returns(false)
  423. .withArgs("cfrAddons")
  424. .returns(true);
  425. Router._updateMessageProviders();
  426. assert.equal(Router.state.providers.length, 1);
  427. assert.equal(Router.state.providers[0].id, providers[0].id);
  428. });
  429. it("should not return provider `foo` because no categories are enabled", () => {
  430. const providers = [
  431. {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
  432. ];
  433. sandbox.stub(ASRouterPreferences, "providers").value(providers);
  434. sandbox.stub(ASRouterPreferences, "getUserPreference")
  435. .withArgs("cfrFeatures").returns(false)
  436. .withArgs("cfrAddons")
  437. .returns(false);
  438. Router._updateMessageProviders();
  439. assert.equal(Router.state.providers.length, 0);
  440. });
  441. });
  442. describe("#handleMessageRequest", () => {
  443. it("should get unblocked messages that match the trigger", async () => {
  444. const message = {id: "1", campaign: "foocampaign", trigger: {id: "foo"}};
  445. await Router.setState({messages: [message]});
  446. // Just return the first message provided as arg
  447. sandbox.stub(Router, "_findMessage").callsFake(messages => messages[0]);
  448. const result = Router.handleMessageRequest({id: "foo"});
  449. assert.deepEqual(result, message);
  450. });
  451. });
  452. describe("blocking", () => {
  453. it("should not return a blocked message", async () => {
  454. // Block all messages except the first
  455. await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
  456. const targetStub = {sendAsyncMessage: sandbox.stub()};
  457. await Router.sendNextMessage(targetStub);
  458. assert.calledOnce(targetStub.sendAsyncMessage);
  459. assert.equal(Router.state.lastMessageId, ALL_MESSAGE_IDS[0]);
  460. });
  461. it("should not return a message from a blocked campaign", async () => {
  462. // Block all messages except the first
  463. await Router.setState(() => ({
  464. messages: [{id: "foo", campaign: "foocampaign"}, {id: "bar"}],
  465. messageBlockList: ["foocampaign"],
  466. }));
  467. const targetStub = {sendAsyncMessage: sandbox.stub()};
  468. await Router.sendNextMessage(targetStub);
  469. assert.calledOnce(targetStub.sendAsyncMessage);
  470. assert.equal(Router.state.lastMessageId, "bar");
  471. });
  472. it("should not return a message from a blocked provider", async () => {
  473. // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
  474. // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
  475. await Router.setState(() => ({providerBlockList: [FAKE_LOCAL_PROVIDER.id]}));
  476. const targetStub = {sendAsyncMessage: sandbox.stub()};
  477. await Router.sendNextMessage(targetStub);
  478. assert.calledOnce(targetStub.sendAsyncMessage);
  479. assert.equal(Router.state.lastMessageId, FAKE_REMOTE_MESSAGES[0].id);
  480. });
  481. it("should not return a message if all messages are blocked", async () => {
  482. await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS}));
  483. const targetStub = {sendAsyncMessage: sandbox.stub()};
  484. await Router.sendNextMessage(targetStub);
  485. assert.calledOnce(targetStub.sendAsyncMessage);
  486. assert.equal(Router.state.lastMessageId, null);
  487. });
  488. });
  489. describe("#uninit", () => {
  490. it("should remove the message listener on the RemotePageManager", () => {
  491. const [, listenerAdded] = channel.addMessageListener.firstCall.args;
  492. assert.isFunction(listenerAdded);
  493. Router.uninit();
  494. assert.calledWith(channel.removeMessageListener, CHILD_TO_PARENT_MESSAGE_NAME, listenerAdded);
  495. });
  496. it("should unregister the trigger listeners", () => {
  497. for (const listener of ASRouterTriggerListeners.values()) {
  498. sandbox.spy(listener, "uninit");
  499. }
  500. Router.uninit();
  501. for (const listener of ASRouterTriggerListeners.values()) {
  502. assert.calledOnce(listener.uninit);
  503. }
  504. });
  505. it("should set .dispatchToAS to null", () => {
  506. Router.uninit();
  507. assert.isNull(Router.dispatchToAS);
  508. });
  509. it("should save previousSessionEnd", () => {
  510. Router.uninit();
  511. assert.calledOnce(Router._storage.set);
  512. assert.calledWithExactly(Router._storage.set, "previousSessionEnd", sinon.match.number);
  513. });
  514. });
  515. describe("onMessage", () => {
  516. describe("#onMessage: SNIPPETS_REQUEST", () => {
  517. it("should set state.lastMessageId to a message id", async () => {
  518. await Router.onMessage(fakeAsyncMessage({type: "SNIPPETS_REQUEST"}));
  519. assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
  520. });
  521. it("should send a message back to the to the target", async () => {
  522. // force the only message to be a regular message so getRandomItemFromArray picks it
  523. await Router.setState({messages: [{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}]});
  524. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  525. await Router.onMessage(msg);
  526. const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
  527. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: currentMessage});
  528. });
  529. it("should send a message back to the to the target if there is a bundle, too", async () => {
  530. // force the only message to be a bundled message so getRandomItemFromArray picks it
  531. sandbox.stub(Router, "_findProvider").returns(null);
  532. await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
  533. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  534. await Router.onMessage(msg);
  535. const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
  536. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
  537. assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
  538. assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
  539. });
  540. it("should properly order the message's bundle if specified", async () => {
  541. // force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
  542. sandbox.stub(Router, "_findProvider").returns(null);
  543. const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
  544. const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
  545. await Router.setState({messages: [secondMessage, firstMessage]});
  546. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  547. await Router.onMessage(msg);
  548. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
  549. assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
  550. assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
  551. assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
  552. });
  553. it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
  554. // force the only message to be a bundled message that needs 2 messages in the bundle
  555. await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
  556. const bundle = await Router._getBundledMessages(Router.state.messages[0]);
  557. assert.equal(bundle, null);
  558. });
  559. it("should send down extra attributes in the bundle if they exist", async () => {
  560. sandbox.stub(Router, "_findProvider").returns({getExtraAttributes() { return Promise.resolve({header: "header"}); }});
  561. await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
  562. const result = await Router._getBundledMessages(Router.state.messages[0]);
  563. assert.equal(result.extraTemplateStrings.header, "header");
  564. });
  565. it("should send a CLEAR_ALL message if no bundle available", async () => {
  566. // force the only message to be a bundled message that needs 2 messages in the bundle
  567. await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
  568. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  569. await Router.onMessage(msg);
  570. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
  571. });
  572. it("should send a CLEAR_ALL message if no messages are available", async () => {
  573. await Router.setState({messages: []});
  574. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  575. await Router.onMessage(msg);
  576. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
  577. });
  578. it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
  579. const url = "https://snippets-admin.mozilla.org/foo";
  580. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
  581. await Router.onMessage(msg);
  582. assert.calledWith(global.fetch, url);
  583. assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
  584. });
  585. it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
  586. const url = "https://snippets-admin.mozilla.org/foo";
  587. const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint: {url}}});
  588. await Router.onMessage(msg);
  589. assert.calledWith(global.fetch, url);
  590. assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
  591. });
  592. it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
  593. const url = "https://snippets-admin.mozilla.org/foo";
  594. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
  595. await Router.onMessage(msg);
  596. assert.calledWithExactly(Router.dispatchToAS, ac.OnlyToOneContent({type: "SNIPPETS_PREVIEW_MODE"}, msg.target.portID));
  597. });
  598. it("should not add a url that is not from a whitelisted host", async () => {
  599. const url = "https://mozilla.org";
  600. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
  601. await Router.onMessage(msg);
  602. assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
  603. });
  604. it("should reject bad urls", async () => {
  605. const url = "foo";
  606. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
  607. await Router.onMessage(msg);
  608. assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
  609. });
  610. });
  611. describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
  612. it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
  613. await Router.setState({lastMessageId: "foo"});
  614. const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
  615. await Router.onMessage(msg);
  616. assert.isTrue(Router.state.messageBlockList.includes("foo"));
  617. assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
  618. });
  619. it("should add the campaign to the messageBlockList instead of id if .campaign is specified and not select messages of that campaign again", async () => {
  620. await Router.setState({
  621. messages: [
  622. {id: "1", campaign: "foocampaign"},
  623. {id: "2", campaign: "foocampaign"},
  624. ],
  625. });
  626. const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "1"}});
  627. await Router.onMessage(msg);
  628. assert.isTrue(Router.state.messageBlockList.includes("foocampaign"));
  629. assert.isEmpty(Router._getUnblockedMessages());
  630. });
  631. it("should not broadcast CLEAR_MESSAGE if preventDismiss is true", async () => {
  632. const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo", preventDismiss: true}});
  633. await Router.onMessage(msg);
  634. assert.notCalled(channel.sendAsyncMessage);
  635. });
  636. });
  637. describe("#onMessage: DISMISS_MESSAGE_BY_ID", () => {
  638. it("should reply with CLEAR_MESSAGE with the correct id", async () => {
  639. const msg = fakeAsyncMessage({type: "DISMISS_MESSAGE_BY_ID", data: {id: "foo"}});
  640. await Router.onMessage(msg);
  641. assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
  642. });
  643. });
  644. describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
  645. it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
  646. const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
  647. await Router.onMessage(msg);
  648. assert.isTrue(Router.state.providerBlockList.includes("bar"));
  649. assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
  650. });
  651. });
  652. describe("#onMessage: DISMISS_BUNDLE", () => {
  653. it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
  654. await Router.setState({lastMessageId: "foo"});
  655. const msg = fakeAsyncMessage({type: "DISMISS_BUNDLE", data: {bundle: FAKE_BUNDLE}});
  656. await Router.onMessage(msg);
  657. assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
  658. });
  659. });
  660. describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
  661. it("should remove the id from the messageBlockList", async () => {
  662. await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
  663. assert.isTrue(Router.state.messageBlockList.includes("foo"));
  664. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
  665. assert.isFalse(Router.state.messageBlockList.includes("foo"));
  666. });
  667. it("should remove the campaign from the messageBlockList if it is defined", async () => {
  668. await Router.setState({messages: [{id: "1", campaign: "foo"}]});
  669. await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "1"}}));
  670. assert.isTrue(Router.state.messageBlockList.includes("foo"), "blocklist has campaign id");
  671. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "1"}}));
  672. assert.isFalse(Router.state.messageBlockList.includes("foo"), "campaign id removed from blocklist");
  673. });
  674. it("should save the messageBlockList", async () => {
  675. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
  676. assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
  677. });
  678. });
  679. describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
  680. it("should remove the id from the providerBlockList", async () => {
  681. await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
  682. assert.isTrue(Router.state.providerBlockList.includes("foo"));
  683. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
  684. assert.isFalse(Router.state.providerBlockList.includes("foo"));
  685. });
  686. it("should save the providerBlockList", async () => {
  687. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
  688. assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
  689. });
  690. });
  691. describe("#onMessage: UNBLOCK_BUNDLE", () => {
  692. it("should remove all the ids in the bundle from the messageBlockList", async () => {
  693. await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
  694. assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
  695. assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
  696. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
  697. assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
  698. assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
  699. });
  700. it("should save the messageBlockList", async () => {
  701. await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
  702. assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
  703. });
  704. });
  705. describe("#onMessage: ADMIN_CONNECT_STATE", () => {
  706. it("should send a message containing the whole state", async () => {
  707. sandbox.stub(Router, "getTargetingParameters").resolves({});
  708. const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE"});
  709. await Router.onMessage(msg);
  710. assert.calledOnce(msg.target.sendAsyncMessage);
  711. assert.deepEqual(msg.target.sendAsyncMessage.firstCall.args[1], {
  712. type: "ADMIN_SET_STATE",
  713. data: Object.assign({}, Router.state, {providerPrefs: ASRouterPreferences.providers, userPrefs: ASRouterPreferences.getAllUserPreferences(), targetingParameters: {}, errors: Router.errors}),
  714. });
  715. });
  716. });
  717. describe("#onMessage: SNIPPETS_REQUEST", () => {
  718. it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
  719. sandbox.stub(Router, "sendNextMessage").resolves();
  720. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  721. await Router.onMessage(msg);
  722. assert.calledOnce(Router.sendNextMessage);
  723. assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
  724. });
  725. it("should return the preview message if that's available and remove it from Router.state", async () => {
  726. const expectedObj = {provider: "preview"};
  727. Router.setState({messages: [expectedObj]});
  728. await Router.sendNextMessage(channel);
  729. assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: expectedObj});
  730. assert.isUndefined(Router.state.messages.find(m => m.provider === "preview"));
  731. });
  732. it("should call _getBundledMessages if we request a message that needs to be bundled", async () => {
  733. sandbox.stub(Router, "_getBundledMessages").resolves();
  734. // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
  735. const [, testMessage] = Router.state.messages;
  736. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
  737. await Router.onMessage(msg);
  738. assert.calledOnce(Router._getBundledMessages);
  739. });
  740. it("should properly pick another message of the same template if it is bundled; force = true", async () => {
  741. // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
  742. const [, testMessage1, testMessage2] = Router.state.messages;
  743. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
  744. await Router.onMessage(msg);
  745. // Expected object should have some properties of the original message it picked (testMessage1)
  746. // plus the bundled content of the others that it picked of the same template (testMessage2)
  747. const expectedObj = {
  748. template: testMessage1.template,
  749. provider: testMessage1.provider,
  750. bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}],
  751. };
  752. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
  753. });
  754. it("should properly pick another message of the same template if it is bundled; force = false", async () => {
  755. // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
  756. const [, testMessage1, testMessage2] = Router.state.messages;
  757. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
  758. await Router.setMessageById(testMessage1.id, msg.target, false);
  759. // Expected object should have some properties of the original message it picked (testMessage1)
  760. // plus the bundled content of the others that it picked of the same template (testMessage2)
  761. const expectedObj = {
  762. template: testMessage1.template,
  763. provider: testMessage1.provider,
  764. bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}],
  765. };
  766. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
  767. });
  768. it("should get the bundle and send the message if the message has a bundle", async () => {
  769. sandbox.stub(Router, "sendNextMessage").resolves();
  770. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  771. msg.bundled = 2; // force this message to want to be bundled
  772. await Router.onMessage(msg);
  773. assert.calledOnce(Router.sendNextMessage);
  774. });
  775. });
  776. describe("#onMessage: TRIGGER", () => {
  777. it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
  778. sandbox.stub(Router, "_findMessage").resolves();
  779. const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
  780. await Router.onMessage(msg);
  781. assert.calledOnce(Router._findMessage);
  782. assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
  783. });
  784. it("consider the trigger when picking a message", async () => {
  785. const messages = [
  786. {id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
  787. ];
  788. const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
  789. const message = await Router._findMessage(messages, data.data.trigger);
  790. assert.equal(message, messages[0]);
  791. });
  792. it("should pick a message with the right targeting and trigger", async () => {
  793. let messages = [
  794. {id: "foo1", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
  795. {id: "foo2", template: "simple_template", bundled: 2, trigger: {id: "bar"}, content: {title: "Foo2", body: "Foo123-2"}},
  796. {id: "foo3", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}},
  797. ];
  798. sandbox.stub(Router, "_findProvider").returns(null);
  799. await Router.setState({messages});
  800. const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
  801. let {bundle} = await Router._getBundledMessages(messages[0], target, {id: "foo"});
  802. assert.equal(bundle.length, 2);
  803. // it should have picked foo1 and foo3 only
  804. assert.isTrue(bundle.every(elem => elem.id === "foo1" || elem.id === "foo3"));
  805. });
  806. it("should have previousSessionEnd in the message context", () => {
  807. assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
  808. });
  809. });
  810. describe(".includeBundle", () => {
  811. it("should send a message with .includeBundle property with specified length and template", async () => {
  812. let messages = [
  813. {id: "trailhead", template: "trailhead", includeBundle: {length: 2, template: "foo", trigger: {id: "foo"}}, trigger: {id: "firstRun"}, content: {}},
  814. {id: "foo2", template: "foo", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo2", body: "Foo123-2"}},
  815. {id: "foo3", template: "foo", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}},
  816. ];
  817. sandbox.stub(Router, "_findProvider").returns(null);
  818. await Router.setState({messages});
  819. const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
  820. await Router.onMessage(msg);
  821. const [, resp] = msg.target.sendAsyncMessage.firstCall.args;
  822. assert.propertyVal(resp, "type", "SET_MESSAGE");
  823. assert.isArray(resp.data.bundle, "resp.data.bundle");
  824. assert.lengthOf(resp.data.bundle, 2, "resp.data.bundle");
  825. });
  826. });
  827. describe("#onMessage: OVERRIDE_MESSAGE", () => {
  828. it("should broadcast a SET_MESSAGE message to all clients with a particular id", async () => {
  829. const [testMessage] = Router.state.messages;
  830. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
  831. await Router.onMessage(msg);
  832. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
  833. });
  834. it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
  835. sandbox.stub(CFRPageActions, "forceRecommendation");
  836. const testMessage = {id: "foo", template: "cfr_doorhanger"};
  837. await Router.setState({messages: [testMessage]});
  838. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
  839. await Router.onMessage(msg);
  840. assert.notCalled(msg.target.sendAsyncMessage);
  841. assert.calledOnce(CFRPageActions.forceRecommendation);
  842. });
  843. it("should call BookmarkPanelHub._forceShowMessage the provider is cfr-fxa", async () => {
  844. const testMessage = {id: "foo", template: "fxa_bookmark_panel"};
  845. await Router.setState({messages: [testMessage]});
  846. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
  847. await Router.onMessage(msg);
  848. assert.notCalled(msg.target.sendAsyncMessage);
  849. assert.calledOnce(FakeBookmarkPanelHub._forceShowMessage);
  850. });
  851. it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
  852. sandbox.stub(CFRPageActions, "addRecommendation");
  853. const testMessage = {id: "foo", template: "cfr_doorhanger"};
  854. await Router.setState({messages: [testMessage]});
  855. await Router._sendMessageToTarget(testMessage, {}, {param: {}}, false);
  856. assert.calledOnce(CFRPageActions.addRecommendation);
  857. });
  858. it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
  859. const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
  860. await Router.onMessage(msg);
  861. assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
  862. });
  863. });
  864. describe("#onMessage: Onboarding actions", () => {
  865. it("should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW", async () => {
  866. let [testMessage] = Router.state.messages;
  867. const msg = fakeExecuteUserAction({type: "OPEN_PRIVATE_BROWSER_WINDOW", data: testMessage});
  868. await Router.onMessage(msg);
  869. assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
  870. });
  871. it("should call openLinkIn with the correct params on OPEN_URL", async () => {
  872. let [testMessage] = Router.state.messages;
  873. testMessage.button_action = {type: "OPEN_URL", data: {args: "some/url.com", where: "tabshifted"}};
  874. const msg = fakeExecuteUserAction(testMessage.button_action);
  875. await Router.onMessage(msg);
  876. assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
  877. assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
  878. "some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined, "csp": null});
  879. });
  880. it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
  881. let [testMessage] = Router.state.messages;
  882. testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {args: "something"}};
  883. const msg = fakeExecuteUserAction(testMessage.button_action);
  884. await Router.onMessage(msg);
  885. assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
  886. assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
  887. });
  888. });
  889. describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
  890. beforeEach(() => {
  891. globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
  892. });
  893. it("should call openLinkIn with the correct params on OPEN_URL", async () => {
  894. let [testMessage] = Router.state.messages;
  895. testMessage.button_action = {type: "SHOW_FIREFOX_ACCOUNTS"};
  896. const msg = fakeExecuteUserAction(testMessage.button_action);
  897. await Router.onMessage(msg);
  898. assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
  899. assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
  900. "some/url", "current", {"private": false, "triggeringPrincipal": undefined, "csp": null});
  901. });
  902. });
  903. describe("#onMessage: OPEN_PREFERENCES_PAGE", () => {
  904. it("should call openPreferences with the correct params on OPEN_PREFERENCES_PAGE", async () => {
  905. let [testMessage] = Router.state.messages;
  906. testMessage.button_action = {type: "OPEN_PREFERENCES_PAGE", data: {category: "something"}};
  907. const msg = fakeExecuteUserAction(testMessage.button_action);
  908. await Router.onMessage(msg);
  909. assert.calledOnce(msg.target.browser.ownerGlobal.openPreferences);
  910. assert.calledWith(msg.target.browser.ownerGlobal.openPreferences, "something");
  911. });
  912. });
  913. describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
  914. it("should call installAddonFromURL with correct arguments", async () => {
  915. sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
  916. const msg = fakeExecuteUserAction({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
  917. await Router.onMessage(msg);
  918. assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
  919. assert.calledWithExactly(MessageLoaderUtils.installAddonFromURL, msg.target.browser, "foo.com");
  920. });
  921. it("should add/remove observers for `webextension-install-notify`", async () => {
  922. sandbox.spy(global.Services.obs, "addObserver");
  923. sandbox.spy(global.Services.obs, "removeObserver");
  924. sandbox.spy(Router, "blockMessageById");
  925. sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
  926. const msg = fakeExecuteUserAction({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
  927. await Router.onMessage(msg);
  928. assert.calledOnce(global.Services.obs.addObserver);
  929. const [cb] = global.Services.obs.addObserver.firstCall.args;
  930. cb();
  931. assert.calledOnce(global.Services.obs.removeObserver);
  932. assert.calledOnce(channel.sendAsyncMessage);
  933. assert.calledOnce(Router.blockMessageById);
  934. assert.calledWithExactly(Router.blockMessageById, "RETURN_TO_AMO_1");
  935. });
  936. });
  937. describe("#onMessage: PIN_CURRENT_TAB", () => {
  938. it("should call pin tab with the selectedTab", async () => {
  939. const msg = fakeExecuteUserAction({type: "PIN_CURRENT_TAB"});
  940. const {gBrowser, ConfirmationHint} = msg.target.browser.ownerGlobal;
  941. await Router.onMessage(msg);
  942. assert.calledOnce(gBrowser.pinTab);
  943. assert.calledWithExactly(gBrowser.pinTab, gBrowser.selectedTab);
  944. assert.calledOnce(ConfirmationHint.show);
  945. assert.calledWithExactly(ConfirmationHint.show, gBrowser.selectedTab, "pinTab", {showDescription: true});
  946. });
  947. });
  948. describe("#dispatch(action, target)", () => {
  949. it("should an action and target to onMessage", async () => {
  950. // use the IMPRESSION action to make sure actions are actually getting processed
  951. sandbox.stub(Router, "addImpression");
  952. sandbox.spy(Router, "onMessage");
  953. const target = {};
  954. const action = {type: "IMPRESSION"};
  955. Router.dispatch(action, target);
  956. assert.calledWith(Router.onMessage, {data: action, target});
  957. assert.calledOnce(Router.addImpression);
  958. });
  959. });
  960. describe("#onMessage: DOORHANGER_TELEMETRY", () => {
  961. it("should dispatch an AS_ROUTER_TELEMETRY_USER_EVENT on DOORHANGER_TELEMETRY message", async () => {
  962. const msg = fakeAsyncMessage({type: "DOORHANGER_TELEMETRY", data: {message_id: "foo"}});
  963. dispatchStub.reset();
  964. await Router.onMessage(msg);
  965. assert.calledOnce(dispatchStub);
  966. const [action] = dispatchStub.firstCall.args;
  967. assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
  968. assert.equal(action.data.message_id, "foo");
  969. });
  970. });
  971. describe("#onMessage: EXPIRE_QUERY_CACHE", () => {
  972. it("should clear all QueryCache getters", async () => {
  973. const msg = fakeAsyncMessage({type: "EXPIRE_QUERY_CACHE"});
  974. sandbox.stub(QueryCache, "expireAll");
  975. await Router.onMessage(msg);
  976. assert.calledOnce(QueryCache.expireAll);
  977. });
  978. });
  979. describe("#onMessage: ENABLE_PROVIDER", () => {
  980. it("should enable the provider via ASRouterPreferences", async () => {
  981. const msg = fakeAsyncMessage({type: "ENABLE_PROVIDER", data: "foo"});
  982. sandbox.stub(ASRouterPreferences, "enableOrDisableProvider");
  983. await Router.onMessage(msg);
  984. assert.calledWith(ASRouterPreferences.enableOrDisableProvider, "foo", true);
  985. });
  986. });
  987. describe("#onMessage: DISABLE_PROVIDER", () => {
  988. it("should disable the provider via ASRouterPreferences", async () => {
  989. const msg = fakeAsyncMessage({type: "DISABLE_PROVIDER", data: "foo"});
  990. sandbox.stub(ASRouterPreferences, "enableOrDisableProvider");
  991. await Router.onMessage(msg);
  992. assert.calledWith(ASRouterPreferences.enableOrDisableProvider, "foo", false);
  993. });
  994. });
  995. describe("#onMessage: RESET_PROVIDER_PREF", () => {
  996. it("should reset provider pref via ASRouterPreferences", async () => {
  997. const msg = fakeAsyncMessage({type: "RESET_PROVIDER_PREF", data: "foo"});
  998. sandbox.stub(ASRouterPreferences, "resetProviderPref");
  999. await Router.onMessage(msg);
  1000. assert.calledOnce(ASRouterPreferences.resetProviderPref);
  1001. });
  1002. });
  1003. describe("#onMessage: SET_PROVIDER_USER_PREF", () => {
  1004. it("should set provider user pref via ASRouterPreferences", async () => {
  1005. const msg = fakeAsyncMessage({type: "SET_PROVIDER_USER_PREF", data: {id: "foo", value: true}});
  1006. sandbox.stub(ASRouterPreferences, "setUserPreference");
  1007. await Router.onMessage(msg);
  1008. assert.calledWith(ASRouterPreferences.setUserPreference, "foo", true);
  1009. });
  1010. });
  1011. describe("#onMessage: EVALUATE_JEXL_EXPRESSION", () => {
  1012. it("should call evaluateExpression", async () => {
  1013. const msg = fakeAsyncMessage({type: "EVALUATE_JEXL_EXPRESSION", data: {foo: true}});
  1014. sandbox.stub(Router, "evaluateExpression");
  1015. await Router.onMessage(msg);
  1016. assert.calledOnce(Router.evaluateExpression);
  1017. assert.calledWithExactly(Router.evaluateExpression, msg.target, msg.data.data);
  1018. });
  1019. });
  1020. describe("#onMessage: FORCE_ATTRIBUTION", () => {
  1021. beforeEach(() => {
  1022. global.Cc["@mozilla.org/mac-attribution;1"] = {
  1023. getService: () => ({setReferrerUrl: sinon.spy()}),
  1024. };
  1025. global.Cc["@mozilla.org/process/environment;1"] = {
  1026. getService: () => ({set: sandbox.stub()}),
  1027. };
  1028. });
  1029. afterEach(() => {
  1030. globals.restore();
  1031. });
  1032. it("should call forceAttribution", async () => {
  1033. const msg = fakeAsyncMessage({type: "FORCE_ATTRIBUTION", data: {foo: true}});
  1034. sandbox.stub(Router, "forceAttribution");
  1035. await Router.onMessage(msg);
  1036. assert.calledOnce(Router.forceAttribution);
  1037. assert.calledWithExactly(Router.forceAttribution, msg.data.data);
  1038. });
  1039. it("should force attribution and update providers", async () => {
  1040. sandbox.stub(Router, "_updateMessageProviders");
  1041. sandbox.stub(Router, "loadMessagesFromAllProviders");
  1042. sandbox.stub(fakeAttributionCode, "_clearCache");
  1043. sandbox.stub(fakeAttributionCode, "getAttrDataAsync");
  1044. const msg = fakeAsyncMessage({type: "FORCE_ATTRIBUTION", data: {foo: true}});
  1045. await Router.onMessage(msg);
  1046. assert.calledOnce(fakeAttributionCode._clearCache);
  1047. assert.calledOnce(fakeAttributionCode.getAttrDataAsync);
  1048. assert.calledOnce(Router._updateMessageProviders);
  1049. assert.calledOnce(Router.loadMessagesFromAllProviders);
  1050. });
  1051. });
  1052. describe("#onMessage: default", () => {
  1053. it("should report unknown messages", () => {
  1054. const msg = fakeAsyncMessage({type: "FOO"});
  1055. sandbox.stub(Cu, "reportError");
  1056. Router.onMessage(msg);
  1057. assert.calledOnce(Cu.reportError);
  1058. });
  1059. });
  1060. });
  1061. describe("_triggerHandler", () => {
  1062. it("should call #onMessage with the correct trigger", () => {
  1063. sinon.spy(Router, "onMessage");
  1064. const target = {};
  1065. const trigger = {id: "FAKE_TRIGGER", param: "some fake param"};
  1066. Router._triggerHandler(target, trigger);
  1067. assert.calledOnce(Router.onMessage);
  1068. assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", data: {trigger}}});
  1069. });
  1070. });
  1071. describe("#UITour", () => {
  1072. let showMenuStub;
  1073. beforeEach(() => {
  1074. showMenuStub = sandbox.stub();
  1075. globals.set("UITour", {showMenu: showMenuStub});
  1076. });
  1077. it("should call UITour.showMenu with the correct params on OPEN_APPLICATIONS_MENU", async () => {
  1078. const msg = fakeExecuteUserAction({type: "OPEN_APPLICATIONS_MENU", data: {args: "appMenu"}});
  1079. await Router.onMessage(msg);
  1080. assert.calledOnce(showMenuStub);
  1081. assert.calledWith(showMenuStub, msg.target.browser.ownerGlobal, "appMenu");
  1082. });
  1083. });
  1084. describe("valid preview endpoint", () => {
  1085. it("should report an error if url protocol is not https", () => {
  1086. sandbox.stub(Cu, "reportError");
  1087. assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
  1088. assert.calledTwice(Cu.reportError);
  1089. });
  1090. });
  1091. describe("impressions", () => {
  1092. async function addProviderWithFrequency(id, frequency) {
  1093. await Router.setState(state => {
  1094. const newProvider = {id, frequency};
  1095. const providers = [...state.providers, newProvider];
  1096. return {providers};
  1097. });
  1098. }
  1099. describe("frequency normalisation", () => {
  1100. beforeEach(async () => {
  1101. const messages = [{frequency: {custom: [{period: "daily", cap: 10}]}}];
  1102. const provider = {id: "foo", frequency: {custom: [{period: "daily", cap: 100}]}, messages, enabled: true};
  1103. await createRouterAndInit([provider]);
  1104. });
  1105. it("period aliases in provider frequency caps should be normalised", () => {
  1106. const [provider] = Router.state.providers;
  1107. assert.equal(provider.frequency.custom[0].period, ONE_DAY_IN_MS);
  1108. });
  1109. it("period aliases in message frequency caps should be normalised", async () => {
  1110. const [message] = Router.state.messages;
  1111. assert.equal(message.frequency.custom[0].period, ONE_DAY_IN_MS);
  1112. });
  1113. });
  1114. describe("#addImpression", () => {
  1115. it("should add a message impression and update _storage with the current time if the message has frequency caps", async () => {
  1116. clock.tick(42);
  1117. const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", provider: FAKE_LOCAL_PROVIDER.id, frequency: {lifetime: 5}}});
  1118. await Router.onMessage(msg);
  1119. assert.isArray(Router.state.messageImpressions.foo);
  1120. assert.deepEqual(Router.state.messageImpressions.foo, [42]);
  1121. assert.calledWith(Router._storage.set, "messageImpressions", {foo: [42]});
  1122. });
  1123. it("should not add a message impression if the message doesn't have frequency caps", async () => {
  1124. // Note that storage.set is called during initialization, so it needs to be reset
  1125. Router._storage.set.reset();
  1126. clock.tick(42);
  1127. const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
  1128. await Router.onMessage(msg);
  1129. assert.notProperty(Router.state.messageImpressions, "foo");
  1130. assert.notCalled(Router._storage.set);
  1131. });
  1132. it("should add a provider impression and update _storage with the current time if the message's provider has frequency caps", async () => {
  1133. clock.tick(42);
  1134. await addProviderWithFrequency("foo", {lifetime: 5});
  1135. const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
  1136. await Router.onMessage(msg);
  1137. assert.isArray(Router.state.providerImpressions.foo);
  1138. assert.deepEqual(Router.state.providerImpressions.foo, [42]);
  1139. assert.calledWith(Router._storage.set, "providerImpressions", {foo: [42]});
  1140. });
  1141. it("should not add a provider impression if the message's provider doesn't have frequency caps", async () => {
  1142. // Note that storage.set is called during initialization, so it needs to be reset
  1143. Router._storage.set.reset();
  1144. clock.tick(42);
  1145. // Add "foo" provider with no frequency
  1146. await addProviderWithFrequency("foo", null);
  1147. const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
  1148. await Router.onMessage(msg);
  1149. assert.notProperty(Router.state.providerImpressions, "foo");
  1150. assert.notCalled(Router._storage.set);
  1151. });
  1152. it("should only send impressions for one message", async () => {
  1153. const getElementById = sandbox.stub().returns({
  1154. setAttribute: sandbox.stub(),
  1155. style: {setProperty: sandbox.stub()},
  1156. addEventListener: sandbox.stub(),
  1157. });
  1158. const data = {param: {host: "mozilla.com", url: "https://mozilla.com"}};
  1159. const target = {
  1160. sendAsyncMessage: sandbox.stub(),
  1161. documentURI: {scheme: "https", host: "mozilla.com"},
  1162. };
  1163. target.ownerGlobal = {gBrowser: {selectedBrowser: target}, document: {getElementById}, promiseDocumentFlushed: sandbox.stub().resolves([{width: 0}]), setTimeout: sandbox.stub()};
  1164. const firstMessage = {...FAKE_RECOMMENDATION, id: "first_message"};
  1165. const secondMessage = {...FAKE_RECOMMENDATION, id: "second_message"};
  1166. await Router.setState({messages: [firstMessage, secondMessage]});
  1167. global.DOMLocalization = class DOMLocalization {};
  1168. sandbox.spy(CFRPageActions, "addRecommendation");
  1169. sandbox.stub(Router, "addImpression").resolves();
  1170. await Router.setMessageById("first_message", target, false, {data});
  1171. await Router.setMessageById("second_message", target, false, {data});
  1172. assert.calledTwice(CFRPageActions.addRecommendation);
  1173. const [firstReturn, secondReturn] = CFRPageActions.addRecommendation.returnValues;
  1174. assert.isTrue(await firstReturn);
  1175. // Adding the second message should fail.
  1176. assert.isFalse(await secondReturn);
  1177. assert.calledOnce(Router.addImpression);
  1178. });
  1179. });
  1180. describe("#isBelowFrequencyCaps", () => {
  1181. it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
  1182. sinon.spy(Router, "_isBelowItemFrequencyCap");
  1183. const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
  1184. const fooMessageImpressions = [0, 1];
  1185. const barProviderImpressions = [0, 1, 2];
  1186. const message = {id: "foo", provider: "bar", frequency: {lifetime: 3}};
  1187. const provider = {id: "bar", frequency: {lifetime: 5}};
  1188. await Router.setState(state => {
  1189. // Add provider
  1190. const providers = [...state.providers, provider];
  1191. // Add fooMessageImpressions
  1192. const messageImpressions = Object.assign({}, state.messageImpressions); // eslint-disable-line no-shadow
  1193. messageImpressions.foo = fooMessageImpressions;
  1194. // Add barProviderImpressions
  1195. const providerImpressions = Object.assign({}, state.providerImpressions); // eslint-disable-line no-shadow
  1196. providerImpressions.bar = barProviderImpressions;
  1197. return {providers, messageImpressions, providerImpressions};
  1198. });
  1199. await Router.isBelowFrequencyCaps(message);
  1200. assert.calledTwice(Router._isBelowItemFrequencyCap);
  1201. assert.calledWithExactly(Router._isBelowItemFrequencyCap, message, fooMessageImpressions, MAX_MESSAGE_LIFETIME_CAP);
  1202. assert.calledWithExactly(Router._isBelowItemFrequencyCap, provider, barProviderImpressions);
  1203. });
  1204. });
  1205. describe("#_isBelowItemFrequencyCap", () => {
  1206. it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
  1207. const item = {id: "foo", frequency: {lifetime: 5}};
  1208. const impressions = [0, 1];
  1209. const maxLifetimeCap = 1;
  1210. const result = Router._isBelowItemFrequencyCap(item, impressions, maxLifetimeCap);
  1211. assert.isFalse(result);
  1212. });
  1213. describe("lifetime frequency caps", () => {
  1214. it("should return true if .frequency is not defined on the item", () => {
  1215. const item = {id: "foo"};
  1216. const impressions = [0, 1];
  1217. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1218. assert.isTrue(result);
  1219. });
  1220. it("should return true if there are no impressions", () => {
  1221. const item = {id: "foo", frequency: {lifetime: 10, custom: [{period: ONE_DAY_IN_MS, cap: 2}]}};
  1222. const impressions = [];
  1223. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1224. assert.isTrue(result);
  1225. });
  1226. it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
  1227. const item = {id: "foo", frequency: {lifetime: 3}};
  1228. const impressions = [0, 1];
  1229. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1230. assert.isTrue(result);
  1231. });
  1232. it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
  1233. const item = {id: "foo", frequency: {lifetime: 3}};
  1234. const impressions = [0, 1, 2];
  1235. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1236. assert.isFalse(result);
  1237. });
  1238. it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
  1239. const item = {id: "foo", frequency: {lifetime: 3}};
  1240. const impressions = [0, 1, 2, 3];
  1241. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1242. assert.isFalse(result);
  1243. });
  1244. });
  1245. describe("custom frequency caps", () => {
  1246. it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
  1247. clock.tick(ONE_DAY_IN_MS + 10);
  1248. const item = {id: "foo", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 2}], lifetime: 3}};
  1249. const impressions = [0, ONE_DAY_IN_MS + 1];
  1250. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1251. assert.isTrue(result);
  1252. });
  1253. it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
  1254. clock.tick(200);
  1255. const item = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
  1256. const impressions = [0, 160, 161];
  1257. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1258. assert.isFalse(result);
  1259. });
  1260. it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
  1261. clock.tick(ONE_DAY_IN_MS + 200);
  1262. const itemTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
  1263. const itemFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY_IN_MS, cap: 3}]}};
  1264. const impressions = [0, ONE_DAY_IN_MS + 160, ONE_DAY_IN_MS - 100, ONE_DAY_IN_MS - 200];
  1265. assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
  1266. assert.isFalse(Router._isBelowItemFrequencyCap(itemFalse, impressions));
  1267. });
  1268. it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
  1269. clock.tick(ONE_DAY_IN_MS + 10);
  1270. const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 2}], lifetime: 3}};
  1271. const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
  1272. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1273. assert.isFalse(result);
  1274. });
  1275. it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
  1276. clock.tick(ONE_DAY_IN_MS + 10);
  1277. const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 2}]}};
  1278. const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
  1279. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1280. assert.isTrue(result);
  1281. });
  1282. it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
  1283. clock.tick(ONE_DAY_IN_MS + 10);
  1284. const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 2}]}};
  1285. const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1, ONE_DAY_IN_MS + 2, ONE_DAY_IN_MS + 3];
  1286. const result = Router._isBelowItemFrequencyCap(item, impressions);
  1287. assert.isFalse(result);
  1288. });
  1289. });
  1290. });
  1291. describe("#getLongestPeriod", () => {
  1292. it("should return the period if there is only one definition", () => {
  1293. const message = {id: "foo", frequency: {custom: [{period: 200, cap: 2}]}};
  1294. assert.equal(Router.getLongestPeriod(message), 200);
  1295. });
  1296. it("should return the longest period if there are more than one definitions", () => {
  1297. const message = {id: "foo", frequency: {custom: [{period: 1000, cap: 3}, {period: ONE_DAY_IN_MS, cap: 5}, {period: 100, cap: 2}]}};
  1298. assert.equal(Router.getLongestPeriod(message), ONE_DAY_IN_MS);
  1299. });
  1300. it("should return null if there are is no .frequency", () => {
  1301. const message = {id: "foo"};
  1302. assert.isNull(Router.getLongestPeriod(message));
  1303. });
  1304. it("should return null if there are is no .frequency.custom", () => {
  1305. const message = {id: "foo", frequency: {lifetime: 10}};
  1306. assert.isNull(Router.getLongestPeriod(message));
  1307. });
  1308. });
  1309. describe("cleanup on init", () => {
  1310. it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
  1311. const messages = [{id: "foo", frequency: {lifetime: 10}}];
  1312. messageImpressions = {foo: [0], bar: [0, 1]};
  1313. // Impressions for "bar" should be removed since that id does not exist in messages
  1314. const result = {foo: [0]};
  1315. await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
  1316. assert.calledWith(Router._storage.set, "messageImpressions", result);
  1317. assert.deepEqual(Router.state.messageImpressions, result);
  1318. });
  1319. it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
  1320. const CURRENT_TIME = ONE_DAY_IN_MS * 2;
  1321. clock.tick(CURRENT_TIME);
  1322. const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 5}]}}];
  1323. messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
  1324. // Only 0 and 1 are more than 24 hours before CURRENT_TIME
  1325. const result = {foo: [CURRENT_TIME - 10]};
  1326. await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
  1327. assert.calledWith(Router._storage.set, "messageImpressions", result);
  1328. assert.deepEqual(Router.state.messageImpressions, result);
  1329. });
  1330. it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
  1331. const CURRENT_TIME = ONE_DAY_IN_MS * 2;
  1332. clock.tick(CURRENT_TIME);
  1333. const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY_IN_MS, cap: 5}, {period: 100, cap: 2}]}}];
  1334. messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
  1335. // Only 0 and 1 are more than 24 hours before CURRENT_TIME
  1336. const result = {foo: [CURRENT_TIME - 10]};
  1337. await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
  1338. assert.calledWith(Router._storage.set, "messageImpressions", result);
  1339. assert.deepEqual(Router.state.messageImpressions, result);
  1340. });
  1341. it("should clear messageImpressions if they are not properly formatted", async () => {
  1342. const messages = [{id: "foo", frequency: {lifetime: 10}}];
  1343. // this is impromperly formatted since messageImpressions are supposed to be an array
  1344. messageImpressions = {foo: 0};
  1345. const result = {};
  1346. await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
  1347. assert.calledWith(Router._storage.set, "messageImpressions", result);
  1348. assert.deepEqual(Router.state.messageImpressions, result);
  1349. });
  1350. it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
  1351. const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
  1352. messageImpressions = {foo: [0], bar: []};
  1353. await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
  1354. assert.notCalled(Router._storage.set);
  1355. assert.deepEqual(Router.state.messageImpressions, messageImpressions);
  1356. });
  1357. });
  1358. });
  1359. describe("handle targeting errors", () => {
  1360. it("should dispatch an event when a targeting expression throws an error", async () => {
  1361. sandbox.stub(global.FilterExpressions, "eval").returns(Promise.reject(new Error("fake error")));
  1362. await Router.setState({messages: [{id: "foo", targeting: "foo2.[[("}]});
  1363. const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
  1364. dispatchStub.reset();
  1365. await Router.onMessage(msg);
  1366. assert.calledOnce(dispatchStub);
  1367. const [action] = dispatchStub.firstCall.args;
  1368. assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
  1369. assert.equal(action.data.message_id, "foo");
  1370. });
  1371. });
  1372. describe("trailhead", () => {
  1373. it("should call .setupTrailhead on init", async () => {
  1374. sandbox.spy(Router, "setupTrailhead");
  1375. sandbox.stub(Router, "_generateTrailheadBranches").resolves({experiment: "", interrupt: "join", triplet: "privacy"});
  1376. sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
  1377. await Router.init(channel, createFakeStorage(), dispatchStub);
  1378. assert.calledOnce(Router.setupTrailhead);
  1379. assert.propertyVal(Router.state, "trailheadInitialized", true);
  1380. });
  1381. it("should call .setupTrailhead on init but return early if the DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
  1382. sandbox.spy(Router, "setupTrailhead");
  1383. sandbox.spy(Router, "_generateTrailheadBranches");
  1384. sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
  1385. await Router.init(channel, createFakeStorage(), dispatchStub);
  1386. assert.calledOnce(Router.setupTrailhead);
  1387. assert.notCalled(Router._generateTrailheadBranches);
  1388. assert.propertyVal(Router.state, "trailheadInitialized", false);
  1389. });
  1390. it("should call .setupTrailhead and set the DID_SEE_ABOUT_WELCOME_PREF on a firstRun TRIGGER message", async () => {
  1391. sandbox.spy(Router, "setupTrailhead");
  1392. const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
  1393. await Router.onMessage(msg);
  1394. assert.calledOnce(Router.setupTrailhead);
  1395. });
  1396. it("should have trailheadInterrupt and trailheadTriplet in the message context", async () => {
  1397. sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
  1398. sandbox.stub(Router, "_generateTrailheadBranches").resolves({experiment: "", interrupt: "join", triplet: "privacy"});
  1399. await Router.setupTrailhead();
  1400. assert.propertyVal(Router._getMessagesContext(), "trailheadInterrupt", "join");
  1401. assert.propertyVal(Router._getMessagesContext(), "trailheadTriplet", "privacy");
  1402. });
  1403. describe(".setupTrailhead", () => {
  1404. let getBoolPrefStub;
  1405. let setStringPrefStub;
  1406. let setBoolPrefStub;
  1407. beforeEach(() => {
  1408. getBoolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref");
  1409. getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
  1410. getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF).returns(false);
  1411. setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref");
  1412. setBoolPrefStub = sandbox.stub(global.Services.prefs, "setBoolPref");
  1413. });
  1414. const configWithInterruptsExperiment = {experiment: "interrupts", interrupt: "join", triplet: "privacy"};
  1415. const configWithTripletsExperiment = {experiment: "triplets", interrupt: "join", triplet: "privacy"};
  1416. const configWithoutExperiment = {experiment: "", interrupt: "control", triplet: ""};
  1417. it("should generates an experiment/branch configuration and update Router.state", async () => {
  1418. const config = configWithoutExperiment;
  1419. sandbox.stub(Router, "_generateTrailheadBranches").resolves(config);
  1420. await Router.setupTrailhead();
  1421. assert.propertyVal(Router.state, "trailheadInitialized", true);
  1422. assert.propertyVal(Router.state, "trailheadInterrupt", config.interrupt);
  1423. assert.propertyVal(Router.state, "trailheadTriplet", config.triplet);
  1424. });
  1425. it("should only run once", async () => {
  1426. sandbox.spy(Router, "setState");
  1427. await Router.setupTrailhead();
  1428. await Router.setupTrailhead();
  1429. await Router.setupTrailhead();
  1430. assert.calledOnce(Router.setState);
  1431. });
  1432. it("should return early if DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
  1433. getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
  1434. await Router.setupTrailhead();
  1435. sandbox.spy(Router, "setState");
  1436. assert.notCalled(Router.setState);
  1437. });
  1438. it("should set active interrupts experiment if one is defined", async () => {
  1439. sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithInterruptsExperiment);
  1440. sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
  1441. sandbox.spy(Router, "_sendTrailheadEnrollEvent");
  1442. await Router.setupTrailhead();
  1443. assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
  1444. assert.calledWith(setStringPrefStub, TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF, "join");
  1445. assert.calledWith(Router._sendTrailheadEnrollEvent, {
  1446. experiment: "activity-stream-firstrun-trailhead-interrupts",
  1447. type: "as-firstrun",
  1448. branch: "join",
  1449. });
  1450. });
  1451. it("should set active triplets experiment if one is defined", async () => {
  1452. sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithTripletsExperiment);
  1453. sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
  1454. sandbox.spy(Router, "_sendTrailheadEnrollEvent");
  1455. await Router.setupTrailhead();
  1456. assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
  1457. assert.calledWith(setBoolPrefStub, TRAILHEAD_CONFIG.TRIPLETS_ENROLLED_PREF, true);
  1458. assert.calledWith(Router._sendTrailheadEnrollEvent, {
  1459. experiment: "activity-stream-firstrun-trailhead-triplets",
  1460. type: "as-firstrun",
  1461. branch: "privacy",
  1462. });
  1463. });
  1464. it("should not set an active experiment if no experiment is defined", async () => {
  1465. sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithoutExperiment);
  1466. sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
  1467. await Router.setupTrailhead();
  1468. assert.notCalled(global.TelemetryEnvironment.setExperimentActive);
  1469. assert.notCalled(setStringPrefStub);
  1470. });
  1471. });
  1472. describe("._generateTrailheadBranches", () => {
  1473. async function checkReturnValue(expected) {
  1474. const result = await Router._generateTrailheadBranches();
  1475. assert.propertyVal(result, "experiment", expected.experiment);
  1476. assert.propertyVal(result, "interrupt", expected.interrupt);
  1477. assert.propertyVal(result, "triplet", expected.triplet);
  1478. }
  1479. it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
  1480. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
  1481. checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
  1482. });
  1483. it("should return control experience with no experiment if attribution data contains an addon source", async () => {
  1484. sandbox.stub(fakeAttributionCode, "getAttrDataAsync").resolves({source: "addons.mozilla.org"});
  1485. checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
  1486. });
  1487. it("should use values in override pref if it is set with no experiment", async () => {
  1488. getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("join-privacy");
  1489. checkReturnValue({experiment: "", interrupt: "join", triplet: "privacy"});
  1490. getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("nofirstrun");
  1491. checkReturnValue({experiment: "", interrupt: "nofirstrun", triplet: ""});
  1492. });
  1493. it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
  1494. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
  1495. checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
  1496. });
  1497. it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
  1498. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
  1499. checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
  1500. });
  1501. it("should roll for experiment if locale is in TRAILHEAD_LOCALES", async () => {
  1502. sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
  1503. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
  1504. checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
  1505. });
  1506. it("should roll for experiment if attribution data is empty", async () => {
  1507. sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
  1508. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
  1509. sandbox.stub(fakeAttributionCode, "getAttrDataAsync").resolves(null);
  1510. checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
  1511. });
  1512. it("should roll for experiment if attribution data rejects with an error", async () => {
  1513. sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
  1514. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
  1515. sandbox.stub(fakeAttributionCode, "getAttrDataAsync").rejects(new Error("whoops"));
  1516. checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
  1517. });
  1518. it("should roll a triplet experiment", async () => {
  1519. sandbox.stub(global.Sampling, "ratioSample").resolves(2); // 2 = triplets experiment
  1520. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
  1521. checkReturnValue({experiment: "triplets", interrupt: "join", triplet: "multidevice"});
  1522. });
  1523. it("should roll no experiment", async () => {
  1524. sandbox.stub(global.Sampling, "ratioSample").resolves(0); // 0 = no experiment
  1525. sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
  1526. checkReturnValue({experiment: "", interrupt: "join", triplet: "supercharge"});
  1527. });
  1528. });
  1529. });
  1530. describe("chooseBranch", () => {
  1531. it("should call .ratioSample with the second value in each branch and return one of the first values", async () => {
  1532. sandbox.stub(global.Sampling, "ratioSample").resolves(0);
  1533. const result = await chooseBranch("bleep", [["foo", 14], ["bar", 42]]);
  1534. assert.calledWith(global.Sampling.ratioSample, "bleep", [14, 42]);
  1535. assert.equal(result, "foo");
  1536. });
  1537. it("should use 1 as the default ratio", async () => {
  1538. sandbox.stub(global.Sampling, "ratioSample").resolves(1);
  1539. const result = await chooseBranch("bleep", [["foo"], ["bar"]]);
  1540. assert.calledWith(global.Sampling.ratioSample, "bleep", [1, 1]);
  1541. assert.equal(result, "bar");
  1542. });
  1543. });
  1544. });