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 });