import { combineReducers, createStore } from "redux"; import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; import { Section, SectionIntl, _Sections as Sections, } from "content-src/components/Sections/Sections"; import { actionTypes as at } from "common/Actions.sys.mjs"; import { mount, shallow } from "enzyme"; import { PlaceholderCard } from "content-src/components/Card/Card"; import { PocketLoggedInCta } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta"; import { Provider } from "react-redux"; import React from "react"; import { Topics } from "content-src/components/Topics/Topics"; import { TopSites } from "content-src/components/TopSites/TopSites"; function mountSectionWithProps(props) { const store = createStore(combineReducers(reducers), INITIAL_STATE); return mount(
); } function mountSectionIntlWithProps(props) { const store = createStore(combineReducers(reducers), INITIAL_STATE); return mount( ); } describe("", () => { let wrapper; let FAKE_SECTIONS; beforeEach(() => { FAKE_SECTIONS = new Array(5).fill(null).map((v, i) => ({ id: `foo_bar_${i}`, title: `Foo Bar ${i}`, enabled: !!(i % 2), rows: [], })); wrapper = shallow( i.id).join(",") }, }} /> ); }); it("should render a Sections element", () => { assert.ok(wrapper.exists()); }); it("should render a Section for each one passed in props.Sections with .enabled === true", () => { const sectionElems = wrapper.find(SectionIntl); assert.lengthOf(sectionElems, 2); sectionElems.forEach((section, i) => { assert.equal(section.props().id, FAKE_SECTIONS[2 * i + 1].id); assert.equal(section.props().enabled, true); }); }); it("should render Top Sites if feeds.topsites pref is true", () => { wrapper = shallow( ); assert.equal(wrapper.find(TopSites).length, 1); }); it("should NOT render Top Sites if feeds.topsites pref is false", () => { wrapper = shallow( ); assert.equal(wrapper.find(TopSites).length, 0); }); it("should render the sections in the order specifed by sectionOrder pref", () => { wrapper = shallow( ); let sections = wrapper.find(SectionIntl); assert.lengthOf(sections, 2); assert.equal(sections.first().props().id, "foo_bar_1"); assert.equal(sections.last().props().id, "foo_bar_3"); wrapper = shallow( ); sections = wrapper.find(SectionIntl); assert.lengthOf(sections, 2); assert.equal(sections.first().props().id, "foo_bar_3"); assert.equal(sections.last().props().id, "foo_bar_1"); }); }); describe("
", () => { let wrapper; let FAKE_SECTION; beforeEach(() => { FAKE_SECTION = { id: `foo_bar_1`, pref: { collapsed: false }, title: `Foo Bar 1`, rows: [{ link: "http://localhost", index: 0 }], emptyState: { icon: "check", message: "Some message", }, rowsPref: "section.rows", maxRows: 4, Prefs: { values: { "section.rows": 2 } }, }; wrapper = mountSectionIntlWithProps(FAKE_SECTION); }); describe("placeholders", () => { const CARDS_PER_ROW = 3; const fakeSite = { link: "http://localhost" }; function renderWithSites(rows) { const store = createStore(combineReducers(reducers), INITIAL_STATE); return mount(
); } it("should return 2 row of placeholders if realRows is 0", () => { wrapper = renderWithSites([]); assert.lengthOf(wrapper.find(PlaceholderCard), 6); }); it("should fill in the rest of the rows", () => { wrapper = renderWithSites(new Array(CARDS_PER_ROW).fill(fakeSite)); assert.lengthOf( wrapper.find(PlaceholderCard), CARDS_PER_ROW, "CARDS_PER_ROW" ); wrapper = renderWithSites(new Array(CARDS_PER_ROW + 1).fill(fakeSite)); assert.lengthOf(wrapper.find(PlaceholderCard), 2, "CARDS_PER_ROW + 1"); wrapper = renderWithSites(new Array(CARDS_PER_ROW + 2).fill(fakeSite)); assert.lengthOf(wrapper.find(PlaceholderCard), 1, "CARDS_PER_ROW + 2"); wrapper = renderWithSites( new Array(2 * CARDS_PER_ROW - 1).fill(fakeSite) ); assert.lengthOf(wrapper.find(PlaceholderCard), 1, "CARDS_PER_ROW - 1"); }); it("should not add placeholders all the rows are full", () => { wrapper = renderWithSites(new Array(2 * CARDS_PER_ROW).fill(fakeSite)); assert.lengthOf(wrapper.find(PlaceholderCard), 0, "2 rows"); }); }); describe("empty state", () => { beforeEach(() => { Object.assign(FAKE_SECTION, { initialized: true, dispatch: () => {}, rows: [], emptyState: { message: "Some message", }, }); wrapper = shallow(
); }); it("should be shown when rows is empty and initialized is true", () => { assert.ok(wrapper.find(".empty-state").exists()); }); it("should not be shown in initialized is false", () => { Object.assign(FAKE_SECTION, { initialized: false, rows: [], emptyState: { message: "Some message", }, }); wrapper = shallow(
); assert.isFalse(wrapper.find(".empty-state").exists()); }); it("no icon should be shown", () => { assert.lengthOf(wrapper.find(".icon"), 0); }); }); describe("topics component", () => { let TOP_STORIES_SECTION; beforeEach(() => { TOP_STORIES_SECTION = { id: "topstories", title: "TopStories", pref: { collapsed: false }, rows: [{ guid: 1, link: "http://localhost", isDefault: true }], topics: [], read_more_endpoint: "http://localhost/read-more", maxRows: 1, eventSource: "TOP_STORIES", }; }); it("should not render for empty topics", () => { wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION); assert.lengthOf(wrapper.find(".topic"), 0); }); it("should render for non-empty topics", () => { TOP_STORIES_SECTION.topics = [{ name: "topic1", url: "topic-url1" }]; wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 1); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); }); it("should delay render of third rec to give time for potential spoc", async () => { TOP_STORIES_SECTION.rows = [ { guid: 1, link: "http://localhost" }, { guid: 2, link: "http://localhost" }, { guid: 3, link: "http://localhost" }, ]; wrapper = shallow(
); assert.lengthOf(wrapper.find(PlaceholderCard), 1); wrapper.setProps({ Pocket: { waitingForSpoc: false, pocketCta: {}, }, }); assert.lengthOf(wrapper.find(PlaceholderCard), 0); }); it("should render container for uninitialized topics to ensure content doesn't shift", () => { delete TOP_STORIES_SECTION.topics; wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION); assert.lengthOf(wrapper.find(".top-stories-bottom-container"), 1); assert.lengthOf(wrapper.find(Topics), 0); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); }); it("should render a pocket cta if not logged in and set to display cta", () => { TOP_STORIES_SECTION.topics = [{ name: "topic1", url: "topic-url1" }]; wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 0); assert.lengthOf(wrapper.find(PocketLoggedInCta), 1); }); it("should render nothing while loading to avoid a flicker of log in state", () => { TOP_STORIES_SECTION.topics = [{ name: "topic1", url: "topic-url1" }]; wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 0); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); }); it("should render a topics list if set to not display cta with either logged or out", () => { TOP_STORIES_SECTION.topics = [{ name: "topic1", url: "topic-url1" }]; wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 1); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 1); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); }); it("should render nothing if set to display a cta and not logged in or out (waiting for state)", () => { TOP_STORIES_SECTION.topics = [{ name: "topic1", url: "topic-url1" }]; wrapper = shallow(
); assert.lengthOf(wrapper.find(Topics), 0); assert.lengthOf(wrapper.find(PocketLoggedInCta), 0); }); }); describe("impression stats", () => { const FAKE_TOPSTORIES_SECTION_PROPS = { id: "TopStories", title: "Foo Bar 1", pref: { collapsed: false }, maxRows: 1, rows: [{ guid: 1 }, { guid: 2 }], shouldSendImpressionStats: true, document: { visibilityState: "visible", addEventListener: sinon.stub(), removeEventListener: sinon.stub(), }, eventSource: "TOP_STORIES", options: { personalized: false }, }; function renderSection(props = {}) { return shallow(
); } it("should send impression with the right stats when the page loads", () => { const dispatch = sinon.spy(); renderSection({ dispatch }); assert.calledOnce(dispatch); const [action] = dispatch.firstCall.args; assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS); assert.equal(action.data.source, "TOP_STORIES"); assert.deepEqual(action.data.tiles, [{ id: 1 }, { id: 2 }]); }); it("should not send impression stats if not configured", () => { const dispatch = sinon.spy(); const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { shouldSendImpressionStats: false, dispatch, }); renderSection(props); assert.notCalled(dispatch); }); it("should not send impression stats if the section is collapsed", () => { const dispatch = sinon.spy(); const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { pref: { collapsed: true }, }); renderSection(props); assert.notCalled(dispatch); }); it("should send 1 impression when the page becomes visibile after loading", () => { const props = { dispatch: sinon.spy(), document: { visibilityState: "hidden", addEventListener: sinon.spy(), removeEventListener: sinon.spy(), }, }; renderSection(props); // Was the event listener added? assert.calledWith(props.document.addEventListener, "visibilitychange"); // Make sure dispatch wasn't called yet assert.notCalled(props.dispatch); // Simulate a visibilityChange event const [, listener] = props.document.addEventListener.firstCall.args; props.document.visibilityState = "visible"; listener(); // Did we actually dispatch an event? assert.calledOnce(props.dispatch); assert.equal( props.dispatch.firstCall.args[0].type, at.TELEMETRY_IMPRESSION_STATS ); // Did we remove the event listener? assert.calledWith( props.document.removeEventListener, "visibilitychange", listener ); }); it("should remove visibility change listener when section is removed", () => { const props = { dispatch: sinon.spy(), document: { visibilityState: "hidden", addEventListener: sinon.spy(), removeEventListener: sinon.spy(), }, }; const section = renderSection(props); assert.calledWith(props.document.addEventListener, "visibilitychange"); const [, listener] = props.document.addEventListener.firstCall.args; section.unmount(); assert.calledWith( props.document.removeEventListener, "visibilitychange", listener ); }); it("should send an impression if props are updated and props.rows are different", () => { const props = { dispatch: sinon.spy() }; wrapper = renderSection(props); props.dispatch.resetHistory(); // New rows wrapper.setProps( Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { rows: [{ guid: 123 }], }) ); assert.calledOnce(props.dispatch); }); it("should not send an impression if props are updated but props.rows are the same", () => { const props = { dispatch: sinon.spy() }; wrapper = renderSection(props); props.dispatch.resetHistory(); // Only update the disclaimer prop wrapper.setProps( Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { disclaimer: { id: "bar" }, }) ); assert.notCalled(props.dispatch); }); it("should not send an impression if props are updated and props.rows are the same but section is collapsed", () => { const props = { dispatch: sinon.spy() }; wrapper = renderSection(props); props.dispatch.resetHistory(); // New rows and collapsed wrapper.setProps( Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { rows: [{ guid: 123 }], pref: { collapsed: true }, }) ); assert.notCalled(props.dispatch); // Expand the section. Now the impression stats should be sent wrapper.setProps( Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { rows: [{ guid: 123 }], pref: { collapsed: false }, }) ); assert.calledOnce(props.dispatch); }); it("should not send an impression if props are updated but GUIDs are the same", () => { const props = { dispatch: sinon.spy() }; wrapper = renderSection(props); props.dispatch.resetHistory(); wrapper.setProps( Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, { rows: [{ guid: 1 }, { guid: 2 }], }) ); assert.notCalled(props.dispatch); }); it("should only send the latest impression on a visibility change", () => { const listeners = new Set(); const props = { dispatch: sinon.spy(), document: { visibilityState: "hidden", addEventListener: (ev, cb) => listeners.add(cb), removeEventListener: (ev, cb) => listeners.delete(cb), }, }; wrapper = renderSection(props); // Update twice wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 123 }] })); wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 2432 }] })); assert.notCalled(props.dispatch); // Simulate listeners getting called props.document.visibilityState = "visible"; listeners.forEach(l => l()); // Make sure we only sent the latest event assert.calledOnce(props.dispatch); const [action] = props.dispatch.firstCall.args; assert.deepEqual(action.data.tiles, [{ id: 2432 }]); }); }); describe("tab rehydrated", () => { it("should fire NEW_TAB_REHYDRATED event", () => { const dispatch = sinon.spy(); const TOP_STORIES_SECTION = { id: "topstories", title: "TopStories", pref: { collapsed: false }, initialized: false, rows: [{ guid: 1, link: "http://localhost", isDefault: true }], topics: [], read_more_endpoint: "http://localhost/read-more", maxRows: 1, eventSource: "TOP_STORIES", }; wrapper = shallow(
); assert.notCalled(dispatch); wrapper.setProps({ initialized: true }); assert.calledOnce(dispatch); const [action] = dispatch.firstCall.args; assert.equal("NEW_TAB_REHYDRATED", action.type); }); }); describe("#numRows", () => { it("should return maxRows if there is no rowsPref set", () => { delete FAKE_SECTION.rowsPref; wrapper = mountSectionIntlWithProps(FAKE_SECTION); assert.equal( wrapper.find(Section).instance().numRows, FAKE_SECTION.maxRows ); }); it("should return number of rows set in Pref if rowsPref is set", () => { const numRows = 2; Object.assign(FAKE_SECTION, { rowsPref: "section.rows", maxRows: 4, Prefs: { values: { "section.rows": numRows } }, }); wrapper = mountSectionWithProps(FAKE_SECTION); assert.equal(wrapper.find(Section).instance().numRows, numRows); }); it("should return number of rows set in Pref even if higher than maxRows value", () => { const numRows = 10; Object.assign(FAKE_SECTION, { rowsPref: "section.rows", maxRows: 4, Prefs: { values: { "section.rows": numRows } }, }); wrapper = mountSectionWithProps(FAKE_SECTION); assert.equal(wrapper.find(Section).instance().numRows, numRows); }); }); });