selectLayoutRender.test.js (17657B)
1 import { combineReducers, createStore } from "redux"; 2 import { actionTypes as at } from "common/Actions.mjs"; 3 import { GlobalOverrider } from "test/unit/utils"; 4 import { reducers } from "common/Reducers.sys.mjs"; 5 import { selectLayoutRender } from "content-src/lib/selectLayoutRender"; 6 const FAKE_LAYOUT = [ 7 { 8 width: 3, 9 components: [ 10 { type: "foo", feed: { url: "foo.com" }, properties: { items: 2 } }, 11 ], 12 }, 13 ]; 14 const FAKE_FEEDS = { 15 "foo.com": { data: { recommendations: [{ id: "foo" }, { id: "bar" }] } }, 16 }; 17 18 describe("selectLayoutRender", () => { 19 let store; 20 let globals; 21 22 beforeEach(() => { 23 globals = new GlobalOverrider(); 24 store = createStore(combineReducers(reducers)); 25 }); 26 27 afterEach(() => { 28 globals.restore(); 29 }); 30 31 it("should return an empty array given initial state", () => { 32 const { layoutRender } = selectLayoutRender({ 33 state: store.getState().DiscoveryStream, 34 prefs: {}, 35 rollCache: [], 36 }); 37 assert.deepEqual(layoutRender, []); 38 }); 39 40 it("should add .data property from feeds to each component in .layout", () => { 41 store.dispatch({ 42 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 43 data: { layout: FAKE_LAYOUT }, 44 }); 45 store.dispatch({ 46 type: at.DISCOVERY_STREAM_FEED_UPDATE, 47 data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, 48 }); 49 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 50 51 const { layoutRender } = selectLayoutRender({ 52 state: store.getState().DiscoveryStream, 53 }); 54 55 assert.lengthOf(layoutRender, 1); 56 assert.propertyVal(layoutRender[0], "width", 3); 57 assert.deepEqual(layoutRender[0].components[0], { 58 type: "foo", 59 feed: { url: "foo.com" }, 60 properties: { items: 2 }, 61 data: { 62 recommendations: [ 63 { id: "foo", pos: 0 }, 64 { id: "bar", pos: 1 }, 65 ], 66 sections: [], 67 }, 68 }); 69 }); 70 71 it("should return layout with placeholder data if feed doesn't have data", () => { 72 store.dispatch({ 73 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 74 data: { layout: FAKE_LAYOUT }, 75 }); 76 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 77 78 const { layoutRender } = selectLayoutRender({ 79 state: store.getState().DiscoveryStream, 80 }); 81 82 assert.lengthOf(layoutRender, 1); 83 assert.propertyVal(layoutRender[0], "width", 3); 84 assert.deepEqual(layoutRender[0].components[0].data.recommendations, [ 85 { placeholder: true }, 86 { placeholder: true }, 87 ]); 88 }); 89 90 it("should return layout with empty spocs data if feed isn't defined but spocs is", () => { 91 const fakeLayout = [ 92 { 93 width: 3, 94 components: [{ type: "foo", spocs: { positions: [{ index: 2 }] } }], 95 }, 96 ]; 97 store.dispatch({ 98 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 99 data: { layout: fakeLayout }, 100 }); 101 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 102 103 const { layoutRender } = selectLayoutRender({ 104 state: store.getState().DiscoveryStream, 105 }); 106 107 assert.lengthOf(layoutRender, 1); 108 assert.propertyVal(layoutRender[0], "width", 3); 109 assert.deepEqual(layoutRender[0].components[0].data.spocs, []); 110 }); 111 112 it("should return layout with spocs data if feed isn't defined but spocs is", () => { 113 const fakeLayout = [ 114 { 115 width: 3, 116 components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }], 117 }, 118 ]; 119 store.dispatch({ 120 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 121 data: { layout: fakeLayout }, 122 }); 123 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 124 store.dispatch({ 125 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 126 data: { 127 lastUpdated: 0, 128 spocs: { 129 newtab_spocs: { 130 items: [{ id: 1 }, { id: 2 }, { id: 3 }], 131 }, 132 }, 133 }, 134 }); 135 136 const { layoutRender } = selectLayoutRender({ 137 state: store.getState().DiscoveryStream, 138 }); 139 140 assert.lengthOf(layoutRender, 1); 141 assert.propertyVal(layoutRender[0], "width", 3); 142 assert.deepEqual(layoutRender[0].components[0].data.spocs, [ 143 { id: 1, pos: 0 }, 144 { id: 2, pos: 1 }, 145 { id: 3, pos: 2 }, 146 ]); 147 }); 148 149 it("should return layout with no spocs data if feed and spocs are unavailable", () => { 150 const fakeLayout = [ 151 { 152 width: 3, 153 components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }], 154 }, 155 ]; 156 store.dispatch({ 157 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 158 data: { layout: fakeLayout }, 159 }); 160 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 161 store.dispatch({ 162 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 163 data: { 164 lastUpdated: 0, 165 spocs: { 166 spocs: { 167 items: [], 168 }, 169 }, 170 }, 171 }); 172 173 const { layoutRender } = selectLayoutRender({ 174 state: store.getState().DiscoveryStream, 175 }); 176 177 assert.lengthOf(layoutRender, 1); 178 assert.propertyVal(layoutRender[0], "width", 3); 179 assert.equal(layoutRender[0].components[0].data.spocs.length, 0); 180 }); 181 182 it("should return feed data offset by layout set prop", () => { 183 const fakeLayout = [ 184 { 185 width: 3, 186 components: [ 187 { type: "foo", properties: { offset: 1 }, feed: { url: "foo.com" } }, 188 ], 189 }, 190 ]; 191 store.dispatch({ 192 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 193 data: { layout: fakeLayout }, 194 }); 195 store.dispatch({ 196 type: at.DISCOVERY_STREAM_FEED_UPDATE, 197 data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, 198 }); 199 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 200 201 const { layoutRender } = selectLayoutRender({ 202 state: store.getState().DiscoveryStream, 203 }); 204 205 assert.deepEqual(layoutRender[0].components[0].data, { 206 recommendations: [{ id: "bar" }], 207 sections: [], 208 }); 209 }); 210 211 it("should return spoc result when there are more positions than spocs", () => { 212 const fakeSpocConfig = { 213 positions: [{ index: 0 }, { index: 1 }, { index: 2 }], 214 }; 215 const fakeLayout = [ 216 { 217 width: 3, 218 components: [ 219 { type: "foo", feed: { url: "foo.com" }, spocs: fakeSpocConfig }, 220 ], 221 }, 222 ]; 223 const fakeSpocsData = { 224 lastUpdated: 0, 225 spocs: { newtab_spocs: { items: ["fooSpoc", "barSpoc"] } }, 226 }; 227 228 store.dispatch({ 229 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 230 data: { layout: fakeLayout }, 231 }); 232 store.dispatch({ 233 type: at.DISCOVERY_STREAM_FEED_UPDATE, 234 data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, 235 }); 236 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 237 store.dispatch({ 238 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 239 data: fakeSpocsData, 240 }); 241 242 const { layoutRender } = selectLayoutRender({ 243 state: store.getState().DiscoveryStream, 244 }); 245 246 assert.lengthOf(layoutRender, 1); 247 assert.deepEqual( 248 layoutRender[0].components[0].data.recommendations[0], 249 "fooSpoc" 250 ); 251 assert.deepEqual( 252 layoutRender[0].components[0].data.recommendations[1], 253 "barSpoc" 254 ); 255 assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], { 256 id: "foo", 257 }); 258 assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], { 259 id: "bar", 260 }); 261 }); 262 263 it("should return a layout with feeds of items length with positions", () => { 264 const fakeLayout = [ 265 { 266 width: 3, 267 components: [ 268 { type: "foo", properties: { items: 3 }, feed: { url: "foo.com" } }, 269 ], 270 }, 271 ]; 272 const fakeRecommendations = [ 273 { name: "item1" }, 274 { name: "item2" }, 275 { name: "item3" }, 276 { name: "item4" }, 277 ]; 278 const fakeFeeds = { 279 "foo.com": { data: { recommendations: fakeRecommendations } }, 280 }; 281 store.dispatch({ 282 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 283 data: { layout: fakeLayout }, 284 }); 285 store.dispatch({ 286 type: at.DISCOVERY_STREAM_FEED_UPDATE, 287 data: { feed: fakeFeeds["foo.com"], url: "foo.com" }, 288 }); 289 store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); 290 291 const { layoutRender } = selectLayoutRender({ 292 state: store.getState().DiscoveryStream, 293 }); 294 295 const { recommendations } = layoutRender[0].components[0].data; 296 assert.equal(recommendations.length, 4); 297 assert.equal(recommendations[0].pos, 0); 298 assert.equal(recommendations[1].pos, 1); 299 assert.equal(recommendations[2].pos, 2); 300 assert.equal(recommendations[3].pos, undefined); 301 }); 302 303 it("should render everything if everything is ready", () => { 304 const fakeLayout = [ 305 { 306 width: 3, 307 components: [ 308 { type: "foo1" }, 309 { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, 310 { type: "foo3", properties: { items: 3 }, feed: { url: "foo3.com" } }, 311 { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, 312 { type: "foo5" }, 313 ], 314 }, 315 ]; 316 store.dispatch({ 317 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 318 data: { layout: fakeLayout }, 319 }); 320 store.dispatch({ 321 type: at.DISCOVERY_STREAM_FEED_UPDATE, 322 data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, 323 }); 324 store.dispatch({ 325 type: at.DISCOVERY_STREAM_FEED_UPDATE, 326 data: { feed: { data: { recommendations: [] } }, url: "foo3.com" }, 327 }); 328 store.dispatch({ 329 type: at.DISCOVERY_STREAM_FEED_UPDATE, 330 data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, 331 }); 332 333 const { layoutRender } = selectLayoutRender({ 334 state: store.getState().DiscoveryStream, 335 }); 336 337 assert.equal(layoutRender[0].components[0].type, "foo1"); 338 assert.equal(layoutRender[0].components[1].type, "foo2"); 339 assert.equal(layoutRender[0].components[2].type, "foo3"); 340 assert.equal(layoutRender[0].components[3].type, "foo4"); 341 assert.equal(layoutRender[0].components[4].type, "foo5"); 342 }); 343 344 it("should stop rendering feeds if we hit a not ready spoc", () => { 345 const fakeLayout = [ 346 { 347 width: 3, 348 components: [ 349 { type: "foo1" }, 350 { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, 351 { 352 type: "foo3", 353 properties: { items: 3 }, 354 feed: { url: "foo3.com" }, 355 spocs: { positions: [{ index: 0 }] }, 356 }, 357 { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, 358 { type: "foo5" }, 359 ], 360 }, 361 ]; 362 store.dispatch({ 363 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 364 data: { layout: fakeLayout }, 365 }); 366 store.dispatch({ 367 type: at.DISCOVERY_STREAM_FEED_UPDATE, 368 data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, 369 }); 370 store.dispatch({ 371 type: at.DISCOVERY_STREAM_FEED_UPDATE, 372 data: { feed: { data: { recommendations: [] } }, url: "foo3.com" }, 373 }); 374 store.dispatch({ 375 type: at.DISCOVERY_STREAM_FEED_UPDATE, 376 data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, 377 }); 378 379 const { layoutRender } = selectLayoutRender({ 380 state: store.getState().DiscoveryStream, 381 }); 382 383 assert.equal(layoutRender[0].components[0].type, "foo1"); 384 assert.equal(layoutRender[0].components[1].type, "foo2"); 385 assert.deepEqual(layoutRender[0].components[2].data.recommendations, [ 386 { placeholder: true }, 387 { placeholder: true }, 388 { placeholder: true }, 389 ]); 390 }); 391 392 it("should not render a spoc if there are no available spocs", () => { 393 const fakeLayout = [ 394 { 395 width: 3, 396 components: [ 397 { type: "foo1" }, 398 { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, 399 { 400 type: "foo3", 401 properties: { items: 3 }, 402 feed: { url: "foo3.com" }, 403 spocs: { positions: [{ index: 0 }] }, 404 }, 405 { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, 406 { type: "foo5" }, 407 ], 408 }, 409 ]; 410 const fakeSpocsData = { lastUpdated: 0, spocs: { spocs: [] } }; 411 store.dispatch({ 412 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 413 data: { layout: fakeLayout }, 414 }); 415 store.dispatch({ 416 type: at.DISCOVERY_STREAM_FEED_UPDATE, 417 data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, 418 }); 419 store.dispatch({ 420 type: at.DISCOVERY_STREAM_FEED_UPDATE, 421 data: { 422 feed: { data: { recommendations: [{ name: "rec" }] } }, 423 url: "foo3.com", 424 }, 425 }); 426 store.dispatch({ 427 type: at.DISCOVERY_STREAM_FEED_UPDATE, 428 data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, 429 }); 430 store.dispatch({ 431 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 432 data: fakeSpocsData, 433 }); 434 435 const { layoutRender } = selectLayoutRender({ 436 state: store.getState().DiscoveryStream, 437 }); 438 439 assert.deepEqual(layoutRender[0].components[2].data.recommendations[0], { 440 name: "rec", 441 pos: 0, 442 }); 443 }); 444 445 it("should not render a row if no components exist after filter in that row", () => { 446 const fakeLayout = [ 447 { 448 width: 3, 449 components: [{ type: "TopSites" }], 450 }, 451 { 452 width: 3, 453 components: [{ type: "Message" }], 454 }, 455 ]; 456 store.dispatch({ 457 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 458 data: { layout: fakeLayout }, 459 }); 460 461 const { layoutRender } = selectLayoutRender({ 462 state: store.getState().DiscoveryStream, 463 prefs: { "feeds.topsites": true }, 464 }); 465 466 assert.equal(layoutRender[0].components[0].type, "TopSites"); 467 assert.equal(layoutRender[1], undefined); 468 }); 469 470 it("should not render a component if filtered", () => { 471 const fakeLayout = [ 472 { 473 width: 3, 474 components: [{ type: "Message" }, { type: "TopSites" }], 475 }, 476 ]; 477 store.dispatch({ 478 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 479 data: { layout: fakeLayout }, 480 }); 481 482 const { layoutRender } = selectLayoutRender({ 483 state: store.getState().DiscoveryStream, 484 prefs: { "feeds.topsites": true }, 485 }); 486 487 assert.equal(layoutRender[0].components[0].type, "TopSites"); 488 assert.equal(layoutRender[0].components[1], undefined); 489 }); 490 491 it("should skip rendering a spoc in position if that spoc is blocked for that session", () => { 492 const fakeLayout = [ 493 { 494 width: 3, 495 components: [ 496 { 497 type: "foo1", 498 properties: { items: 3 }, 499 feed: { url: "foo1.com" }, 500 spocs: { positions: [{ index: 0 }] }, 501 }, 502 ], 503 }, 504 ]; 505 const fakeSpocsData = { 506 lastUpdated: 0, 507 spocs: { 508 newtab_spocs: { items: [{ name: "spoc", url: "https://foo.com" }] }, 509 }, 510 }; 511 store.dispatch({ 512 type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, 513 data: { layout: fakeLayout }, 514 }); 515 store.dispatch({ 516 type: at.DISCOVERY_STREAM_FEED_UPDATE, 517 data: { 518 feed: { data: { recommendations: [{ name: "rec" }] } }, 519 url: "foo1.com", 520 }, 521 }); 522 store.dispatch({ 523 type: at.DISCOVERY_STREAM_SPOCS_UPDATE, 524 data: fakeSpocsData, 525 }); 526 527 const { layoutRender: layout1 } = selectLayoutRender({ 528 state: store.getState().DiscoveryStream, 529 }); 530 531 store.dispatch({ 532 type: at.DISCOVERY_STREAM_SPOC_BLOCKED, 533 data: { url: "https://foo.com" }, 534 }); 535 536 const { layoutRender: layout2 } = selectLayoutRender({ 537 state: store.getState().DiscoveryStream, 538 }); 539 540 assert.deepEqual(layout1[0].components[0].data.recommendations[0], { 541 name: "spoc", 542 url: "https://foo.com", 543 pos: 0, 544 }); 545 assert.deepEqual(layout2[0].components[0].data.recommendations[0], { 546 name: "rec", 547 pos: 0, 548 }); 549 }); 550 551 it("should include Widgets when widgets.system.enabled is true", () => { 552 const { layoutRender } = selectLayoutRender({ 553 prefs: { 554 "widgets.system.enabled": true, 555 "feeds.section.topstories": false, 556 "feeds.system.topstories": false, 557 }, 558 state: { 559 layout: [ 560 { 561 width: 12, 562 components: [{ type: "Widgets" }], 563 }, 564 ], 565 }, 566 }); 567 568 assert.lengthOf(layoutRender, 1); 569 assert.lengthOf(layoutRender[0].components, 1); 570 assert.propertyVal(layoutRender[0].components[0], "type", "Widgets"); 571 }); 572 573 it("should include Widgets when (Nimbus) widgetsConfig.enabled is true", () => { 574 const { layoutRender } = selectLayoutRender({ 575 prefs: { 576 widgetsConfig: { enabled: true }, 577 "feeds.section.topstories": false, 578 "feeds.system.topstories": false, 579 }, 580 state: { 581 layout: [ 582 { 583 width: 12, 584 components: [{ type: "Widgets" }], 585 }, 586 ], 587 }, 588 }); 589 590 assert.lengthOf(layoutRender, 1); 591 assert.lengthOf(layoutRender[0].components, 1); 592 assert.propertyVal(layoutRender[0].components[0], "type", "Widgets"); 593 }); 594 595 it("should filter out Widgets when both widget prefs are false", () => { 596 const { layoutRender } = selectLayoutRender({ 597 prefs: { 598 "widgets.system.enabled": false, 599 widgetsConfig: { enabled: false }, 600 "feeds.section.topstories": false, 601 "feeds.system.topstories": false, 602 }, 603 state: { 604 layout: [ 605 { 606 width: 12, 607 components: [{ type: "Widgets" }], 608 }, 609 ], 610 }, 611 }); 612 613 assert.lengthOf(layoutRender, 0); 614 }); 615 });