tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Sections.test.jsx (14822B)


      1 import { combineReducers, createStore } from "redux";
      2 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
      3 import {
      4  Section,
      5  SectionIntl,
      6  _Sections as Sections,
      7 } from "content-src/components/Sections/Sections";
      8 import { actionTypes as at } from "common/Actions.mjs";
      9 import { mount, shallow } from "enzyme";
     10 import { PlaceholderCard } from "content-src/components/Card/Card";
     11 import { Provider } from "react-redux";
     12 import React from "react";
     13 import { TopSites } from "content-src/components/TopSites/TopSites";
     14 
     15 function mountSectionWithProps(props) {
     16  const store = createStore(combineReducers(reducers), INITIAL_STATE);
     17  return mount(
     18    <Provider store={store}>
     19      <Section {...props} />
     20    </Provider>
     21  );
     22 }
     23 
     24 function mountSectionIntlWithProps(props) {
     25  const store = createStore(combineReducers(reducers), INITIAL_STATE);
     26  return mount(
     27    <Provider store={store}>
     28      <SectionIntl {...props} />
     29    </Provider>
     30  );
     31 }
     32 
     33 describe("<Sections>", () => {
     34  let wrapper;
     35  let FAKE_SECTIONS;
     36  beforeEach(() => {
     37    FAKE_SECTIONS = new Array(5).fill(null).map((v, i) => ({
     38      id: `foo_bar_${i}`,
     39      title: `Foo Bar ${i}`,
     40      enabled: !!(i % 2),
     41      rows: [],
     42    }));
     43    wrapper = shallow(
     44      <Sections
     45        Sections={FAKE_SECTIONS}
     46        Prefs={{
     47          values: { sectionOrder: FAKE_SECTIONS.map(i => i.id).join(",") },
     48        }}
     49      />
     50    );
     51  });
     52  it("should render a Sections element", () => {
     53    assert.ok(wrapper.exists());
     54  });
     55  it("should render a Section for each one passed in props.Sections with .enabled === true", () => {
     56    const sectionElems = wrapper.find(SectionIntl);
     57    assert.lengthOf(sectionElems, 2);
     58    sectionElems.forEach((section, i) => {
     59      assert.equal(section.props().id, FAKE_SECTIONS[2 * i + 1].id);
     60      assert.equal(section.props().enabled, true);
     61    });
     62  });
     63  it("should render Top Sites if feeds.topsites pref is true", () => {
     64    wrapper = shallow(
     65      <Sections
     66        Sections={FAKE_SECTIONS}
     67        Prefs={{
     68          values: {
     69            "feeds.topsites": true,
     70            sectionOrder: "topsites,topstories,highlights",
     71          },
     72        }}
     73      />
     74    );
     75    assert.equal(wrapper.find(TopSites).length, 1);
     76  });
     77  it("should NOT render Top Sites if feeds.topsites pref is false", () => {
     78    wrapper = shallow(
     79      <Sections
     80        Sections={FAKE_SECTIONS}
     81        Prefs={{
     82          values: {
     83            "feeds.topsites": false,
     84            sectionOrder: "topsites,topstories,highlights",
     85          },
     86        }}
     87      />
     88    );
     89    assert.equal(wrapper.find(TopSites).length, 0);
     90  });
     91  it("should render the sections in the order specifed by sectionOrder pref", () => {
     92    wrapper = shallow(
     93      <Sections
     94        Sections={FAKE_SECTIONS}
     95        Prefs={{ values: { sectionOrder: "foo_bar_1,foo_bar_3" } }}
     96      />
     97    );
     98    let sections = wrapper.find(SectionIntl);
     99    assert.lengthOf(sections, 2);
    100    assert.equal(sections.first().props().id, "foo_bar_1");
    101    assert.equal(sections.last().props().id, "foo_bar_3");
    102    wrapper = shallow(
    103      <Sections
    104        Sections={FAKE_SECTIONS}
    105        Prefs={{ values: { sectionOrder: "foo_bar_3,foo_bar_1" } }}
    106      />
    107    );
    108    sections = wrapper.find(SectionIntl);
    109    assert.lengthOf(sections, 2);
    110    assert.equal(sections.first().props().id, "foo_bar_3");
    111    assert.equal(sections.last().props().id, "foo_bar_1");
    112  });
    113 });
    114 
    115 describe("<Section>", () => {
    116  let wrapper;
    117  let FAKE_SECTION;
    118 
    119  beforeEach(() => {
    120    FAKE_SECTION = {
    121      id: `foo_bar_1`,
    122      pref: { collapsed: false },
    123      title: `Foo Bar 1`,
    124      rows: [{ link: "http://localhost", index: 0 }],
    125      emptyState: {
    126        icon: "check",
    127        message: "Some message",
    128      },
    129      rowsPref: "section.rows",
    130      maxRows: 4,
    131      Prefs: { values: { "section.rows": 2 } },
    132    };
    133    wrapper = mountSectionIntlWithProps(FAKE_SECTION);
    134  });
    135 
    136  describe("placeholders", () => {
    137    const CARDS_PER_ROW = 3;
    138    const fakeSite = { link: "http://localhost" };
    139    function renderWithSites(rows) {
    140      const store = createStore(combineReducers(reducers), INITIAL_STATE);
    141      return mount(
    142        <Provider store={store}>
    143          <Section {...FAKE_SECTION} rows={rows} />
    144        </Provider>
    145      );
    146    }
    147 
    148    it("should return 2 row of placeholders if realRows is 0", () => {
    149      wrapper = renderWithSites([]);
    150      assert.lengthOf(wrapper.find(PlaceholderCard), 6);
    151    });
    152    it("should fill in the rest of the rows", () => {
    153      wrapper = renderWithSites(new Array(CARDS_PER_ROW).fill(fakeSite));
    154      assert.lengthOf(
    155        wrapper.find(PlaceholderCard),
    156        CARDS_PER_ROW,
    157        "CARDS_PER_ROW"
    158      );
    159 
    160      wrapper = renderWithSites(new Array(CARDS_PER_ROW + 1).fill(fakeSite));
    161      assert.lengthOf(wrapper.find(PlaceholderCard), 2, "CARDS_PER_ROW + 1");
    162 
    163      wrapper = renderWithSites(new Array(CARDS_PER_ROW + 2).fill(fakeSite));
    164      assert.lengthOf(wrapper.find(PlaceholderCard), 1, "CARDS_PER_ROW + 2");
    165 
    166      wrapper = renderWithSites(
    167        new Array(2 * CARDS_PER_ROW - 1).fill(fakeSite)
    168      );
    169      assert.lengthOf(wrapper.find(PlaceholderCard), 1, "CARDS_PER_ROW - 1");
    170    });
    171    it("should not add placeholders all the rows are full", () => {
    172      wrapper = renderWithSites(new Array(2 * CARDS_PER_ROW).fill(fakeSite));
    173      assert.lengthOf(wrapper.find(PlaceholderCard), 0, "2 rows");
    174    });
    175  });
    176 
    177  describe("empty state", () => {
    178    beforeEach(() => {
    179      Object.assign(FAKE_SECTION, {
    180        initialized: true,
    181        dispatch: () => {},
    182        rows: [],
    183        emptyState: {
    184          message: "Some message",
    185        },
    186      });
    187      wrapper = shallow(<Section {...FAKE_SECTION} />);
    188    });
    189    it("should be shown when rows is empty and initialized is true", () => {
    190      assert.ok(wrapper.find(".empty-state").exists());
    191    });
    192    it("should not be shown in initialized is false", () => {
    193      Object.assign(FAKE_SECTION, {
    194        initialized: false,
    195        rows: [],
    196        emptyState: {
    197          message: "Some message",
    198        },
    199      });
    200      wrapper = shallow(<Section {...FAKE_SECTION} />);
    201      assert.isFalse(wrapper.find(".empty-state").exists());
    202    });
    203    it("no icon should be shown", () => {
    204      assert.lengthOf(wrapper.find(".icon"), 0);
    205    });
    206  });
    207 
    208  describe("impression stats", () => {
    209    const FAKE_TOPSTORIES_SECTION_PROPS = {
    210      id: "TopStories",
    211      title: "Foo Bar 1",
    212      pref: { collapsed: false },
    213      maxRows: 1,
    214      rows: [{ guid: 1 }, { guid: 2 }],
    215      shouldSendImpressionStats: true,
    216 
    217      document: {
    218        visibilityState: "visible",
    219        addEventListener: sinon.stub(),
    220        removeEventListener: sinon.stub(),
    221      },
    222      eventSource: "TOP_STORIES",
    223      options: { personalized: false },
    224    };
    225 
    226    function renderSection(props = {}) {
    227      return shallow(<Section {...FAKE_TOPSTORIES_SECTION_PROPS} {...props} />);
    228    }
    229 
    230    it("should send impression with the right stats when the page loads", () => {
    231      const dispatch = sinon.spy();
    232      renderSection({ dispatch });
    233 
    234      assert.calledOnce(dispatch);
    235 
    236      const [action] = dispatch.firstCall.args;
    237      assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS);
    238      assert.equal(action.data.source, "TOP_STORIES");
    239      assert.deepEqual(action.data.tiles, [{ id: 1 }, { id: 2 }]);
    240    });
    241    it("should not send impression stats if not configured", () => {
    242      const dispatch = sinon.spy();
    243      const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    244        shouldSendImpressionStats: false,
    245        dispatch,
    246      });
    247      renderSection(props);
    248      assert.notCalled(dispatch);
    249    });
    250    it("should not send impression stats if the section is collapsed", () => {
    251      const dispatch = sinon.spy();
    252      const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    253        pref: { collapsed: true },
    254      });
    255      renderSection(props);
    256      assert.notCalled(dispatch);
    257    });
    258    it("should send 1 impression when the page becomes visibile after loading", () => {
    259      const props = {
    260        dispatch: sinon.spy(),
    261        document: {
    262          visibilityState: "hidden",
    263          addEventListener: sinon.spy(),
    264          removeEventListener: sinon.spy(),
    265        },
    266      };
    267 
    268      renderSection(props);
    269 
    270      // Was the event listener added?
    271      assert.calledWith(props.document.addEventListener, "visibilitychange");
    272 
    273      // Make sure dispatch wasn't called yet
    274      assert.notCalled(props.dispatch);
    275 
    276      // Simulate a visibilityChange event
    277      const [, listener] = props.document.addEventListener.firstCall.args;
    278      props.document.visibilityState = "visible";
    279      listener();
    280 
    281      // Did we actually dispatch an event?
    282      assert.calledOnce(props.dispatch);
    283      assert.equal(
    284        props.dispatch.firstCall.args[0].type,
    285        at.TELEMETRY_IMPRESSION_STATS
    286      );
    287 
    288      // Did we remove the event listener?
    289      assert.calledWith(
    290        props.document.removeEventListener,
    291        "visibilitychange",
    292        listener
    293      );
    294    });
    295    it("should remove visibility change listener when section is removed", () => {
    296      const props = {
    297        dispatch: sinon.spy(),
    298        document: {
    299          visibilityState: "hidden",
    300          addEventListener: sinon.spy(),
    301          removeEventListener: sinon.spy(),
    302        },
    303      };
    304 
    305      const section = renderSection(props);
    306      assert.calledWith(props.document.addEventListener, "visibilitychange");
    307      const [, listener] = props.document.addEventListener.firstCall.args;
    308 
    309      section.unmount();
    310      assert.calledWith(
    311        props.document.removeEventListener,
    312        "visibilitychange",
    313        listener
    314      );
    315    });
    316    it("should send an impression if props are updated and props.rows are different", () => {
    317      const props = { dispatch: sinon.spy() };
    318      wrapper = renderSection(props);
    319      props.dispatch.resetHistory();
    320 
    321      // New rows
    322      wrapper.setProps(
    323        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    324          rows: [{ guid: 123 }],
    325        })
    326      );
    327 
    328      assert.calledOnce(props.dispatch);
    329    });
    330    it("should not send an impression if props are updated but props.rows are the same", () => {
    331      const props = { dispatch: sinon.spy() };
    332      wrapper = renderSection(props);
    333      props.dispatch.resetHistory();
    334 
    335      // Only update the disclaimer prop
    336      wrapper.setProps(
    337        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    338          disclaimer: { id: "bar" },
    339        })
    340      );
    341 
    342      assert.notCalled(props.dispatch);
    343    });
    344    it("should not send an impression if props are updated and props.rows are the same but section is collapsed", () => {
    345      const props = { dispatch: sinon.spy() };
    346      wrapper = renderSection(props);
    347      props.dispatch.resetHistory();
    348 
    349      // New rows and collapsed
    350      wrapper.setProps(
    351        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    352          rows: [{ guid: 123 }],
    353          pref: { collapsed: true },
    354        })
    355      );
    356 
    357      assert.notCalled(props.dispatch);
    358 
    359      // Expand the section. Now the impression stats should be sent
    360      wrapper.setProps(
    361        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    362          rows: [{ guid: 123 }],
    363          pref: { collapsed: false },
    364        })
    365      );
    366 
    367      assert.calledOnce(props.dispatch);
    368    });
    369    it("should not send an impression if props are updated but GUIDs are the same", () => {
    370      const props = { dispatch: sinon.spy() };
    371      wrapper = renderSection(props);
    372      props.dispatch.resetHistory();
    373 
    374      wrapper.setProps(
    375        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {
    376          rows: [{ guid: 1 }, { guid: 2 }],
    377        })
    378      );
    379 
    380      assert.notCalled(props.dispatch);
    381    });
    382    it("should only send the latest impression on a visibility change", () => {
    383      const listeners = new Set();
    384      const props = {
    385        dispatch: sinon.spy(),
    386        document: {
    387          visibilityState: "hidden",
    388          addEventListener: (ev, cb) => listeners.add(cb),
    389          removeEventListener: (ev, cb) => listeners.delete(cb),
    390        },
    391      };
    392 
    393      wrapper = renderSection(props);
    394 
    395      // Update twice
    396      wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 123 }] }));
    397      wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 2432 }] }));
    398 
    399      assert.notCalled(props.dispatch);
    400 
    401      // Simulate listeners getting called
    402      props.document.visibilityState = "visible";
    403      listeners.forEach(l => l());
    404 
    405      // Make sure we only sent the latest event
    406      assert.calledOnce(props.dispatch);
    407      const [action] = props.dispatch.firstCall.args;
    408      assert.deepEqual(action.data.tiles, [{ id: 2432 }]);
    409    });
    410  });
    411 
    412  describe("tab rehydrated", () => {
    413    it("should fire NEW_TAB_REHYDRATED event", () => {
    414      const dispatch = sinon.spy();
    415      const TOP_STORIES_SECTION = {
    416        id: "topstories",
    417        title: "TopStories",
    418        pref: { collapsed: false },
    419        initialized: false,
    420        rows: [{ guid: 1, link: "http://localhost", isDefault: true }],
    421        read_more_endpoint: "http://localhost/read-more",
    422        maxRows: 1,
    423        eventSource: "TOP_STORIES",
    424      };
    425      wrapper = shallow(
    426        <Section
    427          Pocket={{ waitingForSpoc: true, pocketCta: {} }}
    428          {...TOP_STORIES_SECTION}
    429          dispatch={dispatch}
    430        />
    431      );
    432      assert.notCalled(dispatch);
    433 
    434      wrapper.setProps({ initialized: true });
    435 
    436      assert.calledOnce(dispatch);
    437      const [action] = dispatch.firstCall.args;
    438      assert.equal("NEW_TAB_REHYDRATED", action.type);
    439    });
    440  });
    441 
    442  describe("#numRows", () => {
    443    it("should return maxRows if there is no rowsPref set", () => {
    444      delete FAKE_SECTION.rowsPref;
    445      wrapper = mountSectionIntlWithProps(FAKE_SECTION);
    446      assert.equal(
    447        wrapper.find(Section).instance().numRows,
    448        FAKE_SECTION.maxRows
    449      );
    450    });
    451 
    452    it("should return number of rows set in Pref if rowsPref is set", () => {
    453      const numRows = 2;
    454      Object.assign(FAKE_SECTION, {
    455        rowsPref: "section.rows",
    456        maxRows: 4,
    457        Prefs: { values: { "section.rows": numRows } },
    458      });
    459      wrapper = mountSectionWithProps(FAKE_SECTION);
    460      assert.equal(wrapper.find(Section).instance().numRows, numRows);
    461    });
    462 
    463    it("should return number of rows set in Pref even if higher than maxRows value", () => {
    464      const numRows = 10;
    465      Object.assign(FAKE_SECTION, {
    466        rowsPref: "section.rows",
    467        maxRows: 4,
    468        Prefs: { values: { "section.rows": numRows } },
    469      });
    470      wrapper = mountSectionWithProps(FAKE_SECTION);
    471      assert.equal(wrapper.find(Section).instance().numRows, numRows);
    472    });
    473  });
    474 });