SectionsManager.test.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. "use strict";
  2. import {actionCreators as ac, actionTypes as at, CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE, PRELOAD_MESSAGE_TYPE} from "common/Actions.jsm";
  3. import {EventEmitter, GlobalOverrider} from "test/unit/utils";
  4. import {SectionsFeed, SectionsManager} from "lib/SectionsManager.jsm";
  5. const FAKE_ID = "FAKE_ID";
  6. const FAKE_OPTIONS = {icon: "FAKE_ICON", title: "FAKE_TITLE"};
  7. const FAKE_ROWS = [{url: "1.example.com", type: "bookmark"}, {url: "2.example.com", type: "pocket"}, {url: "3.example.com", type: "history"}];
  8. const FAKE_TRENDING_ROWS = [{url: "bar", type: "trending"}];
  9. const FAKE_URL = "2.example.com";
  10. const FAKE_CARD_OPTIONS = {title: "Some fake title"};
  11. describe("SectionsManager", () => {
  12. let globals;
  13. let fakeServices;
  14. let fakePlacesUtils;
  15. let sandbox;
  16. let storage;
  17. beforeEach(async () => {
  18. sandbox = sinon.createSandbox();
  19. globals = new GlobalOverrider();
  20. fakeServices = {prefs: {getBoolPref: sandbox.stub(), addObserver: sandbox.stub(), removeObserver: sandbox.stub()}};
  21. fakePlacesUtils = {history: {update: sinon.stub(), insert: sinon.stub()}};
  22. globals.set({
  23. Services: fakeServices,
  24. PlacesUtils: fakePlacesUtils,
  25. });
  26. // Redecorate SectionsManager to remove any listeners that have been added
  27. EventEmitter.decorate(SectionsManager);
  28. storage = {
  29. get: sandbox.stub().resolves(),
  30. set: sandbox.stub().resolves(),
  31. };
  32. });
  33. afterEach(() => {
  34. globals.restore();
  35. sandbox.restore();
  36. });
  37. describe("#init", () => {
  38. it("should initialise the sections map with the built in sections", async () => {
  39. SectionsManager.sections.clear();
  40. SectionsManager.initialized = false;
  41. await SectionsManager.init({}, storage);
  42. assert.equal(SectionsManager.sections.size, 2);
  43. assert.ok(SectionsManager.sections.has("topstories"));
  44. assert.ok(SectionsManager.sections.has("highlights"));
  45. });
  46. it("should set .initialized to true", async () => {
  47. SectionsManager.sections.clear();
  48. SectionsManager.initialized = false;
  49. await SectionsManager.init({}, storage);
  50. assert.ok(SectionsManager.initialized);
  51. });
  52. it("should add observer for context menu prefs", async () => {
  53. SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"};
  54. await SectionsManager.init({}, storage);
  55. assert.calledOnce(fakeServices.prefs.addObserver);
  56. assert.calledWith(fakeServices.prefs.addObserver, "MENU_ITEM_PREF", SectionsManager);
  57. });
  58. it("should save the reference to `storage` passed in", async () => {
  59. await SectionsManager.init({}, storage);
  60. assert.equal(SectionsManager._storage, storage);
  61. });
  62. });
  63. describe("#uninit", () => {
  64. it("should remove observer for context menu prefs", () => {
  65. SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"};
  66. SectionsManager.initialized = true;
  67. SectionsManager.uninit();
  68. assert.calledOnce(fakeServices.prefs.removeObserver);
  69. assert.calledWith(fakeServices.prefs.removeObserver, "MENU_ITEM_PREF", SectionsManager);
  70. assert.isFalse(SectionsManager.initialized);
  71. });
  72. });
  73. describe("#addBuiltInSection", () => {
  74. it("should not report an error if options is undefined", async () => {
  75. globals.sandbox.spy(global.Cu, "reportError");
  76. SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve());
  77. await SectionsManager.addBuiltInSection("feeds.section.topstories", undefined);
  78. assert.notCalled(Cu.reportError);
  79. });
  80. it("should report an error if options is malformed", async () => {
  81. globals.sandbox.spy(global.Cu, "reportError");
  82. SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve());
  83. await SectionsManager.addBuiltInSection("feeds.section.topstories", "invalid");
  84. assert.calledOnce(Cu.reportError);
  85. });
  86. it("should not throw if the indexedDB operation fails", async () => {
  87. globals.sandbox.spy(global.Cu, "reportError");
  88. storage.get = sandbox.stub().throws();
  89. SectionsManager._storage = storage;
  90. try {
  91. await SectionsManager.addBuiltInSection("feeds.section.topstories");
  92. } catch (e) {
  93. assert.fail();
  94. }
  95. assert.calledOnce(storage.get);
  96. assert.calledOnce(Cu.reportError);
  97. });
  98. });
  99. describe("#updateSectionPrefs", () => {
  100. it("should update the collapsed value of the section", async () => {
  101. sandbox.stub(SectionsManager, "updateSection");
  102. let topstories = SectionsManager.sections.get("topstories");
  103. assert.isFalse(topstories.pref.collapsed);
  104. await SectionsManager.updateSectionPrefs("topstories", {collapsed: true});
  105. topstories = SectionsManager.sections.get("topstories");
  106. assert.isTrue(SectionsManager.updateSection.args[0][1].pref.collapsed);
  107. });
  108. it("should ignore invalid ids", async () => {
  109. sandbox.stub(SectionsManager, "updateSection");
  110. await SectionsManager.updateSectionPrefs("foo", {collapsed: true});
  111. assert.notCalled(SectionsManager.updateSection);
  112. });
  113. });
  114. describe("#addSection", () => {
  115. it("should add the id to sections and emit an ADD_SECTION event", () => {
  116. const spy = sinon.spy();
  117. SectionsManager.on(SectionsManager.ADD_SECTION, spy);
  118. SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
  119. assert.ok(SectionsManager.sections.has(FAKE_ID));
  120. assert.calledOnce(spy);
  121. assert.calledWith(spy, SectionsManager.ADD_SECTION, FAKE_ID, FAKE_OPTIONS);
  122. });
  123. });
  124. describe("#removeSection", () => {
  125. it("should remove the id from sections and emit an REMOVE_SECTION event", () => {
  126. // Ensure we start with the id in the set
  127. assert.ok(SectionsManager.sections.has(FAKE_ID));
  128. const spy = sinon.spy();
  129. SectionsManager.on(SectionsManager.REMOVE_SECTION, spy);
  130. SectionsManager.removeSection(FAKE_ID);
  131. assert.notOk(SectionsManager.sections.has(FAKE_ID));
  132. assert.calledOnce(spy);
  133. assert.calledWith(spy, SectionsManager.REMOVE_SECTION, FAKE_ID);
  134. });
  135. });
  136. describe("#enableSection", () => {
  137. it("should call updateSection with {enabled: true}", () => {
  138. sinon.spy(SectionsManager, "updateSection");
  139. SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
  140. SectionsManager.enableSection(FAKE_ID);
  141. assert.calledOnce(SectionsManager.updateSection);
  142. assert.calledWith(SectionsManager.updateSection, FAKE_ID, {enabled: true}, true);
  143. SectionsManager.updateSection.restore();
  144. });
  145. it("should emit an ENABLE_SECTION event", () => {
  146. const spy = sinon.spy();
  147. SectionsManager.on(SectionsManager.ENABLE_SECTION, spy);
  148. SectionsManager.enableSection(FAKE_ID);
  149. assert.calledOnce(spy);
  150. assert.calledWith(spy, SectionsManager.ENABLE_SECTION, FAKE_ID);
  151. });
  152. });
  153. describe("#disableSection", () => {
  154. it("should call updateSection with {enabled: false, rows: [], initialized: false}", () => {
  155. sinon.spy(SectionsManager, "updateSection");
  156. SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
  157. SectionsManager.disableSection(FAKE_ID);
  158. assert.calledOnce(SectionsManager.updateSection);
  159. assert.calledWith(SectionsManager.updateSection, FAKE_ID, {enabled: false, rows: [], initialized: false}, true);
  160. SectionsManager.updateSection.restore();
  161. });
  162. it("should emit a DISABLE_SECTION event", () => {
  163. const spy = sinon.spy();
  164. SectionsManager.on(SectionsManager.DISABLE_SECTION, spy);
  165. SectionsManager.disableSection(FAKE_ID);
  166. assert.calledOnce(spy);
  167. assert.calledWith(spy, SectionsManager.DISABLE_SECTION, FAKE_ID);
  168. });
  169. });
  170. describe("#updateSection", () => {
  171. it("should emit an UPDATE_SECTION event with correct arguments", () => {
  172. SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
  173. const spy = sinon.spy();
  174. const dedupeConfigurations = [{id: "topstories", dedupeFrom: ["highlights"]}];
  175. SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
  176. SectionsManager.updateSection(FAKE_ID, {rows: FAKE_ROWS}, true);
  177. assert.calledOnce(spy);
  178. assert.calledWith(spy, SectionsManager.UPDATE_SECTION, FAKE_ID, {rows: FAKE_ROWS, dedupeConfigurations}, true);
  179. });
  180. it("should do nothing if the section doesn't exist", () => {
  181. SectionsManager.removeSection(FAKE_ID);
  182. const spy = sinon.spy();
  183. SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
  184. SectionsManager.updateSection(FAKE_ID, {rows: FAKE_ROWS}, true);
  185. assert.notCalled(spy);
  186. });
  187. it("should update all sections", () => {
  188. SectionsManager.sections.clear();
  189. const updateSectionOrig = SectionsManager.updateSection;
  190. SectionsManager.updateSection = sinon.spy();
  191. SectionsManager.addSection("ID1", {title: "FAKE_TITLE_1"});
  192. SectionsManager.addSection("ID2", {title: "FAKE_TITLE_2"});
  193. SectionsManager.updateSections();
  194. assert.calledTwice(SectionsManager.updateSection);
  195. assert.calledWith(SectionsManager.updateSection, "ID1", {title: "FAKE_TITLE_1"}, true);
  196. assert.calledWith(SectionsManager.updateSection, "ID2", {title: "FAKE_TITLE_2"}, true);
  197. SectionsManager.updateSection = updateSectionOrig;
  198. });
  199. it("context menu pref change should update sections", async () => {
  200. let observer;
  201. const services = {prefs: {getBoolPref: sinon.spy(), addObserver: (pref, o) => (observer = o), removeObserver: sinon.spy()}};
  202. globals.set("Services", services);
  203. SectionsManager.updateSections = sinon.spy();
  204. SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"};
  205. await SectionsManager.init({}, storage);
  206. observer.observe("", "nsPref:changed", "MENU_ITEM_PREF");
  207. assert.calledOnce(SectionsManager.updateSections);
  208. });
  209. });
  210. describe("#_addCardTypeLinkMenuOptions", () => {
  211. const addCardTypeLinkMenuOptionsOrig = SectionsManager._addCardTypeLinkMenuOptions;
  212. const contextMenuOptionsOrig = SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES;
  213. beforeEach(() => {
  214. // Add a topstories section and a highlights section, with types for each card
  215. SectionsManager.addSection("topstories", {FAKE_TRENDING_ROWS});
  216. SectionsManager.addSection("highlights", {FAKE_ROWS});
  217. });
  218. it("should only call _addCardTypeLinkMenuOptions if the section update is for highlights", () => {
  219. SectionsManager._addCardTypeLinkMenuOptions = sinon.spy();
  220. SectionsManager.updateSection("topstories", {rows: FAKE_ROWS}, false);
  221. assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions);
  222. SectionsManager.updateSection("highlights", {rows: FAKE_ROWS}, false);
  223. assert.calledWith(SectionsManager._addCardTypeLinkMenuOptions, FAKE_ROWS);
  224. });
  225. it("should only call _addCardTypeLinkMenuOptions if the section update has rows", () => {
  226. SectionsManager._addCardTypeLinkMenuOptions = sinon.spy();
  227. SectionsManager.updateSection("highlights", {}, false);
  228. assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions);
  229. });
  230. it("should assign the correct context menu options based on the type of highlight", () => {
  231. SectionsManager._addCardTypeLinkMenuOptions = addCardTypeLinkMenuOptionsOrig;
  232. SectionsManager.updateSection("highlights", {rows: FAKE_ROWS}, false);
  233. const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS;
  234. // FAKE_ROWS was added in the following order: bookmark, pocket, history
  235. assert.deepEqual(highlights[0].contextMenuOptions, SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.bookmark);
  236. assert.deepEqual(highlights[1].contextMenuOptions, SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.pocket);
  237. assert.deepEqual(highlights[2].contextMenuOptions, SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.history);
  238. });
  239. it("should throw an error if you are assigning a context menu to a non-existant highlight type", () => {
  240. globals.sandbox.spy(global.Cu, "reportError");
  241. SectionsManager.updateSection("highlights", {rows: [{url: "foo", type: "badtype"}]}, false);
  242. const highlights = SectionsManager.sections.get("highlights").rows;
  243. assert.calledOnce(Cu.reportError);
  244. assert.equal(highlights[0].contextMenuOptions, undefined);
  245. });
  246. it("should filter out context menu options that are in CONTEXT_MENU_PREFS", () => {
  247. const services = {prefs: {getBoolPref: o => SectionsManager.CONTEXT_MENU_PREFS[o] !== "RemoveMe", addObserver() {}, removeObserver() {}}};
  248. globals.set("Services", services);
  249. SectionsManager.CONTEXT_MENU_PREFS = {"RemoveMe": "RemoveMe"};
  250. SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = {
  251. "bookmark": ["KeepMe", "RemoveMe"],
  252. "pocket": ["KeepMe", "RemoveMe"],
  253. "history": ["KeepMe", "RemoveMe"],
  254. };
  255. SectionsManager.updateSection("highlights", {rows: FAKE_ROWS}, false);
  256. const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS;
  257. // Only keep context menu options that were not supposed to be removed based on CONTEXT_MENU_PREFS
  258. assert.deepEqual(highlights[0].contextMenuOptions, ["KeepMe"]);
  259. assert.deepEqual(highlights[1].contextMenuOptions, ["KeepMe"]);
  260. assert.deepEqual(highlights[2].contextMenuOptions, ["KeepMe"]);
  261. SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = contextMenuOptionsOrig;
  262. globals.restore();
  263. });
  264. });
  265. describe("#onceInitialized", () => {
  266. it("should call the callback immediately if SectionsManager is initialised", () => {
  267. SectionsManager.initialized = true;
  268. const callback = sinon.spy();
  269. SectionsManager.onceInitialized(callback);
  270. assert.calledOnce(callback);
  271. });
  272. it("should bind the callback to .once(INIT) if SectionsManager is not initialised", () => {
  273. SectionsManager.initialized = false;
  274. sinon.spy(SectionsManager, "once");
  275. const callback = () => {};
  276. SectionsManager.onceInitialized(callback);
  277. assert.calledOnce(SectionsManager.once);
  278. assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback);
  279. });
  280. });
  281. describe("#updateSectionCard", () => {
  282. it("should emit an UPDATE_SECTION_CARD event with correct arguments", () => {
  283. SectionsManager.addSection(FAKE_ID, Object.assign({}, FAKE_OPTIONS, {rows: FAKE_ROWS}));
  284. const spy = sinon.spy();
  285. SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
  286. SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
  287. assert.calledOnce(spy);
  288. assert.calledWith(spy, SectionsManager.UPDATE_SECTION_CARD, FAKE_ID,
  289. FAKE_URL, FAKE_CARD_OPTIONS, true);
  290. });
  291. it("should do nothing if the section doesn't exist", () => {
  292. SectionsManager.removeSection(FAKE_ID);
  293. const spy = sinon.spy();
  294. SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
  295. SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
  296. assert.notCalled(spy);
  297. });
  298. });
  299. describe("#removeSectionCard", () => {
  300. it("should dispatch an SECTION_UPDATE action in which cards corresponding to the given url are removed", () => {
  301. const rows = [{url: "foo.com"}, {url: "bar.com"}];
  302. SectionsManager.addSection(FAKE_ID, Object.assign({}, FAKE_OPTIONS, {rows}));
  303. const spy = sinon.spy();
  304. SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
  305. SectionsManager.removeSectionCard(FAKE_ID, "foo.com");
  306. assert.calledOnce(spy);
  307. assert.equal(spy.firstCall.args[1], FAKE_ID);
  308. assert.deepEqual(spy.firstCall.args[2].rows, [{url: "bar.com"}]);
  309. });
  310. it("should do nothing if the section doesn't exist", () => {
  311. SectionsManager.removeSection(FAKE_ID);
  312. const spy = sinon.spy();
  313. SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
  314. SectionsManager.removeSectionCard(FAKE_ID, "bar.com");
  315. assert.notCalled(spy);
  316. });
  317. });
  318. describe("#updateBookmarkMetadata", () => {
  319. beforeEach(() => {
  320. let rows = [{
  321. url: "bar",
  322. title: "title",
  323. description: "description",
  324. image: "image",
  325. type: "trending",
  326. }];
  327. SectionsManager.addSection("topstories", {rows});
  328. // Simulate 2 sections.
  329. rows = [{
  330. url: "foo",
  331. title: "title",
  332. description: "description",
  333. image: "image",
  334. type: "bookmark",
  335. }];
  336. SectionsManager.addSection("highlights", {rows});
  337. });
  338. it("shouldn't call PlacesUtils if URL is not in topstories", () => {
  339. SectionsManager.updateBookmarkMetadata({url: "foo"});
  340. assert.notCalled(fakePlacesUtils.history.update);
  341. });
  342. it("should call PlacesUtils.history.update", () => {
  343. SectionsManager.updateBookmarkMetadata({url: "bar"});
  344. assert.calledOnce(fakePlacesUtils.history.update);
  345. assert.calledWithExactly(fakePlacesUtils.history.update, {
  346. url: "bar",
  347. title: "title",
  348. description: "description",
  349. previewImageURL: "image",
  350. });
  351. });
  352. it("should call PlacesUtils.history.insert", () => {
  353. SectionsManager.updateBookmarkMetadata({url: "bar"});
  354. assert.calledOnce(fakePlacesUtils.history.insert);
  355. assert.calledWithExactly(fakePlacesUtils.history.insert, {
  356. url: "bar",
  357. title: "title",
  358. visits: [{}],
  359. });
  360. });
  361. });
  362. });
  363. describe("SectionsFeed", () => {
  364. let feed;
  365. let sandbox;
  366. let storage;
  367. beforeEach(() => {
  368. sandbox = sinon.createSandbox();
  369. SectionsManager.sections.clear();
  370. SectionsManager.initialized = false;
  371. storage = {
  372. get: sandbox.stub().resolves(),
  373. set: sandbox.stub().resolves(),
  374. };
  375. feed = new SectionsFeed();
  376. feed.store = {dispatch: sinon.spy()};
  377. feed.store = {
  378. dispatch: sinon.spy(),
  379. getState() { return this.state; },
  380. state: {
  381. Prefs: {
  382. values: {
  383. "sectionOrder": "topsites,topstories,highlights",
  384. "feeds.topsites": true,
  385. },
  386. },
  387. Sections: [{initialized: false}],
  388. },
  389. dbStorage: {getDbTable: sandbox.stub().returns(storage)},
  390. };
  391. });
  392. afterEach(() => {
  393. feed.uninit();
  394. });
  395. describe("#init", () => {
  396. it("should create a SectionsFeed", () => {
  397. assert.instanceOf(feed, SectionsFeed);
  398. });
  399. it("should bind appropriate listeners", () => {
  400. sinon.spy(SectionsManager, "on");
  401. feed.init();
  402. assert.callCount(SectionsManager.on, 4);
  403. for (const [event, listener] of [
  404. [SectionsManager.ADD_SECTION, feed.onAddSection],
  405. [SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
  406. [SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
  407. [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard],
  408. ]) {
  409. assert.calledWith(SectionsManager.on, event, listener);
  410. }
  411. });
  412. it("should call onAddSection for any already added sections in SectionsManager", async () => {
  413. await SectionsManager.init({}, storage);
  414. assert.ok(SectionsManager.sections.has("topstories"));
  415. assert.ok(SectionsManager.sections.has("highlights"));
  416. const topstories = SectionsManager.sections.get("topstories");
  417. const highlights = SectionsManager.sections.get("highlights");
  418. sinon.spy(feed, "onAddSection");
  419. feed.init();
  420. assert.calledTwice(feed.onAddSection);
  421. assert.calledWith(feed.onAddSection, SectionsManager.ADD_SECTION, "topstories", topstories);
  422. assert.calledWith(feed.onAddSection, SectionsManager.ADD_SECTION, "highlights", highlights);
  423. });
  424. });
  425. describe("#uninit", () => {
  426. it("should unbind all listeners", () => {
  427. sinon.spy(SectionsManager, "off");
  428. feed.init();
  429. feed.uninit();
  430. assert.callCount(SectionsManager.off, 4);
  431. for (const [event, listener] of [
  432. [SectionsManager.ADD_SECTION, feed.onAddSection],
  433. [SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
  434. [SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
  435. [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard],
  436. ]) {
  437. assert.calledWith(SectionsManager.off, event, listener);
  438. }
  439. });
  440. it("should emit an UNINIT event and set SectionsManager.initialized to false", () => {
  441. const spy = sinon.spy();
  442. SectionsManager.on(SectionsManager.UNINIT, spy);
  443. feed.init();
  444. feed.uninit();
  445. assert.calledOnce(spy);
  446. assert.notOk(SectionsManager.initialized);
  447. });
  448. });
  449. describe("#onAddSection", () => {
  450. it("should broadcast a SECTION_REGISTER action with the correct data", () => {
  451. feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS);
  452. const [action] = feed.store.dispatch.firstCall.args;
  453. assert.equal(action.type, "SECTION_REGISTER");
  454. assert.deepEqual(action.data, Object.assign({id: FAKE_ID}, FAKE_OPTIONS));
  455. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  456. assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
  457. });
  458. it("should prepend id to sectionOrder pref if not already included", () => {
  459. feed.store.state.Sections = [{id: "topstories", enabled: true}, {id: "highlights", enabled: true}];
  460. feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS);
  461. assert.calledWith(feed.store.dispatch, {
  462. data: {name: "sectionOrder", value: `${FAKE_ID},topsites,topstories,highlights`},
  463. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  464. type: "SET_PREF",
  465. });
  466. });
  467. });
  468. describe("#onRemoveSection", () => {
  469. it("should broadcast a SECTION_DEREGISTER action with the correct data", () => {
  470. feed.onRemoveSection(null, FAKE_ID);
  471. const [action] = feed.store.dispatch.firstCall.args;
  472. assert.equal(action.type, "SECTION_DEREGISTER");
  473. assert.deepEqual(action.data, FAKE_ID);
  474. // Should be broadcast
  475. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  476. assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
  477. });
  478. });
  479. describe("#onUpdateSection", () => {
  480. it("should do nothing if no options are provided", () => {
  481. feed.onUpdateSection(null, FAKE_ID, null);
  482. assert.notCalled(feed.store.dispatch);
  483. });
  484. it("should dispatch a SECTION_UPDATE action with the correct data", () => {
  485. feed.onUpdateSection(null, FAKE_ID, {rows: FAKE_ROWS});
  486. const [action] = feed.store.dispatch.firstCall.args;
  487. assert.equal(action.type, "SECTION_UPDATE");
  488. assert.deepEqual(action.data, {id: FAKE_ID, rows: FAKE_ROWS});
  489. // Should be not broadcast by default, but should update the preloaded tab, so check meta
  490. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  491. assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE);
  492. });
  493. it("should broadcast the action only if shouldBroadcast is true", () => {
  494. feed.onUpdateSection(null, FAKE_ID, {rows: FAKE_ROWS}, true);
  495. const [action] = feed.store.dispatch.firstCall.args;
  496. // Should be broadcast
  497. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  498. assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
  499. });
  500. });
  501. describe("#onUpdateSectionCard", () => {
  502. it("should do nothing if no options are provided", () => {
  503. feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null);
  504. assert.notCalled(feed.store.dispatch);
  505. });
  506. it("should dispatch a SECTION_UPDATE_CARD action with the correct data", () => {
  507. feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS);
  508. const [action] = feed.store.dispatch.firstCall.args;
  509. assert.equal(action.type, "SECTION_UPDATE_CARD");
  510. assert.deepEqual(action.data, {id: FAKE_ID, url: FAKE_URL, options: FAKE_CARD_OPTIONS});
  511. // Should be not broadcast by default, but should update the preloaded tab, so check meta
  512. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  513. assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE);
  514. });
  515. it("should broadcast the action only if shouldBroadcast is true", () => {
  516. feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
  517. const [action] = feed.store.dispatch.firstCall.args;
  518. // Should be broadcast
  519. assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
  520. assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
  521. });
  522. });
  523. describe("#onAction", () => {
  524. it("should bind this.init to SectionsManager.INIT on INIT", () => {
  525. sinon.spy(SectionsManager, "once");
  526. feed.onAction({type: "INIT"});
  527. assert.calledOnce(SectionsManager.once);
  528. assert.calledWith(SectionsManager.once, SectionsManager.INIT, feed.init);
  529. });
  530. it("should call SectionsManager.init on action PREFS_INITIAL_VALUES", () => {
  531. sinon.spy(SectionsManager, "init");
  532. feed.onAction({type: "PREFS_INITIAL_VALUES", data: {foo: "bar"}});
  533. assert.calledOnce(SectionsManager.init);
  534. assert.calledWith(SectionsManager.init, {foo: "bar"});
  535. assert.calledOnce(feed.store.dbStorage.getDbTable);
  536. assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs");
  537. });
  538. it("should call SectionsManager.addBuiltInSection on suitable PREF_CHANGED events", () => {
  539. sinon.spy(SectionsManager, "addBuiltInSection");
  540. feed.onAction({type: "PREF_CHANGED", data: {name: "feeds.section.topstories.options", value: "foo"}});
  541. assert.calledOnce(SectionsManager.addBuiltInSection);
  542. assert.calledWith(SectionsManager.addBuiltInSection, "feeds.section.topstories", "foo");
  543. });
  544. it("should fire SECTION_OPTIONS_UPDATED on suitable PREF_CHANGED events", async () => {
  545. await feed.onAction({type: "PREF_CHANGED", data: {name: "feeds.section.topstories.options", value: "foo"}});
  546. assert.calledOnce(feed.store.dispatch);
  547. const [action] = feed.store.dispatch.firstCall.args;
  548. assert.equal(action.type, "SECTION_OPTIONS_CHANGED");
  549. assert.equal(action.data, "topstories");
  550. });
  551. it("should call SectionsManager.disableSection on SECTION_DISABLE", () => {
  552. sinon.spy(SectionsManager, "disableSection");
  553. feed.onAction({type: "SECTION_DISABLE", data: 1234});
  554. assert.calledOnce(SectionsManager.disableSection);
  555. assert.calledWith(SectionsManager.disableSection, 1234);
  556. SectionsManager.disableSection.restore();
  557. });
  558. it("should call SectionsManager.enableSection on SECTION_ENABLE", () => {
  559. sinon.spy(SectionsManager, "enableSection");
  560. feed.onAction({type: "SECTION_ENABLE", data: 1234});
  561. assert.calledOnce(SectionsManager.enableSection);
  562. assert.calledWith(SectionsManager.enableSection, 1234);
  563. SectionsManager.enableSection.restore();
  564. });
  565. it("should call the feed's uninit on UNINIT", () => {
  566. sinon.stub(feed, "uninit");
  567. feed.onAction({type: "UNINIT"});
  568. assert.calledOnce(feed.uninit);
  569. });
  570. it("should emit a ACTION_DISPATCHED event and forward any action in ACTIONS_TO_PROXY if there are any sections", () => {
  571. const spy = sinon.spy();
  572. const allowedActions = SectionsManager.ACTIONS_TO_PROXY;
  573. const disallowedActions = ["PREF_CHANGED", "OPEN_PRIVATE_WINDOW"];
  574. feed.init();
  575. SectionsManager.on(SectionsManager.ACTION_DISPATCHED, spy);
  576. // Make sure we start with no sections - no event should be emitted
  577. SectionsManager.sections.clear();
  578. feed.onAction({type: allowedActions[0]});
  579. assert.notCalled(spy);
  580. // Then add a section and check correct behaviour
  581. SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);
  582. for (const action of allowedActions.concat(disallowedActions)) {
  583. feed.onAction({type: action});
  584. }
  585. for (const action of allowedActions) {
  586. assert.calledWith(spy, "ACTION_DISPATCHED", action);
  587. }
  588. for (const action of disallowedActions) {
  589. assert.neverCalledWith(spy, "ACTION_DISPATCHED", action);
  590. }
  591. });
  592. it("should call updateBookmarkMetadata on PLACES_BOOKMARK_ADDED", () => {
  593. const stub = sinon.stub(SectionsManager, "updateBookmarkMetadata");
  594. feed.onAction({type: "PLACES_BOOKMARK_ADDED", data: {}});
  595. assert.calledOnce(stub);
  596. });
  597. it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => {
  598. const stub = sinon.stub(SectionsManager, "updateSectionPrefs");
  599. feed.onAction({type: "UPDATE_SECTION_PREFS", data: {}});
  600. assert.calledOnce(stub);
  601. });
  602. it("should call SectionManager.removeSectionCard on WEBEXT_DISMISS", () => {
  603. const stub = sinon.stub(SectionsManager, "removeSectionCard");
  604. feed.onAction(ac.WebExtEvent(at.WEBEXT_DISMISS, {source: "Foo", url: "bar.com"}));
  605. assert.calledOnce(stub);
  606. assert.calledWith(stub, "Foo", "bar.com");
  607. });
  608. it("should call the feed's moveSection on SECTION_MOVE", () => {
  609. sinon.stub(feed, "moveSection");
  610. const id = "topsites";
  611. const direction = +1;
  612. feed.onAction({type: "SECTION_MOVE", data: {id, direction}});
  613. assert.calledOnce(feed.moveSection);
  614. assert.calledWith(feed.moveSection, id, direction);
  615. });
  616. });
  617. describe("#moveSection", () => {
  618. it("should Move Down correctly", () => {
  619. feed.store.state.Sections = [{id: "topstories", enabled: true}, {id: "highlights", enabled: true}];
  620. feed.moveSection("topsites", +1);
  621. assert.calledOnce(feed.store.dispatch);
  622. assert.calledWith(feed.store.dispatch, {
  623. data: {name: "sectionOrder", value: "topstories,topsites,highlights"},
  624. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  625. type: "SET_PREF",
  626. });
  627. feed.store.dispatch.resetHistory();
  628. feed.moveSection("topstories", +1);
  629. assert.calledOnce(feed.store.dispatch);
  630. assert.calledWith(feed.store.dispatch, {
  631. data: {name: "sectionOrder", value: "topsites,highlights,topstories"},
  632. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  633. type: "SET_PREF",
  634. });
  635. });
  636. it("should Move Up correctly", () => {
  637. feed.store.state.Sections = [{id: "topstories", enabled: true}, {id: "highlights", enabled: true}];
  638. feed.moveSection("topstories", -1);
  639. assert.calledOnce(feed.store.dispatch);
  640. assert.calledWith(feed.store.dispatch, {
  641. data: {name: "sectionOrder", value: "topstories,topsites,highlights"},
  642. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  643. type: "SET_PREF",
  644. });
  645. feed.store.dispatch.resetHistory();
  646. feed.moveSection("highlights", -1);
  647. assert.calledOnce(feed.store.dispatch);
  648. assert.calledWith(feed.store.dispatch, {
  649. data: {name: "sectionOrder", value: "topsites,highlights,topstories"},
  650. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  651. type: "SET_PREF",
  652. });
  653. });
  654. it("should skip over sections that aren't enabled", () => {
  655. feed.store.state.Sections = [{id: "topstories", enabled: false}, {id: "highlights", enabled: true}];
  656. feed.moveSection("highlights", -1);
  657. assert.calledOnce(feed.store.dispatch);
  658. assert.calledWith(feed.store.dispatch, {
  659. data: {name: "sectionOrder", value: "highlights,topsites,topstories"},
  660. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  661. type: "SET_PREF",
  662. });
  663. feed.store.dispatch.resetHistory();
  664. feed.moveSection("topsites", +1);
  665. assert.calledOnce(feed.store.dispatch);
  666. assert.calledWith(feed.store.dispatch, {
  667. data: {name: "sectionOrder", value: "topstories,highlights,topsites"},
  668. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  669. type: "SET_PREF",
  670. });
  671. });
  672. });
  673. });