diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/newtab/test/unit/lib/SectionsManager.test.js | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/lib/SectionsManager.test.js b/browser/components/newtab/test/unit/lib/SectionsManager.test.js new file mode 100644 index 0000000000..b3a9abd70c --- /dev/null +++ b/browser/components/newtab/test/unit/lib/SectionsManager.test.js @@ -0,0 +1,897 @@ +"use strict"; +import { + actionCreators as ac, + actionTypes as at, + CONTENT_MESSAGE_TYPE, + MAIN_MESSAGE_TYPE, + PRELOAD_MESSAGE_TYPE, +} from "common/Actions.sys.mjs"; +import { EventEmitter, GlobalOverrider } from "test/unit/utils"; +import { SectionsFeed, SectionsManager } from "lib/SectionsManager.sys.mjs"; + +const FAKE_ID = "FAKE_ID"; +const FAKE_OPTIONS = { icon: "FAKE_ICON", title: "FAKE_TITLE" }; +const FAKE_ROWS = [ + { url: "1.example.com", type: "bookmark" }, + { url: "2.example.com", type: "pocket" }, + { url: "3.example.com", type: "history" }, +]; +const FAKE_TRENDING_ROWS = [{ url: "bar", type: "trending" }]; +const FAKE_URL = "2.example.com"; +const FAKE_CARD_OPTIONS = { title: "Some fake title" }; + +describe("SectionsManager", () => { + let globals; + let fakeServices; + let fakePlacesUtils; + let sandbox; + let storage; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + globals = new GlobalOverrider(); + fakeServices = { + prefs: { + getBoolPref: sandbox.stub(), + addObserver: sandbox.stub(), + removeObserver: sandbox.stub(), + }, + }; + fakePlacesUtils = { + history: { update: sinon.stub(), insert: sinon.stub() }, + }; + globals.set({ + Services: fakeServices, + PlacesUtils: fakePlacesUtils, + NimbusFeatures: { + newtab: { getAllVariables: sandbox.stub() }, + pocketNewtab: { getAllVariables: sandbox.stub() }, + }, + }); + // Redecorate SectionsManager to remove any listeners that have been added + EventEmitter.decorate(SectionsManager); + storage = { + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves(), + }; + }); + + afterEach(() => { + globals.restore(); + sandbox.restore(); + }); + + describe("#init", () => { + it("should initialise the sections map with the built in sections", async () => { + SectionsManager.sections.clear(); + SectionsManager.initialized = false; + await SectionsManager.init({}, storage); + assert.equal(SectionsManager.sections.size, 2); + assert.ok(SectionsManager.sections.has("topstories")); + assert.ok(SectionsManager.sections.has("highlights")); + }); + it("should set .initialized to true", async () => { + SectionsManager.sections.clear(); + SectionsManager.initialized = false; + await SectionsManager.init({}, storage); + assert.ok(SectionsManager.initialized); + }); + it("should add observer for context menu prefs", async () => { + SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; + await SectionsManager.init({}, storage); + assert.calledOnce(fakeServices.prefs.addObserver); + assert.calledWith( + fakeServices.prefs.addObserver, + "MENU_ITEM_PREF", + SectionsManager + ); + }); + it("should save the reference to `storage` passed in", async () => { + await SectionsManager.init({}, storage); + + assert.equal(SectionsManager._storage, storage); + }); + }); + describe("#uninit", () => { + it("should remove observer for context menu prefs", () => { + SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; + SectionsManager.initialized = true; + SectionsManager.uninit(); + assert.calledOnce(fakeServices.prefs.removeObserver); + assert.calledWith( + fakeServices.prefs.removeObserver, + "MENU_ITEM_PREF", + SectionsManager + ); + assert.isFalse(SectionsManager.initialized); + }); + }); + describe("#addBuiltInSection", () => { + it("should not report an error if options is undefined", async () => { + globals.sandbox.spy(global.console, "error"); + SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve()); + await SectionsManager.addBuiltInSection( + "feeds.section.topstories", + undefined + ); + + assert.notCalled(console.error); + }); + it("should report an error if options is malformed", async () => { + globals.sandbox.spy(global.console, "error"); + SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve()); + await SectionsManager.addBuiltInSection( + "feeds.section.topstories", + "invalid" + ); + + assert.calledOnce(console.error); + }); + it("should not throw if the indexedDB operation fails", async () => { + globals.sandbox.spy(global.console, "error"); + storage.get = sandbox.stub().throws(); + SectionsManager._storage = storage; + + try { + await SectionsManager.addBuiltInSection("feeds.section.topstories"); + } catch (e) { + assert.fail(); + } + + assert.calledOnce(storage.get); + assert.calledOnce(console.error); + }); + }); + describe("#updateSectionPrefs", () => { + it("should update the collapsed value of the section", async () => { + sandbox.stub(SectionsManager, "updateSection"); + let topstories = SectionsManager.sections.get("topstories"); + assert.isFalse(topstories.pref.collapsed); + + await SectionsManager.updateSectionPrefs("topstories", { + collapsed: true, + }); + topstories = SectionsManager.sections.get("topstories"); + + assert.isTrue(SectionsManager.updateSection.args[0][1].pref.collapsed); + }); + it("should ignore invalid ids", async () => { + sandbox.stub(SectionsManager, "updateSection"); + await SectionsManager.updateSectionPrefs("foo", { collapsed: true }); + + assert.notCalled(SectionsManager.updateSection); + }); + }); + describe("#addSection", () => { + it("should add the id to sections and emit an ADD_SECTION event", () => { + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.ADD_SECTION, spy); + SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); + assert.ok(SectionsManager.sections.has(FAKE_ID)); + assert.calledOnce(spy); + assert.calledWith( + spy, + SectionsManager.ADD_SECTION, + FAKE_ID, + FAKE_OPTIONS + ); + }); + }); + describe("#removeSection", () => { + it("should remove the id from sections and emit an REMOVE_SECTION event", () => { + // Ensure we start with the id in the set + assert.ok(SectionsManager.sections.has(FAKE_ID)); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.REMOVE_SECTION, spy); + SectionsManager.removeSection(FAKE_ID); + assert.notOk(SectionsManager.sections.has(FAKE_ID)); + assert.calledOnce(spy); + assert.calledWith(spy, SectionsManager.REMOVE_SECTION, FAKE_ID); + }); + }); + describe("#enableSection", () => { + it("should call updateSection with {enabled: true}", () => { + sinon.spy(SectionsManager, "updateSection"); + SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); + SectionsManager.enableSection(FAKE_ID); + assert.calledOnce(SectionsManager.updateSection); + assert.calledWith( + SectionsManager.updateSection, + FAKE_ID, + { enabled: true }, + true + ); + SectionsManager.updateSection.restore(); + }); + it("should emit an ENABLE_SECTION event", () => { + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.ENABLE_SECTION, spy); + SectionsManager.enableSection(FAKE_ID); + assert.calledOnce(spy); + assert.calledWith(spy, SectionsManager.ENABLE_SECTION, FAKE_ID); + }); + }); + describe("#disableSection", () => { + it("should call updateSection with {enabled: false, rows: [], initialized: false}", () => { + sinon.spy(SectionsManager, "updateSection"); + SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); + SectionsManager.disableSection(FAKE_ID); + assert.calledOnce(SectionsManager.updateSection); + assert.calledWith( + SectionsManager.updateSection, + FAKE_ID, + { enabled: false, rows: [], initialized: false }, + true + ); + SectionsManager.updateSection.restore(); + }); + it("should emit a DISABLE_SECTION event", () => { + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.DISABLE_SECTION, spy); + SectionsManager.disableSection(FAKE_ID); + assert.calledOnce(spy); + assert.calledWith(spy, SectionsManager.DISABLE_SECTION, FAKE_ID); + }); + }); + describe("#updateSection", () => { + it("should emit an UPDATE_SECTION event with correct arguments", () => { + SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); + const spy = sinon.spy(); + const dedupeConfigurations = [ + { id: "topstories", dedupeFrom: ["highlights"] }, + ]; + SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); + SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true); + assert.calledOnce(spy); + assert.calledWith( + spy, + SectionsManager.UPDATE_SECTION, + FAKE_ID, + { rows: FAKE_ROWS, dedupeConfigurations }, + true + ); + }); + it("should do nothing if the section doesn't exist", () => { + SectionsManager.removeSection(FAKE_ID); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); + SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true); + assert.notCalled(spy); + }); + it("should update all sections", () => { + SectionsManager.sections.clear(); + const updateSectionOrig = SectionsManager.updateSection; + SectionsManager.updateSection = sinon.spy(); + + SectionsManager.addSection("ID1", { title: "FAKE_TITLE_1" }); + SectionsManager.addSection("ID2", { title: "FAKE_TITLE_2" }); + SectionsManager.updateSections(); + + assert.calledTwice(SectionsManager.updateSection); + assert.calledWith( + SectionsManager.updateSection, + "ID1", + { title: "FAKE_TITLE_1" }, + true + ); + assert.calledWith( + SectionsManager.updateSection, + "ID2", + { title: "FAKE_TITLE_2" }, + true + ); + SectionsManager.updateSection = updateSectionOrig; + }); + it("context menu pref change should update sections", async () => { + let observer; + const services = { + prefs: { + getBoolPref: sinon.spy(), + addObserver: (pref, o) => (observer = o), + removeObserver: sinon.spy(), + }, + }; + globals.set("Services", services); + + SectionsManager.updateSections = sinon.spy(); + SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; + await SectionsManager.init({}, storage); + observer.observe("", "nsPref:changed", "MENU_ITEM_PREF"); + + assert.calledOnce(SectionsManager.updateSections); + }); + }); + describe("#_addCardTypeLinkMenuOptions", () => { + const addCardTypeLinkMenuOptionsOrig = + SectionsManager._addCardTypeLinkMenuOptions; + const contextMenuOptionsOrig = + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES; + beforeEach(() => { + // Add a topstories section and a highlights section, with types for each card + SectionsManager.addSection("topstories", { FAKE_TRENDING_ROWS }); + SectionsManager.addSection("highlights", { FAKE_ROWS }); + }); + it("should only call _addCardTypeLinkMenuOptions if the section update is for highlights", () => { + SectionsManager._addCardTypeLinkMenuOptions = sinon.spy(); + SectionsManager.updateSection("topstories", { rows: FAKE_ROWS }, false); + assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions); + + SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); + assert.calledWith(SectionsManager._addCardTypeLinkMenuOptions, FAKE_ROWS); + }); + it("should only call _addCardTypeLinkMenuOptions if the section update has rows", () => { + SectionsManager._addCardTypeLinkMenuOptions = sinon.spy(); + SectionsManager.updateSection("highlights", {}, false); + assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions); + }); + it("should assign the correct context menu options based on the type of highlight", () => { + SectionsManager._addCardTypeLinkMenuOptions = + addCardTypeLinkMenuOptionsOrig; + + SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); + const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS; + + // FAKE_ROWS was added in the following order: bookmark, pocket, history + assert.deepEqual( + highlights[0].contextMenuOptions, + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.bookmark + ); + assert.deepEqual( + highlights[1].contextMenuOptions, + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.pocket + ); + assert.deepEqual( + highlights[2].contextMenuOptions, + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.history + ); + }); + it("should throw an error if you are assigning a context menu to a non-existant highlight type", () => { + globals.sandbox.spy(global.console, "error"); + SectionsManager.updateSection( + "highlights", + { rows: [{ url: "foo", type: "badtype" }] }, + false + ); + const highlights = SectionsManager.sections.get("highlights").rows; + assert.calledOnce(console.error); + assert.equal(highlights[0].contextMenuOptions, undefined); + }); + it("should filter out context menu options that are in CONTEXT_MENU_PREFS", () => { + const services = { + prefs: { + getBoolPref: o => + SectionsManager.CONTEXT_MENU_PREFS[o] !== "RemoveMe", + addObserver() {}, + removeObserver() {}, + }, + }; + globals.set("Services", services); + SectionsManager.CONTEXT_MENU_PREFS = { RemoveMe: "RemoveMe" }; + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = { + bookmark: ["KeepMe", "RemoveMe"], + pocket: ["KeepMe", "RemoveMe"], + history: ["KeepMe", "RemoveMe"], + }; + SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); + const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS; + + // Only keep context menu options that were not supposed to be removed based on CONTEXT_MENU_PREFS + assert.deepEqual(highlights[0].contextMenuOptions, ["KeepMe"]); + assert.deepEqual(highlights[1].contextMenuOptions, ["KeepMe"]); + assert.deepEqual(highlights[2].contextMenuOptions, ["KeepMe"]); + SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = + contextMenuOptionsOrig; + globals.restore(); + }); + }); + describe("#onceInitialized", () => { + it("should call the callback immediately if SectionsManager is initialised", () => { + SectionsManager.initialized = true; + const callback = sinon.spy(); + SectionsManager.onceInitialized(callback); + assert.calledOnce(callback); + }); + it("should bind the callback to .once(INIT) if SectionsManager is not initialised", () => { + SectionsManager.initialized = false; + sinon.spy(SectionsManager, "once"); + const callback = () => {}; + SectionsManager.onceInitialized(callback); + assert.calledOnce(SectionsManager.once); + assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback); + }); + }); + describe("#updateSectionCard", () => { + it("should emit an UPDATE_SECTION_CARD event with correct arguments", () => { + SectionsManager.addSection( + FAKE_ID, + Object.assign({}, FAKE_OPTIONS, { rows: FAKE_ROWS }) + ); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy); + SectionsManager.updateSectionCard( + FAKE_ID, + FAKE_URL, + FAKE_CARD_OPTIONS, + true + ); + assert.calledOnce(spy); + assert.calledWith( + spy, + SectionsManager.UPDATE_SECTION_CARD, + FAKE_ID, + FAKE_URL, + FAKE_CARD_OPTIONS, + true + ); + }); + it("should do nothing if the section doesn't exist", () => { + SectionsManager.removeSection(FAKE_ID); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy); + SectionsManager.updateSectionCard( + FAKE_ID, + FAKE_URL, + FAKE_CARD_OPTIONS, + true + ); + assert.notCalled(spy); + }); + }); + describe("#removeSectionCard", () => { + it("should dispatch an SECTION_UPDATE action in which cards corresponding to the given url are removed", () => { + const rows = [{ url: "foo.com" }, { url: "bar.com" }]; + + SectionsManager.addSection( + FAKE_ID, + Object.assign({}, FAKE_OPTIONS, { rows }) + ); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); + SectionsManager.removeSectionCard(FAKE_ID, "foo.com"); + + assert.calledOnce(spy); + assert.equal(spy.firstCall.args[1], FAKE_ID); + assert.deepEqual(spy.firstCall.args[2].rows, [{ url: "bar.com" }]); + }); + it("should do nothing if the section doesn't exist", () => { + SectionsManager.removeSection(FAKE_ID); + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); + SectionsManager.removeSectionCard(FAKE_ID, "bar.com"); + assert.notCalled(spy); + }); + }); + describe("#updateBookmarkMetadata", () => { + beforeEach(() => { + let rows = [ + { + url: "bar", + title: "title", + description: "description", + image: "image", + type: "trending", + }, + ]; + SectionsManager.addSection("topstories", { rows }); + // Simulate 2 sections. + rows = [ + { + url: "foo", + title: "title", + description: "description", + image: "image", + type: "bookmark", + }, + ]; + SectionsManager.addSection("highlights", { rows }); + }); + + it("shouldn't call PlacesUtils if URL is not in topstories", () => { + SectionsManager.updateBookmarkMetadata({ url: "foo" }); + + assert.notCalled(fakePlacesUtils.history.update); + }); + it("should call PlacesUtils.history.update", () => { + SectionsManager.updateBookmarkMetadata({ url: "bar" }); + + assert.calledOnce(fakePlacesUtils.history.update); + assert.calledWithExactly(fakePlacesUtils.history.update, { + url: "bar", + title: "title", + description: "description", + previewImageURL: "image", + }); + }); + it("should call PlacesUtils.history.insert", () => { + SectionsManager.updateBookmarkMetadata({ url: "bar" }); + + assert.calledOnce(fakePlacesUtils.history.insert); + assert.calledWithExactly(fakePlacesUtils.history.insert, { + url: "bar", + title: "title", + visits: [{}], + }); + }); + }); +}); + +describe("SectionsFeed", () => { + let feed; + let sandbox; + let storage; + let globals; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + SectionsManager.sections.clear(); + SectionsManager.initialized = false; + globals = new GlobalOverrider(); + globals.set("NimbusFeatures", { + newtab: { getAllVariables: sandbox.stub() }, + pocketNewtab: { getAllVariables: sandbox.stub() }, + }); + storage = { + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves(), + }; + feed = new SectionsFeed(); + feed.store = { dispatch: sinon.spy() }; + feed.store = { + dispatch: sinon.spy(), + getState() { + return this.state; + }, + state: { + Prefs: { + values: { + sectionOrder: "topsites,topstories,highlights", + "feeds.topsites": true, + }, + }, + Sections: [{ initialized: false }], + }, + dbStorage: { getDbTable: sandbox.stub().returns(storage) }, + }; + }); + afterEach(() => { + feed.uninit(); + globals.restore(); + }); + describe("#init", () => { + it("should create a SectionsFeed", () => { + assert.instanceOf(feed, SectionsFeed); + }); + it("should bind appropriate listeners", () => { + sinon.spy(SectionsManager, "on"); + feed.init(); + assert.callCount(SectionsManager.on, 4); + for (const [event, listener] of [ + [SectionsManager.ADD_SECTION, feed.onAddSection], + [SectionsManager.REMOVE_SECTION, feed.onRemoveSection], + [SectionsManager.UPDATE_SECTION, feed.onUpdateSection], + [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard], + ]) { + assert.calledWith(SectionsManager.on, event, listener); + } + }); + it("should call onAddSection for any already added sections in SectionsManager", async () => { + await SectionsManager.init({}, storage); + assert.ok(SectionsManager.sections.has("topstories")); + assert.ok(SectionsManager.sections.has("highlights")); + const topstories = SectionsManager.sections.get("topstories"); + const highlights = SectionsManager.sections.get("highlights"); + sinon.spy(feed, "onAddSection"); + feed.init(); + assert.calledTwice(feed.onAddSection); + assert.calledWith( + feed.onAddSection, + SectionsManager.ADD_SECTION, + "topstories", + topstories + ); + assert.calledWith( + feed.onAddSection, + SectionsManager.ADD_SECTION, + "highlights", + highlights + ); + }); + }); + describe("#uninit", () => { + it("should unbind all listeners", () => { + sinon.spy(SectionsManager, "off"); + feed.init(); + feed.uninit(); + assert.callCount(SectionsManager.off, 4); + for (const [event, listener] of [ + [SectionsManager.ADD_SECTION, feed.onAddSection], + [SectionsManager.REMOVE_SECTION, feed.onRemoveSection], + [SectionsManager.UPDATE_SECTION, feed.onUpdateSection], + [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard], + ]) { + assert.calledWith(SectionsManager.off, event, listener); + } + }); + it("should emit an UNINIT event and set SectionsManager.initialized to false", () => { + const spy = sinon.spy(); + SectionsManager.on(SectionsManager.UNINIT, spy); + feed.init(); + feed.uninit(); + assert.calledOnce(spy); + assert.notOk(SectionsManager.initialized); + }); + }); + describe("#onAddSection", () => { + it("should broadcast a SECTION_REGISTER action with the correct data", () => { + feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS); + const [action] = feed.store.dispatch.firstCall.args; + assert.equal(action.type, "SECTION_REGISTER"); + assert.deepEqual( + action.data, + Object.assign({ id: FAKE_ID }, FAKE_OPTIONS) + ); + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); + }); + it("should prepend id to sectionOrder pref if not already included", () => { + feed.store.state.Sections = [ + { id: "topstories", enabled: true }, + { id: "highlights", enabled: true }, + ]; + feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS); + assert.calledWith(feed.store.dispatch, { + data: { + name: "sectionOrder", + value: `${FAKE_ID},topsites,topstories,highlights`, + }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + }); + }); + describe("#onRemoveSection", () => { + it("should broadcast a SECTION_DEREGISTER action with the correct data", () => { + feed.onRemoveSection(null, FAKE_ID); + const [action] = feed.store.dispatch.firstCall.args; + assert.equal(action.type, "SECTION_DEREGISTER"); + assert.deepEqual(action.data, FAKE_ID); + // Should be broadcast + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); + }); + }); + describe("#onUpdateSection", () => { + it("should do nothing if no options are provided", () => { + feed.onUpdateSection(null, FAKE_ID, null); + assert.notCalled(feed.store.dispatch); + }); + it("should dispatch a SECTION_UPDATE action with the correct data", () => { + feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS }); + const [action] = feed.store.dispatch.firstCall.args; + assert.equal(action.type, "SECTION_UPDATE"); + assert.deepEqual(action.data, { id: FAKE_ID, rows: FAKE_ROWS }); + // Should be not broadcast by default, but should update the preloaded tab, so check meta + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE); + }); + it("should broadcast the action only if shouldBroadcast is true", () => { + feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS }, true); + const [action] = feed.store.dispatch.firstCall.args; + // Should be broadcast + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); + }); + }); + describe("#onUpdateSectionCard", () => { + it("should do nothing if no options are provided", () => { + feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null); + assert.notCalled(feed.store.dispatch); + }); + it("should dispatch a SECTION_UPDATE_CARD action with the correct data", () => { + feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS); + const [action] = feed.store.dispatch.firstCall.args; + assert.equal(action.type, "SECTION_UPDATE_CARD"); + assert.deepEqual(action.data, { + id: FAKE_ID, + url: FAKE_URL, + options: FAKE_CARD_OPTIONS, + }); + // Should be not broadcast by default, but should update the preloaded tab, so check meta + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE); + }); + it("should broadcast the action only if shouldBroadcast is true", () => { + feed.onUpdateSectionCard( + null, + FAKE_ID, + FAKE_URL, + FAKE_CARD_OPTIONS, + true + ); + const [action] = feed.store.dispatch.firstCall.args; + // Should be broadcast + assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); + assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); + }); + }); + describe("#onAction", () => { + it("should bind this.init to SectionsManager.INIT on INIT", () => { + sinon.spy(SectionsManager, "once"); + feed.onAction({ type: "INIT" }); + assert.calledOnce(SectionsManager.once); + assert.calledWith(SectionsManager.once, SectionsManager.INIT, feed.init); + }); + it("should call SectionsManager.init on action PREFS_INITIAL_VALUES", () => { + sinon.spy(SectionsManager, "init"); + feed.onAction({ type: "PREFS_INITIAL_VALUES", data: { foo: "bar" } }); + assert.calledOnce(SectionsManager.init); + assert.calledWith(SectionsManager.init, { foo: "bar" }); + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs"); + }); + it("should call SectionsManager.addBuiltInSection on suitable PREF_CHANGED events", () => { + sinon.spy(SectionsManager, "addBuiltInSection"); + feed.onAction({ + type: "PREF_CHANGED", + data: { name: "feeds.section.topstories.options", value: "foo" }, + }); + assert.calledOnce(SectionsManager.addBuiltInSection); + assert.calledWith( + SectionsManager.addBuiltInSection, + "feeds.section.topstories", + "foo" + ); + }); + it("should fire SECTION_OPTIONS_UPDATED on suitable PREF_CHANGED events", async () => { + await feed.onAction({ + type: "PREF_CHANGED", + data: { name: "feeds.section.topstories.options", value: "foo" }, + }); + assert.calledOnce(feed.store.dispatch); + const [action] = feed.store.dispatch.firstCall.args; + assert.equal(action.type, "SECTION_OPTIONS_CHANGED"); + assert.equal(action.data, "topstories"); + }); + it("should call SectionsManager.disableSection on SECTION_DISABLE", () => { + sinon.spy(SectionsManager, "disableSection"); + feed.onAction({ type: "SECTION_DISABLE", data: 1234 }); + assert.calledOnce(SectionsManager.disableSection); + assert.calledWith(SectionsManager.disableSection, 1234); + SectionsManager.disableSection.restore(); + }); + it("should call SectionsManager.enableSection on SECTION_ENABLE", () => { + sinon.spy(SectionsManager, "enableSection"); + feed.onAction({ type: "SECTION_ENABLE", data: 1234 }); + assert.calledOnce(SectionsManager.enableSection); + assert.calledWith(SectionsManager.enableSection, 1234); + SectionsManager.enableSection.restore(); + }); + it("should call the feed's uninit on UNINIT", () => { + sinon.stub(feed, "uninit"); + + feed.onAction({ type: "UNINIT" }); + + assert.calledOnce(feed.uninit); + }); + it("should emit a ACTION_DISPATCHED event and forward any action in ACTIONS_TO_PROXY if there are any sections", () => { + const spy = sinon.spy(); + const allowedActions = SectionsManager.ACTIONS_TO_PROXY; + const disallowedActions = ["PREF_CHANGED", "OPEN_PRIVATE_WINDOW"]; + feed.init(); + SectionsManager.on(SectionsManager.ACTION_DISPATCHED, spy); + // Make sure we start with no sections - no event should be emitted + SectionsManager.sections.clear(); + feed.onAction({ type: allowedActions[0] }); + assert.notCalled(spy); + // Then add a section and check correct behaviour + SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); + for (const action of allowedActions.concat(disallowedActions)) { + feed.onAction({ type: action }); + } + for (const action of allowedActions) { + assert.calledWith(spy, "ACTION_DISPATCHED", action); + } + for (const action of disallowedActions) { + assert.neverCalledWith(spy, "ACTION_DISPATCHED", action); + } + }); + it("should call updateBookmarkMetadata on PLACES_BOOKMARK_ADDED", () => { + const stub = sinon.stub(SectionsManager, "updateBookmarkMetadata"); + + feed.onAction({ type: "PLACES_BOOKMARK_ADDED", data: {} }); + + assert.calledOnce(stub); + }); + it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => { + const stub = sinon.stub(SectionsManager, "updateSectionPrefs"); + + feed.onAction({ type: "UPDATE_SECTION_PREFS", data: {} }); + + assert.calledOnce(stub); + }); + it("should call SectionManager.removeSectionCard on WEBEXT_DISMISS", () => { + const stub = sinon.stub(SectionsManager, "removeSectionCard"); + + feed.onAction( + ac.WebExtEvent(at.WEBEXT_DISMISS, { source: "Foo", url: "bar.com" }) + ); + + assert.calledOnce(stub); + assert.calledWith(stub, "Foo", "bar.com"); + }); + it("should call the feed's moveSection on SECTION_MOVE", () => { + sinon.stub(feed, "moveSection"); + const id = "topsites"; + const direction = +1; + feed.onAction({ type: "SECTION_MOVE", data: { id, direction } }); + + assert.calledOnce(feed.moveSection); + assert.calledWith(feed.moveSection, id, direction); + }); + }); + describe("#moveSection", () => { + it("should Move Down correctly", () => { + feed.store.state.Sections = [ + { id: "topstories", enabled: true }, + { id: "highlights", enabled: true }, + ]; + feed.moveSection("topsites", +1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "topstories,topsites,highlights" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + feed.store.dispatch.resetHistory(); + feed.moveSection("topstories", +1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "topsites,highlights,topstories" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + }); + it("should Move Up correctly", () => { + feed.store.state.Sections = [ + { id: "topstories", enabled: true }, + { id: "highlights", enabled: true }, + ]; + feed.moveSection("topstories", -1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "topstories,topsites,highlights" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + feed.store.dispatch.resetHistory(); + feed.moveSection("highlights", -1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "topsites,highlights,topstories" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + }); + it("should skip over sections that aren't enabled", () => { + feed.store.state.Sections = [ + { id: "topstories", enabled: false }, + { id: "highlights", enabled: true }, + ]; + feed.moveSection("highlights", -1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "highlights,topsites,topstories" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + feed.store.dispatch.resetHistory(); + feed.moveSection("topsites", +1); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { name: "sectionOrder", value: "topstories,highlights,topsites" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + }); + }); +}); |