summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents')
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx354
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx134
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx544
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx138
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx51
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx73
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx146
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx151
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx57
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx50
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx92
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx94
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx41
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx16
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx278
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx131
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx29
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx56
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx22
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx238
20 files changed, 2695 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
new file mode 100644
index 0000000000..418a731ba1
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
@@ -0,0 +1,354 @@
+import {
+ _CardGrid as CardGrid,
+ IntersectionObserver,
+ RecentSavesContainer,
+ OnboardingExperience,
+ DSSubHeader,
+} from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
+import { combineReducers, createStore } from "redux";
+import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
+import { Provider } from "react-redux";
+import {
+ DSCard,
+ PlaceholderDSCard,
+} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
+import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
+import {
+ actionCreators as ac,
+ actionTypes as at,
+} from "common/Actions.sys.mjs";
+import React from "react";
+import { shallow, mount } from "enzyme";
+
+// Wrap this around any component that uses useSelector,
+// or any mount that uses a child that uses redux.
+function WrapWithProvider({ children, state = INITIAL_STATE }) {
+ let store = createStore(combineReducers(reducers), state);
+ return <Provider store={store}>{children}</Provider>;
+}
+
+describe("<CardGrid>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow(
+ <CardGrid
+ Prefs={INITIAL_STATE.Prefs}
+ DiscoveryStream={INITIAL_STATE.DiscoveryStream}
+ />
+ );
+ });
+
+ it("should render an empty div", () => {
+ assert.ok(wrapper.exists());
+ assert.lengthOf(wrapper.children(), 0);
+ });
+
+ it("should render DSCards", () => {
+ wrapper.setProps({ items: 2, data: { recommendations: [{}, {}] } });
+
+ assert.lengthOf(wrapper.find(".ds-card-grid").children(), 2);
+ assert.equal(wrapper.find(".ds-card-grid").children().at(0).type(), DSCard);
+ });
+
+ it("should add 4 card classname to card grid", () => {
+ wrapper.setProps({
+ fourCardLayout: true,
+ data: { recommendations: [{}, {}] },
+ });
+
+ assert.ok(wrapper.find(".ds-card-grid-four-card-variant").exists());
+ });
+
+ it("should add no description classname to card grid", () => {
+ wrapper.setProps({
+ hideCardBackground: true,
+ data: { recommendations: [{}, {}] },
+ });
+
+ assert.ok(wrapper.find(".ds-card-grid-hide-background").exists());
+ });
+
+ it("should render sub header in the middle of the card grid for both regular and compact", () => {
+ const commonProps = {
+ essentialReadsHeader: true,
+ editorsPicksHeader: true,
+ items: 12,
+ data: {
+ recommendations: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
+ },
+ Prefs: INITIAL_STATE.Prefs,
+ DiscoveryStream: INITIAL_STATE.DiscoveryStream,
+ };
+ wrapper = mount(
+ <WrapWithProvider>
+ <CardGrid {...commonProps} />
+ </WrapWithProvider>
+ );
+
+ assert.ok(wrapper.find(DSSubHeader).exists());
+
+ wrapper.setProps({
+ compact: true,
+ });
+ wrapper = mount(
+ <WrapWithProvider>
+ <CardGrid {...commonProps} compact={true} />
+ </WrapWithProvider>
+ );
+
+ assert.ok(wrapper.find(DSSubHeader).exists());
+ });
+
+ it("should add/hide description classname to card grid", () => {
+ wrapper.setProps({
+ data: { recommendations: [{}, {}] },
+ });
+
+ assert.ok(wrapper.find(".ds-card-grid-include-descriptions").exists());
+
+ wrapper.setProps({
+ hideDescriptions: true,
+ data: { recommendations: [{}, {}] },
+ });
+
+ assert.ok(!wrapper.find(".ds-card-grid-include-descriptions").exists());
+ });
+
+ it("should create a widget card", () => {
+ wrapper.setProps({
+ widgets: {
+ positions: [{ index: 1 }],
+ data: [{ type: "TopicsWidget" }],
+ },
+ data: {
+ recommendations: [{}, {}, {}],
+ },
+ });
+
+ assert.ok(wrapper.find(TopicsWidget).exists());
+ });
+});
+
+// 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() {}
+
+ disconnect() {}
+ };
+}
+
+describe("<IntersectionObserver>", () => {
+ let wrapper;
+ let fakeWindow;
+ let intersectEntries;
+
+ beforeEach(() => {
+ intersectEntries = [{ isIntersecting: true }];
+ fakeWindow = {
+ IntersectionObserver: buildIntersectionObserver(intersectEntries),
+ };
+ wrapper = mount(<IntersectionObserver windowObj={fakeWindow} />);
+ });
+
+ it("should render an empty div", () => {
+ assert.ok(wrapper.exists());
+ assert.equal(wrapper.children().at(0).type(), "div");
+ });
+
+ it("should fire onIntersecting", () => {
+ const onIntersecting = sinon.stub();
+ wrapper = mount(
+ <IntersectionObserver
+ windowObj={fakeWindow}
+ onIntersecting={onIntersecting}
+ />
+ );
+ assert.calledOnce(onIntersecting);
+ });
+});
+
+describe("<RecentSavesContainer>", () => {
+ let wrapper;
+ let fakeWindow;
+ let intersectEntries;
+ let dispatch;
+
+ beforeEach(() => {
+ dispatch = sinon.stub();
+ intersectEntries = [{ isIntersecting: true }];
+ fakeWindow = {
+ IntersectionObserver: buildIntersectionObserver(intersectEntries),
+ };
+ wrapper = mount(
+ <WrapWithProvider
+ state={{
+ DiscoveryStream: {
+ isUserLoggedIn: true,
+ recentSavesData: [
+ {
+ resolved_id: "resolved_id",
+ top_image_url: "top_image_url",
+ title: "title",
+ resolved_url: "https://resolved_url",
+ domain: "domain",
+ excerpt: "excerpt",
+ },
+ ],
+ experimentData: {
+ utmSource: "utmSource",
+ utmContent: "utmContent",
+ utmCampaign: "utmCampaign",
+ },
+ },
+ }}
+ >
+ <RecentSavesContainer
+ gridClassName="ds-card-grid"
+ windowObj={fakeWindow}
+ dispatch={dispatch}
+ />
+ </WrapWithProvider>
+ ).find(RecentSavesContainer);
+ });
+
+ it("should render an IntersectionObserver when not visible", () => {
+ intersectEntries = [{ isIntersecting: false }];
+ fakeWindow = {
+ IntersectionObserver: buildIntersectionObserver(intersectEntries),
+ };
+ wrapper = mount(
+ <WrapWithProvider>
+ <RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
+ </WrapWithProvider>
+ ).find(RecentSavesContainer);
+
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(IntersectionObserver).exists());
+ });
+
+ it("should render nothing if visible until we log in", () => {
+ assert.ok(!wrapper.find(IntersectionObserver).exists());
+ assert.calledOnce(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.AlsoToMain({
+ type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
+ })
+ );
+ });
+
+ it("should render a grid if visible and logged in", () => {
+ assert.lengthOf(wrapper.find(".ds-card-grid"), 1);
+ assert.lengthOf(wrapper.find(DSSubHeader), 1);
+ assert.lengthOf(wrapper.find(PlaceholderDSCard), 2);
+ assert.lengthOf(wrapper.find(DSCard), 3);
+ });
+
+ it("should render a my list link with proper utm params", () => {
+ assert.equal(
+ wrapper.find(".section-sub-link").at(0).prop("url"),
+ "https://getpocket.com/a?utm_source=utmSource&utm_content=utmContent&utm_campaign=utmCampaign"
+ );
+ });
+
+ it("should fire a UserEvent for my list clicks", () => {
+ wrapper.find(".section-sub-link").at(0).simulate("click");
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: `CARDGRID_RECENT_SAVES_VIEW_LIST`,
+ })
+ );
+ });
+});
+
+describe("<OnboardingExperience>", () => {
+ let wrapper;
+ let fakeWindow;
+ let intersectEntries;
+ let dispatch;
+ let resizeCallback;
+
+ let fakeResizeObserver = class {
+ constructor(callback) {
+ resizeCallback = callback;
+ }
+
+ observe() {}
+
+ unobserve() {}
+
+ disconnect() {}
+ };
+
+ beforeEach(() => {
+ dispatch = sinon.stub();
+ intersectEntries = [{ isIntersecting: true, intersectionRatio: 1 }];
+ fakeWindow = {
+ ResizeObserver: fakeResizeObserver,
+ IntersectionObserver: buildIntersectionObserver(intersectEntries),
+ document: {
+ visibilityState: "visible",
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ },
+ };
+ wrapper = mount(
+ <WrapWithProvider state={{}}>
+ <OnboardingExperience windowObj={fakeWindow} dispatch={dispatch} />
+ </WrapWithProvider>
+ ).find(OnboardingExperience);
+ });
+
+ it("should render a ds-onboarding", () => {
+ assert.ok(wrapper.exists());
+ assert.lengthOf(wrapper.find(".ds-onboarding"), 1);
+ });
+
+ it("should dismiss on dismiss click", () => {
+ wrapper.find(".ds-dismiss-button").simulate("click");
+
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "BLOCK",
+ source: "POCKET_ONBOARDING",
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.SetPref("discoverystream.onboardingExperience.dismissed", true)
+ );
+ assert.equal(wrapper.getDOMNode().style["max-height"], "0px");
+ assert.equal(wrapper.getDOMNode().style.opacity, "0");
+ });
+
+ it("should update max-height on resize", () => {
+ sinon
+ .stub(wrapper.find(".ds-onboarding-ref").getDOMNode(), "offsetHeight")
+ .get(() => 123);
+ resizeCallback();
+ assert.equal(wrapper.getDOMNode().style["max-height"], "123px");
+ });
+
+ it("should fire intersection events", () => {
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "IMPRESSION",
+ source: "POCKET_ONBOARDING",
+ })
+ );
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx
new file mode 100644
index 0000000000..3721508a59
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx
@@ -0,0 +1,134 @@
+import { CollectionCardGrid } from "content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid";
+import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<CollectionCardGrid>", () => {
+ let wrapper;
+ let sandbox;
+ let dispatchStub;
+ const initialSpocs = [
+ { id: 123, url: "123" },
+ { id: 456, url: "456" },
+ { id: 789, url: "789" },
+ ];
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatchStub = sandbox.stub();
+ wrapper = shallow(
+ <CollectionCardGrid
+ dispatch={dispatchStub}
+ type="COLLECTIONCARDGRID"
+ placement={{
+ name: "spocs",
+ }}
+ data={{
+ spocs: initialSpocs,
+ }}
+ spocs={{
+ data: {
+ spocs: {
+ title: "title",
+ context: "context",
+ items: initialSpocs,
+ },
+ },
+ }}
+ />
+ );
+ });
+
+ it("should render an empty div", () => {
+ wrapper = shallow(<CollectionCardGrid />);
+ assert.ok(wrapper.exists());
+ assert.ok(!wrapper.exists(".ds-collection-card-grid"));
+ });
+
+ it("should render a CardGrid", () => {
+ assert.lengthOf(wrapper.find(".ds-collection-card-grid").children(), 1);
+ assert.equal(
+ wrapper.find(".ds-collection-card-grid").children().at(0).type(),
+ CardGrid
+ );
+ });
+
+ it("should inject spocs in every CardGrid rec position", () => {
+ assert.lengthOf(
+ wrapper.find(".ds-collection-card-grid").children().at(0).props().data
+ .recommendations,
+ 3
+ );
+ });
+
+ it("should pass along title and context to CardGrid", () => {
+ assert.equal(
+ wrapper.find(".ds-collection-card-grid").children().at(0).props().title,
+ "title"
+ );
+
+ assert.equal(
+ wrapper.find(".ds-collection-card-grid").children().at(0).props().context,
+ "context"
+ );
+ });
+
+ it("should render nothing without a title", () => {
+ wrapper = shallow(
+ <CollectionCardGrid
+ dispatch={dispatchStub}
+ placement={{
+ name: "spocs",
+ }}
+ data={{
+ spocs: initialSpocs,
+ }}
+ spocs={{
+ data: {
+ spocs: {
+ title: "",
+ context: "context",
+ items: initialSpocs,
+ },
+ },
+ }}
+ />
+ );
+
+ assert.ok(wrapper.exists());
+ assert.ok(!wrapper.exists(".ds-collection-card-grid"));
+ });
+
+ it("should dispatch telemety events on dismiss", () => {
+ wrapper.instance().onDismissClick();
+
+ const firstCall = dispatchStub.getCall(0);
+ const secondCall = dispatchStub.getCall(1);
+ const thirdCall = dispatchStub.getCall(2);
+
+ assert.equal(firstCall.args[0].type, "BLOCK_URL");
+ assert.deepEqual(firstCall.args[0].data, [
+ { url: "123", pocket_id: undefined, isSponsoredTopSite: undefined },
+ { url: "456", pocket_id: undefined, isSponsoredTopSite: undefined },
+ { url: "789", pocket_id: undefined, isSponsoredTopSite: undefined },
+ ]);
+
+ assert.equal(secondCall.args[0].type, "DISCOVERY_STREAM_USER_EVENT");
+ assert.deepEqual(secondCall.args[0].data, {
+ event: "BLOCK",
+ source: "COLLECTIONCARDGRID",
+ action_position: 0,
+ });
+
+ assert.equal(thirdCall.args[0].type, "TELEMETRY_IMPRESSION_STATS");
+ assert.deepEqual(thirdCall.args[0].data, {
+ source: "COLLECTIONCARDGRID",
+ block: 0,
+ tiles: [
+ { id: 123, pos: 0 },
+ { id: 456, pos: 1 },
+ { id: 789, pos: 2 },
+ ],
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
new file mode 100644
index 0000000000..2ebba1d4e5
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
@@ -0,0 +1,544 @@
+import {
+ _DSCard as DSCard,
+ readTimeFromWordCount,
+ DSSource,
+ DefaultMeta,
+ PlaceholderDSCard,
+} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
+import {
+ DSContextFooter,
+ StatusMessage,
+ SponsorLabel,
+} from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
+import {
+ actionCreators as ac,
+ actionTypes as at,
+} from "common/Actions.sys.mjs";
+import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
+import React from "react";
+import { INITIAL_STATE } from "common/Reducers.sys.mjs";
+import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import { shallow, mount } from "enzyme";
+import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
+
+const DEFAULT_PROPS = {
+ url: "about:robots",
+ title: "title",
+ App: {
+ isForStartupCache: false,
+ },
+ DiscoveryStream: INITIAL_STATE.DiscoveryStream,
+};
+
+describe("<DSCard>", () => {
+ let wrapper;
+ let sandbox;
+ let dispatch;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatch = sandbox.stub();
+ wrapper = shallow(<DSCard dispatch={dispatch} {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-card"));
+ });
+
+ it("should render a SafeAnchor", () => {
+ wrapper.setProps({ url: "https://foo.com" });
+
+ assert.equal(wrapper.children().at(0).type(), SafeAnchor);
+ assert.propertyVal(
+ wrapper.children().at(0).props(),
+ "url",
+ "https://foo.com"
+ );
+ });
+
+ it("should pass onLinkClick prop", () => {
+ assert.propertyVal(
+ wrapper.children().at(0).props(),
+ "onLinkClick",
+ wrapper.instance().onLinkClick
+ );
+ });
+
+ it("should render DSLinkMenu", () => {
+ assert.equal(wrapper.children().at(1).type(), DSLinkMenu);
+ });
+
+ it("should start with no .active class", () => {
+ assert.equal(wrapper.find(".active").length, 0);
+ });
+
+ it("should render badges for pocket, bookmark when not a spoc element ", () => {
+ wrapper = mount(<DSCard context_type="bookmark" {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ const contextFooter = wrapper.find(DSContextFooter);
+
+ assert.lengthOf(contextFooter.find(StatusMessage), 1);
+ });
+
+ it("should render Sponsored Context for a spoc element", () => {
+ const context = "Sponsored by Foo";
+ wrapper = mount(
+ <DSCard context_type="bookmark" context={context} {...DEFAULT_PROPS} />
+ );
+ wrapper.setState({ isSeen: true });
+ const contextFooter = wrapper.find(DSContextFooter);
+
+ assert.lengthOf(contextFooter.find(StatusMessage), 0);
+ assert.equal(contextFooter.find(".story-sponsored-label").text(), context);
+ });
+
+ it("should render time to read", () => {
+ const discoveryStream = {
+ ...INITIAL_STATE.DiscoveryStream,
+ readTime: true,
+ };
+ wrapper = mount(
+ <DSCard
+ time_to_read={4}
+ {...DEFAULT_PROPS}
+ DiscoveryStream={discoveryStream}
+ />
+ );
+ wrapper.setState({ isSeen: true });
+ const defaultMeta = wrapper.find(DefaultMeta);
+ assert.lengthOf(defaultMeta, 1);
+ assert.equal(defaultMeta.props().timeToRead, 4);
+ });
+
+ it("should not show save to pocket button for spocs", () => {
+ wrapper.setProps({
+ id: "fooidx",
+ pos: 1,
+ type: "foo",
+ flightId: 12345,
+ saveToPocketCard: true,
+ });
+
+ let stpButton = wrapper.find(".card-stp-button");
+
+ assert.lengthOf(stpButton, 0);
+ });
+
+ it("should show save to pocket button for non-spocs", () => {
+ wrapper.setProps({
+ id: "fooidx",
+ pos: 1,
+ type: "foo",
+ saveToPocketCard: true,
+ });
+
+ let stpButton = wrapper.find(".card-stp-button");
+
+ assert.lengthOf(stpButton, 1);
+ });
+
+ describe("onLinkClick", () => {
+ let fakeWindow;
+
+ beforeEach(() => {
+ fakeWindow = {
+ requestIdleCallback: sinon.stub().returns(1),
+ cancelIdleCallback: sinon.stub(),
+ innerWidth: 1000,
+ innerHeight: 900,
+ };
+ wrapper = mount(
+ <DSCard {...DEFAULT_PROPS} dispatch={dispatch} windowObj={fakeWindow} />
+ );
+ });
+
+ it("should call dispatch with the correct events", () => {
+ wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" });
+
+ wrapper.instance().onLinkClick();
+
+ assert.calledTwice(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "FOO",
+ action_position: 1,
+ value: { card_type: "organic" },
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.ImpressionStats({
+ click: 0,
+ source: "FOO",
+ tiles: [{ id: "fooidx", pos: 1, type: "organic" }],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+
+ it("should set the right card_type on spocs", () => {
+ wrapper.setProps({ id: "fooidx", pos: 1, type: "foo", flightId: 12345 });
+
+ wrapper.instance().onLinkClick();
+
+ assert.calledTwice(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "FOO",
+ action_position: 1,
+ value: { card_type: "spoc" },
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.ImpressionStats({
+ click: 0,
+ source: "FOO",
+ tiles: [{ id: "fooidx", pos: 1, type: "spoc" }],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+
+ it("should call dispatch with a shim", () => {
+ wrapper.setProps({
+ id: "fooidx",
+ pos: 1,
+ type: "foo",
+ shim: {
+ click: "click shim",
+ },
+ });
+
+ wrapper.instance().onLinkClick();
+
+ assert.calledTwice(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "FOO",
+ action_position: 1,
+ value: { card_type: "organic" },
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.ImpressionStats({
+ click: 0,
+ source: "FOO",
+ tiles: [
+ { id: "fooidx", pos: 1, shim: "click shim", type: "organic" },
+ ],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+ });
+
+ describe("DSCard with CTA", () => {
+ beforeEach(() => {
+ wrapper = mount(<DSCard {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ });
+
+ it("should render Default Meta", () => {
+ const default_meta = wrapper.find(DefaultMeta);
+ assert.ok(default_meta.exists());
+ });
+ });
+
+ describe("DSCard with Intersection Observer", () => {
+ beforeEach(() => {
+ wrapper = shallow(<DSCard {...DEFAULT_PROPS} />);
+ });
+
+ it("should render card when seen", () => {
+ let card = wrapper.find("div.ds-card.placeholder");
+ assert.lengthOf(card, 1);
+
+ wrapper.instance().observer = {
+ unobserve: sandbox.stub(),
+ };
+ wrapper.instance().placeholderElement = "element";
+
+ wrapper.instance().onSeen([
+ {
+ isIntersecting: true,
+ },
+ ]);
+
+ assert.isTrue(wrapper.instance().state.isSeen);
+ card = wrapper.find("div.ds-card.placeholder");
+ assert.lengthOf(card, 0);
+ assert.lengthOf(wrapper.find(SafeAnchor), 1);
+ assert.calledOnce(wrapper.instance().observer.unobserve);
+ assert.calledWith(wrapper.instance().observer.unobserve, "element");
+ });
+
+ it("should setup proper placholder ref for isSeen", () => {
+ wrapper.instance().setPlaceholderRef("element");
+ assert.equal(wrapper.instance().placeholderElement, "element");
+ });
+
+ it("should setup observer on componentDidMount", () => {
+ wrapper = mount(<DSCard {...DEFAULT_PROPS} />);
+ assert.isTrue(!!wrapper.instance().observer);
+ });
+ });
+
+ describe("DSCard with Idle Callback", () => {
+ let windowStub = {
+ requestIdleCallback: sinon.stub().returns(1),
+ cancelIdleCallback: sinon.stub(),
+ };
+ beforeEach(() => {
+ wrapper = shallow(<DSCard windowObj={windowStub} {...DEFAULT_PROPS} />);
+ });
+
+ it("should call requestIdleCallback on componentDidMount", () => {
+ assert.calledOnce(windowStub.requestIdleCallback);
+ });
+
+ it("should call cancelIdleCallback on componentWillUnmount", () => {
+ wrapper.instance().componentWillUnmount();
+ assert.calledOnce(windowStub.cancelIdleCallback);
+ });
+ });
+
+ describe("DSCard when rendered for about:home startup cache", () => {
+ beforeEach(() => {
+ const props = {
+ App: {
+ isForStartupCache: true,
+ },
+ DiscoveryStream: INITIAL_STATE.DiscoveryStream,
+ };
+ wrapper = mount(<DSCard {...props} />);
+ });
+
+ it("should be set as isSeen automatically", () => {
+ assert.isTrue(wrapper.instance().state.isSeen);
+ });
+ });
+
+ describe("DSCard onSaveClick", () => {
+ it("should fire telemetry for onSaveClick", () => {
+ wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" });
+ wrapper.instance().onSaveClick();
+
+ assert.calledThrice(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.AlsoToMain({
+ type: at.SAVE_TO_POCKET,
+ data: { site: { url: "about:robots", title: "title" } },
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "SAVE_TO_POCKET",
+ source: "CARDGRID_HOVER",
+ action_position: 1,
+ value: { card_type: "organic" },
+ })
+ );
+ assert.calledWith(
+ dispatch,
+ ac.ImpressionStats({
+ source: "CARDGRID_HOVER",
+ pocket: 0,
+ tiles: [
+ {
+ id: "fooidx",
+ pos: 1,
+ },
+ ],
+ })
+ );
+ });
+ });
+
+ describe("DSCard menu open states", () => {
+ let cardNode;
+ let fakeDocument;
+ let fakeWindow;
+
+ beforeEach(() => {
+ fakeDocument = { l10n: { translateFragment: sinon.stub() } };
+ fakeWindow = {
+ document: fakeDocument,
+ requestIdleCallback: sinon.stub().returns(1),
+ cancelIdleCallback: sinon.stub(),
+ };
+ wrapper = mount(<DSCard {...DEFAULT_PROPS} windowObj={fakeWindow} />);
+ wrapper.setState({ isSeen: true });
+ cardNode = wrapper.getDOMNode();
+ });
+
+ it("Should remove active on Menu Update", () => {
+ // Add active class name to DSCard wrapper
+ // to simulate menu open state
+ cardNode.classList.add("active");
+ assert.equal(
+ cardNode.className,
+ "ds-card ds-card-title-lines-3 ds-card-desc-lines-3 active"
+ );
+
+ wrapper.instance().onMenuUpdate(false);
+ wrapper.update();
+
+ assert.equal(
+ cardNode.className,
+ "ds-card ds-card-title-lines-3 ds-card-desc-lines-3"
+ );
+ });
+
+ it("Should add active on Menu Show", async () => {
+ await wrapper.instance().onMenuShow();
+ wrapper.update();
+ assert.equal(
+ cardNode.className,
+ "ds-card ds-card-title-lines-3 ds-card-desc-lines-3 active"
+ );
+ });
+
+ it("Should add last-item to support resized window", async () => {
+ fakeWindow.scrollMaxX = 20;
+ await wrapper.instance().onMenuShow();
+ wrapper.update();
+ assert.equal(
+ cardNode.className,
+ "ds-card ds-card-title-lines-3 ds-card-desc-lines-3 last-item active"
+ );
+ });
+
+ it("should remove .active and .last-item classes", () => {
+ const instance = wrapper.instance();
+ const remove = sinon.stub();
+ instance.contextMenuButtonHostElement = {
+ classList: { remove },
+ };
+ instance.onMenuUpdate();
+ assert.calledOnce(remove);
+ });
+
+ it("should add .active and .last-item classes", async () => {
+ const instance = wrapper.instance();
+ const add = sinon.stub();
+ instance.contextMenuButtonHostElement = {
+ classList: { add },
+ };
+ await instance.onMenuShow();
+ assert.calledOnce(add);
+ });
+ });
+});
+
+describe("<PlaceholderDSCard> component", () => {
+ it("should have placeholder prop", () => {
+ const wrapper = shallow(<PlaceholderDSCard />);
+ const placeholder = wrapper.prop("placeholder");
+ assert.isTrue(placeholder);
+ });
+
+ it("should contain placeholder div", () => {
+ const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ const card = wrapper.find("div.ds-card.placeholder");
+ assert.lengthOf(card, 1);
+ });
+
+ it("should not be clickable", () => {
+ const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ const anchor = wrapper.find("SafeAnchor.ds-card-link");
+ assert.lengthOf(anchor, 0);
+ });
+
+ it("should not have context menu", () => {
+ const wrapper = shallow(<DSCard placeholder={true} {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ const linkMenu = wrapper.find(DSLinkMenu);
+ assert.lengthOf(linkMenu, 0);
+ });
+});
+
+describe("<DSSource> component", () => {
+ it("should return a default source without compact", () => {
+ const wrapper = shallow(<DSSource source="Mozilla" />);
+
+ let sourceElement = wrapper.find(".source");
+ assert.equal(sourceElement.text(), "Mozilla");
+ });
+ it("should return a default source with compact without a sponsor or time to read", () => {
+ const wrapper = shallow(<DSSource compact={true} source="Mozilla" />);
+
+ let sourceElement = wrapper.find(".source");
+ assert.equal(sourceElement.text(), "Mozilla");
+ });
+ it("should return a SponsorLabel with compact and a sponsor", () => {
+ const wrapper = shallow(
+ <DSSource newSponsoredLabel={true} sponsor="Mozilla" />
+ );
+ const sponsorLabel = wrapper.find(SponsorLabel);
+ assert.lengthOf(sponsorLabel, 1);
+ });
+ it("should return a time to read with compact and without a sponsor but with a time to read", () => {
+ const wrapper = shallow(
+ <DSSource compact={true} source="Mozilla" timeToRead="2000" />
+ );
+
+ let timeToRead = wrapper.find(".time-to-read");
+ assert.lengthOf(timeToRead, 1);
+
+ // Weirdly, we can test for the pressence of fluent, because time to read needs to be translated.
+ // This is also because we did a shallow render, that th contents of fluent would be empty anyway.
+ const fluentOrText = wrapper.find(FluentOrText);
+ assert.lengthOf(fluentOrText, 1);
+ });
+ it("should prioritize a SponsorLabel if for some reason it gets everything", () => {
+ const wrapper = shallow(
+ <DSSource
+ newSponsoredLabel={true}
+ sponsor="Mozilla"
+ source="Mozilla"
+ timeToRead="2000"
+ />
+ );
+ const sponsorLabel = wrapper.find(SponsorLabel);
+ assert.lengthOf(sponsorLabel, 1);
+ });
+});
+
+describe("readTimeFromWordCount function", () => {
+ it("should return proper read time", () => {
+ const result = readTimeFromWordCount(2000);
+ assert.equal(result, 10);
+ });
+ it("should return false with falsey word count", () => {
+ assert.isFalse(readTimeFromWordCount());
+ assert.isFalse(readTimeFromWordCount(0));
+ assert.isFalse(readTimeFromWordCount(""));
+ assert.isFalse(readTimeFromWordCount(null));
+ assert.isFalse(readTimeFromWordCount(undefined));
+ });
+ it("should return NaN with invalid word count", () => {
+ assert.isNaN(readTimeFromWordCount("zero"));
+ assert.isNaN(readTimeFromWordCount({}));
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
new file mode 100644
index 0000000000..08ac7868ce
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx
@@ -0,0 +1,138 @@
+import {
+ DSContextFooter,
+ StatusMessage,
+ DSMessageFooter,
+} from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
+import React from "react";
+import { mount } from "enzyme";
+import { cardContextTypes } from "content-src/components/Card/types.js";
+import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText.jsx";
+
+describe("<DSContextFooter>", () => {
+ let wrapper;
+ let sandbox;
+ const bookmarkBadge = "bookmark";
+ const removeBookmarkBadge = "removedBookmark";
+ const context = "Sponsored by Babel";
+ const sponsored_by_override = "Sponsored override";
+ const engagement = "Popular";
+
+ beforeEach(() => {
+ wrapper = mount(<DSContextFooter />);
+ sandbox = sinon.createSandbox();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => assert.isTrue(wrapper.exists()));
+ it("should not render an engagement status if display_engagement_labels is false", () => {
+ wrapper = mount(
+ <DSContextFooter
+ display_engagement_labels={false}
+ engagement={engagement}
+ />
+ );
+
+ const engagementLabel = wrapper.find(".story-view-count");
+ assert.equal(engagementLabel.length, 0);
+ });
+ it("should render a badge if a proper badge prop is passed", () => {
+ wrapper = mount(
+ <DSContextFooter context_type={bookmarkBadge} engagement={engagement} />
+ );
+ const { fluentID } = cardContextTypes[bookmarkBadge];
+
+ assert.lengthOf(wrapper.find(".story-view-count"), 0);
+ const statusLabel = wrapper.find(".story-context-label");
+ assert.equal(statusLabel.prop("data-l10n-id"), fluentID);
+ });
+ it("should only render a sponsored context if pass a sponsored context", async () => {
+ wrapper = mount(
+ <DSContextFooter
+ context_type={bookmarkBadge}
+ context={context}
+ engagement={engagement}
+ />
+ );
+
+ assert.lengthOf(wrapper.find(".story-view-count"), 0);
+ assert.lengthOf(wrapper.find(StatusMessage), 0);
+ assert.equal(wrapper.find(".story-sponsored-label").text(), context);
+ });
+ it("should render a sponsored_by_override if passed a sponsored_by_override", async () => {
+ wrapper = mount(
+ <DSContextFooter
+ context_type={bookmarkBadge}
+ context={context}
+ sponsored_by_override={sponsored_by_override}
+ engagement={engagement}
+ />
+ );
+
+ assert.equal(
+ wrapper.find(".story-sponsored-label").text(),
+ sponsored_by_override
+ );
+ });
+ it("should render nothing with a sponsored_by_override empty string", async () => {
+ wrapper = mount(
+ <DSContextFooter
+ context_type={bookmarkBadge}
+ context={context}
+ sponsored_by_override=""
+ engagement={engagement}
+ />
+ );
+
+ assert.isFalse(wrapper.find(".story-sponsored-label").exists());
+ });
+ it("should render localized string with sponsor with no sponsored_by_override", async () => {
+ wrapper = mount(
+ <DSContextFooter
+ context_type={bookmarkBadge}
+ context={context}
+ sponsor="Nimoy"
+ engagement={engagement}
+ />
+ );
+
+ assert.equal(
+ wrapper.find(".story-sponsored-label").children().at(0).type(),
+ FluentOrText
+ );
+ });
+ it("should render a new badge if props change from an old badge to a new one", async () => {
+ wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);
+
+ const { fluentID: bookmarkFluentID } = cardContextTypes[bookmarkBadge];
+ const bookmarkStatusMessage = wrapper.find(
+ `div[data-l10n-id='${bookmarkFluentID}']`
+ );
+ assert.isTrue(bookmarkStatusMessage.exists());
+
+ const { fluentID: removeBookmarkFluentID } =
+ cardContextTypes[removeBookmarkBadge];
+
+ wrapper.setProps({ context_type: removeBookmarkBadge });
+ await wrapper.update();
+
+ assert.isEmpty(bookmarkStatusMessage);
+ const removedBookmarkStatusMessage = wrapper.find(
+ `div[data-l10n-id='${removeBookmarkFluentID}']`
+ );
+ assert.isTrue(removedBookmarkStatusMessage.exists());
+ });
+ it("should render a story footer", () => {
+ wrapper = mount(
+ <DSMessageFooter
+ context_type={bookmarkBadge}
+ engagement={engagement}
+ display_engagement_labels={true}
+ />
+ );
+
+ assert.lengthOf(wrapper.find(".story-footer"), 1);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx
new file mode 100644
index 0000000000..2f7e206b4f
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx
@@ -0,0 +1,51 @@
+import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<DSDismiss>", () => {
+ const fakeSpoc = {
+ url: "https://foo.com",
+ guid: "1234",
+ };
+ let wrapper;
+ let sandbox;
+ let onDismissClickStub;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ onDismissClickStub = sandbox.stub();
+ wrapper = shallow(
+ <DSDismiss
+ data={fakeSpoc}
+ onDismissClick={onDismissClickStub}
+ shouldSendImpressionStats={true}
+ />
+ );
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-dismiss").exists());
+ });
+
+ it("should render proper hover state", () => {
+ wrapper.instance().onHover();
+ assert.ok(wrapper.find(".hovering").exists());
+ wrapper.instance().offHover();
+ assert.ok(!wrapper.find(".hovering").exists());
+ });
+
+ it("should dispatch call onDismissClick", () => {
+ wrapper.instance().onDismissClick();
+ assert.calledOnce(onDismissClickStub);
+ });
+
+ it("should add extra classes", () => {
+ wrapper = shallow(<DSDismiss extraClasses="extra-class" />);
+ assert.ok(wrapper.find(".extra-class").exists());
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx
new file mode 100644
index 0000000000..6aa8045299
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx
@@ -0,0 +1,73 @@
+import { DSEmptyState } from "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<DSEmptyState>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow(<DSEmptyState />);
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".section-empty-state").exists());
+ });
+
+ it("should render defaultempty state message", () => {
+ assert.ok(wrapper.find(".empty-state-message").exists());
+ const header = wrapper.find(
+ "h2[data-l10n-id='newtab-discovery-empty-section-topstories-header']"
+ );
+ const paragraph = wrapper.find(
+ "p[data-l10n-id='newtab-discovery-empty-section-topstories-content']"
+ );
+
+ assert.ok(header.exists());
+ assert.ok(paragraph.exists());
+ });
+
+ it("should render failed state message", () => {
+ wrapper = shallow(<DSEmptyState status="failed" />);
+ const button = wrapper.find(
+ "button[data-l10n-id='newtab-discovery-empty-section-topstories-try-again-button']"
+ );
+
+ assert.ok(button.exists());
+ });
+
+ it("should render waiting state message", () => {
+ wrapper = shallow(<DSEmptyState status="waiting" />);
+ const button = wrapper.find(
+ "button[data-l10n-id='newtab-discovery-empty-section-topstories-loading']"
+ );
+
+ assert.ok(button.exists());
+ });
+
+ it("should dispatch DISCOVERY_STREAM_RETRY_FEED on failed state button click", () => {
+ const dispatch = sinon.spy();
+
+ wrapper = shallow(
+ <DSEmptyState
+ status="failed"
+ dispatch={dispatch}
+ feed={{ url: "https://foo.com", data: {} }}
+ />
+ );
+ wrapper.find("button.try-again-button").simulate("click");
+
+ assert.calledTwice(dispatch);
+ let [action] = dispatch.firstCall.args;
+ assert.equal(action.type, "DISCOVERY_STREAM_FEED_UPDATE");
+ assert.deepEqual(action.data.feed, {
+ url: "https://foo.com",
+ data: { status: "waiting" },
+ });
+
+ [action] = dispatch.secondCall.args;
+
+ assert.equal(action.type, "DISCOVERY_STREAM_RETRY_FEED");
+ assert.deepEqual(action.data.feed, { url: "https://foo.com", data: {} });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx
new file mode 100644
index 0000000000..bb2ce3b0b3
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx
@@ -0,0 +1,146 @@
+import { DSImage } from "content-src/components/DiscoveryStreamComponents/DSImage/DSImage";
+import { mount } from "enzyme";
+import React from "react";
+
+describe("Discovery Stream <DSImage>", () => {
+ it("should have a child with class ds-image", () => {
+ const img = mount(<DSImage />);
+ const child = img.find(".ds-image");
+
+ assert.lengthOf(child, 1);
+ });
+
+ it("should set proper sources if only `source` is available", () => {
+ const img = mount(<DSImage source="https://placekitten.com/g/640/480" />);
+
+ assert.equal(
+ img.find("img").prop("src"),
+ "https://placekitten.com/g/640/480"
+ );
+ });
+
+ it("should set proper sources if `rawSource` is available", () => {
+ const testSizes = [
+ {
+ mediaMatcher: "(min-width: 1122px)",
+ width: 296,
+ height: 148,
+ },
+
+ {
+ mediaMatcher: "(min-width: 866px)",
+ width: 218,
+ height: 109,
+ },
+
+ {
+ mediaMatcher: "(max-width: 610px)",
+ width: 202,
+ height: 101,
+ },
+ ];
+
+ const img = mount(
+ <DSImage
+ rawSource="https://placekitten.com/g/640/480"
+ sizes={testSizes}
+ />
+ );
+
+ assert.equal(
+ img.find("img").prop("src"),
+ "https://placekitten.com/g/640/480"
+ );
+ assert.equal(
+ img.find("img").prop("srcSet"),
+ [
+ "https://img-getpocket.cdn.mozilla.net/296x148/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 296w",
+ "https://img-getpocket.cdn.mozilla.net/592x296/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 592w",
+ "https://img-getpocket.cdn.mozilla.net/218x109/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 218w",
+ "https://img-getpocket.cdn.mozilla.net/436x218/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 436w",
+ "https://img-getpocket.cdn.mozilla.net/202x101/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 202w",
+ "https://img-getpocket.cdn.mozilla.net/404x202/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 404w",
+ ].join(",")
+ );
+ });
+
+ it("should fall back to unoptimized when optimized failed", () => {
+ const img = mount(
+ <DSImage
+ source="https://placekitten.com/g/640/480"
+ rawSource="https://placekitten.com/g/640/480"
+ />
+ );
+ img.setState({
+ isSeen: true,
+ containerWidth: 640,
+ containerHeight: 480,
+ });
+
+ img.instance().onOptimizedImageError();
+ img.update();
+
+ assert.equal(
+ img.find("img").prop("src"),
+ "https://placekitten.com/g/640/480"
+ );
+ });
+
+ it("should render a placeholder image with no source and recent save", () => {
+ const img = mount(<DSImage isRecentSave={true} url="foo" title="bar" />);
+ img.setState({ isSeen: true });
+
+ img.update();
+
+ assert.equal(img.find("div").prop("className"), "placeholder-image");
+ });
+
+ it("should render a broken image with a source and a recent save", () => {
+ const img = mount(<DSImage isRecentSave={true} source="foo" />);
+ img.setState({ isSeen: true });
+
+ img.instance().onNonOptimizedImageError();
+ img.update();
+
+ assert.equal(img.find("div").prop("className"), "broken-image");
+ });
+
+ it("should render a broken image without a source and not a recent save", () => {
+ const img = mount(<DSImage isRecentSave={false} />);
+ img.setState({ isSeen: true });
+
+ img.instance().onNonOptimizedImageError();
+ img.update();
+
+ assert.equal(img.find("div").prop("className"), "broken-image");
+ });
+
+ it("should update loaded state when seen", () => {
+ const img = mount(
+ <DSImage rawSource="https://placekitten.com/g/640/480" />
+ );
+
+ img.instance().onLoad();
+ assert.propertyVal(img.state(), "isLoaded", true);
+ });
+
+ describe("DSImage with Idle Callback", () => {
+ let wrapper;
+ let windowStub = {
+ requestIdleCallback: sinon.stub().returns(1),
+ cancelIdleCallback: sinon.stub(),
+ };
+ beforeEach(() => {
+ wrapper = mount(<DSImage windowObj={windowStub} />);
+ });
+
+ it("should call requestIdleCallback on componentDidMount", () => {
+ assert.calledOnce(windowStub.requestIdleCallback);
+ });
+
+ it("should call cancelIdleCallback on componentWillUnmount", () => {
+ wrapper.instance().componentWillUnmount();
+ assert.calledOnce(windowStub.cancelIdleCallback);
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
new file mode 100644
index 0000000000..3aa128a32a
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx
@@ -0,0 +1,151 @@
+import { mount, shallow } from "enzyme";
+import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
+import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
+import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
+import React from "react";
+
+describe("<DSLinkMenu>", () => {
+ let wrapper;
+
+ describe("DS link menu actions", () => {
+ beforeEach(() => {
+ wrapper = mount(<DSLinkMenu />);
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ it("should parse args for fluent correctly ", () => {
+ const title = '"fluent"';
+ wrapper = mount(<DSLinkMenu title={title} />);
+
+ const button = wrapper.find(
+ "button[data-l10n-id='newtab-menu-content-tooltip']"
+ );
+ assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title }));
+ });
+ });
+
+ describe("DS context menu options", () => {
+ const ValidDSLinkMenuProps = {
+ site: {},
+ pocket_button_enabled: true,
+ };
+
+ beforeEach(() => {
+ wrapper = shallow(<DSLinkMenu {...ValidDSLinkMenuProps} />);
+ });
+
+ it("should render a context menu button", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(
+ wrapper.find(ContextMenuButton).exists(),
+ "context menu button exists"
+ );
+ });
+
+ it("should render LinkMenu when context menu button is clicked", () => {
+ let button = wrapper.find(ContextMenuButton);
+ button.simulate("click", { preventDefault: () => {} });
+ assert.equal(wrapper.find(LinkMenu).length, 1);
+ });
+
+ it("should pass dispatch, onShow, site, options, shouldSendImpressionStats, source and index to LinkMenu", () => {
+ wrapper
+ .find(ContextMenuButton)
+ .simulate("click", { preventDefault: () => {} });
+ const linkMenuProps = wrapper.find(LinkMenu).props();
+ [
+ "dispatch",
+ "onShow",
+ "site",
+ "index",
+ "options",
+ "source",
+ "shouldSendImpressionStats",
+ ].forEach(prop => assert.property(linkMenuProps, prop));
+ });
+
+ it("should pass through the correct menu options to LinkMenu", () => {
+ wrapper
+ .find(ContextMenuButton)
+ .simulate("click", { preventDefault: () => {} });
+ const linkMenuProps = wrapper.find(LinkMenu).props();
+ assert.deepEqual(linkMenuProps.options, [
+ "CheckBookmark",
+ "CheckArchiveFromPocket",
+ "CheckSavedToPocket",
+ "Separator",
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ ]);
+ });
+
+ it("should pass through the correct menu options to LinkMenu for spocs", () => {
+ wrapper = shallow(
+ <DSLinkMenu
+ {...ValidDSLinkMenuProps}
+ flightId="1234"
+ showPrivacyInfo={true}
+ />
+ );
+ wrapper
+ .find(ContextMenuButton)
+ .simulate("click", { preventDefault: () => {} });
+ const linkMenuProps = wrapper.find(LinkMenu).props();
+ assert.deepEqual(linkMenuProps.options, [
+ "CheckBookmark",
+ "CheckArchiveFromPocket",
+ "CheckSavedToPocket",
+ "Separator",
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ "ShowPrivacyInfo",
+ ]);
+ });
+
+ it("should pass through the correct menu options to LinkMenu for save to Pocket button", () => {
+ wrapper = shallow(
+ <DSLinkMenu {...ValidDSLinkMenuProps} saveToPocketCard={true} />
+ );
+ wrapper
+ .find(ContextMenuButton)
+ .simulate("click", { preventDefault: () => {} });
+ const linkMenuProps = wrapper.find(LinkMenu).props();
+ assert.deepEqual(linkMenuProps.options, [
+ "CheckBookmark",
+ "CheckArchiveFromPocket",
+ "CheckDeleteFromPocket",
+ "Separator",
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ ]);
+ });
+
+ it("should pass through the correct menu options to LinkMenu if Pocket is disabled", () => {
+ wrapper = shallow(
+ <DSLinkMenu {...ValidDSLinkMenuProps} pocket_button_enabled={false} />
+ );
+ wrapper
+ .find(ContextMenuButton)
+ .simulate("click", { preventDefault: () => {} });
+ const linkMenuProps = wrapper.find(LinkMenu).props();
+ assert.deepEqual(linkMenuProps.options, [
+ "CheckBookmark",
+ "CheckArchiveFromPocket",
+ "Separator",
+ "OpenInNewWindow",
+ "OpenInPrivateWindow",
+ "Separator",
+ "BlockUrl",
+ ]);
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx
new file mode 100644
index 0000000000..7d9f13cc8a
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx
@@ -0,0 +1,57 @@
+import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage";
+import React from "react";
+import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
+import { mount } from "enzyme";
+
+describe("<DSMessage>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(<DSMessage />);
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-message").exists());
+ });
+
+ it("should render an icon", () => {
+ wrapper.setProps({ icon: "foo" });
+
+ assert.ok(wrapper.find(".glyph").exists());
+ assert.propertyVal(
+ wrapper.find(".glyph").props().style,
+ "backgroundImage",
+ `url(foo)`
+ );
+ });
+
+ it("should render a title", () => {
+ wrapper.setProps({ title: "foo" });
+
+ assert.ok(wrapper.find(".title-text").exists());
+ assert.equal(wrapper.find(".title-text").text(), "foo");
+ });
+
+ it("should render a SafeAnchor", () => {
+ wrapper.setProps({ link_text: "foo", link_url: "https://foo.com" });
+
+ assert.equal(wrapper.find(".title").children().at(0).type(), SafeAnchor);
+ });
+
+ it("should render a FluentOrText", () => {
+ wrapper.setProps({
+ link_text: "link_text",
+ title: "title",
+ link_url: "https://link_url.com",
+ });
+
+ assert.equal(
+ wrapper.find(".title-text").children().at(0).type(),
+ FluentOrText
+ );
+
+ assert.equal(wrapper.find(".link a").children().at(0).type(), FluentOrText);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx
new file mode 100644
index 0000000000..b4b743c7ff
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx
@@ -0,0 +1,50 @@
+import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal";
+import { shallow, mount } from "enzyme";
+import { actionCreators as ac } from "common/Actions.sys.mjs";
+import React from "react";
+
+describe("Discovery Stream <DSPrivacyModal>", () => {
+ let sandbox;
+ let dispatch;
+ let wrapper;
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatch = sandbox.stub();
+ wrapper = shallow(<DSPrivacyModal dispatch={dispatch} />);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should contain a privacy notice", () => {
+ const modal = mount(<DSPrivacyModal />);
+ const child = modal.find(".privacy-notice");
+
+ assert.lengthOf(child, 1);
+ });
+
+ it("should call dispatch when modal is closed", () => {
+ wrapper.instance().closeModal();
+ assert.calledOnce(dispatch);
+ });
+
+ it("should call dispatch with the correct events for onLearnLinkClick", () => {
+ wrapper.instance().onLearnLinkClick();
+
+ assert.calledOnce(dispatch);
+ assert.calledWith(
+ dispatch,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK_PRIVACY_INFO",
+ source: "DS_PRIVACY_MODAL",
+ })
+ );
+ });
+
+ it("should call dispatch with the correct events for onManageLinkClick", () => {
+ wrapper.instance().onManageLinkClick();
+
+ assert.calledOnce(dispatch);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx
new file mode 100644
index 0000000000..904f98e439
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx
@@ -0,0 +1,92 @@
+import { DSSignup } from "content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<DSSignup>", () => {
+ let wrapper;
+ let sandbox;
+ let dispatchStub;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatchStub = sandbox.stub();
+ wrapper = shallow(
+ <DSSignup
+ data={{
+ spocs: [
+ {
+ shim: { impression: "1234" },
+ id: "1234",
+ },
+ ],
+ }}
+ type="SIGNUP"
+ dispatch={dispatchStub}
+ />
+ );
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-signup").exists());
+ });
+
+ it("should dispatch a click event on click", () => {
+ wrapper.instance().onLinkClick();
+
+ assert.calledTwice(dispatchStub);
+ assert.deepEqual(dispatchStub.firstCall.args[0].data, {
+ event: "CLICK",
+ source: "SIGNUP",
+ action_position: 0,
+ });
+ assert.deepEqual(dispatchStub.secondCall.args[0].data, {
+ source: "SIGNUP",
+ click: 0,
+ tiles: [{ id: "1234", pos: 0 }],
+ });
+ });
+
+ it("Should remove active on Menu Update", () => {
+ wrapper.setState = sandbox.stub();
+ wrapper.instance().onMenuButtonUpdate(false);
+ assert.calledWith(wrapper.setState, { active: false, lastItem: false });
+ });
+
+ it("Should add active on Menu Show", async () => {
+ wrapper.setState = sandbox.stub();
+ wrapper.instance().nextAnimationFrame = () => {};
+ await wrapper.instance().onMenuShow();
+ assert.calledWith(wrapper.setState, { active: true, lastItem: false });
+ });
+
+ it("Should add last-item to support resized window", async () => {
+ const fakeWindow = { scrollMaxX: "20" };
+ wrapper = shallow(<DSSignup windowObj={fakeWindow} />);
+ wrapper.setState = sandbox.stub();
+ wrapper.instance().nextAnimationFrame = () => {};
+ await wrapper.instance().onMenuShow();
+ assert.calledWith(wrapper.setState, { active: true, lastItem: true });
+ });
+
+ it("Should add last-item and active classes", () => {
+ wrapper.setState({
+ active: true,
+ lastItem: true,
+ });
+ assert.ok(wrapper.find(".last-item").exists());
+ assert.ok(wrapper.find(".active").exists());
+ });
+
+ it("Should call rAF from nextAnimationFrame", () => {
+ const fakeWindow = { requestAnimationFrame: sinon.stub() };
+ wrapper = shallow(<DSSignup windowObj={fakeWindow} />);
+
+ wrapper.instance().nextAnimationFrame();
+ assert.calledOnce(fakeWindow.requestAnimationFrame);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx
new file mode 100644
index 0000000000..1888e194af
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx
@@ -0,0 +1,94 @@
+import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<DSTextPromo>", () => {
+ let wrapper;
+ let sandbox;
+ let dispatchStub;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatchStub = sandbox.stub();
+ wrapper = shallow(
+ <DSTextPromo
+ data={{
+ spocs: [
+ {
+ shim: { impression: "1234" },
+ id: "1234",
+ },
+ ],
+ }}
+ type="TEXTPROMO"
+ dispatch={dispatchStub}
+ />
+ );
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-text-promo").exists());
+ });
+
+ it("should render a header", () => {
+ wrapper.setProps({ header: "foo" });
+ assert.ok(wrapper.find(".text").exists());
+ });
+
+ it("should render a subtitle", () => {
+ wrapper.setProps({ subtitle: "foo" });
+ assert.ok(wrapper.find(".subtitle").exists());
+ });
+
+ it("should dispatch a click event on click", () => {
+ wrapper.instance().onLinkClick();
+
+ assert.calledTwice(dispatchStub);
+ assert.deepEqual(dispatchStub.firstCall.args[0].data, {
+ event: "CLICK",
+ source: "TEXTPROMO",
+ action_position: 0,
+ });
+ assert.deepEqual(dispatchStub.secondCall.args[0].data, {
+ source: "TEXTPROMO",
+ click: 0,
+ tiles: [{ id: "1234", pos: 0 }],
+ });
+ });
+
+ it("should dispath telemety events on dismiss", () => {
+ wrapper.instance().onDismissClick();
+
+ const firstCall = dispatchStub.getCall(0);
+ const secondCall = dispatchStub.getCall(1);
+ const thirdCall = dispatchStub.getCall(2);
+
+ assert.equal(firstCall.args[0].type, "BLOCK_URL");
+ assert.deepEqual(firstCall.args[0].data, [
+ {
+ url: undefined,
+ pocket_id: undefined,
+ isSponsoredTopSite: undefined,
+ },
+ ]);
+
+ assert.equal(secondCall.args[0].type, "DISCOVERY_STREAM_USER_EVENT");
+ assert.deepEqual(secondCall.args[0].data, {
+ event: "BLOCK",
+ source: "TEXTPROMO",
+ action_position: 0,
+ });
+
+ assert.equal(thirdCall.args[0].type, "TELEMETRY_IMPRESSION_STATS");
+ assert.deepEqual(thirdCall.args[0].data, {
+ source: "TEXTPROMO",
+ block: 0,
+ tiles: [{ id: "1234", pos: 0 }],
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx
new file mode 100644
index 0000000000..d8c16d8e71
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx
@@ -0,0 +1,41 @@
+import { combineReducers, createStore } from "redux";
+import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
+import { Highlights } from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights";
+import { mount } from "enzyme";
+import { Provider } from "react-redux";
+import React from "react";
+
+describe("Discovery Stream <Highlights>", () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ it("should render nothing with no highlights data", () => {
+ const store = createStore(combineReducers(reducers), { ...INITIAL_STATE });
+
+ wrapper = mount(
+ <Provider store={store}>
+ <Highlights />
+ </Provider>
+ );
+
+ assert.ok(wrapper.isEmptyRender());
+ });
+
+ it("should render highlights", () => {
+ const store = createStore(combineReducers(reducers), {
+ ...INITIAL_STATE,
+ Sections: [{ id: "highlights", enabled: true }],
+ });
+
+ wrapper = mount(
+ <Provider store={store}>
+ <Highlights />
+ </Provider>
+ );
+
+ assert.lengthOf(wrapper.find(".ds-highlights"), 1);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx
new file mode 100644
index 0000000000..03538df6f2
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx
@@ -0,0 +1,16 @@
+import { HorizontalRule } from "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<HorizontalRule>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow(<HorizontalRule />);
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-hr").exists());
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
new file mode 100644
index 0000000000..1d4778e342
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
@@ -0,0 +1,278 @@
+"use strict";
+
+import {
+ ImpressionStats,
+ INTERSECTION_RATIO,
+} from "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats";
+import { actionTypes as at } from "common/Actions.sys.mjs";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<ImpressionStats>", () => {
+ const SOURCE = "TEST_SOURCE";
+ const FullIntersectEntries = [
+ { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO },
+ ];
+ const ZeroIntersectEntries = [
+ { isIntersecting: false, intersectionRatio: 0 },
+ ];
+ const PartialIntersectEntries = [
+ { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO / 2 },
+ ];
+
+ // 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() {}
+ };
+ }
+
+ const DEFAULT_PROPS = {
+ rows: [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ],
+ source: SOURCE,
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ document: {
+ visibilityState: "visible",
+ addEventListener: sinon.stub(),
+ removeEventListener: sinon.stub(),
+ },
+ };
+
+ const InnerEl = () => <div>Inner Element</div>;
+
+ function renderImpressionStats(props = {}) {
+ return shallow(
+ <ImpressionStats {...DEFAULT_PROPS} {...props}>
+ <InnerEl />
+ </ImpressionStats>
+ );
+ }
+
+ it("should render props.children", () => {
+ const wrapper = renderImpressionStats();
+ assert.ok(wrapper.contains(<InnerEl />));
+ });
+ it("should not send loaded content nor impression when the page is not visible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ };
+ renderImpressionStats(props);
+
+ assert.notCalled(dispatch);
+ });
+ it("should noly send loaded content but not impression when the wrapped item is not visbible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // This one is for loaded content.
+ assert.calledOnce(dispatch);
+ const [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+ });
+ it("should not send impression when the wrapped item is visbible but below the ratio", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(PartialIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // This one is for loaded content.
+ assert.calledOnce(dispatch);
+ });
+ it("should send a loaded content and an impression when the page is visible and the wrapped item meets the visibility ratio", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ assert.calledTwice(dispatch);
+
+ let [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+
+ [action] = dispatch.secondCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0, type: "organic" },
+ { id: 2, pos: 1, type: "organic" },
+ { id: 3, pos: 2, type: "organic" },
+ ]);
+ });
+ it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => {
+ const dispatch = sinon.spy();
+ const flightId = "a_flight_id";
+ const props = {
+ dispatch,
+ flightId,
+ rows: [{ id: 1, pos: 1, advertiser: "test advertiser" }],
+ source: "TOP_SITES",
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // Loaded content + DISCOVERY_STREAM_SPOC_IMPRESSION + TOP_SITES_SPONSORED_IMPRESSION_STATS + impression
+ assert.callCount(dispatch, 4);
+
+ const [action] = dispatch.secondCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_SPOC_IMPRESSION);
+ assert.deepEqual(action.data, { flightId });
+ });
+ it("should send a TOP_SITES_SPONSORED_IMPRESSION_STATS when the wrapped item has a flightId", () => {
+ const dispatch = sinon.spy();
+ const flightId = "a_flight_id";
+ const props = {
+ dispatch,
+ flightId,
+ rows: [{ id: 1, pos: 1, advertiser: "test advertiser" }],
+ source: "TOP_SITES",
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // Loaded content + DISCOVERY_STREAM_SPOC_IMPRESSION + TOP_SITES_SPONSORED_IMPRESSION_STATS + impression
+ assert.callCount(dispatch, 4);
+
+ const [action] = dispatch.getCall(2).args;
+ assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);
+ assert.deepEqual(action.data, {
+ type: "impression",
+ tile_id: 1,
+ source: "newtab",
+ advertiser: "test advertiser",
+ position: 1,
+ });
+ });
+ it("should send an impression when the wrapped item transiting from invisible to visible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),
+ };
+ const wrapper = renderImpressionStats(props);
+
+ // For the loaded content
+ assert.calledOnce(dispatch);
+
+ let [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+
+ dispatch.resetHistory();
+ wrapper.instance().impressionObserver.callback(FullIntersectEntries);
+
+ // For the impression
+ assert.calledOnce(dispatch);
+
+ [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0, type: "organic" },
+ { id: 2, pos: 1, type: "organic" },
+ { id: 3, pos: 2, type: "organic" },
+ ]);
+ });
+ it("should remove visibility change listener when the wrapper is removed", () => {
+ const props = {
+ dispatch: sinon.spy(),
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ IntersectionObserver,
+ };
+
+ const wrapper = renderImpressionStats(props);
+ assert.calledWith(props.document.addEventListener, "visibilitychange");
+ const [, listener] = props.document.addEventListener.firstCall.args;
+
+ wrapper.unmount();
+ assert.calledWith(
+ props.document.removeEventListener,
+ "visibilitychange",
+ listener
+ );
+ });
+ it("should unobserve the intersection observer when the wrapper is removed", () => {
+ const IntersectionObserver =
+ buildIntersectionObserver(ZeroIntersectEntries);
+ const spy = sinon.spy(IntersectionObserver.prototype, "unobserve");
+ const props = { dispatch: sinon.spy(), IntersectionObserver };
+
+ const wrapper = renderImpressionStats(props);
+ wrapper.unmount();
+
+ assert.calledOnce(spy);
+ });
+ it("should only send the latest impression on a visibility change", () => {
+ const listeners = new Set();
+ const props = {
+ dispatch: sinon.spy(),
+ document: {
+ visibilityState: "hidden",
+ addEventListener: (ev, cb) => listeners.add(cb),
+ removeEventListener: (ev, cb) => listeners.delete(cb),
+ },
+ };
+
+ const wrapper = renderImpressionStats(props);
+
+ // Update twice
+ wrapper.setProps({ ...props, ...{ rows: [{ id: 123, pos: 4 }] } });
+ wrapper.setProps({ ...props, ...{ rows: [{ id: 2432, pos: 5 }] } });
+
+ assert.notCalled(props.dispatch);
+
+ // Simulate listeners getting called
+ props.document.visibilityState = "visible";
+ listeners.forEach(l => l());
+
+ // Make sure we only sent the latest event
+ assert.calledTwice(props.dispatch);
+ const [action] = props.dispatch.firstCall.args;
+ assert.deepEqual(action.data.tiles, [{ id: 2432, pos: 5 }]);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx
new file mode 100644
index 0000000000..ef5baf50c1
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx
@@ -0,0 +1,131 @@
+import {
+ Navigation,
+ Topic,
+} from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation";
+import React from "react";
+import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText";
+import { shallow, mount } from "enzyme";
+
+const DEFAULT_PROPS = {
+ App: {
+ isForStartupCache: false,
+ },
+};
+
+describe("<Navigation>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(<Navigation header={{}} locale="en-US" />);
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ });
+
+ it("should render a title", () => {
+ wrapper.setProps({ header: { title: "Foo" } });
+
+ assert.equal(wrapper.find(".ds-navigation-header").text(), "Foo");
+ });
+
+ it("should not render a title", () => {
+ wrapper.setProps({ header: null });
+
+ assert.lengthOf(wrapper.find(".ds-navigation-header"), 0);
+ });
+
+ it("should set default alignment", () => {
+ assert.lengthOf(wrapper.find(".ds-navigation-centered"), 1);
+ });
+
+ it("should set custom alignment", () => {
+ wrapper.setProps({ alignment: "left-align" });
+
+ assert.lengthOf(wrapper.find(".ds-navigation-left-align"), 1);
+ });
+
+ it("should set default of no links", () => {
+ assert.lengthOf(wrapper.find("ul").children(), 0);
+ });
+
+ it("should render a FluentOrText", () => {
+ wrapper.setProps({ header: { title: "Foo" } });
+
+ assert.equal(
+ wrapper.find(".ds-navigation").children().at(0).type(),
+ FluentOrText
+ );
+ });
+
+ it("should render 2 Topics", () => {
+ wrapper.setProps({
+ links: [
+ { url: "https://foo.com", name: "foo" },
+ { url: "https://bar.com", name: "bar" },
+ ],
+ });
+
+ assert.lengthOf(wrapper.find("ul").children(), 2);
+ });
+
+ it("should render 2 extra Topics", () => {
+ wrapper.setProps({
+ newFooterSection: true,
+ links: [
+ { url: "https://foo.com", name: "foo" },
+ { url: "https://bar.com", name: "bar" },
+ ],
+ extraLinks: [
+ { url: "https://foo.com", name: "foo" },
+ { url: "https://bar.com", name: "bar" },
+ ],
+ });
+
+ assert.lengthOf(wrapper.find("ul").children(), 4);
+ });
+});
+
+describe("<Topic>", () => {
+ let wrapper;
+ let sandbox;
+
+ beforeEach(() => {
+ wrapper = shallow(<Topic url="https://foo.com" name="foo" />);
+ sandbox = sinon.createSandbox();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should pass onLinkClick prop", () => {
+ assert.propertyVal(
+ wrapper.at(0).props(),
+ "onLinkClick",
+ wrapper.instance().onLinkClick
+ );
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.equal(wrapper.type(), SafeAnchor);
+ });
+
+ describe("onLinkClick", () => {
+ let dispatch;
+
+ beforeEach(() => {
+ dispatch = sandbox.stub();
+ wrapper = shallow(<Topic dispatch={dispatch} {...DEFAULT_PROPS} />);
+ wrapper.setState({ isSeen: true });
+ });
+
+ it("should call dispatch", () => {
+ wrapper.instance().onLinkClick({ target: { text: `Must Reads` } });
+
+ assert.calledOnce(dispatch);
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx
new file mode 100644
index 0000000000..285cc16c0e
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx
@@ -0,0 +1,29 @@
+import { PrivacyLink } from "content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<PrivacyLink>", () => {
+ let wrapper;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ wrapper = shallow(
+ <PrivacyLink
+ properties={{
+ url: "url",
+ title: "Privacy Link",
+ }}
+ />
+ );
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-privacy-link").exists());
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx
new file mode 100644
index 0000000000..5d643869b8
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx
@@ -0,0 +1,56 @@
+import React from "react";
+import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import { shallow } from "enzyme";
+
+describe("Discovery Stream <SafeAnchor>", () => {
+ let warnStub;
+ let sandbox;
+ beforeEach(() => {
+ warnStub = sinon.stub(console, "warn");
+ sandbox = sinon.createSandbox();
+ });
+ afterEach(() => {
+ warnStub.restore();
+ sandbox.restore();
+ });
+ it("should render with anchor", () => {
+ const wrapper = shallow(<SafeAnchor />);
+ assert.lengthOf(wrapper.find("a"), 1);
+ });
+ it("should render with anchor target for http", () => {
+ const wrapper = shallow(<SafeAnchor url="http://example.com" />);
+ assert.equal(wrapper.find("a").prop("href"), "http://example.com");
+ });
+ it("should render with anchor target for https", () => {
+ const wrapper = shallow(<SafeAnchor url="https://example.com" />);
+ assert.equal(wrapper.find("a").prop("href"), "https://example.com");
+ });
+ it("should not allow javascript: URIs", () => {
+ const wrapper = shallow(<SafeAnchor url="javascript:foo()" />); // eslint-disable-line no-script-url
+ assert.equal(wrapper.find("a").prop("href"), "");
+ assert.calledOnce(warnStub);
+ });
+ it("should not warn if the URL is falsey ", () => {
+ const wrapper = shallow(<SafeAnchor url="" />);
+ assert.equal(wrapper.find("a").prop("href"), "");
+ assert.notCalled(warnStub);
+ });
+ it("should dispatch an event on click", () => {
+ const dispatchStub = sandbox.stub();
+ const fakeEvent = { preventDefault: sandbox.stub(), currentTarget: {} };
+ const wrapper = shallow(<SafeAnchor dispatch={dispatchStub} />);
+
+ wrapper.find("a").simulate("click", fakeEvent);
+
+ assert.calledOnce(dispatchStub);
+ assert.calledOnce(fakeEvent.preventDefault);
+ });
+ it("should call onLinkClick if provided", () => {
+ const onLinkClickStub = sandbox.stub();
+ const wrapper = shallow(<SafeAnchor onLinkClick={onLinkClickStub} />);
+
+ wrapper.find("a").simulate("click");
+
+ assert.calledOnce(onLinkClickStub);
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx
new file mode 100644
index 0000000000..b5ea007022
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { SectionTitle } from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle";
+import { shallow } from "enzyme";
+
+describe("<SectionTitle>", () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow(<SectionTitle header={{}} />);
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-section-title").exists());
+ });
+
+ it("should render a subtitle", () => {
+ wrapper.setProps({ header: { title: "Foo", subtitle: "Bar" } });
+
+ assert.equal(wrapper.find(".subtitle").text(), "Bar");
+ });
+});
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx
new file mode 100644
index 0000000000..f879600a8f
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx
@@ -0,0 +1,238 @@
+import { combineReducers, createStore } from "redux";
+import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
+import { Provider } from "react-redux";
+import {
+ _TopicsWidget as TopicsWidgetBase,
+ TopicsWidget,
+} from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
+import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
+import {
+ actionCreators as ac,
+ actionTypes as at,
+} from "common/Actions.sys.mjs";
+import { mount } from "enzyme";
+import React from "react";
+
+describe("Discovery Stream <TopicsWidget>", () => {
+ let sandbox;
+ let wrapper;
+ let dispatch;
+ let fakeWindow;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ dispatch = sandbox.stub();
+ fakeWindow = {
+ innerWidth: 1000,
+ innerHeight: 900,
+ };
+
+ wrapper = mount(
+ <TopicsWidgetBase
+ dispatch={dispatch}
+ source="CARDGRID_WIDGET"
+ position={2}
+ id={1}
+ windowObj={fakeWindow}
+ DiscoveryStream={{
+ experimentData: {
+ utmCampaign: "utmCampaign",
+ utmContent: "utmContent",
+ utmSource: "utmSource",
+ },
+ }}
+ />
+ );
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render", () => {
+ assert.ok(wrapper.exists());
+ assert.ok(wrapper.find(".ds-topics-widget").exists());
+ });
+
+ it("should connect with DiscoveryStream store", () => {
+ let store = createStore(combineReducers(reducers), INITIAL_STATE);
+ wrapper = mount(
+ <Provider store={store}>
+ <TopicsWidget />
+ </Provider>
+ );
+
+ const topicsWidget = wrapper.find(TopicsWidgetBase);
+ assert.ok(topicsWidget.exists());
+ assert.lengthOf(topicsWidget, 1);
+ assert.deepEqual(
+ topicsWidget.props().DiscoveryStream.experimentData,
+ INITIAL_STATE.DiscoveryStream.experimentData
+ );
+ });
+
+ describe("dispatch", () => {
+ it("should dispatch loaded event", () => {
+ assert.callCount(dispatch, 1);
+ const [first] = dispatch.getCalls();
+ assert.calledWith(
+ first,
+ ac.DiscoveryStreamLoadedContent({
+ source: "CARDGRID_WIDGET",
+ tiles: [
+ {
+ id: 1,
+ pos: 2,
+ },
+ ],
+ })
+ );
+ });
+
+ it("should dispatch click event for technology", () => {
+ // Click technology topic.
+ wrapper.find(SafeAnchor).at(0).simulate("click");
+
+ // First call is DiscoveryStreamLoadedContent, which is already tested.
+ const [second, third, fourth] = dispatch.getCalls().slice(1, 4);
+
+ assert.callCount(dispatch, 4);
+ assert.calledWith(
+ second,
+ ac.OnlyToMain({
+ type: at.OPEN_LINK,
+ data: {
+ event: {
+ altKey: undefined,
+ button: undefined,
+ ctrlKey: undefined,
+ metaKey: undefined,
+ shiftKey: undefined,
+ },
+ referrer: "https://getpocket.com/recommendations",
+ url: "https://getpocket.com/explore/technology?utm_source=utmSource&utm_content=utmContent&utm_campaign=utmCampaign",
+ },
+ })
+ );
+ assert.calledWith(
+ third,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "CARDGRID_WIDGET",
+ action_position: 2,
+ value: {
+ card_type: "topics_widget",
+ topic: "technology",
+ position_in_card: 0,
+ },
+ })
+ );
+ assert.calledWith(
+ fourth,
+ ac.ImpressionStats({
+ click: 0,
+ source: "CARDGRID_WIDGET",
+ tiles: [{ id: 1, pos: 2 }],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+
+ it("should dispatch click event for must reads", () => {
+ // Click must reads topic.
+ wrapper.find(SafeAnchor).at(8).simulate("click");
+
+ // First call is DiscoveryStreamLoadedContent, which is already tested.
+ const [second, third, fourth] = dispatch.getCalls().slice(1, 4);
+
+ assert.callCount(dispatch, 4);
+ assert.calledWith(
+ second,
+ ac.OnlyToMain({
+ type: at.OPEN_LINK,
+ data: {
+ event: {
+ altKey: undefined,
+ button: undefined,
+ ctrlKey: undefined,
+ metaKey: undefined,
+ shiftKey: undefined,
+ },
+ referrer: "https://getpocket.com/recommendations",
+ url: "https://getpocket.com/collections?utm_source=utmSource&utm_content=utmContent&utm_campaign=utmCampaign",
+ },
+ })
+ );
+ assert.calledWith(
+ third,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "CARDGRID_WIDGET",
+ action_position: 2,
+ value: {
+ card_type: "topics_widget",
+ topic: "must-reads",
+ position_in_card: 8,
+ },
+ })
+ );
+ assert.calledWith(
+ fourth,
+ ac.ImpressionStats({
+ click: 0,
+ source: "CARDGRID_WIDGET",
+ tiles: [{ id: 1, pos: 2 }],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+
+ it("should dispatch click event for more topics", () => {
+ // Click more-topics.
+ wrapper.find(SafeAnchor).at(9).simulate("click");
+
+ // First call is DiscoveryStreamLoadedContent, which is already tested.
+ const [second, third, fourth] = dispatch.getCalls().slice(1, 4);
+
+ assert.callCount(dispatch, 4);
+ assert.calledWith(
+ second,
+ ac.OnlyToMain({
+ type: at.OPEN_LINK,
+ data: {
+ event: {
+ altKey: undefined,
+ button: undefined,
+ ctrlKey: undefined,
+ metaKey: undefined,
+ shiftKey: undefined,
+ },
+ referrer: "https://getpocket.com/recommendations",
+ url: "https://getpocket.com/?utm_source=utmSource&utm_content=utmContent&utm_campaign=utmCampaign",
+ },
+ })
+ );
+ assert.calledWith(
+ third,
+ ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "CARDGRID_WIDGET",
+ action_position: 2,
+ value: { card_type: "topics_widget", topic: "more-topics" },
+ })
+ );
+ assert.calledWith(
+ fourth,
+ ac.ImpressionStats({
+ click: 0,
+ source: "CARDGRID_WIDGET",
+ tiles: [{ id: 1, pos: 2 }],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ })
+ );
+ });
+ });
+});