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"); }); }); });