tor-browser

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

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