import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants";
import {
  TOP_SITES_DEFAULT_ROWS,
  TOP_SITES_MAX_SITES_PER_ROW,
} from "common/Reducers.sys.mjs";
import {
  TopSite,
  TopSiteLink,
  _TopSiteList as TopSiteList,
  TopSitePlaceholder,
  TopSiteAddButton,
} from "content-src/components/TopSites/TopSite";
import {
  INTERSECTION_RATIO,
  TopSiteImpressionWrapper,
} from "content-src/components/TopSites/TopSiteImpressionWrapper";
import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton";
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import React from "react";
import { mount, shallow } from "enzyme";
import { TopSiteForm } from "content-src/components/TopSites/TopSiteForm";
import { TopSiteFormInput } from "content-src/components/TopSites/TopSiteFormInput";
import { _TopSites as TopSites } from "content-src/components/TopSites/TopSites";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";

const perfSvc = {
  mark() {},
  getMostRecentAbsMarkStartByName() {},
};

const DEFAULT_PROPS = {
  Prefs: { values: { featureConfig: {} } },
  TopSites: { initialized: true, rows: [] },
  App: {
    isForStartupCache: false,
  },
  TopSitesRows: TOP_SITES_DEFAULT_ROWS,
  topSiteIconType: () => "no_image",
  dispatch() {},
  perfSvc,
};

const DEFAULT_BLOB_URL = "blob://test";

describe("<TopSites>", () => {
  let sandbox;

  beforeEach(() => {
    sandbox = sinon.createSandbox();
  });

  afterEach(() => {
    sandbox.restore();
  });

  it("should render a TopSites element", () => {
    const wrapper = shallow(<TopSites {...DEFAULT_PROPS} />);
    assert.ok(wrapper.exists());
  });
  describe("#_dispatchTopSitesStats", () => {
    let globals;
    let wrapper;
    let dispatchStatsSpy;

    beforeEach(() => {
      globals = new GlobalOverrider();
      sandbox.stub(DEFAULT_PROPS, "dispatch");
      wrapper = shallow(<TopSites {...DEFAULT_PROPS} />, {
        disableLifecycleMethods: true,
      });
      dispatchStatsSpy = sandbox.spy(
        wrapper.instance(),
        "_dispatchTopSitesStats"
      );
    });
    afterEach(() => {
      globals.restore();
      sandbox.restore();
    });
    it("should call _dispatchTopSitesStats on componentDidMount", () => {
      wrapper.instance().componentDidMount();

      assert.calledOnce(dispatchStatsSpy);
    });
    it("should call _dispatchTopSitesStats on componentDidUpdate", () => {
      wrapper.instance().componentDidUpdate();

      assert.calledOnce(dispatchStatsSpy);
    });
    it("should dispatch SAVE_SESSION_PERF_DATA", () => {
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 0,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count TopSite images - just screenshot", () => {
      const rows = [{ screenshot: true }];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 1,
              tippytop: 0,
              rich_icon: 0,
              no_image: 0,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count TopSite images - custom_screenshot", () => {
      const rows = [{ customScreenshotURL: true }];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 1,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 0,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count TopSite images - rich_icon", () => {
      const rows = [{ faviconSize: MIN_RICH_FAVICON_SIZE }];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 1,
              no_image: 0,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count TopSite images - tippytop", () => {
      const rows = [
        { tippyTopIcon: "foo" },
        { faviconRef: "tippytop" },
        { faviconRef: "foobar" },
      ];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 2,
              rich_icon: 0,
              no_image: 1,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count TopSite images - no image", () => {
      const rows = [{}];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 1,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count pinned Top Sites", () => {
      const rows = [
        { isPinned: true },
        { isPinned: false },
        { isPinned: true },
      ];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 3,
            },
            topsites_pinned: 2,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should correctly count search shortcut Top Sites", () => {
      const rows = [{ searchTopSite: true }, { searchTopSite: true }];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();

      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 2,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 2,
          },
        })
      );
    });
    it("should only count visible top sites on wide layout", () => {
      globals.set("matchMedia", () => ({ matches: true }));
      const rows = [
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
      ];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);

      wrapper.instance()._dispatchTopSitesStats();
      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 8,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
    it("should only count visible top sites on normal layout", () => {
      globals.set("matchMedia", () => ({ matches: false }));
      const rows = [
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
        {},
      ];
      sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
      wrapper.instance()._dispatchTopSitesStats();
      assert.calledOnce(DEFAULT_PROPS.dispatch);
      assert.calledWithExactly(
        DEFAULT_PROPS.dispatch,
        ac.AlsoToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: {
            topsites_icon_stats: {
              custom_screenshot: 0,
              screenshot: 0,
              tippytop: 0,
              rich_icon: 0,
              no_image: 6,
            },
            topsites_pinned: 0,
            topsites_search_shortcuts: 0,
          },
        })
      );
    });
  });
});

