/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { render, fireEvent } from "@testing-library/react"; import { Provider } from "react-redux"; import { actionTypes as at, CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE, } from "common/Actions.mjs"; import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; import { CardSections } from "content-src/components/DiscoveryStreamComponents/CardSections/CardSections"; import { combineReducers, createStore } from "redux"; const PREF_SECTIONS_PERSONALIZATION_ENABLED = "discoverystream.sections.personalization.enabled"; const DEFAULT_PROPS = { type: "CardGrid", firstVisibleTimeStamp: null, ctaButtonSponsors: [""], anySectionsFollowed: false, data: { sections: [ { data: [ { id: "card-1", title: "Card 1", image_src: "image1.jpg", url: "https://example.com", }, { id: "card-2" }, { id: "card-3" }, { id: "card-4" }, ], receivedRank: 0, sectionKey: "section_key", title: "title", layout: { title: "layout_name", responsiveLayouts: [ { columnCount: 1, tiles: [ { size: "large", position: 0, hasAd: false, hasExcerpt: true }, { size: "small", position: 2, hasAd: false, hasExcerpt: false }, { size: "medium", position: 1, hasAd: true, hasExcerpt: true }, { size: "small", position: 3, hasAd: false, hasExcerpt: false }, ], }, ], }, }, ], }, feed: { embed_reference: null, url: "https://merino.services.mozilla.com/api/v1/curated-recommendations", }, }; function WrapWithProvider({ children, state = INITIAL_STATE }) { const store = createStore(combineReducers(reducers), state); return {children}; } describe("", () => { let dispatch; beforeEach(() => { dispatch = jest.fn(); }); afterEach(() => { jest.useRealTimers(); }); it("should render section wrapper when data is provided", () => { const { container } = render( ); expect(container.querySelector(".ds-section-wrapper")).toBeInTheDocument(); }); it("should render null when data is null", () => { const { container } = render( ); expect( container.querySelector(".ds-section-wrapper") ).not.toBeInTheDocument(); }); it("should render DSEmptyState if sections are falsey", () => { const { container } = render( ); expect(container.querySelector(".ds-card-grid.empty")).toBeInTheDocument(); }); it("should render sections and DSCard components for valid data", () => { const { container } = render( ); const { sections } = DEFAULT_PROPS.data; expect(container.querySelectorAll("section")).toHaveLength(sections.length); expect(container.querySelectorAll("article.ds-card")).toHaveLength(4); expect(container.querySelector(".section-title")).toHaveTextContent( "title" ); }); it("should skip a section with no items available for that section", () => { const { container } = render( ); expect(container.querySelector(".ds-section")).toBeInTheDocument(); const { container: container2 } = render( ); expect(container2.querySelector(".ds-section")).not.toBeInTheDocument(); }); it("should render a placeholder", () => { const { container } = render( ); expect(container.querySelector(".ds-card.placeholder")).toBeInTheDocument(); }); it("should pass correct props to DSCard", () => { const { container } = render( ); expect(container.querySelector("a.ds-card-link")).toHaveAttribute( "href", "https://example.com" ); expect(container.querySelector(".ds-card .title")).toHaveTextContent( "Card 1" ); }); it("should apply correct classNames and position from layout data", () => { const { container } = render( ); const cards = container.querySelectorAll("article.ds-card"); expect(cards[0]).toHaveClass( "col-1-large", "col-1-position-0", "col-1-show-excerpt" ); expect(cards[2]).toHaveClass( "col-1-small", "col-1-position-1", "col-1-hide-excerpt" ); }); it("should apply correct class names for cards with and without excerpts", () => { const { container } = render( ); container.querySelectorAll("article.ds-card").forEach(card => { const classNames = card.className; if (classNames.includes("small") || classNames.includes("medium")) { expect(classNames).toContain("hide-excerpt"); expect(classNames).not.toContain("show-excerpt"); } else { expect(classNames).toContain("show-excerpt"); expect(classNames).not.toContain("hide-excerpt"); } }); }); it("should dispatch SECTION_PERSONALIZATION_UPDATE updates with follow and unfollow", () => { const fakeDate = "2020-01-01T00:00:00.000Z"; jest.useFakeTimers({ now: new Date(fakeDate) }); const layout = { title: "layout_name", responsiveLayouts: [ { columnCount: 1, tiles: [ { size: "large", position: 0, hasAd: false, hasExcerpt: true }, { size: "small", position: 2, hasAd: false, hasExcerpt: false }, { size: "medium", position: 1, hasAd: true, hasExcerpt: true }, { size: "small", position: 3, hasAd: false, hasExcerpt: false }, ], }, ], }; const state = { ...INITIAL_STATE, DiscoveryStream: { ...INITIAL_STATE.DiscoveryStream, sectionPersonalization: { section_key_2: { isFollowed: true, isBlocked: false }, }, }, Prefs: { ...INITIAL_STATE.Prefs, values: { ...INITIAL_STATE.Prefs.values, [PREF_SECTIONS_PERSONALIZATION_ENABLED]: true, }, }, }; const { container } = render( ); fireEvent.click(container.querySelector(".section-follow moz-button")); expect(dispatch).toHaveBeenNthCalledWith(1, { type: at.SECTION_PERSONALIZATION_SET, data: { section_key_2: { isFollowed: true, isBlocked: false }, section_key_1: { isFollowed: true, isBlocked: false, followedAt: fakeDate, }, }, meta: { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE }, }); expect(dispatch).toHaveBeenNthCalledWith(2, { type: at.FOLLOW_SECTION, data: { section: "section_key_1", section_position: 0, event_source: "MOZ_BUTTON", }, meta: { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE, skipLocal: true, }, }); fireEvent.click( container.querySelector(".section-follow.following moz-button") ); expect(dispatch).toHaveBeenNthCalledWith(3, { type: at.SECTION_PERSONALIZATION_SET, data: {}, meta: { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE }, }); expect(dispatch).toHaveBeenNthCalledWith(4, { type: at.UNFOLLOW_SECTION, data: { section: "section_key_2", section_position: 1, event_source: "MOZ_BUTTON", }, meta: { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE, skipLocal: true, }, }); }); it("should render when conditions match", () => { const state = { ...INITIAL_STATE, DiscoveryStream: { ...INITIAL_STATE.DiscoveryStream, sectionPersonalization: {}, }, Prefs: { ...INITIAL_STATE.Prefs, values: { ...INITIAL_STATE.Prefs.values, [PREF_SECTIONS_PERSONALIZATION_ENABLED]: true, }, }, Messages: { isVisible: true, messageData: { content: { messageType: "FollowSectionButtonHighlight" }, }, }, }; const layout = { title: "layout_name", responsiveLayouts: [ { columnCount: 1, tiles: [{ size: "large", position: 0, hasExcerpt: true }], }, ], }; const { container } = render( ); expect( container.querySelectorAll(".follow-section-button-highlight") ).toHaveLength(1); }); describe("Keyboard navigation", () => { beforeEach(() => { Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 500, }); }); it("should pass tabIndex={0} to the first card and tabIndex={-1} to other cards", () => { const { container } = render( ); const links = container.querySelectorAll("a.ds-card-link"); expect(links[0]).toHaveAttribute("tabindex", "0"); expect(links[1]).toHaveAttribute("tabindex", "-1"); expect(links[2]).toHaveAttribute("tabindex", "-1"); }); it("should update focused index when onFocus is called", () => { const { container } = render( ); const links = container.querySelectorAll("a.ds-card-link"); fireEvent.focus(links[1]); expect(links[1]).toHaveAttribute("tabindex", "0"); expect(links[0]).toHaveAttribute("tabindex", "-1"); }); describe("handleCardKeyDown", () => { it("should navigate to next card with ArrowRight", () => { const { container } = render( ); const firstCardLink = container.querySelector( "article.ds-card.col-1-position-0 a.ds-card-link" ); const nextCardLink = container.querySelector( "article.ds-card.col-1-position-1 a.ds-card-link" ); const focusSpy = jest.spyOn(nextCardLink, "focus"); fireEvent.keyDown(firstCardLink, { key: "ArrowRight" }); expect(focusSpy).toHaveBeenCalled(); }); it("should navigate to previous card with ArrowLeft", () => { const { container } = render( ); const secondCardLink = container.querySelector( "article.ds-card.col-1-position-1 a.ds-card-link" ); const firstCardLink = container.querySelector( "article.ds-card.col-1-position-0 a.ds-card-link" ); const focusSpy = jest.spyOn(firstCardLink, "focus"); fireEvent.keyDown(secondCardLink, { key: "ArrowLeft" }); expect(focusSpy).toHaveBeenCalled(); }); }); }); describe("Daily Briefing v2 BriefingCard", () => { const MOCK_HEADLINES = [ { id: "h1", section: "daily_brief_section", isHeadline: true, url: "https://example.com/1", title: "Headline 1", publisher: "Publisher 1", }, { id: "h2", section: "daily_brief_section", isHeadline: true, url: "https://example.com/2", title: "Headline 2", publisher: "Publisher 2", }, { id: "h3", section: "daily_brief_section", isHeadline: true, url: "https://example.com/3", title: "Headline 3", publisher: "Publisher 3", }, ]; const createBriefingSectionProps = ({ sectionKey = "daily_brief_section", allowsWidget = true, } = {}) => ({ ...DEFAULT_PROPS, data: { sections: [ { ...DEFAULT_PROPS.data.sections[0], sectionKey, layout: { responsiveLayouts: [ { columnCount: 1, tiles: [{ position: 0, size: "medium", allowsWidget }], }, ], }, }, ], }, }); let state; beforeEach(() => { state = { ...INITIAL_STATE, DiscoveryStream: { ...INITIAL_STATE.DiscoveryStream, feeds: { data: { "https://merino.services.mozilla.com/api/v1/curated-recommendations": { data: { recommendations: [ ...MOCK_HEADLINES, { id: "r1", isHeadline: false }, ], }, lastUpdated: Date.now(), }, }, }, }, Prefs: { ...INITIAL_STATE.Prefs, values: { ...INITIAL_STATE.Prefs.values, "discoverystream.dailyBrief.enabled": true, "discoverystream.dailyBrief.sectionId": "daily_brief_section", }, }, }; }); it("should render BriefingCard when all conditions met", () => { const { container } = render( ); expect(container.querySelector(".briefing-card")).toBeInTheDocument(); expect( container.querySelectorAll(".briefing-card-headline") ).toHaveLength(3); }); it("should not render BriefingCard when fewer than 3 headlines available", () => { state.DiscoveryStream.feeds.data[ "https://merino.services.mozilla.com/api/v1/curated-recommendations" ].data.recommendations = MOCK_HEADLINES.slice(0, 2); const { container } = render( ); expect(container.querySelector(".briefing-card")).not.toBeInTheDocument(); expect(container.querySelector("article.ds-card")).toBeInTheDocument(); }); it("should not render BriefingCard when section key doesn't match", () => { const { container } = render( ); expect(container.querySelector(".briefing-card")).not.toBeInTheDocument(); }); }); });