tor-browser

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

Card.test.jsx (17478B)


      1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
      2 import {
      3  _Card as Card,
      4  PlaceholderCard,
      5 } from "content-src/components/Card/Card";
      6 import { combineReducers, createStore } from "redux";
      7 import { GlobalOverrider } from "test/unit/utils";
      8 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
      9 import { cardContextTypes } from "content-src/components/Card/types";
     10 import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
     11 import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
     12 import { Provider } from "react-redux";
     13 import React from "react";
     14 import { shallow, mount } from "enzyme";
     15 
     16 let DEFAULT_PROPS = {
     17  dispatch: sinon.stub(),
     18  index: 0,
     19  link: {
     20    hostname: "foo",
     21    title: "A title for foo",
     22    url: "http://www.foo.com",
     23    type: "history",
     24    description: "A description for foo",
     25    image: "http://www.foo.com/img.png",
     26    guid: 1,
     27  },
     28  eventSource: "TOP_STORIES",
     29  shouldSendImpressionStats: true,
     30  contextMenuOptions: ["Separator"],
     31 };
     32 
     33 let DEFAULT_BLOB_IMAGE = {
     34  path: "/testpath",
     35  data: new Blob([0]),
     36 };
     37 
     38 function mountCardWithProps(props) {
     39  const store = createStore(combineReducers(reducers), INITIAL_STATE);
     40  return mount(
     41    <Provider store={store}>
     42      <Card {...props} />
     43    </Provider>
     44  );
     45 }
     46 
     47 describe("<Card>", () => {
     48  let globals;
     49  let wrapper;
     50  beforeEach(() => {
     51    globals = new GlobalOverrider();
     52    wrapper = mountCardWithProps(DEFAULT_PROPS);
     53  });
     54  afterEach(() => {
     55    DEFAULT_PROPS.dispatch.reset();
     56    globals.restore();
     57  });
     58  it("should render a Card component", () => assert.ok(wrapper.exists()));
     59  it("should add the right url", () => {
     60    assert.propertyVal(
     61      wrapper.find("a").props(),
     62      "href",
     63      DEFAULT_PROPS.link.url
     64    );
     65 
     66    // test that pocket cards get a special open_url href
     67    const pocketLink = Object.assign({}, DEFAULT_PROPS.link, {
     68      open_url: "getpocket.com/foo",
     69      type: "pocket",
     70    });
     71    wrapper = mount(
     72      <Card {...Object.assign({}, DEFAULT_PROPS, { link: pocketLink })} />
     73    );
     74    assert.propertyVal(wrapper.find("a").props(), "href", pocketLink.open_url);
     75  });
     76  it("should display a title", () =>
     77    assert.equal(wrapper.find(".card-title").text(), DEFAULT_PROPS.link.title));
     78  it("should display a description", () =>
     79    assert.equal(
     80      wrapper.find(".card-description").text(),
     81      DEFAULT_PROPS.link.description
     82    ));
     83  it("should display a host name", () =>
     84    assert.equal(wrapper.find(".card-host-name").text(), "foo"));
     85  it("should have a link menu button", () =>
     86    assert.ok(wrapper.find(".context-menu-button").exists()));
     87  it("should render a link menu when button is clicked", () => {
     88    const button = wrapper.find(".context-menu-button");
     89    assert.equal(wrapper.find(LinkMenu).length, 0);
     90    button.simulate("click", { preventDefault: () => {} });
     91    assert.equal(wrapper.find(LinkMenu).length, 1);
     92  });
     93  it("should pass dispatch, source, onUpdate, site, options, and index to LinkMenu", () => {
     94    wrapper
     95      .find(".context-menu-button")
     96      .simulate("click", { preventDefault: () => {} });
     97    // eslint-disable-next-line no-shadow
     98    const { dispatch, source, onUpdate, site, options, index } = wrapper
     99      .find(LinkMenu)
    100      .props();
    101    assert.equal(dispatch, DEFAULT_PROPS.dispatch);
    102    assert.equal(source, DEFAULT_PROPS.eventSource);
    103    assert.ok(onUpdate);
    104    assert.equal(site, DEFAULT_PROPS.link);
    105    assert.equal(options, DEFAULT_PROPS.contextMenuOptions);
    106    assert.equal(index, DEFAULT_PROPS.index);
    107  });
    108  it("should pass through the correct menu options to LinkMenu if overridden by individual card", () => {
    109    const link = Object.assign({}, DEFAULT_PROPS.link);
    110    link.contextMenuOptions = ["CheckBookmark"];
    111 
    112    wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, { link }));
    113    wrapper
    114      .find(".context-menu-button")
    115      .simulate("click", { preventDefault: () => {} });
    116    // eslint-disable-next-line no-shadow
    117    const { options } = wrapper.find(LinkMenu).props();
    118    assert.equal(options, link.contextMenuOptions);
    119  });
    120  it("should have a context based on type", () => {
    121    wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    122    const cardContext = wrapper.find(".card-context");
    123    const { icon, fluentID } = cardContextTypes[DEFAULT_PROPS.link.type];
    124    assert.isTrue(cardContext.childAt(0).hasClass(`icon-${icon}`));
    125    assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
    126    assert.equal(cardContext.childAt(1).prop("data-l10n-id"), fluentID);
    127  });
    128  it("should support setting custom context", () => {
    129    const linkWithCustomContext = {
    130      type: "history",
    131      context: "Custom",
    132      icon: "icon-url",
    133    };
    134 
    135    wrapper = shallow(
    136      <Card
    137        {...Object.assign({}, DEFAULT_PROPS, { link: linkWithCustomContext })}
    138      />
    139    );
    140    const cardContext = wrapper.find(".card-context");
    141    const { icon } = cardContextTypes[DEFAULT_PROPS.link.type];
    142    assert.isFalse(cardContext.childAt(0).hasClass(`icon-${icon}`));
    143    assert.equal(
    144      cardContext.childAt(0).props().style.backgroundImage,
    145      "url('icon-url')"
    146    );
    147 
    148    assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
    149    assert.equal(cardContext.childAt(1).text(), linkWithCustomContext.context);
    150  });
    151  it("should parse args for fluent correctly", () => {
    152    const title = '"fluent"';
    153    const link = { ...DEFAULT_PROPS.link, title };
    154 
    155    wrapper = mountCardWithProps({ ...DEFAULT_PROPS, link });
    156    let button = wrapper.find(ContextMenuButton).find("button");
    157 
    158    assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title }));
    159  });
    160  it("should have .active class, on card-outer if context menu is open", () => {
    161    const button = wrapper.find(ContextMenuButton);
    162    assert.isFalse(
    163      wrapper.find(".card-outer").hasClass("active"),
    164      "does not have active class"
    165    );
    166    button.simulate("click", { preventDefault: () => {} });
    167    assert.isTrue(
    168      wrapper.find(".card-outer").hasClass("active"),
    169      "has active class"
    170    );
    171  });
    172  it("should send OPEN_DOWNLOAD_FILE if we clicked on a download", () => {
    173    const downloadLink = {
    174      type: "download",
    175      url: "download.mov",
    176    };
    177    wrapper = mountCardWithProps(
    178      Object.assign({}, DEFAULT_PROPS, { link: downloadLink })
    179    );
    180    const card = wrapper.find(".card");
    181    card.simulate("click", { preventDefault: () => {} });
    182    assert.calledThrice(DEFAULT_PROPS.dispatch);
    183 
    184    assert.equal(
    185      DEFAULT_PROPS.dispatch.firstCall.args[0].type,
    186      at.OPEN_DOWNLOAD_FILE
    187    );
    188    assert.deepEqual(
    189      DEFAULT_PROPS.dispatch.firstCall.args[0].data,
    190      downloadLink
    191    );
    192  });
    193  it("should send OPEN_LINK if we clicked on anything other than a download", () => {
    194    const nonDownloadLink = {
    195      type: "history",
    196      url: "download.mov",
    197    };
    198    wrapper = mountCardWithProps(
    199      Object.assign({}, DEFAULT_PROPS, { link: nonDownloadLink })
    200    );
    201    const card = wrapper.find(".card");
    202    const event = {
    203      altKey: "1",
    204      button: "2",
    205      ctrlKey: "3",
    206      metaKey: "4",
    207      shiftKey: "5",
    208    };
    209    card.simulate(
    210      "click",
    211      Object.assign({}, event, { preventDefault: () => {} })
    212    );
    213    assert.calledThrice(DEFAULT_PROPS.dispatch);
    214 
    215    assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);
    216  });
    217  describe("card image display", () => {
    218    const DEFAULT_BLOB_URL = "blob://test";
    219    let url;
    220    beforeEach(() => {
    221      url = {
    222        createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),
    223        revokeObjectURL: globals.sandbox.spy(),
    224      };
    225      globals.set("URL", url);
    226    });
    227    afterEach(() => {
    228      globals.restore();
    229    });
    230    it("should display a regular image correctly and not call revokeObjectURL when unmounted", () => {
    231      wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    232 
    233      assert.isUndefined(wrapper.state("cardImage").path);
    234      assert.equal(wrapper.state("cardImage").url, DEFAULT_PROPS.link.image);
    235      assert.equal(
    236        wrapper.find(".card-preview-image").props().style.backgroundImage,
    237        `url(${wrapper.state("cardImage").url})`
    238      );
    239 
    240      wrapper.unmount();
    241      assert.notCalled(url.revokeObjectURL);
    242    });
    243    it("should display a blob image correctly and revoke blob url when unmounted", () => {
    244      const link = Object.assign({}, DEFAULT_PROPS.link, {
    245        image: DEFAULT_BLOB_IMAGE,
    246      });
    247      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    248 
    249      assert.equal(wrapper.state("cardImage").path, DEFAULT_BLOB_IMAGE.path);
    250      assert.equal(wrapper.state("cardImage").url, DEFAULT_BLOB_URL);
    251      assert.equal(
    252        wrapper.find(".card-preview-image").props().style.backgroundImage,
    253        `url(${wrapper.state("cardImage").url})`
    254      );
    255 
    256      wrapper.unmount();
    257      assert.calledOnce(url.revokeObjectURL);
    258    });
    259    it("should not show an image if there isn't one and not call revokeObjectURL when unmounted", () => {
    260      const link = Object.assign({}, DEFAULT_PROPS.link);
    261      delete link.image;
    262 
    263      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    264 
    265      assert.isNull(wrapper.state("cardImage"));
    266      assert.lengthOf(wrapper.find(".card-preview-image"), 0);
    267 
    268      wrapper.unmount();
    269      assert.notCalled(url.revokeObjectURL);
    270    });
    271    it("should remove current card image if new image is not present", () => {
    272      wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    273 
    274      const otherLink = Object.assign({}, DEFAULT_PROPS.link);
    275      delete otherLink.image;
    276      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));
    277 
    278      assert.isNull(wrapper.state("cardImage"));
    279    });
    280    it("should not create or revoke urls if normal image is already in state", () => {
    281      wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    282 
    283      wrapper.setProps(DEFAULT_PROPS);
    284 
    285      assert.notCalled(url.createObjectURL);
    286      assert.notCalled(url.revokeObjectURL);
    287    });
    288    it("should not create or revoke more urls if blob image is already in state", () => {
    289      const link = Object.assign({}, DEFAULT_PROPS.link, {
    290        image: DEFAULT_BLOB_IMAGE,
    291      });
    292      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    293 
    294      assert.calledOnce(url.createObjectURL);
    295      assert.notCalled(url.revokeObjectURL);
    296 
    297      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link }));
    298 
    299      assert.calledOnce(url.createObjectURL);
    300      assert.notCalled(url.revokeObjectURL);
    301    });
    302    it("should create blob urls for new blobs and revoke existing ones", () => {
    303      const link = Object.assign({}, DEFAULT_PROPS.link, {
    304        image: DEFAULT_BLOB_IMAGE,
    305      });
    306      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    307 
    308      assert.calledOnce(url.createObjectURL);
    309      assert.notCalled(url.revokeObjectURL);
    310 
    311      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {
    312        image: { path: "/newpath", data: new Blob([0]) },
    313      });
    314      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));
    315 
    316      assert.calledTwice(url.createObjectURL);
    317      assert.calledOnce(url.revokeObjectURL);
    318    });
    319    it("should not call createObjectURL and revokeObjectURL for normal images", () => {
    320      wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    321 
    322      assert.notCalled(url.createObjectURL);
    323      assert.notCalled(url.revokeObjectURL);
    324 
    325      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {
    326        image: "https://other/image",
    327      });
    328      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));
    329 
    330      assert.notCalled(url.createObjectURL);
    331      assert.notCalled(url.revokeObjectURL);
    332    });
    333  });
    334  describe("image loading", () => {
    335    let link;
    336    let triggerImage = {};
    337    let uniqueLink = 0;
    338    beforeEach(() => {
    339      global.Image.prototype = {
    340        addEventListener(event, callback) {
    341          triggerImage[event] = () => Promise.resolve(callback());
    342        },
    343      };
    344 
    345      link = Object.assign({}, DEFAULT_PROPS.link);
    346      link.image += uniqueLink++;
    347      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    348    });
    349    it("should have a loaded preview image when the image is loaded", () => {
    350      assert.isFalse(wrapper.find(".card-preview-image").hasClass("loaded"));
    351 
    352      wrapper.setState({ imageLoaded: true });
    353 
    354      assert.isTrue(wrapper.find(".card-preview-image").hasClass("loaded"));
    355    });
    356    it("should start not loaded", () => {
    357      assert.isFalse(wrapper.state("imageLoaded"));
    358    });
    359    it("should be loaded after load", async () => {
    360      await triggerImage.load();
    361 
    362      assert.isTrue(wrapper.state("imageLoaded"));
    363    });
    364    it("should be not be loaded after error ", async () => {
    365      await triggerImage.error();
    366 
    367      assert.isFalse(wrapper.state("imageLoaded"));
    368    });
    369    it("should be not be loaded if image changes", async () => {
    370      await triggerImage.load();
    371      const otherLink = Object.assign({}, link, {
    372        image: "https://other/image",
    373      });
    374 
    375      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));
    376 
    377      assert.isFalse(wrapper.state("imageLoaded"));
    378    });
    379  });
    380  describe("placeholder=true", () => {
    381    beforeEach(() => {
    382      wrapper = mount(<Card placeholder={true} />);
    383    });
    384    it("should render when placeholder=true", () => {
    385      assert.ok(wrapper.exists());
    386    });
    387    it("should add a placeholder class to the outer element", () => {
    388      assert.isTrue(wrapper.find(".card-outer").hasClass("placeholder"));
    389    });
    390    it("should not have a context menu button or LinkMenu", () => {
    391      assert.isFalse(
    392        wrapper.find(ContextMenuButton).exists(),
    393        "context menu button"
    394      );
    395      assert.isFalse(wrapper.find(LinkMenu).exists(), "LinkMenu");
    396    });
    397    it("should not call onLinkClick when the link is clicked", () => {
    398      const spy = sinon.spy(wrapper.instance(), "onLinkClick");
    399      const card = wrapper.find(".card");
    400      card.simulate("click");
    401      assert.notCalled(spy);
    402    });
    403  });
    404  describe("#trackClick", () => {
    405    it("should call dispatch when the link is clicked with the right data", () => {
    406      const card = wrapper.find(".card");
    407      const event = {
    408        altKey: "1",
    409        button: "2",
    410        ctrlKey: "3",
    411        metaKey: "4",
    412        shiftKey: "5",
    413      };
    414      card.simulate(
    415        "click",
    416        Object.assign({}, event, { preventDefault: () => {} })
    417      );
    418      assert.calledThrice(DEFAULT_PROPS.dispatch);
    419 
    420      // first dispatch call is the AlsoToMain message which will open a link in a window, and send some event data
    421      assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);
    422      assert.deepEqual(
    423        DEFAULT_PROPS.dispatch.firstCall.args[0].data.event,
    424        event
    425      );
    426 
    427      // second dispatch call is a UserEvent action for telemetry
    428      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);
    429      assert.calledWith(
    430        DEFAULT_PROPS.dispatch.secondCall,
    431        ac.UserEvent({
    432          event: "CLICK",
    433          source: DEFAULT_PROPS.eventSource,
    434          action_position: DEFAULT_PROPS.index,
    435        })
    436      );
    437 
    438      // third dispatch call is to send impression stats
    439      assert.calledWith(
    440        DEFAULT_PROPS.dispatch.thirdCall,
    441        ac.ImpressionStats({
    442          source: DEFAULT_PROPS.eventSource,
    443          click: 0,
    444          tiles: [{ id: DEFAULT_PROPS.link.guid, pos: DEFAULT_PROPS.index }],
    445        })
    446      );
    447    });
    448    it("should provide card_type to telemetry info if type is not history", () => {
    449      const link = Object.assign({}, DEFAULT_PROPS.link);
    450      link.type = "bookmark";
    451      wrapper = mount(<Card {...Object.assign({}, DEFAULT_PROPS, { link })} />);
    452      const card = wrapper.find(".card");
    453      const event = {
    454        altKey: "1",
    455        button: "2",
    456        ctrlKey: "3",
    457        metaKey: "4",
    458        shiftKey: "5",
    459      };
    460 
    461      card.simulate(
    462        "click",
    463        Object.assign({}, event, { preventDefault: () => {} })
    464      );
    465 
    466      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);
    467      assert.calledWith(
    468        DEFAULT_PROPS.dispatch.secondCall,
    469        ac.UserEvent({
    470          event: "CLICK",
    471          source: DEFAULT_PROPS.eventSource,
    472          action_position: DEFAULT_PROPS.index,
    473          value: { card_type: link.type },
    474        })
    475      );
    476    });
    477    it("should notify Web Extensions with WEBEXT_CLICK if props.isWebExtension is true", () => {
    478      wrapper = mountCardWithProps(
    479        Object.assign({}, DEFAULT_PROPS, {
    480          isWebExtension: true,
    481          eventSource: "MyExtension",
    482          index: 3,
    483        })
    484      );
    485      const card = wrapper.find(".card");
    486      const event = { preventDefault() {} };
    487      card.simulate("click", event);
    488      assert.calledWith(
    489        DEFAULT_PROPS.dispatch,
    490        ac.WebExtEvent(at.WEBEXT_CLICK, {
    491          source: "MyExtension",
    492          url: DEFAULT_PROPS.link.url,
    493          action_position: 3,
    494        })
    495      );
    496    });
    497  });
    498 });
    499 
    500 describe("<PlaceholderCard />", () => {
    501  it("should render a Card with placeholder=true", () => {
    502    const wrapper = mount(
    503      <Provider store={createStore(combineReducers(reducers), INITIAL_STATE)}>
    504        <PlaceholderCard />
    505      </Provider>
    506    );
    507    assert.isTrue(wrapper.find(Card).props().placeholder);
    508  });
    509 });