describe("<TopSiteLink>", () => {
  let globals;
  let link;
  let url;
  beforeEach(() => {
    globals = new GlobalOverrider();
    url = {
      createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),
      revokeObjectURL: globals.sandbox.spy(),
    };
    globals.set("URL", url);
    link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" };
  });
  afterEach(() => globals.restore());
  it("should add the right url", () => {
    link.url = "https://www.foobar.org";
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.propertyVal(
      wrapper.find("a").props(),
      "href",
      "https://www.foobar.org"
    );
  });
  it("should not add the url to the href if it a search shortcut", () => {
    link.searchTopSite = true;
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.isUndefined(wrapper.find("a").props().href);
  });
  it("should have rtl direction automatically set for text", () => {
    const wrapper = shallow(<TopSiteLink link={link} />);

    assert.isTrue(!!wrapper.find("[dir='auto']").length);
  });
  it("should render a title", () => {
    const wrapper = shallow(<TopSiteLink link={link} title="foobar" />);
    const titleEl = wrapper.find(".title");

    assert.equal(titleEl.text(), "foobar");
  });
  it("should have only the title as the text of the link", () => {
    const wrapper = shallow(<TopSiteLink link={link} title="foobar" />);

    assert.equal(wrapper.find("a").text(), "foobar");
  });
  it("should render the pin icon for pinned links", () => {
    link.isPinned = true;
    link.pinnedIndex = 7;
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.equal(wrapper.find(".icon-pin-small").length, 1);
  });
  it("should not render the pin icon for non pinned links", () => {
    link.isPinned = false;
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.equal(wrapper.find(".icon-pin-small").length, 0);
  });
  it("should render the first letter of the title as a fallback for missing icons", () => {
    const wrapper = shallow(<TopSiteLink link={link} title={"foo"} />);
    assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
  });
  it("should render the tippy top icon if provided and not a small icon", () => {
    link.tippyTopIcon = "foo.png";
    link.backgroundColor = "#FFFFFF";
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.lengthOf(wrapper.find(".screenshot"), 0);
    assert.lengthOf(wrapper.find(".default-icon"), 0);
    const tippyTop = wrapper.find(".rich-icon");
    assert.propertyVal(
      tippyTop.props().style,
      "backgroundImage",
      "url(foo.png)"
    );
    assert.propertyVal(tippyTop.props().style, "backgroundColor", "#FFFFFF");
  });
  it("should render a rich icon if provided and not a small icon", () => {
    link.favicon = "foo.png";
    link.faviconSize = 196;
    link.backgroundColor = "#FFFFFF";
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.lengthOf(wrapper.find(".screenshot"), 0);
    assert.lengthOf(wrapper.find(".default-icon"), 0);
    const richIcon = wrapper.find(".rich-icon");
    assert.propertyVal(
      richIcon.props().style,
      "backgroundImage",
      "url(foo.png)"
    );
    assert.propertyVal(richIcon.props().style, "backgroundColor", "#FFFFFF");
  });
  it("should not render a rich icon if it is smaller than 96x96", () => {
    link.favicon = "foo.png";
    link.faviconSize = 48;
    link.backgroundColor = "#FFFFFF";
    const wrapper = shallow(<TopSiteLink link={link} />);
    assert.lengthOf(wrapper.find(".default-icon"), 1);
    assert.equal(wrapper.find(".rich-icon").length, 0);
  });
  it("should apply just the default class name to the outer link if props.className is falsey", () => {
    const wrapper = shallow(<TopSiteLink className={false} />);
    assert.ok(wrapper.find("li").hasClass("top-site-outer"));
  });
  it("should add props.className to the outer link element", () => {
    const wrapper = shallow(<TopSiteLink className="foo bar" />);
    assert.ok(wrapper.find("li").hasClass("top-site-outer foo bar"));
  });
  describe("#_allowDrop", () => {
    let wrapper;
    let event;
    beforeEach(() => {
      event = {
        dataTransfer: {
          types: ["text/topsite-index"],
        },
      };
      wrapper = shallow(
        <TopSiteLink isDraggable={true} onDragEvent={() => {}} />
      );
    });
    it("should be droppable for basic case", () => {
      const result = wrapper.instance()._allowDrop(event);
      assert.isTrue(result);
    });
    it("should not be droppable for sponsored_position", () => {
      wrapper.setProps({ link: { sponsored_position: 1 } });
      const result = wrapper.instance()._allowDrop(event);
      assert.isFalse(result);
    });
    it("should not be droppable for link.type", () => {
      wrapper.setProps({ link: { type: "SPOC" } });
      const result = wrapper.instance()._allowDrop(event);
      assert.isFalse(result);
    });
  });
  describe("#onDragEvent", () => {
    let simulate;
    let wrapper;
    beforeEach(() => {
      wrapper = shallow(
        <TopSiteLink isDraggable={true} onDragEvent={() => {}} />
      );
      simulate = type => {
        const event = {
          dataTransfer: { setData() {}, types: { includes() {} } },
          preventDefault() {
            this.prevented = true;
          },
          target: { blur() {} },
          type,
        };
        wrapper.simulate(type, event);
        return event;
      };
    });
    it("should allow clicks without dragging", () => {
      simulate("mousedown");
      simulate("mouseup");

      const event = simulate("click");

      assert.notOk(event.prevented);
    });
    it("should prevent clicks after dragging", () => {
      simulate("mousedown");
      simulate("dragstart");
      simulate("dragenter");
      simulate("drop");
      simulate("dragend");
      simulate("mouseup");

      const event = simulate("click");

      assert.ok(event.prevented);
    });
    it("should allow clicks after dragging then clicking", () => {
      simulate("mousedown");
      simulate("dragstart");
      simulate("dragenter");
      simulate("drop");
      simulate("dragend");
      simulate("mouseup");
      simulate("click");

      simulate("mousedown");
      simulate("mouseup");

      const event = simulate("click");

      assert.notOk(event.prevented);
    });
    it("should prevent dragging with sponsored_position from dragstart", () => {
      const preventDefault = sinon.stub();
      // eslint-disable-next-line no-shadow
      const blur = sinon.stub();
      wrapper.setProps({ link: { sponsored_position: 1 } });
      wrapper.instance().onDragEvent({
        type: "dragstart",
        preventDefault,
        target: { blur },
      });
      assert.calledOnce(preventDefault);
      assert.calledOnce(blur);
      assert.isUndefined(wrapper.instance().dragged);
    });
    it("should prevent dragging with link.shim from dragstart", () => {
      const preventDefault = sinon.stub();
      // eslint-disable-next-line no-shadow
      const blur = sinon.stub();
      wrapper.setProps({ link: { type: "SPOC" } });
      wrapper.instance().onDragEvent({
        type: "dragstart",
        preventDefault,
        target: { blur },
      });
      assert.calledOnce(preventDefault);
      assert.calledOnce(blur);
      assert.isUndefined(wrapper.instance().dragged);
    });
  });

  describe("#generateColor", () => {
    let colors;
    beforeEach(() => {
      colors = "#0090ED,#FF4F5F,#2AC3A2";
    });

    it("should generate a random color but always pick the same color for the same string", async () => {
      let wrapper = shallow(
        <TopSiteLink colors={colors} title={"food"} link={link} />
      );

      assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
      assert.equal(
        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
        colors.split(",")[1]
      );
      assert.ok(true);
    });

    it("should generate a different random color", async () => {
      let wrapper = shallow(
        <TopSiteLink colors={colors} title={"fam"} link={link} />
      );

      assert.equal(
        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
        colors.split(",")[2]
      );
      assert.ok(true);
    });

    it("should generate a third random color", async () => {
      let wrapper = shallow(<TopSiteLink colors={colors} title={"foo"} />);

      assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f");
      assert.equal(
        wrapper.find(".icon-wrapper").prop("style").backgroundColor,
        colors.split(",")[0]
      );
      assert.ok(true);
    });
  });
});

