Source code

Revision control

Copy as Markdown

Other Tools

/* 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",
},
{ 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,
},
};
function WrapWithProvider({ children, state = INITIAL_STATE }) {
const store = createStore(combineReducers(reducers), state);
return <Provider store={store}>{children}</Provider>;
}
describe("<CardSections />", () => {
let dispatch;
beforeEach(() => {
dispatch = jest.fn();
});
afterEach(() => {
jest.useRealTimers();
});
it("should render section wrapper when data is provided", () => {
const { container } = render(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
expect(container.querySelector(".ds-section-wrapper")).toBeInTheDocument();
});
it("should render null when data is null", () => {
const { container } = render(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} data={null} />
</WrapWithProvider>
);
expect(
container.querySelector(".ds-section-wrapper")
).not.toBeInTheDocument();
});
it("should render DSEmptyState if sections are falsey", () => {
const { container } = render(
<WrapWithProvider>
<CardSections
{...DEFAULT_PROPS}
data={{ ...DEFAULT_PROPS.data, sections: [] }}
/>
</WrapWithProvider>
);
expect(container.querySelector(".ds-card-grid.empty")).toBeInTheDocument();
});
it("should render sections and DSCard components for valid data", () => {
const { container } = render(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
expect(container.querySelector(".ds-section")).toBeInTheDocument();
const { container: container2 } = render(
<WrapWithProvider>
<CardSections
{...DEFAULT_PROPS}
data={{
...DEFAULT_PROPS.data,
sections: [{ ...DEFAULT_PROPS.data.sections[0], data: [] }],
}}
/>
</WrapWithProvider>
);
expect(container2.querySelector(".ds-section")).not.toBeInTheDocument();
});
it("should render a placeholder", () => {
const { container } = render(
<WrapWithProvider>
<CardSections
{...DEFAULT_PROPS}
data={{
...DEFAULT_PROPS.data,
sections: [
{
...DEFAULT_PROPS.data.sections[0],
data: [{ placeholder: true }],
},
],
}}
/>
</WrapWithProvider>
);
expect(container.querySelector(".ds-card.placeholder")).toBeInTheDocument();
});
it("should pass correct props to DSCard", () => {
const { container } = render(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
expect(container.querySelector("a.ds-card-link")).toHaveAttribute(
"href",
);
expect(container.querySelector(".ds-card .title")).toHaveTextContent(
"Card 1"
);
});
it("should apply correct classNames and position from layout data", () => {
const { container } = render(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider state={state}>
<CardSections
dispatch={dispatch}
{...DEFAULT_PROPS}
data={{
...DEFAULT_PROPS.data,
sections: [
{
data: [
{
title: "Card 1",
image_src: "image1.jpg",
},
],
receivedRank: 0,
sectionKey: "section_key_1",
title: "title",
followable: true,
layout,
},
{
data: [
{
title: "Card 2",
image_src: "image2.jpg",
},
],
receivedRank: 0,
sectionKey: "section_key_2",
title: "title",
followable: true,
layout,
},
],
}}
/>
</WrapWithProvider>
);
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 <FollowSectionButtonHighlight> 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(
<WrapWithProvider state={state}>
<CardSections
dispatch={dispatch}
{...DEFAULT_PROPS}
data={{
...DEFAULT_PROPS.data,
sections: [
{
data: [
{
title: "Card 1",
image_src: "image1.jpg",
},
],
receivedRank: 0,
sectionKey: "section_key_1",
title: "title",
followable: true,
layout,
},
{
data: [
{
title: "Card 2",
image_src: "image2.jpg",
},
],
receivedRank: 0,
sectionKey: "section_key_2",
title: "title",
followable: true,
layout,
},
],
}}
/>
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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(
<WrapWithProvider>
<CardSections dispatch={dispatch} {...DEFAULT_PROPS} />
</WrapWithProvider>
);
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,
title: "Headline 1",
publisher: "Publisher 1",
},
{
id: "h2",
section: "daily_brief_section",
isHeadline: true,
title: "Headline 2",
publisher: "Publisher 2",
},
{
id: "h3",
section: "daily_brief_section",
isHeadline: true,
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: {
{
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(
<WrapWithProvider state={state}>
<CardSections dispatch={dispatch} {...createBriefingSectionProps()} />
</WrapWithProvider>
);
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[
].data.recommendations = MOCK_HEADLINES.slice(0, 2);
const { container } = render(
<WrapWithProvider state={state}>
<CardSections dispatch={dispatch} {...createBriefingSectionProps()} />
</WrapWithProvider>
);
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(
<WrapWithProvider state={state}>
<CardSections
dispatch={dispatch}
{...createBriefingSectionProps({ sectionKey: "other-section" })}
/>
</WrapWithProvider>
);
expect(container.querySelector(".briefing-card")).not.toBeInTheDocument();
});
});
});