import "@testing-library/jest-dom";
import React from "react";
import { Callout } from "./index";
import { CalloutProps } from "./types";
import { render, screen } from "@testing-library/react";
// Mock child card components
jest.mock("../cards/simple-card", () => ({
__esModule: true,
default: ({ card }: any) => (
{card?.title || "Simple Card"}
),
}));
jest.mock("../cards/blog-card", () => ({
__esModule: true,
default: ({ title }: any) => (
{title || "Blog Card"}
),
}));
jest.mock("../cards/full-image-card", () => ({
__esModule: true,
default: ({ card }: any) => (
{card?.title || "Full Image Card"}
),
}));
jest.mock("../cards/floating-image-card", () => ({
__esModule: true,
default: ({ card }: any) => (
{card?.title || "Floating Image Card"}
),
}));
jest.mock("../button", () => ({
Button: (props: any) => (
),
}));
jest.mock("@shared/components/animation-wrapper", () => ({
AnimationWrapper: ({ children }: any) => <>{children}>,
}));
jest.mock("@shared/components/text", () => ({
Text: ({ children, as, ...props }: any) => {
const Tag = as || "span";
return {children};
},
}));
jest.mock("@shared/utils", () => ({
cx: (...args: any[]) => args.filter(Boolean).join(" "),
}));
describe("Callout", () => {
const defaultProps: CalloutProps = {
items: [
{ title: "Card 1", description: "Desc 1" },
{ title: "Card 2", description: "Desc 2" },
],
};
it("renders the section element", () => {
const { container } = render();
const section = container.querySelector("section");
expect(section).toBeInTheDocument();
});
it("renders with anchorId", () => {
const { container } = render(
);
const section = container.querySelector("section#test-anchor");
expect(section).toBeInTheDocument();
});
it("renders title as h2 by default", () => {
render();
const heading = screen.getByText("Test Title");
expect(heading.tagName).toBe("H2");
});
it("renders title as h1 when enableHeading is true", () => {
render(
);
const heading = screen.getByText("Main Title");
expect(heading.tagName).toBe("H1");
});
it("renders subtitle", () => {
render();
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
});
it("renders subtitle as h2 when enableHeading is true", () => {
render(
);
const subtitle = screen.getByText("Sub Title");
expect(subtitle.tagName).toBe("H2");
});
it("renders subtitle as h3 by default", () => {
render();
const subtitle = screen.getByText("Sub H3");
expect(subtitle.tagName).toBe("H3");
});
it("renders description", () => {
render();
expect(screen.getByText("A description")).toBeInTheDocument();
});
it("renders finePrint", () => {
render();
expect(screen.getByText("Fine print text")).toBeInTheDocument();
});
it("renders CTA button with label", () => {
render(
);
expect(screen.getByTestId("callout-button")).toHaveTextContent("Click me");
});
it("renders CTA button with buttonLabel fallback", () => {
render(
);
expect(screen.getByTestId("callout-button")).toHaveTextContent(
"ButtonLabel"
);
});
it("does not render CTA section when no cta and no finePrint", () => {
const { container } = render();
expect(
container.querySelector("[data-testid='callout-button']")
).toBeNull();
});
describe("flex layout", () => {
it("uses flex layout by default", () => {
const { container } = render();
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.className).toContain("flex");
expect(gridHolder?.className).toContain("flex-wrap");
});
it("applies per-card width classes based on column calculations", () => {
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
expect(cards.length).toBe(2);
// 2 items, cardStackingMobile=true => md cols = 1 => md:w-full
cards.forEach(card => {
expect(card.className).toContain("md:w-full");
expect(card.className).toContain("lg:w-[calc(50%-0.75rem)]");
});
});
it("uses flex-col layout when cardsWidth is false (stack mode)", () => {
const { container } = render(
);
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.className).toContain("flex");
expect(gridHolder?.className).toContain("flex-col");
});
it("renders cards without wrapper div in stack mode", () => {
const { container } = render(
);
// In stack mode, cards are not wrapped in .callout-card divs
const cards = container.querySelectorAll(".callout-card");
expect(cards.length).toBe(0);
// But simple-card testids should still be present
expect(screen.getAllByTestId("simple-card")).toHaveLength(2);
});
it("renders 1 item with full width on lg", () => {
const { container } = render(
);
const card = container.querySelector(".callout-card");
expect(card?.className).toContain("lg:w-full");
});
it("respects maxCardsPerRow override", () => {
const items = Array.from({ length: 6 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render(
);
const cards = container.querySelectorAll(".callout-card");
// maxCardsPerRow=2, lgCols = min(2, 6) = 2
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(50%-0.75rem)]");
});
});
it("clamps maxCardsPerRow to max 6", () => {
const items = Array.from({ length: 3 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render(
);
const cards = container.querySelectorAll(".callout-card");
// clamp(10) = 6, lgCols = min(6, 3) = 3
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(33.333%-1rem)]");
});
});
it("uses 2 columns on md when cardStackingMobile is false", () => {
const { container } = render(
);
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("md:w-[calc(50%-1rem)]");
});
});
it("uses 1 column on md when cardStackingMobile is true (default)", () => {
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("md:w-full");
});
});
it("removes gutter with noGutter prop", () => {
const { container } = render(
);
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.className).toContain("gap-0");
expect(gridHolder?.className).not.toContain("gap-6");
});
it("has gap-6 by default", () => {
const { container } = render();
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.className).toContain("gap-6");
});
it("calculates 3 columns for 6 items (divisible by 3)", () => {
const items = Array.from({ length: 6 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(33.333%-1rem)]");
});
});
it("calculates 4 columns for 8 items (divisible by 4)", () => {
const items = Array.from({ length: 8 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(25%-1.125rem)]");
});
});
it("calculates 4 columns for 7 items (>6, not div by 3)", () => {
const items = Array.from({ length: 7 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(25%-1.125rem)]");
});
});
it("calculates 3 columns for 5 items (fallback)", () => {
const items = Array.from({ length: 5 }, (_, i) => ({
title: `Item ${i}`,
}));
const { container } = render();
const cards = container.querySelectorAll(".callout-card");
cards.forEach(card => {
expect(card.className).toContain("lg:w-[calc(33.333%-1rem)]");
});
});
});
describe("background colors", () => {
it("applies cream500 background class", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).toContain("bg-[#FFFEEF]");
});
it("applies gray100 background class", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).toContain("bg-fill-secondary");
});
it("applies navy background class", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).toContain("bg-fill-inverse");
});
it("applies transparent background (no additional class)", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).not.toContain("bg-fill");
});
it("uses inline style when background prop is provided", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section).toHaveStyle({
background: "linear-gradient(red, blue)",
});
});
it("background prop overrides backgroundColor class", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).not.toContain("bg-[#FFFEEF]");
expect(section?.style.background).toBe("red");
});
});
describe("text color", () => {
it("applies dark color class by default", () => {
const { container } = render();
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).toContain("text-text");
});
it("applies light color class", () => {
const { container } = render();
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).toContain("text-white");
});
it("applies textColor as inline style on title holder", () => {
const { container } = render(
);
const titleHolder = container.querySelector(".title-holder");
expect(titleHolder).not.toBeNull();
expect(titleHolder!.getAttribute("style")).toContain("color");
});
});
describe("card types", () => {
it("renders simple cards by default", () => {
render();
const cards = screen.getAllByTestId("simple-card");
expect(cards).toHaveLength(2);
});
it("renders blog cards when cardType is blog", () => {
const items = [
{
title: "Blog 1",
slug: "/blog-1",
shortDescription: "Desc",
blogCreationDate: "2024-01-01",
category: "Tech",
},
];
render();
expect(screen.getByTestId("blog-card")).toBeInTheDocument();
});
it("renders fullImage cards when cardType is fullImage", () => {
render();
expect(screen.getAllByTestId("full-image-card")).toHaveLength(2);
});
it("renders floatingImage cards when cardType is floatingImage", () => {
render();
expect(screen.getAllByTestId("floating-image-card")).toHaveLength(2);
});
it("allows per-item cardType override", () => {
const items = [
{ title: "Simple", cardType: "simple" as const },
{ title: "Blog", cardType: "blog" as const, slug: "/b" },
];
render();
expect(screen.getByTestId("simple-card")).toBeInTheDocument();
expect(screen.getByTestId("blog-card")).toBeInTheDocument();
});
it("passes applyBoxShadow to card shadow prop", () => {
const { container } = render(
);
expect(
container.querySelectorAll("[data-testid='simple-card']")
).toHaveLength(2);
});
});
describe("maxWidth and layout", () => {
it("applies max-w-120 when maxWidth is true (default)", () => {
const { container } = render();
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).toContain("max-w-120");
});
it("does not apply max-w-120 when maxWidth is false", () => {
const { container } = render(
);
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).not.toContain("max-w-120");
});
it("applies containerClassName to section", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section?.className).toContain("custom-section");
});
it("applies innerClassName to inner div", () => {
const { container } = render(
);
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).toContain("custom-inner");
});
it("applies noGutter padding class", () => {
const { container } = render(
);
const inner =
container.querySelector(".callout-container")?.parentElement;
expect(inner?.className).toContain("p-0");
});
});
describe("edge cases", () => {
it("handles empty items array", () => {
const { container } = render();
expect(container.querySelector("section")).toBeInTheDocument();
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.children).toHaveLength(0);
});
it("handles items with no properties gracefully", () => {
const { container } = render();
expect(container.querySelector("section")).toBeInTheDocument();
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.children).toHaveLength(2);
});
it("handles unknown backgroundColor value with fallback", () => {
const { container } = render(
);
const section = container.querySelector("section");
expect(section).toBeInTheDocument();
});
it("renders with cardsWidth false and noGutter true", () => {
const { container } = render(
);
const gridHolder = container.querySelector(".card-holder");
expect(gridHolder?.className).toContain("gap-0");
});
it("renders card with shadow prop already set on item", () => {
const items = [{ title: "Card", shadow: true }];
const { container } = render(
);
expect(
container.querySelector("[data-testid='simple-card']")
).toBeInTheDocument();
});
it("calculates lgCols clamping to itemCount", () => {
// When itemCount (1) < desktopCols (4), lgCols = itemCount = 1
const { container } = render(
);
const card = container.querySelector(".callout-card");
expect(card?.className).toContain("lg:w-full");
});
});
});