describe("<TopSite>", () => {
  let link;
  beforeEach(() => {
    link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" };
  });

  // Build IntersectionObserver class with the arg `entries` for the intersect callback.
  function buildIntersectionObserver(entries) {
    return class {
      constructor(callback) {
        this.callback = callback;
      }

      observe() {
        this.callback(entries);
      }

      unobserve() {}
    };
  }

  it("should render a TopSite", () => {
    const wrapper = shallow(<TopSite link={link} />);
    assert.ok(wrapper.exists());
  });

  it("should render a shortened title based off the url", () => {
    link.url = "https://www.foobar.org";
    link.hostname = "foobar";
    link.eTLD = "org";
    const wrapper = shallow(<TopSite link={link} />);

    assert.equal(wrapper.find(TopSiteLink).props().title, "foobar");
  });

  it("should parse args for fluent correctly", () => {
    const title = '"fluent"';
    link.hostname = title;

    const wrapper = mount(<TopSite link={link} />);
    const button = wrapper.find(
      "button[data-l10n-id='newtab-menu-content-tooltip']"
    );
    assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title }));
  });

  it("should have .active class, on top-site-outer if context menu is open", () => {
    const wrapper = shallow(<TopSite link={link} index={1} activeIndex={1} />);
    wrapper.setState({ showContextMenu: true });

    assert.equal(wrapper.find(TopSiteLink).props().className.trim(), "active");
  });
  it("should not add .active class, on top-site-outer if context menu is closed", () => {
    const wrapper = shallow(<TopSite link={link} index={1} />);
    wrapper.setState({ showContextMenu: false, activeTile: 1 });
    assert.equal(wrapper.find(TopSiteLink).props().className, "");
  });
  it("should render a context menu button", () => {
    const wrapper = shallow(<TopSite link={link} />);
    assert.equal(wrapper.find(ContextMenuButton).length, 1);
  });
  it("should render a link menu", () => {
    const wrapper = shallow(<TopSite link={link} />);
    assert.equal(wrapper.find(LinkMenu).length, 1);
  });
  it("should pass onUpdate, site, options, and index to LinkMenu", () => {
    const wrapper = shallow(<TopSite link={link} />);
    const linkMenuProps = wrapper.find(LinkMenu).props();
    ["onUpdate", "site", "index", "options"].forEach(prop =>
      assert.property(linkMenuProps, prop)
    );
  });
  it("should pass through the correct menu options to LinkMenu", () => {
    const wrapper = shallow(<TopSite link={link} />);
    const linkMenuProps = wrapper.find(LinkMenu).props();
    assert.deepEqual(linkMenuProps.options, [
      "CheckPinTopSite",
      "EditTopSite",
      "Separator",
      "OpenInNewWindow",
      "OpenInPrivateWindow",
      "Separator",
      "BlockUrl",
      "DeleteUrl",
    ]);
  });
  it("should record impressions for visible organic Top Sites", () => {
    const dispatch = sinon.stub();
    const wrapper = shallow(
      <TopSite
        link={link}
        index={3}
        dispatch={dispatch}
        IntersectionObserver={buildIntersectionObserver([
          {
            isIntersecting: true,
            intersectionRatio: INTERSECTION_RATIO,
          },
        ])}
        document={{
          visibilityState: "visible",
          addEventListener: sinon.stub(),
          removeEventListener: sinon.stub(),
        }}
      />
    );
    const linkWrapper = wrapper.find(TopSiteLink).dive();
    assert.ok(linkWrapper.exists());
    const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive();
    assert.ok(impressionWrapper.exists());

    assert.calledOnce(dispatch);

    let [action] = dispatch.firstCall.args;
    assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS);

    assert.propertyVal(action.data, "type", "impression");
    assert.propertyVal(action.data, "source", "newtab");
    assert.propertyVal(action.data, "position", 3);
  });
  it("should record impressions for visible sponsored Top Sites", () => {
    const dispatch = sinon.stub();
    const wrapper = shallow(
      <TopSite
        link={Object.assign({}, link, {
          sponsored_position: 2,
          sponsored_tile_id: 12345,
          sponsored_impression_url: "http://impression.example.com/",
        })}
        index={3}
        dispatch={dispatch}
        IntersectionObserver={buildIntersectionObserver([
          {
            isIntersecting: true,
            intersectionRatio: INTERSECTION_RATIO,
          },
        ])}
        document={{
          visibilityState: "visible",
          addEventListener: sinon.stub(),
          removeEventListener: sinon.stub(),
        }}
      />
    );
    const linkWrapper = wrapper.find(TopSiteLink).dive();
    assert.ok(linkWrapper.exists());
    const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive();
    assert.ok(impressionWrapper.exists());

    assert.calledOnce(dispatch);

    let [action] = dispatch.firstCall.args;
    assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);

    assert.propertyVal(action.data, "type", "impression");
    assert.propertyVal(action.data, "tile_id", 12345);
    assert.propertyVal(action.data, "source", "newtab");
    assert.propertyVal(action.data, "position", 3);
    assert.propertyVal(
      action.data,
      "reporting_url",
      "http://impression.example.com/"
    );
    assert.propertyVal(action.data, "advertiser", "foo");
  });

  describe("#onLinkClick", () => {
    it("should call dispatch when the link is clicked", () => {
      const dispatch = sinon.stub();
      const wrapper = shallow(
        <TopSite link={link} index={3} dispatch={dispatch} />
      );

      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });

      let [action] = dispatch.firstCall.args;
      assert.isUserEventAction(action);

      assert.propertyVal(action.data, "event", "CLICK");
      assert.propertyVal(action.data, "source", "TOP_SITES");
      assert.propertyVal(action.data, "action_position", 3);

      [action] = dispatch.secondCall.args;
      assert.propertyVal(action, "type", at.OPEN_LINK);

      // Organic Top Site click event.
      [action] = dispatch.thirdCall.args;
      assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS);

      assert.propertyVal(action.data, "type", "click");
      assert.propertyVal(action.data, "source", "newtab");
      assert.propertyVal(action.data, "position", 3);
    });
    it("should dispatch a UserEventAction with the right data", () => {
      const dispatch = sinon.stub();
      const wrapper = shallow(
        <TopSite
          link={Object.assign({}, link, {
            iconType: "rich_icon",
            isPinned: true,
          })}
          index={3}
          dispatch={dispatch}
        />
      );

      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });

      const [action] = dispatch.firstCall.args;
      assert.isUserEventAction(action);

      assert.propertyVal(action.data, "event", "CLICK");
      assert.propertyVal(action.data, "source", "TOP_SITES");
      assert.propertyVal(action.data, "action_position", 3);
      assert.propertyVal(action.data.value, "card_type", "pinned");
      assert.propertyVal(action.data.value, "icon_type", "rich_icon");
    });
    it("should dispatch a UserEventAction with the right data for search top site", () => {
      const dispatch = sinon.stub();
      const siteInfo = {
        iconType: "tippytop",
        isPinned: true,
        searchTopSite: true,
        hostname: "google",
        label: "@google",
      };
      const wrapper = shallow(
        <TopSite
          link={Object.assign({}, link, siteInfo)}
          index={3}
          dispatch={dispatch}
        />
      );

      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });

      const [action] = dispatch.firstCall.args;
      assert.isUserEventAction(action);

      assert.propertyVal(action.data, "event", "CLICK");
      assert.propertyVal(action.data, "source", "TOP_SITES");
      assert.propertyVal(action.data, "action_position", 3);
      assert.propertyVal(action.data.value, "card_type", "search");
      assert.propertyVal(action.data.value, "icon_type", "tippytop");
      assert.propertyVal(action.data.value, "search_vendor", "google");
    });
    it("should dispatch a UserEventAction with the right data for SPOC top site", () => {
      const dispatch = sinon.stub();
      const siteInfo = {
        id: 1,
        iconType: "custom_screenshot",
        type: "SPOC",
        pos: 1,
        label: "test advertiser",
        shim: { click: "shim_click_id" },
      };
      const wrapper = shallow(
        <TopSite
          link={Object.assign({}, link, siteInfo)}
          index={0}
          dispatch={dispatch}
        />
      );

      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });

      let [action] = dispatch.firstCall.args;
      assert.isUserEventAction(action);

      assert.propertyVal(action.data, "event", "CLICK");
      assert.propertyVal(action.data, "source", "TOP_SITES");
      assert.propertyVal(action.data, "action_position", 0);
      assert.propertyVal(action.data.value, "card_type", "spoc");
      assert.propertyVal(action.data.value, "icon_type", "custom_screenshot");

      // Pocket SPOC click event.
      [action] = dispatch.getCall(2).args;
      assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS);

      assert.propertyVal(action.data, "click", 0);
      assert.propertyVal(action.data, "source", "TOP_SITES");

      [action] = dispatch.getCall(3).args;
      assert.equal(action.type, at.DISCOVERY_STREAM_USER_EVENT);

      assert.propertyVal(action.data, "event", "CLICK");
      assert.propertyVal(action.data, "action_position", 1);
      assert.propertyVal(action.data, "source", "TOP_SITES");
      assert.propertyVal(action.data.value, "card_type", "spoc");
      assert.propertyVal(action.data.value, "tile_id", 1);
      assert.propertyVal(action.data.value, "shim", "shim_click_id");

      // Topsite SPOC click event.
      [action] = dispatch.getCall(4).args;
      assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);

      assert.propertyVal(action.data, "type", "click");
      assert.propertyVal(action.data, "tile_id", 1);
      assert.propertyVal(action.data, "source", "newtab");
      assert.propertyVal(action.data, "position", 1);
      assert.propertyVal(action.data, "advertiser", "test advertiser");
    });
    it("should dispatch OPEN_LINK with the right data", () => {
      const dispatch = sinon.stub();
      const wrapper = shallow(
        <TopSite
          link={Object.assign({}, link, { typedBonus: true })}
          index={3}
          dispatch={dispatch}
        />
      );

      wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} });

      const [action] = dispatch.secondCall.args;
      assert.propertyVal(action, "type", at.OPEN_LINK);
      assert.propertyVal(action.data, "typedBonus", true);
    });
  });
});

