tor-browser

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

TopSites.test.jsx (60494B)


      1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
      2 import { GlobalOverrider } from "test/unit/utils";
      3 import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants";
      4 import {
      5  TOP_SITES_DEFAULT_ROWS,
      6  TOP_SITES_MAX_SITES_PER_ROW,
      7 } from "common/Reducers.sys.mjs";
      8 import {
      9  TopSite,
     10  TopSiteLink,
     11  _TopSiteList as TopSiteList,
     12  TopSitePlaceholder,
     13  TopSiteAddButton,
     14 } from "content-src/components/TopSites/TopSite";
     15 import {
     16  INTERSECTION_RATIO,
     17  TopSiteImpressionWrapper,
     18 } from "content-src/components/TopSites/TopSiteImpressionWrapper";
     19 import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton";
     20 import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
     21 import React from "react";
     22 import { mount, shallow } from "enzyme";
     23 import { TopSiteForm } from "content-src/components/TopSites/TopSiteForm";
     24 import { TopSiteFormInput } from "content-src/components/TopSites/TopSiteFormInput";
     25 import { _TopSites as TopSites } from "content-src/components/TopSites/TopSites";
     26 import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
     27 
     28 const perfSvc = {
     29  mark() {},
     30  getMostRecentAbsMarkStartByName() {},
     31 };
     32 
     33 const DEFAULT_PROPS = {
     34  Prefs: { values: { featureConfig: {} } },
     35  TopSites: { initialized: true, rows: [] },
     36  App: {
     37    isForStartupCache: false,
     38  },
     39  TopSitesRows: TOP_SITES_DEFAULT_ROWS,
     40  topSiteIconType: () => "no_image",
     41  dispatch() {},
     42  perfSvc,
     43 };
     44 
     45 const DEFAULT_BLOB_URL = "blob://test";
     46 
     47 describe("<TopSites>", () => {
     48  let sandbox;
     49 
     50  beforeEach(() => {
     51    sandbox = sinon.createSandbox();
     52  });
     53 
     54  afterEach(() => {
     55    sandbox.restore();
     56  });
     57 
     58  it("should render a TopSites element", () => {
     59    const wrapper = shallow(<TopSites {...DEFAULT_PROPS} />);
     60    assert.ok(wrapper.exists());
     61  });
     62  describe("#_dispatchTopSitesStats", () => {
     63    let globals;
     64    let wrapper;
     65    let dispatchStatsSpy;
     66 
     67    beforeEach(() => {
     68      globals = new GlobalOverrider();
     69      sandbox.stub(DEFAULT_PROPS, "dispatch");
     70      wrapper = shallow(<TopSites {...DEFAULT_PROPS} />, {
     71        disableLifecycleMethods: true,
     72      });
     73      dispatchStatsSpy = sandbox.spy(
     74        wrapper.instance(),
     75        "_dispatchTopSitesStats"
     76      );
     77    });
     78    afterEach(() => {
     79      globals.restore();
     80      sandbox.restore();
     81    });
     82    it("should call _dispatchTopSitesStats on componentDidMount", () => {
     83      wrapper.instance().componentDidMount();
     84 
     85      assert.calledOnce(dispatchStatsSpy);
     86    });
     87    it("should call _dispatchTopSitesStats on componentDidUpdate", () => {
     88      wrapper.instance().componentDidUpdate();
     89 
     90      assert.calledOnce(dispatchStatsSpy);
     91    });
     92    it("should dispatch SAVE_SESSION_PERF_DATA", () => {
     93      wrapper.instance()._dispatchTopSitesStats();
     94 
     95      assert.calledOnce(DEFAULT_PROPS.dispatch);
     96      assert.calledWithExactly(
     97        DEFAULT_PROPS.dispatch,
     98        ac.AlsoToMain({
     99          type: at.SAVE_SESSION_PERF_DATA,
    100          data: {
    101            topsites_icon_stats: {
    102              custom_screenshot: 0,
    103              screenshot: 0,
    104              tippytop: 0,
    105              rich_icon: 0,
    106              no_image: 0,
    107            },
    108            topsites_pinned: 0,
    109            topsites_search_shortcuts: 0,
    110          },
    111        })
    112      );
    113    });
    114    it("should correctly count TopSite images - just screenshot", () => {
    115      const rows = [{ screenshot: true }];
    116      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    117      wrapper.instance()._dispatchTopSitesStats();
    118 
    119      assert.calledOnce(DEFAULT_PROPS.dispatch);
    120      assert.calledWithExactly(
    121        DEFAULT_PROPS.dispatch,
    122        ac.AlsoToMain({
    123          type: at.SAVE_SESSION_PERF_DATA,
    124          data: {
    125            topsites_icon_stats: {
    126              custom_screenshot: 0,
    127              screenshot: 1,
    128              tippytop: 0,
    129              rich_icon: 0,
    130              no_image: 0,
    131            },
    132            topsites_pinned: 0,
    133            topsites_search_shortcuts: 0,
    134          },
    135        })
    136      );
    137    });
    138    it("should correctly count TopSite images - custom_screenshot", () => {
    139      const rows = [{ customScreenshotURL: true }];
    140      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    141      wrapper.instance()._dispatchTopSitesStats();
    142 
    143      assert.calledOnce(DEFAULT_PROPS.dispatch);
    144      assert.calledWithExactly(
    145        DEFAULT_PROPS.dispatch,
    146        ac.AlsoToMain({
    147          type: at.SAVE_SESSION_PERF_DATA,
    148          data: {
    149            topsites_icon_stats: {
    150              custom_screenshot: 1,
    151              screenshot: 0,
    152              tippytop: 0,
    153              rich_icon: 0,
    154              no_image: 0,
    155            },
    156            topsites_pinned: 0,
    157            topsites_search_shortcuts: 0,
    158          },
    159        })
    160      );
    161    });
    162    it("should correctly count TopSite images - rich_icon", () => {
    163      const rows = [{ faviconSize: MIN_RICH_FAVICON_SIZE }];
    164      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    165      wrapper.instance()._dispatchTopSitesStats();
    166 
    167      assert.calledOnce(DEFAULT_PROPS.dispatch);
    168      assert.calledWithExactly(
    169        DEFAULT_PROPS.dispatch,
    170        ac.AlsoToMain({
    171          type: at.SAVE_SESSION_PERF_DATA,
    172          data: {
    173            topsites_icon_stats: {
    174              custom_screenshot: 0,
    175              screenshot: 0,
    176              tippytop: 0,
    177              rich_icon: 1,
    178              no_image: 0,
    179            },
    180            topsites_pinned: 0,
    181            topsites_search_shortcuts: 0,
    182          },
    183        })
    184      );
    185    });
    186    it("should correctly count TopSite images - tippytop", () => {
    187      const rows = [
    188        { tippyTopIcon: "foo" },
    189        { faviconRef: "tippytop" },
    190        { faviconRef: "foobar" },
    191      ];
    192      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    193      wrapper.instance()._dispatchTopSitesStats();
    194 
    195      assert.calledOnce(DEFAULT_PROPS.dispatch);
    196      assert.calledWithExactly(
    197        DEFAULT_PROPS.dispatch,
    198        ac.AlsoToMain({
    199          type: at.SAVE_SESSION_PERF_DATA,
    200          data: {
    201            topsites_icon_stats: {
    202              custom_screenshot: 0,
    203              screenshot: 0,
    204              tippytop: 2,
    205              rich_icon: 0,
    206              no_image: 1,
    207            },
    208            topsites_pinned: 0,
    209            topsites_search_shortcuts: 0,
    210          },
    211        })
    212      );
    213    });
    214    it("should correctly count TopSite images - no image", () => {
    215      const rows = [{}];
    216      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    217      wrapper.instance()._dispatchTopSitesStats();
    218 
    219      assert.calledOnce(DEFAULT_PROPS.dispatch);
    220      assert.calledWithExactly(
    221        DEFAULT_PROPS.dispatch,
    222        ac.AlsoToMain({
    223          type: at.SAVE_SESSION_PERF_DATA,
    224          data: {
    225            topsites_icon_stats: {
    226              custom_screenshot: 0,
    227              screenshot: 0,
    228              tippytop: 0,
    229              rich_icon: 0,
    230              no_image: 1,
    231            },
    232            topsites_pinned: 0,
    233            topsites_search_shortcuts: 0,
    234          },
    235        })
    236      );
    237    });
    238    it("should correctly count pinned Top Sites", () => {
    239      const rows = [
    240        { isPinned: true },
    241        { isPinned: false },
    242        { isPinned: true },
    243      ];
    244      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    245      wrapper.instance()._dispatchTopSitesStats();
    246 
    247      assert.calledOnce(DEFAULT_PROPS.dispatch);
    248      assert.calledWithExactly(
    249        DEFAULT_PROPS.dispatch,
    250        ac.AlsoToMain({
    251          type: at.SAVE_SESSION_PERF_DATA,
    252          data: {
    253            topsites_icon_stats: {
    254              custom_screenshot: 0,
    255              screenshot: 0,
    256              tippytop: 0,
    257              rich_icon: 0,
    258              no_image: 3,
    259            },
    260            topsites_pinned: 2,
    261            topsites_search_shortcuts: 0,
    262          },
    263        })
    264      );
    265    });
    266    it("should correctly count search shortcut Top Sites", () => {
    267      const rows = [{ searchTopSite: true }, { searchTopSite: true }];
    268      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    269      wrapper.instance()._dispatchTopSitesStats();
    270 
    271      assert.calledOnce(DEFAULT_PROPS.dispatch);
    272      assert.calledWithExactly(
    273        DEFAULT_PROPS.dispatch,
    274        ac.AlsoToMain({
    275          type: at.SAVE_SESSION_PERF_DATA,
    276          data: {
    277            topsites_icon_stats: {
    278              custom_screenshot: 0,
    279              screenshot: 0,
    280              tippytop: 0,
    281              rich_icon: 0,
    282              no_image: 2,
    283            },
    284            topsites_pinned: 0,
    285            topsites_search_shortcuts: 2,
    286          },
    287        })
    288      );
    289    });
    290    it("should only count visible top sites on wide layout", () => {
    291      globals.set("matchMedia", () => ({ matches: true }));
    292      const rows = [
    293        {},
    294        {},
    295        {},
    296        {},
    297        {},
    298        {},
    299        {},
    300        {},
    301        {},
    302        {},
    303        {},
    304        {},
    305        {},
    306        {},
    307        {},
    308        {},
    309      ];
    310      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    311 
    312      wrapper.instance()._dispatchTopSitesStats();
    313      assert.calledOnce(DEFAULT_PROPS.dispatch);
    314      assert.calledWithExactly(
    315        DEFAULT_PROPS.dispatch,
    316        ac.AlsoToMain({
    317          type: at.SAVE_SESSION_PERF_DATA,
    318          data: {
    319            topsites_icon_stats: {
    320              custom_screenshot: 0,
    321              screenshot: 0,
    322              tippytop: 0,
    323              rich_icon: 0,
    324              no_image: 8,
    325            },
    326            topsites_pinned: 0,
    327            topsites_search_shortcuts: 0,
    328          },
    329        })
    330      );
    331    });
    332    it("should only count visible top sites on normal layout", () => {
    333      globals.set("matchMedia", () => ({ matches: false }));
    334      const rows = [
    335        {},
    336        {},
    337        {},
    338        {},
    339        {},
    340        {},
    341        {},
    342        {},
    343        {},
    344        {},
    345        {},
    346        {},
    347        {},
    348        {},
    349        {},
    350        {},
    351      ];
    352      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
    353      wrapper.instance()._dispatchTopSitesStats();
    354      assert.calledOnce(DEFAULT_PROPS.dispatch);
    355      assert.calledWithExactly(
    356        DEFAULT_PROPS.dispatch,
    357        ac.AlsoToMain({
    358          type: at.SAVE_SESSION_PERF_DATA,
    359          data: {
    360            topsites_icon_stats: {
    361              custom_screenshot: 0,
    362              screenshot: 0,
    363              tippytop: 0,
    364              rich_icon: 0,
    365              no_image: 6,
    366            },
    367            topsites_pinned: 0,
    368            topsites_search_shortcuts: 0,
    369          },
    370        })
    371      );
    372    });
    373  });
    374 });
    375 
    376 describe("<TopSiteLink>", () => {
    377  let globals;
    378  let link;
    379  let url;
    380  beforeEach(() => {
    381    globals = new GlobalOverrider();
    382    url = {
    383      createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),
    384      revokeObjectURL: globals.sandbox.spy(),
    385    };
    386    globals.set("URL", url);
    387    link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" };
    388  });
    389  afterEach(() => globals.restore());
    390  it("should add the right url", () => {
    391    link.url = "https://www.foobar.org";
    392    const wrapper = shallow(<TopSiteLink link={link} />);
    393    assert.propertyVal(
    394      wrapper.find("a").props(),
    395      "href",
    396      "https://www.foobar.org"
    397    );
    398  });
    399  it("should not add the url to the href if it a search shortcut", () => {
    400    link.searchTopSite = true;
    401    const wrapper = shallow(<TopSiteLink link={link} />);
    402    assert.isUndefined(wrapper.find("a").props().href);
    403  });
    404  it("should have rtl direction automatically set for text", () => {
    405    const wrapper = shallow(<TopSiteLink link={link} />);
    406 
    407    assert.isTrue(!!wrapper.find("[dir='auto']").length);
    408  });
    409  it("should render a title", () => {
    410    const wrapper = shallow(<TopSiteLink link={link} title="foobar" />);
    411    const titleEl = wrapper.find(".title");
    412 
    413    assert.equal(titleEl.text(), "foobar");
    414  });
    415  it("should have only the title as the text of the link", () => {
    416    const wrapper = shallow(<TopSiteLink link={link} title="foobar" />);
    417 
    418    assert.equal(wrapper.find("a").text(), "foobar");
    419  });
    420  it("should render the pin icon for pinned links", () => {
    421    link.isPinned = true;
    422    link.pinnedIndex = 7;
    423    const wrapper = shallow(<TopSiteLink link={link} />);
    424    assert.equal(wrapper.find(".icon-pin-small").length, 1);
    425  });
    426  it("should not render the pin icon for non pinned links", () => {
    427    link.isPinned = false;
    428    const wrapper = shallow(<TopSiteLink link={link} />);
    429    assert.equal(wrapper.find(".icon-pin-small").length, 0);
    430  });
    431  it("should render the first letter of the title as a fallback for missing icons", () => {
    432    const wrapper = shallow(<TopSiteLink link={link} title={"foo"} />);
    433    assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
    434  });
    435  it("should render the tippy top icon if provided and not a small icon", () => {
    436    link.tippyTopIcon = "foo.png";
    437    link.backgroundColor = "#FFFFFF";
    438    const wrapper = shallow(<TopSiteLink link={link} />);
    439    assert.lengthOf(wrapper.find(".screenshot"), 0);
    440    assert.lengthOf(wrapper.find(".default-icon"), 0);
    441    const tippyTop = wrapper.find(".rich-icon");
    442    assert.propertyVal(
    443      tippyTop.props().style,
    444      "backgroundImage",
    445      "url(foo.png)"
    446    );
    447    assert.propertyVal(tippyTop.props().style, "backgroundColor", "#FFFFFF");
    448  });
    449  it("should render a rich icon if provided and not a small icon", () => {
    450    link.favicon = "foo.png";
    451    link.faviconSize = 196;
    452    link.backgroundColor = "#FFFFFF";
    453    const wrapper = shallow(<TopSiteLink link={link} />);
    454    assert.lengthOf(wrapper.find(".screenshot"), 0);
    455    assert.lengthOf(wrapper.find(".default-icon"), 0);
    456    const richIcon = wrapper.find(".rich-icon");
    457    assert.propertyVal(
    458      richIcon.props().style,
    459      "backgroundImage",
    460      "url(foo.png)"
    461    );
    462    assert.propertyVal(richIcon.props().style, "backgroundColor", "#FFFFFF");
    463  });
    464  it("should not render a rich icon if it is smaller than 96x96", () => {
    465    link.favicon = "foo.png";
    466    link.faviconSize = 48;
    467    link.backgroundColor = "#FFFFFF";
    468    const wrapper = shallow(<TopSiteLink link={link} />);
    469    assert.lengthOf(wrapper.find(".default-icon"), 1);
    470    assert.equal(wrapper.find(".rich-icon").length, 0);
    471  });
    472  it("should apply just the default class name to the outer link if props.className is falsey", () => {
    473    const wrapper = shallow(<TopSiteLink className={false} />);
    474    assert.ok(wrapper.find("li").hasClass("top-site-outer"));
    475  });
    476  it("should add props.className to the outer link element", () => {
    477    const wrapper = shallow(<TopSiteLink className="foo bar" />);
    478    assert.ok(wrapper.find("li").hasClass("top-site-outer foo bar"));
    479  });
    480  describe("#_allowDrop", () => {
    481    let wrapper;
    482    let event;
    483    beforeEach(() => {
    484      event = {
    485        dataTransfer: {
    486          types: ["text/topsite-index"],
    487        },
    488      };
    489      wrapper = shallow(
    490        <TopSiteLink isDraggable={true} onDragEvent={() => {}} />
    491      );
    492    });
    493    it("should be droppable for basic case", () => {
    494      const result = wrapper.instance()._allowDrop(event);
    495      assert.isTrue(result);
    496    });
    497    it("should not be droppable for sponsored_position", () => {
    498      wrapper.setProps({ link: { sponsored_position: 1 } });
    499      const result = wrapper.instance()._allowDrop(event);
    500      assert.isFalse(result);
    501    });
    502    it("should not be droppable for link.type", () => {
    503      wrapper.setProps({ link: { type: "SPOC" } });
    504      const result = wrapper.instance()._allowDrop(event);
    505      assert.isFalse(result);
    506    });
    507  });
    508  describe("#onDragEvent", () => {
    509    let simulate;
    510    let wrapper;
    511    beforeEach(() => {
    512      wrapper = shallow(
    513        <TopSiteLink isDraggable={true} onDragEvent={() => {}} />
    514      );
    515      simulate = type => {
    516        const event = {
    517          dataTransfer: { setData() {}, types: { includes() {} } },
    518          preventDefault() {
    519            this.prevented = true;
    520          },
    521          target: { blur() {} },
    522          type,
    523        };
    524        wrapper.simulate(type, event);
    525        return event;
    526      };
    527    });
    528    it("should allow clicks without dragging", () => {
    529      simulate("mousedown");
    530      simulate("mouseup");
    531 
    532      const event = simulate("click");
    533 
    534      assert.notOk(event.prevented);
    535    });
    536    it("should prevent clicks after dragging", () => {
    537      simulate("mousedown");
    538      simulate("dragstart");
    539      simulate("dragenter");
    540      simulate("drop");
    541      simulate("dragend");
    542      simulate("mouseup");
    543 
    544      const event = simulate("click");
    545 
    546      assert.ok(event.prevented);
    547    });
    548    it("should allow clicks after dragging then clicking", () => {
    549      simulate("mousedown");
    550      simulate("dragstart");
    551      simulate("dragenter");
    552      simulate("drop");
    553      simulate("dragend");
    554      simulate("mouseup");
    555      simulate("click");
    556 
    557      simulate("mousedown");
    558      simulate("mouseup");
    559 
    560      const event = simulate("click");
    561 
    562      assert.notOk(event.prevented);
    563    });
    564    it("should prevent dragging with sponsored_position from dragstart", () => {
    565      const preventDefault = sinon.stub();
    566      // eslint-disable-next-line no-shadow
    567      const blur = sinon.stub();
    568      wrapper.setProps({ link: { sponsored_position: 1 } });
    569      wrapper.instance().onDragEvent({
    570        type: "dragstart",
    571        preventDefault,
    572        target: { blur },
    573      });
    574      assert.calledOnce(preventDefault);
    575      assert.calledOnce(blur);
    576      assert.isUndefined(wrapper.instance().dragged);
    577    });
    578    it("should prevent dragging with link.shim from dragstart", () => {
    579      const preventDefault = sinon.stub();
    580      // eslint-disable-next-line no-shadow
    581      const blur = sinon.stub();
    582      wrapper.setProps({ link: { type: "SPOC" } });
    583      wrapper.instance().onDragEvent({
    584        type: "dragstart",
    585        preventDefault,
    586        target: { blur },
    587      });
    588      assert.calledOnce(preventDefault);
    589      assert.calledOnce(blur);
    590      assert.isUndefined(wrapper.instance().dragged);
    591    });
    592  });
    593 
    594  describe("#generateColor", () => {
    595    let colors;
    596    beforeEach(() => {
    597      colors = "#0090ED,#FF4F5F,#2AC3A2";
    598    });
    599 
    600    it("should generate a random color but always pick the same color for the same string", async () => {
    601      let wrapper = shallow(
    602        <TopSiteLink colors={colors} title={"food"} link={link} />
    603      );
    604 
    605      assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
    606      assert.equal(
    607        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
    608        colors.split(",")[1]
    609      );
    610      assert.ok(true);
    611    });
    612 
    613    it("should generate a different random color", async () => {
    614      let wrapper = shallow(
    615        <TopSiteLink colors={colors} title={"fam"} link={link} />
    616      );
    617 
    618      assert.equal(
    619        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
    620        colors.split(",")[2]
    621      );
    622      assert.ok(true);
    623    });
    624 
    625    it("should generate a third random color", async () => {
    626      let wrapper = shallow(<TopSiteLink colors={colors} title={"foo"} />);
    627 
    628      assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
    629      assert.equal(
    630        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
    631        colors.split(",")[0]
    632      );
    633      assert.ok(true);
    634    });
    635  });
    636 });
    637 
    638 describe("<TopSite>", () => {
    639  let link;
    640  beforeEach(() => {
    641    link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" };
    642  });
    643 
    644  // Build IntersectionObserver class with the arg `entries` for the intersect callback.
    645  function buildIntersectionObserver(entries) {
    646    return class {
    647      constructor(callback) {
    648        this.callback = callback;
    649      }
    650 
    651      observe() {
    652        this.callback(entries);
    653      }
    654 
    655      unobserve() {}
    656    };
    657  }
    658 
    659  it("should render a TopSite", () => {
    660    const wrapper = shallow(<TopSite link={link} />);
    661    assert.ok(wrapper.exists());
    662  });
    663 
    664  it("should render a shortened title based off the url", () => {
    665    link.url = "https://www.foobar.org";
    666    link.hostname = "foobar";
    667    link.eTLD = "org";
    668    const wrapper = shallow(<TopSite link={link} />);
    669 
    670    assert.equal(wrapper.find(TopSiteLink).props().title, "foobar");
    671  });
    672 
    673  it("should parse args for fluent correctly", () => {
    674    const title = '"fluent"';
    675    link.hostname = title;
    676 
    677    const wrapper = mount(<TopSite link={link} />);
    678    const button = wrapper.find(
    679      "button[data-l10n-id='newtab-menu-content-tooltip']"
    680    );
    681    assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title }));
    682  });
    683 
    684  it("should have .active class, on top-site-outer if context menu is open", () => {
    685    const wrapper = shallow(<TopSite link={link} index={1} activeIndex={1} />);
    686    wrapper.setState({ showContextMenu: true });
    687 
    688    assert.equal(wrapper.find(TopSiteLink).props().className.trim(), "active");
    689  });
    690  it("should not add .active class, on top-site-outer if context menu is closed", () => {
    691    const wrapper = shallow(<TopSite link={link} index={1} />);
    692    wrapper.setState({ showContextMenu: false, activeTile: 1 });
    693    assert.equal(wrapper.find(TopSiteLink).props().className, "");
    694  });
    695  it("should render a context menu button", () => {
    696    const wrapper = shallow(<TopSite link={link} />);
    697    assert.equal(wrapper.find(ContextMenuButton).length, 1);
    698  });
    699  it("should render a link menu", () => {
    700    const wrapper = shallow(<TopSite link={link} />);
    701    assert.equal(wrapper.find(LinkMenu).length, 1);
    702  });
    703  it("should pass onUpdate, site, options, and index to LinkMenu", () => {
    704    const wrapper = shallow(<TopSite link={link} />);
    705    const linkMenuProps = wrapper.find(LinkMenu).props();
    706    ["onUpdate", "site", "index", "options"].forEach(prop =>
    707      assert.property(linkMenuProps, prop)
    708    );
    709  });
    710  it("should pass through the correct menu options to LinkMenu", () => {
    711    const wrapper = shallow(<TopSite link={link} />);
    712    const linkMenuProps = wrapper.find(LinkMenu).props();
    713    assert.deepEqual(linkMenuProps.options, [
    714      "CheckPinTopSite",
    715      "EditTopSite",
    716      "Separator",
    717      "OpenInNewWindow",
    718      "OpenInPrivateWindow",
    719      "Separator",
    720      "BlockUrl",
    721      "DeleteUrl",
    722    ]);
    723  });
    724  it("should record impressions for visible organic Top Sites", () => {
    725    const dispatch = sinon.stub();
    726    const wrapper = shallow(
    727      <TopSite
    728        link={link}
    729        index={3}
    730        dispatch={dispatch}
    731        IntersectionObserver={buildIntersectionObserver([
    732          {
    733            isIntersecting: true,
    734            intersectionRatio: INTERSECTION_RATIO,
    735          },
    736        ])}
    737        document={{
    738          visibilityState: "visible",
    739          addEventListener: sinon.stub(),
    740          removeEventListener: sinon.stub(),
    741        }}
    742      />
    743    );
    744    const linkWrapper = wrapper.find(TopSiteLink).dive();
    745    assert.ok(linkWrapper.exists());
    746    const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive();
    747    assert.ok(impressionWrapper.exists());
    748 
    749    assert.calledOnce(dispatch);
    750 
    751    let [action] = dispatch.firstCall.args;
    752    assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS);
    753 
    754    assert.propertyVal(action.data, "type", "impression");
    755    assert.propertyVal(action.data, "source", "newtab");
    756    assert.propertyVal(action.data, "position", 3);
    757  });
    758  it("should record impressions for visible sponsored Top Sites", () => {
    759    const dispatch = sinon.stub();
    760    const wrapper = shallow(
    761      <TopSite
    762        link={Object.assign({}, link, {
    763          sponsored_position: 2,
    764          sponsored_tile_id: 12345,
    765          sponsored_impression_url: "http://impression.example.com/",
    766        })}
    767        index={3}
    768        dispatch={dispatch}
    769        IntersectionObserver={buildIntersectionObserver([
    770          {
    771            isIntersecting: true,
    772            intersectionRatio: INTERSECTION_RATIO,
    773          },
    774        ])}
    775        document={{
    776          visibilityState: "visible",
    777          addEventListener: sinon.stub(),
    778          removeEventListener: sinon.stub(),
    779        }}
    780      />
    781    );
    782    const linkWrapper = wrapper.find(TopSiteLink).dive();
    783    assert.ok(linkWrapper.exists());
    784    const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive();
    785    assert.ok(impressionWrapper.exists());
    786 
    787    assert.calledOnce(dispatch);
    788 
    789    let [action] = dispatch.firstCall.args;
    790    assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);
    791 
    792    assert.propertyVal(action.data, "type", "impression");
    793    assert.propertyVal(action.data, "tile_id", 12345);
    794    assert.propertyVal(action.data, "source", "newtab");
    795    assert.propertyVal(action.data, "position", 3);
    796    assert.propertyVal(
    797      action.data,
    798      "reporting_url",
    799      "http://impression.example.com/"
    800    );
    801    assert.propertyVal(action.data, "advertiser", "foo");
    802  });
    803 
    804  describe("#onLinkClick", () => {
    805    it("should call dispatch when the link is clicked", () => {
    806      const dispatch = sinon.stub();
    807      const wrapper = shallow(
    808        <TopSite link={link} index={3} dispatch={dispatch} />
    809      );
    810 
    811      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });
    812 
    813      let [action] = dispatch.firstCall.args;
    814      assert.isUserEventAction(action);
    815 
    816      assert.propertyVal(action.data, "event", "CLICK");
    817      assert.propertyVal(action.data, "source", "TOP_SITES");
    818      assert.propertyVal(action.data, "action_position", 3);
    819 
    820      [action] = dispatch.secondCall.args;
    821      assert.propertyVal(action, "type", at.OPEN_LINK);
    822 
    823      // Organic Top Site click event.
    824      [action] = dispatch.thirdCall.args;
    825      assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS);
    826 
    827      assert.propertyVal(action.data, "type", "click");
    828      assert.propertyVal(action.data, "source", "newtab");
    829      assert.propertyVal(action.data, "position", 3);
    830    });
    831    it("should dispatch a UserEventAction with the right data", () => {
    832      const dispatch = sinon.stub();
    833      const wrapper = shallow(
    834        <TopSite
    835          link={Object.assign({}, link, {
    836            iconType: "rich_icon",
    837            isPinned: true,
    838          })}
    839          index={3}
    840          dispatch={dispatch}
    841        />
    842      );
    843 
    844      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });
    845 
    846      const [action] = dispatch.firstCall.args;
    847      assert.isUserEventAction(action);
    848 
    849      assert.propertyVal(action.data, "event", "CLICK");
    850      assert.propertyVal(action.data, "source", "TOP_SITES");
    851      assert.propertyVal(action.data, "action_position", 3);
    852      assert.propertyVal(action.data.value, "card_type", "pinned");
    853      assert.propertyVal(action.data.value, "icon_type", "rich_icon");
    854    });
    855    it("should dispatch a UserEventAction with the right data for search top site", () => {
    856      const dispatch = sinon.stub();
    857      const siteInfo = {
    858        iconType: "tippytop",
    859        isPinned: true,
    860        searchTopSite: true,
    861        hostname: "google",
    862        label: "@google",
    863      };
    864      const wrapper = shallow(
    865        <TopSite
    866          link={Object.assign({}, link, siteInfo)}
    867          index={3}
    868          dispatch={dispatch}
    869        />
    870      );
    871 
    872      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });
    873 
    874      const [action] = dispatch.firstCall.args;
    875      assert.isUserEventAction(action);
    876 
    877      assert.propertyVal(action.data, "event", "CLICK");
    878      assert.propertyVal(action.data, "source", "TOP_SITES");
    879      assert.propertyVal(action.data, "action_position", 3);
    880      assert.propertyVal(action.data.value, "card_type", "search");
    881      assert.propertyVal(action.data.value, "icon_type", "tippytop");
    882      assert.propertyVal(action.data.value, "search_vendor", "google");
    883    });
    884    it("should dispatch a UserEventAction with the right data for SPOC top site", () => {
    885      const dispatch = sinon.stub();
    886      const siteInfo = {
    887        id: 1,
    888        iconType: "custom_screenshot",
    889        type: "SPOC",
    890        pos: 1,
    891        label: "test advertiser",
    892        shim: { click: "shim_click_id" },
    893      };
    894      const wrapper = shallow(
    895        <TopSite
    896          link={Object.assign({}, link, siteInfo)}
    897          index={0}
    898          dispatch={dispatch}
    899        />
    900      );
    901 
    902      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });
    903 
    904      let [action] = dispatch.firstCall.args;
    905      assert.isUserEventAction(action);
    906 
    907      assert.propertyVal(action.data, "event", "CLICK");
    908      assert.propertyVal(action.data, "source", "TOP_SITES");
    909      assert.propertyVal(action.data, "action_position", 0);
    910      assert.propertyVal(action.data.value, "card_type", "spoc");
    911      assert.propertyVal(action.data.value, "icon_type", "custom_screenshot");
    912 
    913      // Pocket SPOC click event.
    914      [action] = dispatch.getCall(2).args;
    915      assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS);
    916 
    917      assert.propertyVal(action.data, "click", 0);
    918      assert.propertyVal(action.data, "source", "TOP_SITES");
    919 
    920      [action] = dispatch.getCall(3).args;
    921      assert.equal(action.type, at.DISCOVERY_STREAM_USER_EVENT);
    922 
    923      assert.propertyVal(action.data, "event", "CLICK");
    924      assert.propertyVal(action.data, "action_position", 1);
    925      assert.propertyVal(action.data, "source", "TOP_SITES");
    926      assert.propertyVal(action.data.value, "card_type", "spoc");
    927      assert.propertyVal(action.data.value, "tile_id", 1);
    928      assert.propertyVal(action.data.value, "shim", "shim_click_id");
    929 
    930      // Topsite SPOC click event.
    931      [action] = dispatch.getCall(4).args;
    932      assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);
    933 
    934      assert.propertyVal(action.data, "type", "click");
    935      assert.propertyVal(action.data, "tile_id", 1);
    936      assert.propertyVal(action.data, "source", "newtab");
    937      assert.propertyVal(action.data, "position", 1);
    938      assert.propertyVal(action.data, "advertiser", "test advertiser");
    939    });
    940    it("should dispatch OPEN_LINK with the right data", () => {
    941      const dispatch = sinon.stub();
    942      const wrapper = shallow(
    943        <TopSite
    944          link={Object.assign({}, link, { typedBonus: true })}
    945          index={3}
    946          dispatch={dispatch}
    947        />
    948      );
    949 
    950      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });
    951 
    952      const [action] = dispatch.secondCall.args;
    953      assert.propertyVal(action, "type", at.OPEN_LINK);
    954      assert.propertyVal(action.data, "typedBonus", true);
    955    });
    956  });
    957 });
    958 
    959 describe("<TopSiteForm>", () => {
    960  let wrapper;
    961  let sandbox;
    962 
    963  function testSetup(props = {}) {
    964    sandbox = sinon.createSandbox();
    965    const customProps = Object.assign(
    966      {},
    967      { onClose: sandbox.spy(), dispatch: sandbox.spy() },
    968      props
    969    );
    970    wrapper = mount(<TopSiteForm {...customProps} />);
    971  }
    972 
    973  describe("validateForm", () => {
    974    beforeEach(() => testSetup({ site: { url: "http://foo" } }));
    975 
    976    it("should return true for a correct URL", () => {
    977      wrapper.setState({ url: "foo" });
    978 
    979      assert.isTrue(wrapper.instance().validateForm());
    980    });
    981 
    982    it("should return false for a incorrect URL", () => {
    983      wrapper.setState({ url: " " });
    984 
    985      assert.isNull(wrapper.instance().validateForm());
    986      assert.isTrue(wrapper.state().validationError);
    987    });
    988 
    989    it("should return true for a correct custom screenshot URL", () => {
    990      wrapper.setState({ customScreenshotUrl: "foo" });
    991 
    992      assert.isTrue(wrapper.instance().validateForm());
    993    });
    994 
    995    it("should return false for a incorrect custom screenshot URL", () => {
    996      wrapper.setState({ customScreenshotUrl: " " });
    997 
    998      assert.isNull(wrapper.instance().validateForm());
    999    });
   1000 
   1001    it("should return true for an empty custom screenshot URL", () => {
   1002      wrapper.setState({ customScreenshotUrl: "" });
   1003 
   1004      assert.isTrue(wrapper.instance().validateForm());
   1005    });
   1006 
   1007    it("should return false for file: protocol", () => {
   1008      wrapper.setState({ customScreenshotUrl: "file:///C:/Users/foo" });
   1009 
   1010      assert.isFalse(wrapper.instance().validateForm());
   1011    });
   1012  });
   1013 
   1014  describe("#previewButton", () => {
   1015    beforeEach(() =>
   1016      testSetup({
   1017        site: { customScreenshotURL: "http://foo.com" },
   1018        previewResponse: null,
   1019      })
   1020    );
   1021 
   1022    it("should render the preview button on invalid urls", () => {
   1023      assert.equal(0, wrapper.find(".preview").length);
   1024 
   1025      wrapper.setState({ customScreenshotUrl: " " });
   1026 
   1027      assert.equal(1, wrapper.find(".preview").length);
   1028    });
   1029 
   1030    it("should render the preview button when input value updated", () => {
   1031      assert.equal(0, wrapper.find(".preview").length);
   1032 
   1033      wrapper.setState({
   1034        customScreenshotUrl: "http://baz.com",
   1035        screenshotPreview: null,
   1036      });
   1037 
   1038      assert.equal(1, wrapper.find(".preview").length);
   1039    });
   1040  });
   1041 
   1042  describe("preview request", () => {
   1043    beforeEach(() => {
   1044      testSetup({
   1045        site: { customScreenshotURL: "http://foo.com", url: "http://foo.com" },
   1046        previewResponse: null,
   1047      });
   1048    });
   1049 
   1050    it("shouldn't dispatch a request for invalid urls", () => {
   1051      wrapper.setState({ customScreenshotUrl: " ", url: "foo" });
   1052 
   1053      wrapper.find(".preview").simulate("click");
   1054 
   1055      assert.notCalled(wrapper.props().dispatch);
   1056    });
   1057 
   1058    it("should dispatch a PREVIEW_REQUEST", () => {
   1059      wrapper.setState({ customScreenshotUrl: "screenshot" });
   1060      wrapper.find(".preview").simulate("submit");
   1061 
   1062      assert.calledTwice(wrapper.props().dispatch);
   1063      assert.calledWith(
   1064        wrapper.props().dispatch,
   1065        ac.AlsoToMain({
   1066          type: at.PREVIEW_REQUEST,
   1067          data: { url: "http://screenshot" },
   1068        })
   1069      );
   1070      assert.calledWith(
   1071        wrapper.props().dispatch,
   1072        ac.UserEvent({
   1073          event: "PREVIEW_REQUEST",
   1074          source: "TOP_SITES",
   1075        })
   1076      );
   1077    });
   1078  });
   1079 
   1080  describe("#TopSiteLink", () => {
   1081    beforeEach(() => {
   1082      testSetup();
   1083    });
   1084 
   1085    it("should display a TopSiteLink preview", () => {
   1086      assert.equal(wrapper.find(TopSiteLink).length, 1);
   1087    });
   1088 
   1089    it("should display an icon for tippyTop sites", () => {
   1090      wrapper.setProps({ site: { tippyTopIcon: "bar" } });
   1091 
   1092      assert.equal(
   1093        wrapper.find(".top-site-icon").getDOMNode().style["background-image"],
   1094        'url("bar")'
   1095      );
   1096    });
   1097 
   1098    it("should not display a preview screenshot", () => {
   1099      wrapper.setProps({ previewResponse: "foo", previewUrl: "foo" });
   1100 
   1101      assert.lengthOf(wrapper.find(".screenshot"), 0);
   1102    });
   1103 
   1104    it("should not render any icon on error", () => {
   1105      wrapper.setProps({ previewResponse: "" });
   1106 
   1107      assert.equal(wrapper.find(".top-site-icon").length, 0);
   1108    });
   1109 
   1110    it("should render the search icon when searchTopSite is true", () => {
   1111      wrapper.setProps({ site: { tippyTopIcon: "bar", searchTopSite: true } });
   1112 
   1113      assert.equal(
   1114        wrapper.find(".rich-icon").getDOMNode().style["background-image"],
   1115        'url("bar")'
   1116      );
   1117      assert.isTrue(wrapper.find(".search-topsite").exists());
   1118    });
   1119  });
   1120 
   1121  describe("#addMode", () => {
   1122    beforeEach(() => testSetup());
   1123 
   1124    it("should render the component", () => {
   1125      assert.ok(wrapper.find(TopSiteForm).exists());
   1126    });
   1127    it("should have the correct header", () => {
   1128      assert.equal(
   1129        wrapper.findWhere(
   1130          n =>
   1131            n.length &&
   1132            n.prop("data-l10n-id") === "newtab-topsites-add-shortcut-header"
   1133        ).length,
   1134        1
   1135      );
   1136    });
   1137    it("should have the correct button text", () => {
   1138      assert.equal(
   1139        wrapper.findWhere(
   1140          n =>
   1141            n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button"
   1142        ).length,
   1143        0
   1144      );
   1145      assert.equal(
   1146        wrapper.findWhere(
   1147          n =>
   1148            n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button"
   1149        ).length,
   1150        1
   1151      );
   1152    });
   1153    it("should not render a preview button", () => {
   1154      assert.equal(0, wrapper.find(".custom-image-input-container").length);
   1155    });
   1156    it("should call onClose if Cancel button is clicked", () => {
   1157      wrapper.find(".cancel").simulate("click");
   1158      assert.calledOnce(wrapper.instance().props.onClose);
   1159    });
   1160    it("should set validationError if url is empty", () => {
   1161      assert.equal(wrapper.state().validationError, false);
   1162      wrapper.find(".done").simulate("submit");
   1163      assert.equal(wrapper.state().validationError, true);
   1164    });
   1165    it("should set validationError if url is invalid", () => {
   1166      wrapper.setState({ url: "not valid" });
   1167      assert.equal(wrapper.state().validationError, false);
   1168      wrapper.find(".done").simulate("submit");
   1169      assert.equal(wrapper.state().validationError, true);
   1170    });
   1171    it("should call onClose and dispatch with right args if URL is valid", () => {
   1172      wrapper.setState({ url: "valid.com", label: "a label" });
   1173      wrapper.find(".done").simulate("submit");
   1174      assert.calledOnce(wrapper.instance().props.onClose);
   1175      assert.calledWith(wrapper.instance().props.dispatch, {
   1176        data: {
   1177          site: { label: "a label", url: "http://valid.com" },
   1178          index: -1,
   1179        },
   1180        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1181        type: at.TOP_SITES_PIN,
   1182      });
   1183      assert.calledWith(wrapper.instance().props.dispatch, {
   1184        data: {
   1185          action_position: -1,
   1186          source: "TOP_SITES",
   1187          event: "TOP_SITES_ADD",
   1188        },
   1189        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1190        type: at.TELEMETRY_USER_EVENT,
   1191      });
   1192    });
   1193    it("should not pass empty string label in dispatch data", () => {
   1194      wrapper.setState({ url: "valid.com", label: "" });
   1195      wrapper.find(".done").simulate("submit");
   1196      assert.calledWith(wrapper.instance().props.dispatch, {
   1197        data: { site: { url: "http://valid.com" }, index: -1 },
   1198        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1199        type: at.TOP_SITES_PIN,
   1200      });
   1201    });
   1202    it("should open the custom screenshot input", () => {
   1203      assert.isFalse(wrapper.state().showCustomScreenshotForm);
   1204 
   1205      wrapper.find(A11yLinkButton).simulate("click");
   1206 
   1207      assert.isTrue(wrapper.state().showCustomScreenshotForm);
   1208    });
   1209  });
   1210 
   1211  describe("edit existing Topsite", () => {
   1212    beforeEach(() =>
   1213      testSetup({
   1214        site: {
   1215          url: "https://foo.bar",
   1216          label: "baz",
   1217          customScreenshotURL: "http://foo",
   1218        },
   1219        index: 7,
   1220      })
   1221    );
   1222 
   1223    it("should render the component", () => {
   1224      assert.ok(wrapper.find(TopSiteForm).exists());
   1225    });
   1226    it("should have the correct header", () => {
   1227      assert.equal(
   1228        wrapper.findWhere(
   1229          n => n.prop("data-l10n-id") === "newtab-topsites-edit-shortcut-header"
   1230        ).length,
   1231        1
   1232      );
   1233    });
   1234    it("should have the correct button text", () => {
   1235      assert.equal(
   1236        wrapper.findWhere(
   1237          n => n.prop("data-l10n-id") === "newtab-topsites-add-button"
   1238        ).length,
   1239        0
   1240      );
   1241      assert.equal(
   1242        wrapper.findWhere(
   1243          n => n.prop("data-l10n-id") === "newtab-topsites-save-button"
   1244        ).length,
   1245        1
   1246      );
   1247    });
   1248    it("should call onClose if Cancel button is clicked", () => {
   1249      wrapper.find(".cancel").simulate("click");
   1250      assert.calledOnce(wrapper.instance().props.onClose);
   1251    });
   1252    it("should show error and not call onClose or dispatch if URL is empty", () => {
   1253      wrapper.setState({ url: "" });
   1254      assert.equal(wrapper.state().validationError, false);
   1255      wrapper.find(".done").simulate("submit");
   1256      assert.equal(wrapper.state().validationError, true);
   1257      assert.notCalled(wrapper.instance().props.onClose);
   1258      assert.notCalled(wrapper.instance().props.dispatch);
   1259    });
   1260    it("should show error and not call onClose or dispatch if URL is invalid", () => {
   1261      wrapper.setState({ url: "not valid" });
   1262      assert.equal(wrapper.state().validationError, false);
   1263      wrapper.find(".done").simulate("submit");
   1264      assert.equal(wrapper.state().validationError, true);
   1265      assert.notCalled(wrapper.instance().props.onClose);
   1266      assert.notCalled(wrapper.instance().props.dispatch);
   1267    });
   1268    it("should call onClose and dispatch with right args if URL is valid", () => {
   1269      wrapper.find(".done").simulate("submit");
   1270      assert.calledOnce(wrapper.instance().props.onClose);
   1271      assert.calledTwice(wrapper.instance().props.dispatch);
   1272      assert.calledWith(wrapper.instance().props.dispatch, {
   1273        data: {
   1274          site: {
   1275            label: "baz",
   1276            url: "https://foo.bar",
   1277            customScreenshotURL: "http://foo",
   1278          },
   1279          index: 7,
   1280        },
   1281        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1282        type: at.TOP_SITES_PIN,
   1283      });
   1284      assert.calledWith(wrapper.instance().props.dispatch, {
   1285        data: {
   1286          action_position: 7,
   1287          source: "TOP_SITES",
   1288          event: "TOP_SITES_EDIT",
   1289          hasTitleChanged: false,
   1290          hasURLChanged: false,
   1291        },
   1292        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1293        type: at.TELEMETRY_USER_EVENT,
   1294      });
   1295    });
   1296    it("should set customScreenshotURL to null if it was removed", () => {
   1297      wrapper.setState({ customScreenshotUrl: "" });
   1298 
   1299      wrapper.find(".done").simulate("submit");
   1300 
   1301      assert.calledWith(wrapper.instance().props.dispatch, {
   1302        data: {
   1303          site: {
   1304            label: "baz",
   1305            url: "https://foo.bar",
   1306            customScreenshotURL: null,
   1307          },
   1308          index: 7,
   1309        },
   1310        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1311        type: at.TOP_SITES_PIN,
   1312      });
   1313    });
   1314    it("should call onClose and dispatch with right args if URL is valid (negative index)", () => {
   1315      wrapper.setProps({ index: -1 });
   1316      wrapper.find(".done").simulate("submit");
   1317      assert.calledOnce(wrapper.instance().props.onClose);
   1318      assert.calledTwice(wrapper.instance().props.dispatch);
   1319      assert.calledWith(wrapper.instance().props.dispatch, {
   1320        data: {
   1321          site: {
   1322            label: "baz",
   1323            url: "https://foo.bar",
   1324            customScreenshotURL: "http://foo",
   1325          },
   1326          index: -1,
   1327        },
   1328        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1329        type: at.TOP_SITES_PIN,
   1330      });
   1331    });
   1332    it("should not pass empty string label in dispatch data", () => {
   1333      wrapper.setState({ label: "" });
   1334      wrapper.find(".done").simulate("submit");
   1335      assert.calledWith(wrapper.instance().props.dispatch, {
   1336        data: {
   1337          site: { url: "https://foo.bar", customScreenshotURL: "http://foo" },
   1338          index: 7,
   1339        },
   1340        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1341        type: at.TOP_SITES_PIN,
   1342      });
   1343    });
   1344    it("should render the save button if custom screenshot request finished", () => {
   1345      wrapper.setState({
   1346        customScreenshotUrl: "foo",
   1347        screenshotPreview: "custom",
   1348      });
   1349      assert.equal(0, wrapper.find(".preview").length);
   1350      assert.equal(1, wrapper.find(".done").length);
   1351    });
   1352    it("should render the save button if custom screenshot url was cleared", () => {
   1353      wrapper.setState({ customScreenshotUrl: "" });
   1354      wrapper.setProps({ site: { customScreenshotURL: "foo" } });
   1355      assert.equal(0, wrapper.find(".preview").length);
   1356      assert.equal(1, wrapper.find(".done").length);
   1357    });
   1358  });
   1359 
   1360  describe("#previewMode", () => {
   1361    beforeEach(() => testSetup({ previewResponse: null }));
   1362 
   1363    it("should transition from save to preview", () => {
   1364      wrapper.setProps({
   1365        site: { url: "https://foo.bar", customScreenshotURL: "baz" },
   1366        index: 7,
   1367      });
   1368 
   1369      assert.equal(
   1370        wrapper.findWhere(
   1371          n =>
   1372            n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button"
   1373        ).length,
   1374        1
   1375      );
   1376 
   1377      wrapper.setState({ customScreenshotUrl: "foo" });
   1378 
   1379      assert.equal(
   1380        wrapper.findWhere(
   1381          n =>
   1382            n.length &&
   1383            n.prop("data-l10n-id") === "newtab-topsites-preview-button"
   1384        ).length,
   1385        1
   1386      );
   1387    });
   1388 
   1389    it("should transition from add to preview", () => {
   1390      assert.equal(
   1391        wrapper.findWhere(
   1392          n =>
   1393            n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button"
   1394        ).length,
   1395        1
   1396      );
   1397 
   1398      wrapper.setState({ customScreenshotUrl: "foo" });
   1399 
   1400      assert.equal(
   1401        wrapper.findWhere(
   1402          n =>
   1403            n.length &&
   1404            n.prop("data-l10n-id") === "newtab-topsites-preview-button"
   1405        ).length,
   1406        1
   1407      );
   1408    });
   1409  });
   1410 
   1411  describe("#validateUrl", () => {
   1412    it("should properly validate URLs", () => {
   1413      testSetup();
   1414      assert.ok(wrapper.instance().validateUrl("mozilla.org"));
   1415      assert.ok(wrapper.instance().validateUrl("https://mozilla.org"));
   1416      assert.ok(wrapper.instance().validateUrl("http://mozilla.org"));
   1417      assert.ok(
   1418        wrapper
   1419          .instance()
   1420          .validateUrl(
   1421            "https://mozilla.invisionapp.com/d/main/#/projects/prototypes"
   1422          )
   1423      );
   1424      assert.ok(wrapper.instance().validateUrl("httpfoobar"));
   1425      assert.ok(wrapper.instance().validateUrl("httpsfoo.bar"));
   1426      assert.isNull(wrapper.instance().validateUrl("mozilla org"));
   1427      assert.isNull(wrapper.instance().validateUrl(""));
   1428    });
   1429  });
   1430 
   1431  describe("#cleanUrl", () => {
   1432    it("should properly prepend http:// to URLs when required", () => {
   1433      testSetup();
   1434      assert.equal(
   1435        "http://mozilla.org",
   1436        wrapper.instance().cleanUrl("mozilla.org")
   1437      );
   1438      assert.equal(
   1439        "http://https.org",
   1440        wrapper.instance().cleanUrl("https.org")
   1441      );
   1442      assert.equal("http://httpcom", wrapper.instance().cleanUrl("httpcom"));
   1443      assert.equal(
   1444        "http://mozilla.org",
   1445        wrapper.instance().cleanUrl("http://mozilla.org")
   1446      );
   1447      assert.equal(
   1448        "https://firefox.com",
   1449        wrapper.instance().cleanUrl("https://firefox.com")
   1450      );
   1451    });
   1452  });
   1453 });
   1454 
   1455 describe("<TopSiteList>", () => {
   1456  const APP = { isForStartupCache: { App: false } };
   1457 
   1458  it("should render a TopSiteList element", () => {
   1459    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />);
   1460    assert.ok(wrapper.exists());
   1461  });
   1462  it("should render a TopSite for each link with the right url", () => {
   1463    const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }];
   1464    const wrapper = shallow(
   1465      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
   1466    );
   1467    const links = wrapper.find(TopSite);
   1468    assert.lengthOf(links, 2);
   1469    rows.forEach((row, i) =>
   1470      assert.equal(links.get(i).props.link.url, row.url)
   1471    );
   1472  });
   1473  it("should slice the TopSite rows to the TopSitesRows pref", () => {
   1474    const rows = [];
   1475    for (
   1476      let i = 0;
   1477      i < TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW + 3;
   1478      i++
   1479    ) {
   1480      rows.push({ url: `https://foo${i}.com` });
   1481    }
   1482    const wrapper = shallow(
   1483      <TopSiteList
   1484        {...DEFAULT_PROPS}
   1485        TopSites={{ rows }}
   1486        TopSitesRows={TOP_SITES_DEFAULT_ROWS}
   1487        App={APP}
   1488      />
   1489    );
   1490    const links = wrapper.find(TopSite);
   1491    assert.lengthOf(
   1492      links,
   1493      TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW
   1494    );
   1495  });
   1496  it("should add a add topsite button if there is availible space in the row", () => {
   1497    const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }];
   1498    const availibleRows = 1;
   1499    const wrapper = shallow(
   1500      <TopSiteList
   1501        {...DEFAULT_PROPS}
   1502        TopSites={{ rows }}
   1503        TopSitesRows={availibleRows}
   1504        App={APP}
   1505      />
   1506    );
   1507    assert.lengthOf(wrapper.find(TopSite), 2, "topSites");
   1508    assert.lengthOf(
   1509      wrapper.find(TopSiteAddButton),
   1510      availibleRows >= wrapper.find(TopSite).length ? 0 : 1,
   1511      "placeholders"
   1512    );
   1513  });
   1514  it("should fill sponsored top sites with placeholders while rendering for startup cache", () => {
   1515    const rows = [
   1516      { url: "https://sponsored01.com", sponsored_position: 1 },
   1517      { url: "https://sponsored02.com", sponsored_position: 2 },
   1518      { url: "https://sponsored03.com", type: "SPOC" },
   1519      { url: "https://foo.com" },
   1520      { url: "https://bar.com" },
   1521    ];
   1522    const wrapper = shallow(
   1523      <TopSiteList
   1524        {...DEFAULT_PROPS}
   1525        TopSites={{ rows }}
   1526        TopSitesRows={1}
   1527        App={{ isForStartupCache: { TopSites: true } }}
   1528      />
   1529    );
   1530    assert.lengthOf(wrapper.find(TopSite), 2, "topSites");
   1531    assert.lengthOf(wrapper.find(TopSitePlaceholder), 3, "placeholders");
   1532  });
   1533  it("should update state onDragStart and clear it onDragEnd", () => {
   1534    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={{ APP }} />);
   1535    const instance = wrapper.instance();
   1536    const index = 7;
   1537    const link = { url: "https://foo.com" };
   1538    const title = "foo";
   1539    instance.onDragEvent({ type: "dragstart" }, index, link, title);
   1540    assert.equal(instance.state.draggedIndex, index);
   1541    assert.equal(instance.state.draggedSite, link);
   1542    assert.equal(instance.state.draggedTitle, title);
   1543    instance.onDragEvent({ type: "dragend" });
   1544    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);
   1545  });
   1546  it("should clear state when new props arrive after a drop", () => {
   1547    const site1 = { url: "https://foo.com" };
   1548    const site2 = { url: "https://bar.com" };
   1549    const rows = [site1, site2];
   1550    const wrapper = shallow(
   1551      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
   1552    );
   1553    const instance = wrapper.instance();
   1554    instance.setState({
   1555      draggedIndex: 1,
   1556      draggedSite: site2,
   1557      draggedTitle: "bar",
   1558      topSitesPreview: [],
   1559    });
   1560    wrapper.setProps({ TopSites: { rows: [site2, site1] } });
   1561    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);
   1562  });
   1563  it("should dispatch events on drop", () => {
   1564    const dispatch = sinon.spy();
   1565    const wrapper = shallow(
   1566      <TopSiteList {...DEFAULT_PROPS} dispatch={dispatch} App={APP} />
   1567    );
   1568    const instance = wrapper.instance();
   1569    const index = 7;
   1570    const link = { url: "https://foo.com", customScreenshotURL: "foo" };
   1571    const title = "foo";
   1572    instance.onDragEvent({ type: "dragstart" }, index, link, title);
   1573    dispatch.resetHistory();
   1574    instance.onDragEvent({ type: "drop" }, 3);
   1575    assert.calledTwice(dispatch);
   1576    assert.calledWith(dispatch, {
   1577      data: {
   1578        draggedFromIndex: 7,
   1579        index: 3,
   1580        site: {
   1581          label: "foo",
   1582          url: "https://foo.com",
   1583          customScreenshotURL: "foo",
   1584        },
   1585      },
   1586      meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1587      type: "TOP_SITES_INSERT",
   1588    });
   1589    assert.calledWith(dispatch, {
   1590      data: { action_position: 3, event: "DROP", source: "TOP_SITES" },
   1591      meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
   1592      type: "TELEMETRY_USER_EVENT",
   1593    });
   1594  });
   1595  it("should make a topSitesPreview onDragEnter", () => {
   1596    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />);
   1597    const instance = wrapper.instance();
   1598    const site = { url: "https://foo.com" };
   1599    instance.setState({
   1600      draggedIndex: 4,
   1601      draggedSite: site,
   1602      draggedTitle: "foo",
   1603    });
   1604    const draggedSite = Object.assign({}, site, {
   1605      isPinned: true,
   1606      isDragged: true,
   1607    });
   1608    instance.onDragEvent({ type: "dragenter" }, 2);
   1609    assert.ok(instance.state.topSitesPreview);
   1610    assert.deepEqual(instance.state.topSitesPreview[2], draggedSite);
   1611  });
   1612  it("should _makeTopSitesPreview correctly", () => {
   1613    const site1 = { url: "https://foo.com" };
   1614    const site2 = { url: "https://bar.com" };
   1615    const site3 = { url: "https://baz.com" };
   1616    const rows = [site1, site2, site3];
   1617    let wrapper = shallow(
   1618      <TopSiteList
   1619        {...DEFAULT_PROPS}
   1620        TopSites={{ rows }}
   1621        TopSitesRows={1}
   1622        App={APP}
   1623      />
   1624    );
   1625    const addButton = { isAddButton: true };
   1626    let instance = wrapper.instance();
   1627    instance.setState({
   1628      draggedIndex: 0,
   1629      draggedSite: site1,
   1630      draggedTitle: "foo",
   1631    });
   1632    let draggedSite = Object.assign({}, site1, {
   1633      isPinned: true,
   1634      isDragged: true,
   1635    });
   1636    assert.deepEqual(instance._makeTopSitesPreview(1), [
   1637      site2,
   1638      draggedSite,
   1639      site3,
   1640      addButton,
   1641      null,
   1642      null,
   1643      null,
   1644      null,
   1645    ]);
   1646    assert.deepEqual(instance._makeTopSitesPreview(2), [
   1647      site2,
   1648      site3,
   1649      draggedSite,
   1650      addButton,
   1651      null,
   1652      null,
   1653      null,
   1654      null,
   1655    ]);
   1656    assert.deepEqual(instance._makeTopSitesPreview(3), [
   1657      site2,
   1658      site3,
   1659      addButton,
   1660      draggedSite,
   1661      null,
   1662      null,
   1663      null,
   1664      null,
   1665    ]);
   1666    site2.isPinned = true;
   1667    assert.deepEqual(instance._makeTopSitesPreview(1), [
   1668      site2,
   1669      draggedSite,
   1670      site3,
   1671      addButton,
   1672      null,
   1673      null,
   1674      null,
   1675      null,
   1676    ]);
   1677    assert.deepEqual(instance._makeTopSitesPreview(2), [
   1678      site3,
   1679      site2,
   1680      draggedSite,
   1681      addButton,
   1682      null,
   1683      null,
   1684      null,
   1685      null,
   1686    ]);
   1687    site3.isPinned = true;
   1688    assert.deepEqual(instance._makeTopSitesPreview(1), [
   1689      site2,
   1690      draggedSite,
   1691      site3,
   1692      addButton,
   1693      null,
   1694      null,
   1695      null,
   1696      null,
   1697    ]);
   1698    assert.deepEqual(instance._makeTopSitesPreview(2), [
   1699      site2,
   1700      site3,
   1701      draggedSite,
   1702      addButton,
   1703      null,
   1704      null,
   1705      null,
   1706      null,
   1707    ]);
   1708    site2.isPinned = false;
   1709    assert.deepEqual(instance._makeTopSitesPreview(1), [
   1710      site2,
   1711      draggedSite,
   1712      site3,
   1713      addButton,
   1714      null,
   1715      null,
   1716      null,
   1717      null,
   1718    ]);
   1719    assert.deepEqual(instance._makeTopSitesPreview(2), [
   1720      site2,
   1721      site3,
   1722      draggedSite,
   1723      addButton,
   1724      null,
   1725      null,
   1726      null,
   1727      null,
   1728    ]);
   1729    site3.isPinned = false;
   1730    instance.setState({
   1731      draggedIndex: 1,
   1732      draggedSite: site2,
   1733      draggedTitle: "bar",
   1734    });
   1735    draggedSite = Object.assign({}, site2, { isPinned: true, isDragged: true });
   1736    assert.deepEqual(instance._makeTopSitesPreview(0), [
   1737      draggedSite,
   1738      site1,
   1739      site3,
   1740      addButton,
   1741      null,
   1742      null,
   1743      null,
   1744      null,
   1745    ]);
   1746    assert.deepEqual(instance._makeTopSitesPreview(2), [
   1747      site1,
   1748      site3,
   1749      draggedSite,
   1750      addButton,
   1751      null,
   1752      null,
   1753      null,
   1754      null,
   1755    ]);
   1756    site2.type = "SPOC";
   1757    instance.setState({
   1758      draggedIndex: 2,
   1759      draggedSite: site3,
   1760      draggedTitle: "baz",
   1761    });
   1762    draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true });
   1763    assert.deepEqual(instance._makeTopSitesPreview(0), [
   1764      draggedSite,
   1765      site2,
   1766      site1,
   1767      addButton,
   1768      null,
   1769      null,
   1770      null,
   1771      null,
   1772    ]);
   1773    site2.type = "";
   1774    site2.sponsored_position = 2;
   1775    instance.setState({
   1776      draggedIndex: 2,
   1777      draggedSite: site3,
   1778      draggedTitle: "baz",
   1779    });
   1780    draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true });
   1781    assert.deepEqual(instance._makeTopSitesPreview(0), [
   1782      draggedSite,
   1783      site2,
   1784      site1,
   1785      addButton,
   1786      null,
   1787      null,
   1788      null,
   1789      null,
   1790    ]);
   1791  });
   1792  it("should add a className hide-for-narrow to sites after 6/row", () => {
   1793    const rows = [];
   1794    for (let i = 0; i < TOP_SITES_MAX_SITES_PER_ROW; i++) {
   1795      rows.push({ url: `https://foo${i}.com` });
   1796    }
   1797    const wrapper = mount(
   1798      <TopSiteList
   1799        {...DEFAULT_PROPS}
   1800        TopSites={{ rows }}
   1801        TopSitesRows={1}
   1802        App={APP}
   1803      />
   1804    );
   1805    assert.lengthOf(wrapper.find("li.hide-for-narrow"), 2);
   1806  });
   1807 
   1808  describe("Keyboard navigation", () => {
   1809    let sandbox;
   1810    let wrapper;
   1811    let instance;
   1812    let mockAnchor;
   1813    let mockTargetSibling;
   1814 
   1815    beforeEach(() => {
   1816      sandbox = sinon.createSandbox();
   1817      const rows = [
   1818        { url: "https://foo.com" },
   1819        { url: "https://bar.com" },
   1820        { url: "https://baz.com" },
   1821      ];
   1822      wrapper = shallow(
   1823        <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
   1824      );
   1825      instance = wrapper.instance();
   1826 
   1827      mockAnchor = { focus: sandbox.spy(), tabIndex: -1 };
   1828      mockTargetSibling = { querySelector: sandbox.stub().returns(mockAnchor) };
   1829    });
   1830 
   1831    afterEach(() => {
   1832      sandbox.restore();
   1833    });
   1834 
   1835    it("should navigate to next site with ArrowRight", () => {
   1836      instance.focusedRef = { nextSibling: mockTargetSibling };
   1837      const mockEvent = { key: "ArrowRight" };
   1838 
   1839      instance.onKeyDown(mockEvent);
   1840 
   1841      assert.calledOnce(mockTargetSibling.querySelector);
   1842      assert.calledWith(mockTargetSibling.querySelector, "a");
   1843      assert.calledOnce(mockAnchor.focus);
   1844      assert.equal(mockAnchor.tabIndex, 0);
   1845    });
   1846 
   1847    it("should navigate to previous site with ArrowLeft", () => {
   1848      instance.focusedRef = { previousSibling: mockTargetSibling };
   1849      const mockEvent = { key: "ArrowLeft" };
   1850 
   1851      instance.onKeyDown(mockEvent);
   1852 
   1853      assert.calledOnce(mockTargetSibling.querySelector);
   1854      assert.calledWith(mockTargetSibling.querySelector, "a");
   1855      assert.calledOnce(mockAnchor.focus);
   1856      assert.equal(mockAnchor.tabIndex, 0);
   1857    });
   1858  });
   1859 });
   1860 
   1861 describe("TopSiteAddButton", () => {
   1862  it("should dispatch a TOP_SITES_EDIT action when the addbutton is clicked", () => {
   1863    const dispatch = sinon.spy();
   1864    const wrapper = shallow(
   1865      <TopSiteAddButton dispatch={dispatch} index={7} isAddButton={true} />
   1866    );
   1867 
   1868    wrapper.find(".add-button").first().simulate("click");
   1869 
   1870    assert.calledOnce(dispatch);
   1871    assert.calledWithExactly(dispatch, {
   1872      type: at.TOP_SITES_EDIT,
   1873      data: { index: 7 },
   1874    });
   1875  });
   1876 });
   1877 
   1878 describe("#TopSiteFormInput", () => {
   1879  let wrapper;
   1880  let onChangeStub;
   1881 
   1882  describe("no errors", () => {
   1883    beforeEach(() => {
   1884      onChangeStub = sinon.stub();
   1885 
   1886      wrapper = mount(
   1887        <TopSiteFormInput
   1888          titleId="newtab-topsites-title-label"
   1889          placeholderId="newtab-topsites-title-input"
   1890          errorMessageId="newtab-topsites-url-validation"
   1891          onChange={onChangeStub}
   1892          value="foo"
   1893        />
   1894      );
   1895    });
   1896 
   1897    it("should render the provided title", () => {
   1898      const title = wrapper.find("span");
   1899      assert.propertyVal(
   1900        title.props(),
   1901        "data-l10n-id",
   1902        "newtab-topsites-title-label"
   1903      );
   1904    });
   1905 
   1906    it("should render the provided value", () => {
   1907      const input = wrapper.find("input");
   1908 
   1909      assert.equal(input.getDOMNode().value, "foo");
   1910    });
   1911 
   1912    it("should render the clear button if cb is provided", () => {
   1913      assert.equal(wrapper.find(".icon-clear-input").length, 0);
   1914 
   1915      wrapper.setProps({ onClear: sinon.stub() });
   1916 
   1917      assert.equal(wrapper.find(".icon-clear-input").length, 1);
   1918    });
   1919 
   1920    it("should show the loading indicator", () => {
   1921      assert.equal(wrapper.find(".loading-container").length, 0);
   1922 
   1923      wrapper.setProps({ loading: true });
   1924 
   1925      assert.equal(wrapper.find(".loading-container").length, 1);
   1926    });
   1927    it("should disable the input when loading indicator is present", () => {
   1928      assert.isFalse(wrapper.find("input").getDOMNode().disabled);
   1929 
   1930      wrapper.setProps({ loading: true });
   1931 
   1932      assert.isTrue(wrapper.find("input").getDOMNode().disabled);
   1933    });
   1934  });
   1935 
   1936  describe("with error", () => {
   1937    beforeEach(() => {
   1938      onChangeStub = sinon.stub();
   1939 
   1940      wrapper = mount(
   1941        <TopSiteFormInput
   1942          titleId="newtab-topsites-title-label"
   1943          placeholderId="newtab-topsites-title-input"
   1944          onChange={onChangeStub}
   1945          validationError={true}
   1946          errorMessageId="newtab-topsites-url-validation"
   1947          value="foo"
   1948        />
   1949      );
   1950    });
   1951 
   1952    it("should render the error message", () => {
   1953      assert.equal(
   1954        wrapper.findWhere(
   1955          n => n.prop("data-l10n-id") === "newtab-topsites-url-validation"
   1956        ).length,
   1957        1
   1958      );
   1959    });
   1960 
   1961    it("should reset the error state on value change", () => {
   1962      wrapper.find("input").simulate("change", { target: { value: "bar" } });
   1963 
   1964      assert.isFalse(wrapper.state().validationError);
   1965    });
   1966  });
   1967 });