import "@testing-library/jest-dom";
import CartRetentionBanner from "./index";
import { TypeCartRetentionBannerFields } from "./types";
import { act, fireEvent, render, screen } from "@testing-library/react";
// Mock dependencies
jest.mock("@shared/components/text", () => ({
Text: ({ children, ...props }: any) => {children},
}));
jest.mock("@shared/contentful/blocks/button", () => ({
Button: ({ children, onClick, buttonLabel, ...props }: any) => (
),
}));
jest.mock("@shared/contentful/blocks/modal", () => ({
Modal: ({ isOpen, onRequestClose, children, ...props }: any) => (
{isOpen && (
<>
{children}
>
)}
),
}));
jest.mock("@shared/hooks/contentful/use-contentful-rich-text", () => ({
renderContentfulRichText: (doc: any, _: any, className: string) => (
Rich Text Content
),
}));
jest.mock("@shared/utils/contentful/to-document", () => ({
toDocument: (json: any) => json,
}));
describe("CartRetentionBanner", () => {
const defaultFields: TypeCartRetentionBannerFields = {
entryTitle: "Cart Banner",
anchorId: "cart-banner",
mainHeading: "Welcome back!",
description: { json: { content: [{ nodeType: "paragraph" }] } },
cta: { buttonLabel: "Continue checkout" },
};
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe("modal behavior", () => {
it("renders a section with anchorId", () => {
render();
const section = document.querySelector("section#cart-banner");
expect(section).toBeInTheDocument();
});
it("renders with data-testid cart-retention-banner", () => {
render();
expect(screen.getByTestId("cart-retention-banner")).toBeInTheDocument();
});
it("opens modal when checkShouldShowBanner returns true", async () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
// Allow useEffect to fire
act(() => {
jest.advanceTimersByTime(200);
});
const modal = screen.getByTestId("modal");
expect(modal).toHaveAttribute("data-open", "true");
});
it("does not open modal when checkShouldShowBanner returns false", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(false);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
const modal = screen.getByTestId("modal");
expect(modal).toHaveAttribute("data-open", "false");
});
it("calls onBannerShown when modal opens", () => {
const onBannerShown = jest.fn();
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(onBannerShown).toHaveBeenCalled();
});
it("closes modal and calls onClose", () => {
const onClose = jest.fn();
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
fireEvent.click(screen.getByTestId("modal-close"));
expect(onClose).toHaveBeenCalled();
});
it("calls onContinueCheckout when CTA button is clicked in modal", () => {
const onContinueCheckout = jest.fn();
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
fireEvent.click(screen.getByTestId("banner-button"));
expect(onContinueCheckout).toHaveBeenCalled();
});
it("uses default heading when mainHeading is not provided", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
const fields = { ...defaultFields, mainHeading: undefined };
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.getByText(/Welcome back/i)).toBeInTheDocument();
});
it("uses default CTA label when cta.buttonLabel is not provided", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
const fields = { ...defaultFields, cta: {} };
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.getByTestId("banner-button")).toHaveTextContent(
"Continue checkout"
);
});
it("renders rich text description in modal", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.getByTestId("rich-text")).toBeInTheDocument();
});
it("does not render rich text when description is undefined", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
const fields = { ...defaultFields, description: undefined };
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.queryByTestId("rich-text")).not.toBeInTheDocument();
});
it("passes type=cart-retention-banner to Modal", () => {
render();
const modal = screen.getByTestId("modal");
expect(modal).toHaveAttribute("data-type", "cart-retention-banner");
});
});
describe("popup container mode", () => {
it("renders inline content when isInPopupContainer is true", () => {
render(
);
// Should render the pop-up-body div directly
const popupBody = document.querySelector(".pop-up-body");
expect(popupBody).toBeInTheDocument();
});
it("renders data-section-type attribute in popup mode", () => {
render(
);
const div = document.querySelector(
'[data-section-type="cart-retention-banner"]'
);
expect(div).toBeInTheDocument();
});
it("renders heading in popup mode", () => {
render(
);
expect(screen.getByText("Welcome back!")).toBeInTheDocument();
});
it("renders default heading in popup mode when mainHeading is undefined", () => {
const fields = { ...defaultFields, mainHeading: undefined };
render();
expect(screen.getByText(/Welcome back/)).toBeInTheDocument();
});
it("renders CTA in popup mode", () => {
render(
);
expect(screen.getByTestId("banner-button")).toHaveTextContent(
"Continue checkout"
);
});
it("calls onContinueCheckout in popup mode", () => {
const onContinueCheckout = jest.fn();
render(
);
fireEvent.click(screen.getByTestId("banner-button"));
expect(onContinueCheckout).toHaveBeenCalled();
});
it("does not render Modal in popup mode", () => {
render(
);
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
});
it("renders rich text in popup mode", () => {
render(
);
expect(screen.getByTestId("rich-text")).toBeInTheDocument();
});
});
describe("timer cleanup", () => {
it("cleans up timeout on unmount", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(false);
const { unmount } = render(
);
unmount();
// No error should occur after unmount
act(() => {
jest.advanceTimersByTime(200);
});
});
});
describe("optional callback branches", () => {
it("handles close without onClose callback", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
// Should not throw when clicking close without onClose
fireEvent.click(screen.getByTestId("modal-close"));
expect(screen.getByTestId("modal")).toHaveAttribute("data-open", "false");
});
it("handles continue checkout without onContinueCheckout callback", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
// Should not throw when clicking CTA without onContinueCheckout
fireEvent.click(screen.getByTestId("banner-button"));
expect(screen.getByTestId("banner-button")).toBeInTheDocument();
});
it("handles continue checkout in popup mode without callback", () => {
render(
);
// Should not throw
fireEvent.click(screen.getByTestId("banner-button"));
expect(screen.getByTestId("banner-button")).toBeInTheDocument();
});
it("opens modal without onBannerShown callback", () => {
const checkShouldShowBanner = jest.fn().mockReturnValue(true);
render(
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.getByTestId("modal")).toHaveAttribute("data-open", "true");
});
it("does not open if checkShouldShowBanner is not provided", () => {
render();
act(() => {
jest.advanceTimersByTime(200);
});
expect(screen.getByTestId("modal")).toHaveAttribute("data-open", "false");
});
it("renders popup without description", () => {
const fields = { ...defaultFields, description: undefined };
render();
expect(screen.queryByTestId("rich-text")).not.toBeInTheDocument();
});
});
});