DSCard.test.jsx (29269B)
1 import { 2 _DSCard as DSCard, 3 readTimeFromWordCount, 4 DSSource, 5 DefaultMeta, 6 PlaceholderDSCard, 7 } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; 8 import { 9 DSContextFooter, 10 StatusMessage, 11 SponsorLabel, 12 } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; 13 import { actionCreators as ac } from "common/Actions.mjs"; 14 import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu"; 15 import { DSImage } from "content-src/components/DiscoveryStreamComponents/DSImage/DSImage"; 16 import React from "react"; 17 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; 18 import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; 19 import { shallow, mount } from "enzyme"; 20 import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; 21 import { Provider } from "react-redux"; 22 import { combineReducers, createStore } from "redux"; 23 24 const DEFAULT_PROPS = { 25 url: "about:robots", 26 title: "title", 27 raw_image_src: "https://picsum.photos/200", 28 icon_src: "https://picsum.photos/200", 29 App: { 30 isForStartupCache: false, 31 }, 32 DiscoveryStream: INITIAL_STATE.DiscoveryStream, 33 Prefs: INITIAL_STATE.Prefs, 34 fetchTimestamp: new Date("March 20, 2024 10:30:44").getTime(), 35 firstVisibleTimestamp: new Date("March 21, 2024 10:11:12").getTime(), 36 }; 37 38 describe("<DSCard>", () => { 39 let wrapper; 40 let sandbox; 41 let dispatch; 42 43 beforeEach(() => { 44 sandbox = sinon.createSandbox(); 45 dispatch = sandbox.stub(); 46 wrapper = shallow(<DSCard dispatch={dispatch} {...DEFAULT_PROPS} />); 47 wrapper.setState({ isSeen: true }); 48 }); 49 50 afterEach(() => { 51 sandbox.restore(); 52 }); 53 54 it("should render", () => { 55 assert.ok(wrapper.exists()); 56 assert.ok(wrapper.find(".ds-card")); 57 }); 58 59 it("should render a SafeAnchor", () => { 60 wrapper.setProps({ url: "https://foo.com" }); 61 62 assert.equal(wrapper.children().at(0).type(), SafeAnchor); 63 assert.propertyVal( 64 wrapper.children().at(0).props(), 65 "url", 66 "https://foo.com" 67 ); 68 }); 69 70 it("should pass onLinkClick prop", () => { 71 assert.propertyVal( 72 wrapper.children().at(0).props(), 73 "onLinkClick", 74 wrapper.instance().onLinkClick 75 ); 76 }); 77 78 it("should pass isSponsored=false when flightId is not provided", () => { 79 assert.propertyVal(wrapper.children().at(0).props(), "isSponsored", false); 80 }); 81 82 it("should pass isSponsored=true when flightId is provided", () => { 83 wrapper.setProps({ flightId: "12345" }); 84 assert.propertyVal(wrapper.children().at(0).props(), "isSponsored", true); 85 }); 86 87 it("should render DSLinkMenu", () => { 88 // Note: <DSLinkMenu> component moved from a direct child element of `.ds-card`. See Bug 1893936 89 const default_link_menu = wrapper.find(DSLinkMenu); 90 assert.ok(default_link_menu.exists()); 91 }); 92 93 it("should start with no .active class", () => { 94 assert.equal(wrapper.find(".active").length, 0); 95 }); 96 97 it("should render badges for pocket, bookmark when not a spoc element ", () => { 98 const store = createStore(combineReducers(reducers), INITIAL_STATE); 99 100 wrapper = mount( 101 <Provider store={store}> 102 <DSCard context_type="bookmark" {...DEFAULT_PROPS} /> 103 </Provider> 104 ); 105 106 const dsCardInstance = wrapper.find(DSCard).instance(); 107 dsCardInstance.setState({ isSeen: true }); 108 wrapper.update(); 109 110 const contextFooter = wrapper.find(DSContextFooter); 111 assert.lengthOf(contextFooter.find(StatusMessage), 1); 112 }); 113 114 it("should render Sponsored Context for a spoc element", () => { 115 // eslint-disable-next-line no-shadow 116 const context = "Sponsored by Foo"; 117 const store = createStore(combineReducers(reducers), INITIAL_STATE); 118 wrapper = mount( 119 <Provider store={store}> 120 <DSCard context_type="bookmark" context={context} {...DEFAULT_PROPS} /> 121 </Provider> 122 ); 123 124 const dsCardInstance = wrapper.find(DSCard).instance(); 125 dsCardInstance.setState({ isSeen: true }); 126 wrapper.update(); 127 128 const contextFooter = wrapper.find(DSContextFooter); 129 130 assert.lengthOf(contextFooter.find(StatusMessage), 0); 131 assert.equal(contextFooter.find(".story-sponsored-label").text(), context); 132 }); 133 134 it("should render time to read", () => { 135 const store = createStore(combineReducers(reducers), INITIAL_STATE); 136 const discoveryStream = { 137 ...INITIAL_STATE.DiscoveryStream, 138 readTime: true, 139 }; 140 wrapper = mount( 141 <Provider store={store}> 142 <DSCard 143 time_to_read={4} 144 {...DEFAULT_PROPS} 145 DiscoveryStream={discoveryStream} 146 Prefs={INITIAL_STATE.Prefs} 147 /> 148 </Provider> 149 ); 150 const dsCardInstance = wrapper.find(DSCard).instance(); 151 dsCardInstance.setState({ isSeen: true }); 152 wrapper.update(); 153 154 const defaultMeta = wrapper.find(DefaultMeta); 155 assert.lengthOf(defaultMeta, 1); 156 assert.equal(defaultMeta.props().timeToRead, 4); 157 }); 158 159 describe("doesLinkTopicMatchSelectedTopic", () => { 160 it("should return 'not-set' when selectedTopics is not set", () => { 161 wrapper.setProps({ 162 id: "fooidx", 163 pos: 1, 164 type: "foo", 165 topic: "bar", 166 selectedTopics: "", 167 availableTopics: "foo, bar, baz, qux", 168 }); 169 const matchesSelectedTopic = wrapper 170 .instance() 171 .doesLinkTopicMatchSelectedTopic(); 172 assert.equal(matchesSelectedTopic, "not-set"); 173 }); 174 175 it("should return 'topic-not-selectable' when topic is not in availableTopics", () => { 176 wrapper.setProps({ 177 id: "fooidx", 178 pos: 1, 179 type: "foo", 180 topic: "qux", 181 selectedTopics: "foo, bar, baz", 182 availableTopics: "foo, bar, baz", 183 }); 184 const matchesSelectedTopic = wrapper 185 .instance() 186 .doesLinkTopicMatchSelectedTopic(); 187 assert.equal(matchesSelectedTopic, "topic-not-selectable"); 188 }); 189 190 it("should return 'true' when topic is in selectedTopics", () => { 191 wrapper.setProps({ 192 id: "fooidx", 193 pos: 1, 194 type: "foo", 195 topic: "qux", 196 selectedTopics: "foo, bar, baz, qux", 197 availableTopics: "foo, bar, baz, qux", 198 }); 199 const matchesSelectedTopic = wrapper 200 .instance() 201 .doesLinkTopicMatchSelectedTopic(); 202 assert.equal(matchesSelectedTopic, "true"); 203 }); 204 205 it("should return 'false' when topic is NOT in selectedTopics", () => { 206 wrapper.setProps({ 207 id: "fooidx", 208 pos: 1, 209 type: "foo", 210 topic: "qux", 211 selectedTopics: "foo, bar, baz", 212 availableTopics: "foo, bar, baz, qux", 213 }); 214 const matchesSelectedTopic = wrapper 215 .instance() 216 .doesLinkTopicMatchSelectedTopic(); 217 assert.equal(matchesSelectedTopic, "false"); 218 }); 219 }); 220 221 describe("onLinkClick", () => { 222 let fakeWindow; 223 224 beforeEach(() => { 225 fakeWindow = { 226 requestIdleCallback: sinon.stub().returns(1), 227 cancelIdleCallback: sinon.stub(), 228 innerWidth: 1000, 229 innerHeight: 900, 230 }; 231 wrapper = shallow( 232 <DSCard {...DEFAULT_PROPS} dispatch={dispatch} windowObj={fakeWindow} /> 233 ); 234 }); 235 236 it("should call dispatch with the correct events", () => { 237 wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" }); 238 239 sandbox 240 .stub(wrapper.instance(), "doesLinkTopicMatchSelectedTopic") 241 .returns(undefined); 242 243 wrapper.instance().onLinkClick(); 244 245 assert.calledTwice(dispatch); 246 assert.calledWith( 247 dispatch, 248 ac.DiscoveryStreamUserEvent({ 249 event: "CLICK", 250 source: "FOO", 251 action_position: 1, 252 value: { 253 event_source: "card", 254 card_type: "organic", 255 recommendation_id: undefined, 256 tile_id: "fooidx", 257 fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, 258 firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, 259 scheduled_corpus_item_id: undefined, 260 corpus_item_id: undefined, 261 recommended_at: undefined, 262 received_rank: undefined, 263 topic: undefined, 264 features: undefined, 265 matches_selected_topic: undefined, 266 selected_topics: undefined, 267 attribution: undefined, 268 format: "medium-card", 269 }, 270 }) 271 ); 272 assert.calledWith( 273 dispatch, 274 ac.ImpressionStats({ 275 click: 0, 276 source: "FOO", 277 tiles: [ 278 { 279 id: "fooidx", 280 pos: 1, 281 type: "organic", 282 recommendation_id: undefined, 283 topic: undefined, 284 selected_topics: undefined, 285 format: "medium-card", 286 }, 287 ], 288 window_inner_width: 1000, 289 window_inner_height: 900, 290 }) 291 ); 292 }); 293 294 it("should set the right card_type on spocs", () => { 295 wrapper.setProps({ 296 id: "fooidx", 297 pos: 1, 298 type: "foo", 299 flightId: 12345, 300 format: "spoc", 301 }); 302 sandbox 303 .stub(wrapper.instance(), "doesLinkTopicMatchSelectedTopic") 304 .returns(undefined); 305 wrapper.instance().onLinkClick(); 306 307 assert.calledTwice(dispatch); 308 assert.calledWith( 309 dispatch, 310 ac.DiscoveryStreamUserEvent({ 311 event: "CLICK", 312 source: "FOO", 313 action_position: 1, 314 value: { 315 event_source: "card", 316 card_type: "spoc", 317 recommendation_id: undefined, 318 tile_id: "fooidx", 319 fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, 320 firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, 321 scheduled_corpus_item_id: undefined, 322 corpus_item_id: undefined, 323 recommended_at: undefined, 324 received_rank: undefined, 325 topic: undefined, 326 features: undefined, 327 matches_selected_topic: undefined, 328 selected_topics: undefined, 329 attribution: undefined, 330 format: "spoc", 331 }, 332 }) 333 ); 334 assert.calledWith( 335 dispatch, 336 ac.ImpressionStats({ 337 click: 0, 338 source: "FOO", 339 tiles: [ 340 { 341 id: "fooidx", 342 pos: 1, 343 type: "spoc", 344 recommendation_id: undefined, 345 topic: undefined, 346 selected_topics: undefined, 347 format: "spoc", 348 }, 349 ], 350 window_inner_width: 1000, 351 window_inner_height: 900, 352 }) 353 ); 354 }); 355 356 it("should call dispatch with a shim", () => { 357 wrapper.setProps({ 358 id: "fooidx", 359 pos: 1, 360 type: "foo", 361 shim: { 362 click: "click shim", 363 }, 364 }); 365 366 sandbox 367 .stub(wrapper.instance(), "doesLinkTopicMatchSelectedTopic") 368 .returns(undefined); 369 wrapper.instance().onLinkClick(); 370 371 assert.calledTwice(dispatch); 372 assert.calledWith( 373 dispatch, 374 ac.DiscoveryStreamUserEvent({ 375 event: "CLICK", 376 source: "FOO", 377 action_position: 1, 378 value: { 379 event_source: "card", 380 card_type: "organic", 381 recommendation_id: undefined, 382 tile_id: "fooidx", 383 shim: "click shim", 384 fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, 385 firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, 386 scheduled_corpus_item_id: undefined, 387 corpus_item_id: undefined, 388 recommended_at: undefined, 389 received_rank: undefined, 390 topic: undefined, 391 features: undefined, 392 matches_selected_topic: undefined, 393 selected_topics: undefined, 394 attribution: undefined, 395 format: "medium-card", 396 }, 397 }) 398 ); 399 assert.calledWith( 400 dispatch, 401 ac.ImpressionStats({ 402 click: 0, 403 source: "FOO", 404 tiles: [ 405 { 406 id: "fooidx", 407 pos: 1, 408 shim: "click shim", 409 type: "organic", 410 recommendation_id: undefined, 411 topic: undefined, 412 selected_topics: undefined, 413 format: "medium-card", 414 }, 415 ], 416 window_inner_width: 1000, 417 window_inner_height: 900, 418 }) 419 ); 420 }); 421 }); 422 423 describe("DSCard with CTA", () => { 424 beforeEach(() => { 425 const store = createStore(combineReducers(reducers), INITIAL_STATE); 426 wrapper = mount( 427 <Provider store={store}> 428 <DSCard {...DEFAULT_PROPS} /> 429 </Provider> 430 ); 431 const dsCardInstance = wrapper.find(DSCard).instance(); 432 dsCardInstance.setState({ isSeen: true }); 433 wrapper.update(); 434 }); 435 436 it("should render Default Meta", () => { 437 const default_meta = wrapper.find(DefaultMeta); 438 assert.ok(default_meta.exists()); 439 }); 440 }); 441 442 describe("DSCard with Intersection Observer", () => { 443 beforeEach(() => { 444 wrapper = shallow(<DSCard {...DEFAULT_PROPS} />); 445 }); 446 447 it("should render card when seen", () => { 448 let card = wrapper.find("div.ds-card.placeholder"); 449 assert.lengthOf(card, 1); 450 451 wrapper.instance().observer = { 452 unobserve: sandbox.stub(), 453 }; 454 wrapper.instance().placeholderElement = "element"; 455 456 wrapper.instance().onSeen([ 457 { 458 isIntersecting: true, 459 }, 460 ]); 461 462 assert.isTrue(wrapper.instance().state.isSeen); 463 card = wrapper.find("div.ds-card.placeholder"); 464 assert.lengthOf(card, 0); 465 assert.lengthOf(wrapper.find(SafeAnchor), 1); 466 assert.calledOnce(wrapper.instance().observer.unobserve); 467 assert.calledWith(wrapper.instance().observer.unobserve, "element"); 468 }); 469 470 it("should setup proper placeholder ref for isSeen", () => { 471 wrapper.instance().setPlaceholderRef("element"); 472 assert.equal(wrapper.instance().placeholderElement, "element"); 473 }); 474 475 it("should setup observer on componentDidMount", () => { 476 const store = createStore(combineReducers(reducers), INITIAL_STATE); 477 478 wrapper = mount( 479 <Provider store={store}> 480 <DSCard {...DEFAULT_PROPS} /> 481 </Provider> 482 ); 483 484 assert.isTrue(!!wrapper.find(DSCard).instance().observer); 485 }); 486 }); 487 488 describe("DSCard with Idle Callback", () => { 489 let windowStub = { 490 requestIdleCallback: sinon.stub().returns(1), 491 cancelIdleCallback: sinon.stub(), 492 }; 493 beforeEach(() => { 494 wrapper = shallow(<DSCard windowObj={windowStub} {...DEFAULT_PROPS} />); 495 }); 496 497 it("should call requestIdleCallback on componentDidMount", () => { 498 assert.calledOnce(windowStub.requestIdleCallback); 499 }); 500 501 it("should call cancelIdleCallback on componentWillUnmount", () => { 502 wrapper.instance().componentWillUnmount(); 503 assert.calledOnce(windowStub.cancelIdleCallback); 504 }); 505 }); 506 507 describe("DSCard when rendered for about:home startup cache", () => { 508 beforeEach(() => { 509 const props = { 510 App: { 511 isForStartupCache: { 512 App: true, 513 }, 514 }, 515 DiscoveryStream: INITIAL_STATE.DiscoveryStream, 516 Prefs: INITIAL_STATE.Prefs, 517 }; 518 const store = createStore(combineReducers(reducers), INITIAL_STATE); 519 wrapper = mount( 520 <Provider store={store}> 521 <DSCard {...props} /> 522 </Provider> 523 ); 524 }); 525 526 it("should be set as isSeen automatically", () => { 527 const dsCardInstance = wrapper.find(DSCard).instance(); 528 assert.isTrue(dsCardInstance.state.isSeen); 529 }); 530 }); 531 532 describe("DSCard menu open states", () => { 533 let cardNode; 534 let fakeDocument; 535 let fakeWindow; 536 537 beforeEach(() => { 538 fakeDocument = { l10n: { translateFragment: sinon.stub() } }; 539 fakeWindow = { 540 document: fakeDocument, 541 requestIdleCallback: sinon.stub().returns(1), 542 cancelIdleCallback: sinon.stub(), 543 }; 544 const store = createStore(combineReducers(reducers), INITIAL_STATE); 545 546 wrapper = mount( 547 <Provider store={store}> 548 <DSCard {...DEFAULT_PROPS} windowObj={fakeWindow} /> 549 </Provider> 550 ); 551 const dsCardInstance = wrapper.find(DSCard).instance(); 552 dsCardInstance.setState({ isSeen: true }); 553 wrapper.update(); 554 cardNode = wrapper.getDOMNode(); 555 }); 556 557 it("Should remove active on Menu Update", () => { 558 // Add active class name to DSCard wrapper 559 // to simulate menu open state 560 cardNode.classList.add("active"); 561 assert.include(cardNode.className, "active"); 562 563 const dsCardInstance = wrapper.find(DSCard).instance(); 564 dsCardInstance.onMenuUpdate(false); 565 wrapper.update(); 566 567 assert.notInclude(cardNode.className, "active"); 568 }); 569 570 it("Should add active on Menu Show", async () => { 571 const dsCardInstance = wrapper.find(DSCard).instance(); 572 await dsCardInstance.onMenuShow(); 573 wrapper.update(); 574 assert.include(cardNode.className, "active"); 575 }); 576 577 it("Should add last-item to support resized window", async () => { 578 fakeWindow.scrollMaxX = 20; 579 const dsCardInstance = wrapper.find(DSCard).instance(); 580 await dsCardInstance.onMenuShow(); 581 wrapper.update(); 582 assert.include(cardNode.className, "last-item"); 583 assert.include(cardNode.className, "active"); 584 }); 585 586 it("should remove .active and .last-item classes", () => { 587 const dsCardInstance = wrapper.find(DSCard).instance(); 588 589 const remove = sinon.stub(); 590 dsCardInstance.contextMenuButtonHostElement = { 591 classList: { remove }, 592 }; 593 dsCardInstance.onMenuUpdate(); 594 assert.calledOnce(remove); 595 }); 596 597 it("should add .active and .last-item classes", async () => { 598 const dsCardInstance = wrapper.find(DSCard).instance(); 599 const add = sinon.stub(); 600 dsCardInstance.contextMenuButtonHostElement = { 601 classList: { add }, 602 }; 603 await dsCardInstance.onMenuShow(); 604 assert.calledOnce(add); 605 }); 606 }); 607 608 describe("DSCard standard sizes", () => { 609 it("should render grid with correct image sizes", async () => { 610 const standardImageSize = { 611 mediaMatcher: "default", 612 width: 296, 613 height: 148, 614 }; 615 const image = wrapper.find(DSImage); 616 assert.deepEqual(image.props().sizes[0], standardImageSize); 617 }); 618 }); 619 620 describe("DSCard medium rectangle format", () => { 621 it("should pass an empty sizes array to the DSImage", async () => { 622 wrapper.setProps({ format: "rectangle" }); 623 const image = wrapper.find(DSImage); 624 assert.deepEqual(image.props().sizes, []); 625 }); 626 }); 627 628 describe("OHTTP images", () => { 629 function mountWithOptions({ prefs, props } = {}) { 630 const store = createStore(combineReducers(reducers), INITIAL_STATE); 631 const prefsState = { 632 ...INITIAL_STATE.Prefs, 633 values: { 634 ...INITIAL_STATE.Prefs.values, 635 "discoverystream.sections.enabled": true, 636 "unifiedAds.ohttp.enabled": true, 637 ohttpImagesConfig: { enabled: true, includeTopStoriesSection: false }, 638 "discoverystream.merino-provider.ohttp.enabled": true, 639 "discoverystream.sections.contextualAds.enabled": true, 640 "discoverystream.sections.personalization.inferred.user.enabled": true, 641 "discoverystream.sections.personalization.inferred.enabled": true, 642 "discoverystream.publisherFavicon.enabled": true, 643 ...prefs, 644 }, 645 }; 646 647 wrapper = mount( 648 <Provider store={store}> 649 <DSCard 650 {...{ 651 ...DEFAULT_PROPS, 652 sectionsCardImageSizes: { 653 1: "medium", 654 2: "medium", 655 3: "medium", 656 4: "medium", 657 }, 658 }} 659 {...props} 660 Prefs={prefsState} 661 /> 662 </Provider> 663 ); 664 return wrapper; 665 } 666 667 function setWrapperIsSeen() { 668 const dsCardInstance = wrapper.find(DSCard).instance(); 669 dsCardInstance.setState({ isSeen: true }); 670 wrapper.update(); 671 } 672 673 it("should set secureImage and faviconSrc for Merino", async () => { 674 wrapper = mountWithOptions(); 675 setWrapperIsSeen(); 676 677 const image = wrapper.find(DSImage); 678 assert.deepEqual(image.at(0).props().secureImage, true); 679 assert.deepEqual(image.at(1).props().secureImage, true); 680 assert.deepEqual(image.at(2).props().secureImage, true); 681 assert.deepEqual(image.at(3).props().secureImage, true); 682 683 const defaultMeta = wrapper.find(DefaultMeta); 684 assert.equal( 685 defaultMeta.props().icon_src, 686 `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(DEFAULT_PROPS.icon_src)}` 687 ); 688 }); 689 690 it("should set secureImage for unified ads", async () => { 691 wrapper = mountWithOptions({ 692 props: { 693 flightId: "flightId", 694 }, 695 prefs: { 696 "unifiedAds.ohttp.enabled": false, 697 }, 698 }); 699 setWrapperIsSeen(); 700 701 let image = wrapper.find(DSImage); 702 assert.deepEqual(image.at(0).props().secureImage, false); 703 assert.deepEqual(image.at(1).props().secureImage, false); 704 assert.deepEqual(image.at(2).props().secureImage, false); 705 assert.deepEqual(image.at(3).props().secureImage, false); 706 707 wrapper = mountWithOptions({ 708 props: { 709 flightId: "flightId", 710 }, 711 prefs: { 712 "unifiedAds.ohttp.enabled": true, 713 }, 714 }); 715 setWrapperIsSeen(); 716 717 image = wrapper.find(DSImage); 718 assert.deepEqual(image.at(0).props().secureImage, true); 719 assert.deepEqual(image.at(1).props().secureImage, true); 720 assert.deepEqual(image.at(2).props().secureImage, true); 721 assert.deepEqual(image.at(3).props().secureImage, true); 722 }); 723 724 it("should not set secureImage or icon_src for top stories", async () => { 725 wrapper = mountWithOptions({ 726 props: { 727 section: "top_stories_section", 728 }, 729 }); 730 setWrapperIsSeen(); 731 732 let image = wrapper.find(DSImage); 733 assert.deepEqual(image.at(0).props().secureImage, false); 734 assert.deepEqual(image.at(1).props().secureImage, false); 735 assert.deepEqual(image.at(2).props().secureImage, false); 736 assert.deepEqual(image.at(3).props().secureImage, false); 737 738 let defaultMeta = wrapper.find(DefaultMeta); 739 assert.equal(defaultMeta.props().icon_src, DEFAULT_PROPS.icon_src); 740 741 wrapper = mountWithOptions({ 742 props: { 743 section: "top_stories_section", 744 }, 745 prefs: { 746 ohttpImagesConfig: { enabled: true, includeTopStoriesSection: true }, 747 }, 748 }); 749 setWrapperIsSeen(); 750 751 image = wrapper.find(DSImage); 752 assert.deepEqual(image.at(0).props().secureImage, true); 753 assert.deepEqual(image.at(1).props().secureImage, true); 754 assert.deepEqual(image.at(2).props().secureImage, true); 755 assert.deepEqual(image.at(3).props().secureImage, true); 756 757 defaultMeta = wrapper.find(DefaultMeta); 758 assert.equal( 759 defaultMeta.props().icon_src, 760 `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(DEFAULT_PROPS.icon_src)}` 761 ); 762 }); 763 764 it("should not be seen on idle callback", async () => { 765 wrapper = mountWithOptions(); 766 const dsCardInstance = wrapper.find(DSCard).instance(); 767 dsCardInstance.onIdleCallback(); 768 wrapper.update(); 769 assert.equal(dsCardInstance.state.isSeen, false); 770 }); 771 }); 772 773 describe("DSCard section images sizes", () => { 774 it("should render sections with correct image sizes", async () => { 775 const cardSizes = { 776 small: { 777 width: 110, 778 height: 117, 779 }, 780 medium: { 781 width: 300, 782 height: 150, 783 }, 784 large: { 785 width: 190, 786 height: 250, 787 }, 788 }; 789 790 const mediaMatcher = { 791 1: "default", 792 2: "(min-width: 724px)", 793 3: "(min-width: 1122px)", 794 4: "(min-width: 1390px)", 795 }; 796 797 wrapper.setProps({ 798 Prefs: { 799 values: { 800 "discoverystream.sections.enabled": true, 801 }, 802 }, 803 sectionsCardImageSizes: { 804 1: "medium", 805 2: "large", 806 3: "small", 807 4: "large", 808 }, 809 }); 810 const image = wrapper.find(DSImage); 811 assert.lengthOf(image, 4); 812 813 assert.equal( 814 image.at(0).props().sizes[0].mediaMatcher, 815 mediaMatcher["1"] 816 ); 817 assert.equal( 818 image.at(0).props().sizes[0].height, 819 cardSizes.medium.height 820 ); 821 assert.equal(image.at(0).props().sizes[0].width, cardSizes.medium.width); 822 823 assert.equal( 824 image.at(1).props().sizes[0].mediaMatcher, 825 mediaMatcher["2"] 826 ); 827 assert.equal(image.at(1).props().sizes[0].height, cardSizes.large.height); 828 assert.equal(image.at(1).props().sizes[0].width, cardSizes.large.width); 829 830 assert.deepEqual( 831 image.at(2).props().sizes[0].mediaMatcher, 832 mediaMatcher["3"] 833 ); 834 assert.equal(image.at(2).props().sizes[0].height, cardSizes.small.height); 835 assert.equal(image.at(2).props().sizes[0].width, cardSizes.small.width); 836 837 assert.equal( 838 image.at(3).props().sizes[0].mediaMatcher, 839 mediaMatcher["4"] 840 ); 841 assert.equal(image.at(3).props().sizes[0].height, cardSizes.large.height); 842 assert.equal(image.at(3).props().sizes[0].width, cardSizes.large.width); 843 }); 844 }); 845 }); 846 847 describe("<PlaceholderDSCard> component", () => { 848 it("should have placeholder prop", () => { 849 const wrapper = shallow(<PlaceholderDSCard />); 850 const placeholder = wrapper.prop("placeholder"); 851 assert.isTrue(placeholder); 852 }); 853 854 it("should contain placeholder div", () => { 855 const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />); 856 wrapper.setState({ isSeen: true }); 857 const card = wrapper.find("div.ds-card.placeholder"); 858 assert.lengthOf(card, 1); 859 }); 860 861 it("should not be clickable", () => { 862 const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />); 863 wrapper.setState({ isSeen: true }); 864 const anchor = wrapper.find("SafeAnchor.ds-card-link"); 865 assert.lengthOf(anchor, 0); 866 }); 867 868 it("should not have context menu", () => { 869 const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />); 870 wrapper.setState({ isSeen: true }); 871 const linkMenu = wrapper.find(DSLinkMenu); 872 assert.lengthOf(linkMenu, 0); 873 }); 874 }); 875 876 describe("<DSSource> component", () => { 877 it("should return a default source without compact", () => { 878 const wrapper = shallow(<DSSource source="Mozilla" />); 879 880 let sourceElement = wrapper.find(".source"); 881 assert.equal(sourceElement.text(), "Mozilla"); 882 }); 883 it("should return a default source with compact without a sponsor or time to read", () => { 884 const wrapper = shallow(<DSSource compact={true} source="Mozilla" />); 885 886 let sourceElement = wrapper.find(".source"); 887 assert.equal(sourceElement.text(), "Mozilla"); 888 }); 889 it("should return a SponsorLabel with compact and a sponsor", () => { 890 const wrapper = shallow( 891 <DSSource newSponsoredLabel={true} sponsor="Mozilla" /> 892 ); 893 const sponsorLabel = wrapper.find(SponsorLabel); 894 assert.lengthOf(sponsorLabel, 1); 895 }); 896 it("should return a time to read with compact and without a sponsor but with a time to read", () => { 897 const wrapper = shallow( 898 <DSSource compact={true} source="Mozilla" timeToRead="2000" /> 899 ); 900 901 let timeToRead = wrapper.find(".time-to-read"); 902 assert.lengthOf(timeToRead, 1); 903 904 // Weirdly, we can test for the pressence of fluent, because time to read needs to be translated. 905 // This is also because we did a shallow render, that th contents of fluent would be empty anyway. 906 const fluentOrText = wrapper.find(FluentOrText); 907 assert.lengthOf(fluentOrText, 1); 908 }); 909 it("should prioritize a SponsorLabel if for some reason it gets everything", () => { 910 const wrapper = shallow( 911 <DSSource 912 newSponsoredLabel={true} 913 sponsor="Mozilla" 914 source="Mozilla" 915 timeToRead="2000" 916 /> 917 ); 918 const sponsorLabel = wrapper.find(SponsorLabel); 919 assert.lengthOf(sponsorLabel, 1); 920 }); 921 }); 922 923 describe("readTimeFromWordCount function", () => { 924 it("should return proper read time", () => { 925 const result = readTimeFromWordCount(2000); 926 assert.equal(result, 10); 927 }); 928 it("should return false with falsey word count", () => { 929 assert.isFalse(readTimeFromWordCount()); 930 assert.isFalse(readTimeFromWordCount(0)); 931 assert.isFalse(readTimeFromWordCount("")); 932 assert.isFalse(readTimeFromWordCount(null)); 933 assert.isFalse(readTimeFromWordCount(undefined)); 934 }); 935 it("should return NaN with invalid word count", () => { 936 assert.isNaN(readTimeFromWordCount("zero")); 937 assert.isNaN(readTimeFromWordCount({})); 938 }); 939 });