Files
community-rule/tests/unit/CardStack.test.jsx
T

237 lines
7.0 KiB
React

import {
renderWithProviders as render,
screen,
cleanup,
fireEvent,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest";
import CardStack from "../../app/components/cards/CardStack";
const SAMPLE_CARDS = [
{
id: "1",
label: "Option A",
supportText: "Description A",
recommended: true,
},
{
id: "2",
label: "Option B",
supportText: "Description B",
recommended: false,
},
{
id: "3",
label: "Option C",
supportText: "Description C",
recommended: true,
},
];
afterEach(() => {
cleanup();
});
describe("CardStack Component", () => {
test("renders header when title is provided", () => {
render(
<CardStack
cards={SAMPLE_CARDS}
title="How should this community communicate?"
description="Pick one or more."
/>,
);
expect(
screen.getByText("How should this community communicate?"),
).toBeInTheDocument();
expect(screen.getByText("Pick one or more.")).toBeInTheDocument();
});
test("renders up to 5 recommended cards in compact (grid) mode", () => {
render(<CardStack cards={SAMPLE_CARDS} expanded={false} />);
expect(screen.getAllByText("Option A").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Option C").length).toBeGreaterThanOrEqual(1);
expect(screen.queryByText("Option B")).not.toBeInTheDocument();
});
test("renders all cards in expanded (list) mode", () => {
render(<CardStack cards={SAMPLE_CARDS} expanded={true} />);
expect(screen.getAllByText("Option A").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Option B").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Option C").length).toBeGreaterThanOrEqual(1);
});
test("hides helper icons on compact, expanded, and single-stack cards", () => {
const { unmount } = render(
<CardStack cards={SAMPLE_CARDS} expanded={false} />,
);
expect(screen.queryByText("?")).not.toBeInTheDocument();
unmount();
const expanded = render(<CardStack cards={SAMPLE_CARDS} expanded={true} />);
expect(screen.queryByText("?")).not.toBeInTheDocument();
expanded.unmount();
render(<CardStack cards={SAMPLE_CARDS} layout="singleStack" />);
expect(screen.queryByText("?")).not.toBeInTheDocument();
});
test("expanded tiles use the compact 142px CardSelection height", () => {
render(<CardStack cards={SAMPLE_CARDS} expanded={true} />);
const tiles = screen.getAllByRole("button", {
name: "Option A: Description A",
});
expect(tiles.length).toBeGreaterThan(0);
for (const tile of tiles) {
expect(tile.className).toMatch(/142px/);
expect(tile).toHaveClass("flex-col");
expect(tile).not.toHaveClass("flex-row");
}
});
test("shows See all toggle when hasMore is true", () => {
render(<CardStack cards={SAMPLE_CARDS} hasMore={true} />);
expect(
screen.getByRole("button", { name: "See all communication approaches" }),
).toBeInTheDocument();
});
test("does not show toggle when hasMore is false", () => {
render(<CardStack cards={SAMPLE_CARDS} hasMore={false} />);
expect(
screen.queryByRole("button", {
name: "See all communication approaches",
}),
).not.toBeInTheDocument();
});
test("toggle expands when clicked", async () => {
const user = userEvent.setup();
render(<CardStack cards={SAMPLE_CARDS} hasMore={true} />);
const toggle = screen.getByRole("button", {
name: "See all communication approaches",
});
await user.click(toggle);
expect(
screen.getByRole("button", { name: "Show less" }),
).toBeInTheDocument();
});
test("single-stack See all / Show less is centered, not end-aligned", () => {
render(
<CardStack cards={SAMPLE_CARDS} hasMore={true} layout="singleStack" />,
);
const toggle = screen.getByRole("button", {
name: "See all communication approaches",
});
expect(toggle.className).toMatch(/\bself-center\b/);
expect(toggle.className).not.toMatch(/\bself-end\b/);
});
test("calls onCardSelect when a card is clicked", () => {
const onCardSelect = vi.fn();
render(<CardStack cards={SAMPLE_CARDS} onCardSelect={onCardSelect} />);
const cardButtons = screen.getAllByRole("button", {
name: "Option A: Description A",
});
fireEvent.click(cardButtons[0]);
expect(onCardSelect).toHaveBeenCalledWith("1");
});
test("renders with selectedId", () => {
render(<CardStack cards={SAMPLE_CARDS} selectedId="1" />);
expect(screen.getAllByText("SELECTED").length).toBeGreaterThanOrEqual(1);
});
test("compact single-stack shows seven recommended cards and hides the tag", () => {
const cards = Array.from({ length: 8 }, (_, index) => ({
id: String(index),
label: `Card ${index}`,
supportText: `Desc ${index}`,
recommended: true,
}));
render(
<CardStack cards={cards} layout="singleStack" expanded={false} />,
);
expect(
screen.getAllByRole("button", { name: /Card \d: Desc \d/ }),
).toHaveLength(7);
expect(screen.queryByText("RECOMMENDED")).not.toBeInTheDocument();
});
test("See all search filters the expanded single-stack", () => {
render(
<CardStack
cards={SAMPLE_CARDS}
layout="singleStack"
expanded={true}
searchPlaceholder="Search this deck"
/>,
);
const search = screen.getByRole("searchbox", { name: "Search this deck" });
fireEvent.change(search, { target: { value: "Option B" } });
expect(
screen.getByRole("button", { name: "Option B: Description B" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Option A: Description A" }),
).not.toBeInTheDocument();
});
test("compact alternative row opens the named card", () => {
const onAlternativeSelect = vi.fn();
render(
<CardStack
cards={[
{
id: "1",
label: "Option A",
supportText: "Description A",
recommended: true,
alternative: { id: "2", label: "Option B" },
},
]}
layout="singleStack"
alternativePrefix="Another way communities do this:"
onAlternativeSelect={onAlternativeSelect}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Option B" }));
expect(onAlternativeSelect).toHaveBeenCalledWith("2");
});
test("calls onCardSelect when re-clicking an already selected card", () => {
const onCardSelect = vi.fn();
render(
<CardStack
cards={SAMPLE_CARDS}
selectedIds={["1"]}
onCardSelect={onCardSelect}
title="Pick an option"
/>,
);
const cardButtons = screen.getAllByRole("button", {
name: "Option A: Description A",
});
fireEvent.click(cardButtons[0]);
expect(onCardSelect).toHaveBeenCalledWith("1");
expect(screen.queryByRole("button", { name: "Remove policy" })).not.toBeInTheDocument();
});
});