describe("<TopSiteForm>", () => {
  let wrapper;
  let sandbox;

  function testSetup(props = {}) {
    sandbox = sinon.createSandbox();
    const customProps = Object.assign(
      {},
      { onClose: sandbox.spy(), dispatch: sandbox.spy() },
      props
    );
    wrapper = mount(<TopSiteForm {...customProps} />);
  }

  describe("validateForm", () => {
    beforeEach(() => testSetup({ site: { url: "http://foo" } }));

    it("should return true for a correct URL", () => {
      wrapper.setState({ url: "foo" });

      assert.isTrue(wrapper.instance().validateForm());
    });

    it("should return false for a incorrect URL", () => {
      wrapper.setState({ url: " " });

      assert.isNull(wrapper.instance().validateForm());
      assert.isTrue(wrapper.state().validationError);
    });

    it("should return true for a correct custom screenshot URL", () => {
      wrapper.setState({ customScreenshotUrl: "foo" });

      assert.isTrue(wrapper.instance().validateForm());
    });

    it("should return false for a incorrect custom screenshot URL", () => {
      wrapper.setState({ customScreenshotUrl: " " });

      assert.isNull(wrapper.instance().validateForm());
    });

    it("should return true for an empty custom screenshot URL", () => {
      wrapper.setState({ customScreenshotUrl: "" });

      assert.isTrue(wrapper.instance().validateForm());
    });

    it("should return false for file: protocol", () => {
      wrapper.setState({ customScreenshotUrl: "file:///C:/Users/foo" });

      assert.isFalse(wrapper.instance().validateForm());
    });
  });

  describe("#previewButton", () => {
    beforeEach(() =>
      testSetup({
        site: { customScreenshotURL: "http://foo.com" },
        previewResponse: null,
      })
    );

    it("should render the preview button on invalid urls", () => {
      assert.equal(0, wrapper.find(".preview").length);

      wrapper.setState({ customScreenshotUrl: " " });

      assert.equal(1, wrapper.find(".preview").length);
    });

    it("should render the preview button when input value updated", () => {
      assert.equal(0, wrapper.find(".preview").length);

      wrapper.setState({
        customScreenshotUrl: "http://baz.com",
        screenshotPreview: null,
      });

      assert.equal(1, wrapper.find(".preview").length);
    });
  });

  describe("preview request", () => {
    beforeEach(() => {
      testSetup({
        site: { customScreenshotURL: "http://foo.com", url: "http://foo.com" },
        previewResponse: null,
      });
    });

    it("shouldn't dispatch a request for invalid urls", () => {
      wrapper.setState({ customScreenshotUrl: " ", url: "foo" });

      wrapper.find(".preview").simulate("click");

      assert.notCalled(wrapper.props().dispatch);
    });

    it("should dispatch a PREVIEW_REQUEST", () => {
      wrapper.setState({ customScreenshotUrl: "screenshot" });
      wrapper.find(".preview").simulate("submit");

      assert.calledTwice(wrapper.props().dispatch);
      assert.calledWith(
        wrapper.props().dispatch,
        ac.AlsoToMain({
          type: at.PREVIEW_REQUEST,
          data: { url: "http://screenshot" },
        })
      );
      assert.calledWith(
        wrapper.props().dispatch,
        ac.UserEvent({
          event: "PREVIEW_REQUEST",
          source: "TOP_SITES",
        })
      );
    });
  });

  describe("#TopSiteLink", () => {
    beforeEach(() => {
      testSetup();
    });

    it("should display a TopSiteLink preview", () => {
      assert.equal(wrapper.find(TopSiteLink).length, 1);
    });

    it("should display an icon for tippyTop sites", () => {
      wrapper.setProps({ site: { tippyTopIcon: "bar" } });

      assert.equal(
        wrapper.find(".top-site-icon").getDOMNode().style["background-image"],
        'url("bar")'
      );
    });

    it("should not display a preview screenshot", () => {
      wrapper.setProps({ previewResponse: "foo", previewUrl: "foo" });

      assert.lengthOf(wrapper.find(".screenshot"), 0);
    });

    it("should not render any icon on error", () => {
      wrapper.setProps({ previewResponse: "" });

      assert.equal(wrapper.find(".top-site-icon").length, 0);
    });

    it("should render the search icon when searchTopSite is true", () => {
      wrapper.setProps({ site: { tippyTopIcon: "bar", searchTopSite: true } });

      assert.equal(
        wrapper.find(".rich-icon").getDOMNode().style["background-image"],
        'url("bar")'
      );
      assert.isTrue(wrapper.find(".search-topsite").exists());
    });
  });

  describe("#addMode", () => {
    beforeEach(() => testSetup());

    it("should render the component", () => {
      assert.ok(wrapper.find(TopSiteForm).exists());
    });
    it("should have the correct header", () => {
      assert.equal(
        wrapper.findWhere(
          n =>
            n.length &&
            n.prop("data-l10n-id") === "newtab-topsites-add-shortcut-header"
        ).length,
        1
      );
    });
    it("should have the correct button text", () => {
      assert.equal(
        wrapper.findWhere(
          n =>
            n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button"
        ).length,
        0
      );
      assert.equal(
        wrapper.findWhere(
          n =>
            n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button"
        ).length,
        1
      );
    });
    it("should not render a preview button", () => {
      assert.equal(0, wrapper.find(".custom-image-input-container").length);
    });
    it("should call onClose if Cancel button is clicked", () => {
      wrapper.find(".cancel").simulate("click");
      assert.calledOnce(wrapper.instance().props.onClose);
    });
    it("should set validationError if url is empty", () => {
      assert.equal(wrapper.state().validationError, false);
      wrapper.find(".done").simulate("submit");
      assert.equal(wrapper.state().validationError, true);
    });
    it("should set validationError if url is invalid", () => {
      wrapper.setState({ url: "not valid" });
      assert.equal(wrapper.state().validationError, false);
      wrapper.find(".done").simulate("submit");
      assert.equal(wrapper.state().validationError, true);
    });
    it("should call onClose and dispatch with right args if URL is valid", () => {
      wrapper.setState({ url: "valid.com", label: "a label" });
      wrapper.find(".done").simulate("submit");
      assert.calledOnce(wrapper.instance().props.onClose);
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          site: { label: "a label", url: "http://valid.com" },
          index: -1,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          action_position: -1,
          source: "TOP_SITES",
          event: "TOP_SITES_ADD",
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TELEMETRY_USER_EVENT,
      });
    });
    it("should not pass empty string label in dispatch data", () => {
      wrapper.setState({ url: "valid.com", label: "" });
      wrapper.find(".done").simulate("submit");
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: { site: { url: "http://valid.com" }, index: -1 },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
    });
    it("should open the custom screenshot input", () => {
      assert.isFalse(wrapper.state().showCustomScreenshotForm);

      wrapper.find(A11yLinkButton).simulate("click");

      assert.isTrue(wrapper.state().showCustomScreenshotForm);
    });
  });

  describe("edit existing Topsite", () => {
    beforeEach(() =>
      testSetup({
        site: {
          url: "https://foo.bar",
          label: "baz",
          customScreenshotURL: "http://foo",
        },
        index: 7,
      })
    );

    it("should render the component", () => {
      assert.ok(wrapper.find(TopSiteForm).exists());
    });
    it("should have the correct header", () => {
      assert.equal(
        wrapper.findWhere(
          n => n.prop("data-l10n-id") === "newtab-topsites-edit-shortcut-header"
        ).length,
        1
      );
    });
    it("should have the correct button text", () => {
      assert.equal(
        wrapper.findWhere(
          n => n.prop("data-l10n-id") === "newtab-topsites-add-button"
        ).length,
        0
      );
      assert.equal(
        wrapper.findWhere(
          n => n.prop("data-l10n-id") === "newtab-topsites-save-button"
        ).length,
        1
      );
    });
    it("should call onClose if Cancel button is clicked", () => {
      wrapper.find(".cancel").simulate("click");
      assert.calledOnce(wrapper.instance().props.onClose);
    });
    it("should show error and not call onClose or dispatch if URL is empty", () => {
      wrapper.setState({ url: "" });
      assert.equal(wrapper.state().validationError, false);
      wrapper.find(".done").simulate("submit");
      assert.equal(wrapper.state().validationError, true);
      assert.notCalled(wrapper.instance().props.onClose);
      assert.notCalled(wrapper.instance().props.dispatch);
    });
    it("should show error and not call onClose or dispatch if URL is invalid", () => {
      wrapper.setState({ url: "not valid" });
      assert.equal(wrapper.state().validationError, false);
      wrapper.find(".done").simulate("submit");
      assert.equal(wrapper.state().validationError, true);
      assert.notCalled(wrapper.instance().props.onClose);
      assert.notCalled(wrapper.instance().props.dispatch);
    });
    it("should call onClose and dispatch with right args if URL is valid", () => {
      wrapper.find(".done").simulate("submit");
      assert.calledOnce(wrapper.instance().props.onClose);
      assert.calledTwice(wrapper.instance().props.dispatch);
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          site: {
            label: "baz",
            url: "https://foo.bar",
            customScreenshotURL: "http://foo",
          },
          index: 7,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          action_position: 7,
          source: "TOP_SITES",
          event: "TOP_SITES_EDIT",
          hasTitleChanged: false,
          hasURLChanged: false,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TELEMETRY_USER_EVENT,
      });
    });
    it("should set customScreenshotURL to null if it was removed", () => {
      wrapper.setState({ customScreenshotUrl: "" });

      wrapper.find(".done").simulate("submit");

      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          site: {
            label: "baz",
            url: "https://foo.bar",
            customScreenshotURL: null,
          },
          index: 7,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
    });
    it("should call onClose and dispatch with right args if URL is valid (negative index)", () => {
      wrapper.setProps({ index: -1 });
      wrapper.find(".done").simulate("submit");
      assert.calledOnce(wrapper.instance().props.onClose);
      assert.calledTwice(wrapper.instance().props.dispatch);
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          site: {
            label: "baz",
            url: "https://foo.bar",
            customScreenshotURL: "http://foo",
          },
          index: -1,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
    });
    it("should not pass empty string label in dispatch data", () => {
      wrapper.setState({ label: "" });
      wrapper.find(".done").simulate("submit");
      assert.calledWith(wrapper.instance().props.dispatch, {
        data: {
          site: { url: "https://foo.bar", customScreenshotURL: "http://foo" },
          index: 7,
        },
        meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
        type: at.TOP_SITES_PIN,
      });
    });
    it("should render the save button if custom screenshot request finished", () => {
      wrapper.setState({
        customScreenshotUrl: "foo",
        screenshotPreview: "custom",
      });
      assert.equal(0, wrapper.find(".preview").length);
      assert.equal(1, wrapper.find(".done").length);
    });
    it("should render the save button if custom screenshot url was cleared", () => {
      wrapper.setState({ customScreenshotUrl: "" });
      wrapper.setProps({ site: { customScreenshotURL: "foo" } });
      assert.equal(0, wrapper.find(".preview").length);
      assert.equal(1, wrapper.find(".done").length);
    });
  });

  describe("#previewMode", () => {
    beforeEach(() => testSetup({ previewResponse: null }));

    it("should transition from save to preview", () => {
      wrapper.setProps({
        site: { url: "https://foo.bar", customScreenshotURL: "baz" },
        index: 7,
      });

      assert.equal(
        wrapper.findWhere(
          n =>
            n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button"
        ).length,
        1
      );

      wrapper.setState({ customScreenshotUrl: "foo" });

      assert.equal(
        wrapper.findWhere(
          n =>
            n.length &&
            n.prop("data-l10n-id") === "newtab-topsites-preview-button"
        ).length,
        1
      );
    });

    it("should transition from add to preview", () => {
      assert.equal(
        wrapper.findWhere(
          n =>
            n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button"
        ).length,
        1
      );

      wrapper.setState({ customScreenshotUrl: "foo" });

      assert.equal(
        wrapper.findWhere(
          n =>
            n.length &&
            n.prop("data-l10n-id") === "newtab-topsites-preview-button"
        ).length,
        1
      );
    });
  });

  describe("#validateUrl", () => {
    it("should properly validate URLs", () => {
      testSetup();
      assert.ok(wrapper.instance().validateUrl("mozilla.org"));
      assert.ok(wrapper.instance().validateUrl("https://mozilla.org"));
      assert.ok(wrapper.instance().validateUrl("http://mozilla.org"));
      assert.ok(
        wrapper
          .instance()
          .validateUrl(
            "https://mozilla.invisionapp.com/d/main/#/projects/prototypes"
          )
      );
      assert.ok(wrapper.instance().validateUrl("httpfoobar"));
      assert.ok(wrapper.instance().validateUrl("httpsfoo.bar"));
      assert.isNull(wrapper.instance().validateUrl("mozilla org"));
      assert.isNull(wrapper.instance().validateUrl(""));
    });
  });

  describe("#cleanUrl", () => {
    it("should properly prepend http:// to URLs when required", () => {
      testSetup();
      assert.equal(
        "http://mozilla.org",
        wrapper.instance().cleanUrl("mozilla.org")
      );
      assert.equal(
        "http://https.org",
        wrapper.instance().cleanUrl("https.org")
      );
      assert.equal("http://httpcom", wrapper.instance().cleanUrl("httpcom"));
      assert.equal(
        "http://mozilla.org",
        wrapper.instance().cleanUrl("http://mozilla.org")
      );
      assert.equal(
        "https://firefox.com",
        wrapper.instance().cleanUrl("https://firefox.com")
      );
    });
  });
});

describe("<TopSiteList>", () => {
  const APP = { isForStartupCache: { App: false } };

  it("should render a TopSiteList element", () => {
    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />);
    assert.ok(wrapper.exists());
  });
  it("should render a TopSite for each link with the right url", () => {
    const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }];
    const wrapper = shallow(
      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
    );
    const links = wrapper.find(TopSite);
    assert.lengthOf(links, 2);
    rows.forEach((row, i) =>
      assert.equal(links.get(i).props.link.url, row.url)
    );
  });
  it("should slice the TopSite rows to the TopSitesRows pref", () => {
    const rows = [];
    for (
      let i = 0;
      i < TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW + 3;
      i++
    ) {
      rows.push({ url: `https://foo${i}.com` });
    }
    const wrapper = shallow(
      <TopSiteList
        {...DEFAULT_PROPS}
        TopSites={{ rows }}
        TopSitesRows={TOP_SITES_DEFAULT_ROWS}
        App={APP}
      />
    );
    const links = wrapper.find(TopSite);
    assert.lengthOf(
      links,
      TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW
    );
  });
  it("should add a add topsite button if there is availible space in the row", () => {
    const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }];
    const availibleRows = 1;
    const wrapper = shallow(
      <TopSiteList
        {...DEFAULT_PROPS}
        TopSites={{ rows }}
        TopSitesRows={availibleRows}
        App={APP}
      />
    );
    assert.lengthOf(wrapper.find(TopSite), 2, "topSites");
    assert.lengthOf(
      wrapper.find(TopSiteAddButton),
      availibleRows >= wrapper.find(TopSite).length ? 0 : 1,
      "placeholders"
    );
  });
  it("should fill sponsored top sites with placeholders while rendering for startup cache", () => {
    const rows = [
      { url: "https://sponsored01.com", sponsored_position: 1 },
      { url: "https://sponsored02.com", sponsored_position: 2 },
      { url: "https://sponsored03.com", type: "SPOC" },
      { url: "https://foo.com" },
      { url: "https://bar.com" },
    ];
    const wrapper = shallow(
      <TopSiteList
        {...DEFAULT_PROPS}
        TopSites={{ rows }}
        TopSitesRows={1}
        App={{ isForStartupCache: { TopSites: true } }}
      />
    );
    assert.lengthOf(wrapper.find(TopSite), 2, "topSites");
    assert.lengthOf(wrapper.find(TopSitePlaceholder), 3, "placeholders");
  });
  it("should update state onDragStart and clear it onDragEnd", () => {
    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={{ APP }} />);
    const instance = wrapper.instance();
    const index = 7;
    const link = { url: "https://foo.com" };
    const title = "foo";
    instance.onDragEvent({ type: "dragstart" }, index, link, title);
    assert.equal(instance.state.draggedIndex, index);
    assert.equal(instance.state.draggedSite, link);
    assert.equal(instance.state.draggedTitle, title);
    instance.onDragEvent({ type: "dragend" });
    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);
  });
  it("should clear state when new props arrive after a drop", () => {
    const site1 = { url: "https://foo.com" };
    const site2 = { url: "https://bar.com" };
    const rows = [site1, site2];
    const wrapper = shallow(
      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
    );
    const instance = wrapper.instance();
    instance.setState({
      draggedIndex: 1,
      draggedSite: site2,
      draggedTitle: "bar",
      topSitesPreview: [],
    });
    wrapper.setProps({ TopSites: { rows: [site2, site1] } });
    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);
  });
  it("should dispatch events on drop", () => {
    const dispatch = sinon.spy();
    const wrapper = shallow(
      <TopSiteList {...DEFAULT_PROPS} dispatch={dispatch} App={APP} />
    );
    const instance = wrapper.instance();
    const index = 7;
    const link = { url: "https://foo.com", customScreenshotURL: "foo" };
    const title = "foo";
    instance.onDragEvent({ type: "dragstart" }, index, link, title);
    dispatch.resetHistory();
    instance.onDragEvent({ type: "drop" }, 3);
    assert.calledTwice(dispatch);
    assert.calledWith(dispatch, {
      data: {
        draggedFromIndex: 7,
        index: 3,
        site: {
          label: "foo",
          url: "https://foo.com",
          customScreenshotURL: "foo",
        },
      },
      meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
      type: "TOP_SITES_INSERT",
    });
    assert.calledWith(dispatch, {
      data: { action_position: 3, event: "DROP", source: "TOP_SITES" },
      meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
      type: "TELEMETRY_USER_EVENT",
    });
  });
  it("should make a topSitesPreview onDragEnter", () => {
    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />);
    const instance = wrapper.instance();
    const site = { url: "https://foo.com" };
    instance.setState({
      draggedIndex: 4,
      draggedSite: site,
      draggedTitle: "foo",
    });
    const draggedSite = Object.assign({}, site, {
      isPinned: true,
      isDragged: true,
    });
    instance.onDragEvent({ type: "dragenter" }, 2);
    assert.ok(instance.state.topSitesPreview);
    assert.deepEqual(instance.state.topSitesPreview[2], draggedSite);
  });
  it("should _makeTopSitesPreview correctly", () => {
    const site1 = { url: "https://foo.com" };
    const site2 = { url: "https://bar.com" };
    const site3 = { url: "https://baz.com" };
    const rows = [site1, site2, site3];
    let wrapper = shallow(
      <TopSiteList
        {...DEFAULT_PROPS}
        TopSites={{ rows }}
        TopSitesRows={1}
        App={APP}
      />
    );
    const addButton = { isAddButton: true };
    let instance = wrapper.instance();
    instance.setState({
      draggedIndex: 0,
      draggedSite: site1,
      draggedTitle: "foo",
    });
    let draggedSite = Object.assign({}, site1, {
      isPinned: true,
      isDragged: true,
    });
    assert.deepEqual(instance._makeTopSitesPreview(1), [
      site2,
      draggedSite,
      site3,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(2), [
      site2,
      site3,
      draggedSite,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(3), [
      site2,
      site3,
      addButton,
      draggedSite,
      null,
      null,
      null,
      null,
    ]);
    site2.isPinned = true;
    assert.deepEqual(instance._makeTopSitesPreview(1), [
      site2,
      draggedSite,
      site3,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(2), [
      site3,
      site2,
      draggedSite,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    site3.isPinned = true;
    assert.deepEqual(instance._makeTopSitesPreview(1), [
      site2,
      draggedSite,
      site3,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(2), [
      site2,
      site3,
      draggedSite,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    site2.isPinned = false;
    assert.deepEqual(instance._makeTopSitesPreview(1), [
      site2,
      draggedSite,
      site3,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(2), [
      site2,
      site3,
      draggedSite,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    site3.isPinned = false;
    instance.setState({
      draggedIndex: 1,
      draggedSite: site2,
      draggedTitle: "bar",
    });
    draggedSite = Object.assign({}, site2, { isPinned: true, isDragged: true });
    assert.deepEqual(instance._makeTopSitesPreview(0), [
      draggedSite,
      site1,
      site3,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    assert.deepEqual(instance._makeTopSitesPreview(2), [
      site1,
      site3,
      draggedSite,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    site2.type = "SPOC";
    instance.setState({
      draggedIndex: 2,
      draggedSite: site3,
      draggedTitle: "baz",
    });
    draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true });
    assert.deepEqual(instance._makeTopSitesPreview(0), [
      draggedSite,
      site2,
      site1,
      addButton,
      null,
      null,
      null,
      null,
    ]);
    site2.type = "";
    site2.sponsored_position = 2;
    instance.setState({
      draggedIndex: 2,
      draggedSite: site3,
      draggedTitle: "baz",
    });
    draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true });
    assert.deepEqual(instance._makeTopSitesPreview(0), [
      draggedSite,
      site2,
      site1,
      addButton,
      null,
      null,
      null,
      null,
    ]);
  });
  it("should add a className hide-for-narrow to sites after 6/row", () => {
    const rows = [];
    for (let i = 0; i < TOP_SITES_MAX_SITES_PER_ROW; i++) {
      rows.push({ url: `https://foo${i}.com` });
    }
    const wrapper = mount(
      <TopSiteList
        {...DEFAULT_PROPS}
        TopSites={{ rows }}
        TopSitesRows={1}
        App={APP}
      />
    );
    assert.lengthOf(wrapper.find("li.hide-for-narrow"), 2);
  });

  describe("Keyboard navigation", () => {
    let sandbox;
    let wrapper;
    let instance;
    let mockAnchor;
    let mockTargetSibling;

    beforeEach(() => {
      sandbox = sinon.createSandbox();
      const rows = [
        { url: "https://foo.com" },
        { url: "https://bar.com" },
        { url: "https://baz.com" },
      ];
      wrapper = shallow(
        <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} />
      );
      instance = wrapper.instance();

      mockAnchor = { focus: sandbox.spy(), tabIndex: -1 };
      mockTargetSibling = { querySelector: sandbox.stub().returns(mockAnchor) };
    });

    afterEach(() => {
      sandbox.restore();
    });

    it("should navigate to next site with ArrowRight", () => {
      instance.focusedRef = { nextSibling: mockTargetSibling };
      const mockEvent = { key: "ArrowRight" };

      instance.onKeyDown(mockEvent);

      assert.calledOnce(mockTargetSibling.querySelector);
      assert.calledWith(mockTargetSibling.querySelector, "a");
      assert.calledOnce(mockAnchor.focus);
      assert.equal(mockAnchor.tabIndex, 0);
    });

    it("should navigate to previous site with ArrowLeft", () => {
      instance.focusedRef = { previousSibling: mockTargetSibling };
      const mockEvent = { key: "ArrowLeft" };

      instance.onKeyDown(mockEvent);

      assert.calledOnce(mockTargetSibling.querySelector);
      assert.calledWith(mockTargetSibling.querySelector, "a");
      assert.calledOnce(mockAnchor.focus);
      assert.equal(mockAnchor.tabIndex, 0);
    });
  });
});

describe("TopSiteAddButton", () => {
  it("should dispatch a TOP_SITES_EDIT action when the addbutton is clicked", () => {
    const dispatch = sinon.spy();
    const wrapper = shallow(
      <TopSiteAddButton dispatch={dispatch} index={7} isAddButton={true} />
    );

    wrapper.find(".add-button").first().simulate("click");

    assert.calledOnce(dispatch);
    assert.calledWithExactly(dispatch, {
      type: at.TOP_SITES_EDIT,
      data: { index: 7 },
    });
  });
});

describe("#TopSiteFormInput", () => {
  let wrapper;
  let onChangeStub;

  describe("no errors", () => {
    beforeEach(() => {
      onChangeStub = sinon.stub();

      wrapper = mount(
        <TopSiteFormInput
          titleId="newtab-topsites-title-label"
          placeholderId="newtab-topsites-title-input"
          errorMessageId="newtab-topsites-url-validation"
          onChange={onChangeStub}
          value="foo"
        />
      );
    });

    it("should render the provided title", () => {
      const title = wrapper.find("span");
      assert.propertyVal(
        title.props(),
        "data-l10n-id",
        "newtab-topsites-title-label"
      );
    });

    it("should render the provided value", () => {
      const input = wrapper.find("input");

      assert.equal(input.getDOMNode().value, "foo");
    });

    it("should render the clear button if cb is provided", () => {
      assert.equal(wrapper.find(".icon-clear-input").length, 0);

      wrapper.setProps({ onClear: sinon.stub() });

      assert.equal(wrapper.find(".icon-clear-input").length, 1);
    });

    it("should show the loading indicator", () => {
      assert.equal(wrapper.find(".loading-container").length, 0);

      wrapper.setProps({ loading: true });

      assert.equal(wrapper.find(".loading-container").length, 1);
    });
    it("should disable the input when loading indicator is present", () => {
      assert.isFalse(wrapper.find("input").getDOMNode().disabled);

      wrapper.setProps({ loading: true });

      assert.isTrue(wrapper.find("input").getDOMNode().disabled);
    });
  });

  describe("with error", () => {
    beforeEach(() => {
      onChangeStub = sinon.stub();

      wrapper = mount(
        <TopSiteFormInput
          titleId="newtab-topsites-title-label"
          placeholderId="newtab-topsites-title-input"
          onChange={onChangeStub}
          validationError={true}
          errorMessageId="newtab-topsites-url-validation"
          value="foo"
        />
      );
    });

    it("should render the error message", () => {
      assert.equal(
        wrapper.findWhere(
          n => n.prop("data-l10n-id") === "newtab-topsites-url-validation"
        ).length,
        1
      );
    });

    it("should reset the error state on value change", () => {
      wrapper.find("input").simulate("change", { target: { value: "bar" } });

      assert.isFalse(wrapper.state().validationError);
    });
  });
});
