import "@testing-library/jest-dom";
import React from "react";
import { ProductCardCarousel, TabSwitch, TestimonialCarousel } from "./helper";
import {
CarouselWithProductCards,
CarouselWithTestimonialCards,
} from "./types";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
// Mock sub-components
jest.mock("@shared/components/button", () => ({
Button: ({ children, onClick, ...props }: any) => (
),
}));
jest.mock("@shared/components/material-icon", () => ({
MaterialIcon: ({ name }: any) => (
{name}
),
}));
jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
ProductCard: ({
planName,
speed,
onCtaClick,
onToggleExpand,
isExpanded,
}: any) => (
{planName}
),
}));
jest.mock("@shared/contentful/blocks/cards/testimonial-card", () => ({
TestimonialCard: ({ title, isActive }: any) => (
{title}
),
}));
jest.mock("@shared/hooks/use-carousel-swipe", () => ({
useCarouselSwipe: () => ({
currentIndex: 0,
swipeOffset: 0,
isSwiping: false,
isMobile: false,
containerWidth: 1024,
containerRef: { current: null },
handleTouchStart: jest.fn(),
handleTouchMove: jest.fn(),
handleTouchEnd: jest.fn(),
prevSlide: jest.fn(),
nextSlide: jest.fn(),
goToSlide: jest.fn(),
constants: { CARD_OFFSET_PERCENTAGE: 105 },
}),
}));
jest.mock("@shared/utils", () => ({
cx: (...args: any[]) => args.filter(Boolean).join(" "),
}));
describe("ProductCardCarousel", () => {
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
get: () => 900,
});
});
const createProductFields = (
items: any[] = []
): CarouselWithProductCards => ({
__typename: "ComponentCarousel",
sys: { id: "test-carousel" },
title: "Product Carousel",
items: { items },
});
const sampleProduct = {
__typename: "ComponentProductCard" as const,
sys: { id: "prod-1" },
speed: "500 Mbps|Fast internet",
price: "49",
priceSuffix: "99/mo",
techType: "Fiber",
highlighted: false,
productCardDescription: "Great plan",
benefitsTitle: "Benefits",
benefitsExpanded: false,
innerBadge: "New",
benefits: { items: [{ text: "Benefit 1" }] },
giftRewards: { list: { items: [{ text: "Gift" }] } },
cta: { buttonLabel: "Select" },
innerBadgeIcon: { url: "https://example.com/icon.png" },
};
it("returns null when no items and no title", () => {
const fields = createProductFields([]);
fields.title = undefined;
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("returns null when items is undefined", () => {
const fields = {
__typename: "ComponentCarousel" as const,
sys: { id: "test" },
items: { items: undefined as any },
title: undefined,
} as any;
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("renders product cards when items exist", () => {
const fields = createProductFields([sampleProduct]);
render();
expect(screen.getAllByTestId("product-card")).toHaveLength(1);
});
it("renders non-carousel layout for 2 or fewer items", () => {
const fields = createProductFields([
sampleProduct,
{ ...sampleProduct, sys: { id: "prod-2" } },
]);
const { container } = render();
// Non-carousel: wrapped in flex layout
expect(container.querySelector(".flex.w-full")).toBeInTheDocument();
});
it("renders carousel layout for more than 2 items", () => {
const items = Array.from({ length: 4 }, (_, i) => ({
...sampleProduct,
sys: { id: `prod-${i}` },
}));
const fields = createProductFields(items);
const { container } = render();
// Should have mobile stack view (flex-col for md:hidden)
expect(container.querySelector(".flex-col")).toBeInTheDocument();
});
it("parses speed field correctly (planName | planSubtext)", () => {
const fields = createProductFields([sampleProduct]);
render();
const card = screen.getByTestId("product-card");
expect(card).toHaveAttribute("data-plan", "500 Mbps");
expect(card).toHaveAttribute("data-speed", "500 Mbps|Fast internet");
});
it("handles missing speed gracefully", () => {
const item = { ...sampleProduct, speed: undefined };
const fields = createProductFields([item]);
render();
const card = screen.getByTestId("product-card");
expect(card).toHaveAttribute("data-plan", "");
});
it("handles item with all optional fields missing", () => {
const minimalItem = {
__typename: "ComponentProductCard" as const,
sys: { id: "minimal" },
};
const fields = createProductFields([minimalItem]);
render();
const card = screen.getByTestId("product-card");
expect(card).toBeInTheDocument();
});
it("handles item with empty benefits and giftRewards", () => {
const item = {
...sampleProduct,
benefits: undefined,
giftRewards: undefined,
innerBadge: undefined,
innerBadgeIcon: undefined,
priceSuffix: undefined,
price: undefined,
cta: undefined,
};
const fields = createProductFields([item]);
render();
const card = screen.getByTestId("product-card");
expect(card).toBeInTheDocument();
});
it("handles speed without pipe separator", () => {
const item = { ...sampleProduct, speed: "500 Mbps" };
const fields = createProductFields([item]);
render();
const card = screen.getByTestId("product-card");
expect(card).toHaveAttribute("data-plan", "500 Mbps");
});
it("renders Next arrow and hides Previous on first slide in carousel mode", async () => {
const items = Array.from({ length: 4 }, (_, i) => ({
...sampleProduct,
sys: { id: `prod-${i}` },
}));
const fields = createProductFields(items);
render();
fireEvent(window, new Event("resize"));
await waitFor(() =>
expect(screen.getByLabelText("Next")).toBeInTheDocument()
);
expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
});
it("shows Previous after moving forward, then hides it after returning to first slide", async () => {
const items = Array.from({ length: 4 }, (_, i) => ({
...sampleProduct,
sys: { id: `prod-${i}` },
}));
const fields = createProductFields(items);
render();
fireEvent(window, new Event("resize"));
const nextBtn = await screen.findByLabelText("Next");
fireEvent.click(nextBtn);
const prevBtn = await screen.findByLabelText("Previous");
expect(prevBtn).toBeInTheDocument();
fireEvent.click(prevBtn);
expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
});
it("calls nextSlide on Next button click", async () => {
const items = Array.from({ length: 4 }, (_, i) => ({
...sampleProduct,
sys: { id: `prod-${i}` },
}));
const fields = createProductFields(items);
render();
fireEvent(window, new Event("resize"));
const nextBtn = await screen.findByLabelText("Next");
fireEvent.click(nextBtn);
expect(nextBtn).toBeInTheDocument();
});
it("handles toggle expand click for desktop (non-carousel) cards", () => {
const fields = createProductFields([
sampleProduct,
{ ...sampleProduct, sys: { id: "prod-2" } },
]);
render();
const toggleButtons = screen.getAllByTestId("product-toggle");
// Click desktop toggle
fireEvent.click(toggleButtons[0]);
// No error - toggle state changes internally
expect(toggleButtons[0]).toBeInTheDocument();
});
it("handles CTA click on product card", () => {
const fields = createProductFields([sampleProduct]);
render();
const ctaButton = screen.getByTestId("product-cta");
fireEvent.click(ctaButton);
// handleCtaClick just returns; no error
expect(ctaButton).toBeInTheDocument();
});
it("handles toggle expand for mobile cards in carousel mode", () => {
const items = Array.from({ length: 4 }, (_, i) => ({
...sampleProduct,
sys: { id: `prod-${i}` },
}));
const fields = createProductFields(items);
render();
// Mobile view cards are in the flex-col hidden section
const toggleButtons = screen.getAllByTestId("product-toggle");
// The first set of cards are in mobile view (first 4 items)
fireEvent.click(toggleButtons[0]);
expect(toggleButtons[0]).toBeInTheDocument();
});
it("renders with benefitsExpanded true in fields", () => {
const item = { ...sampleProduct, benefitsExpanded: true };
const fields = createProductFields([item, { ...item, sys: { id: "p2" } }]);
render();
// Cards should render with expanded state
const cards = screen.getAllByTestId("product-card");
expect(cards[0]).toHaveAttribute("data-expanded", "true");
});
it("renders when title exists but items are empty", () => {
const fields = createProductFields([]);
fields.title = "Has Title";
const { container } = render();
// Should NOT return null because title exists
expect(container.firstChild).not.toBeNull();
});
it("handles highlighted item (truthy branch)", () => {
const item = {
...sampleProduct,
highlighted: true,
topBadgeText: "Best Deal",
};
const fields = createProductFields([item]);
render();
const card = screen.getByTestId("product-card");
expect(card).toBeInTheDocument();
});
it("handles item with giftRewards.list but no items", () => {
const item = {
...sampleProduct,
giftRewards: { list: { items: undefined as any } },
};
const fields = createProductFields([item]);
render();
const card = screen.getByTestId("product-card");
expect(card).toBeInTheDocument();
});
it("handles item with giftRewards but no list", () => {
const item = {
...sampleProduct,
giftRewards: { list: undefined as any },
};
const fields = createProductFields([item]);
render();
expect(screen.getByTestId("product-card")).toBeInTheDocument();
});
it("handles item with priceSuffix without slash", () => {
const item = { ...sampleProduct, priceSuffix: "99" };
const fields = createProductFields([item]);
render();
expect(screen.getByTestId("product-card")).toBeInTheDocument();
});
it("handles item with innerBadgeIcon but no url", () => {
const item = {
...sampleProduct,
innerBadgeIcon: {} as any,
};
const fields = createProductFields([item]);
render();
expect(screen.getByTestId("product-card")).toBeInTheDocument();
});
it("handles fields with items but items.items is null", () => {
const fields = {
__typename: "ComponentCarousel" as const,
sys: { id: "test" },
items: { items: null as any },
title: "Title present",
} as any;
const { container } = render();
// title exists so it does not return null but items array is empty
expect(container.firstChild).not.toBeNull();
});
it("handles fields with no items property at all", () => {
const fields = {
__typename: "ComponentCarousel" as const,
sys: { id: "test" },
items: undefined as any,
title: undefined,
} as any;
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("renders tabbed view when tabs and activeTab are provided", () => {
const items = [
{ ...sampleProduct, sys: { id: "prod-1" }, productCategory: "Internet" },
{ ...sampleProduct, sys: { id: "prod-2" }, productCategory: "Phone" },
];
const fields = createProductFields(items);
const { container } = render(
);
// Should render tab containers with display block/none
const visibleTab = container.querySelector("[style*='display: block']");
expect(visibleTab).toBeInTheDocument();
});
});
describe("TestimonialCarousel", () => {
const createTestimonialFields = (
items: any[] = []
): CarouselWithTestimonialCards => ({
__typename: "ComponentCarousel",
sys: { id: "test-carousel" },
title: "Testimonials",
items: { items },
});
const sampleTestimonial = {
__typename: "ComponentTestimonialCard" as const,
sys: { id: "test-1" },
title: "Great Service",
author: "John Doe",
role: "CEO",
rating: 5,
quote: "Amazing experience!",
};
it("returns null when no testimonials", () => {
const fields = createTestimonialFields([]);
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("returns null when testimonials items is undefined", () => {
const fields = {
__typename: "ComponentCarousel" as const,
sys: { id: "test" },
title: "Testimonials",
items: { items: undefined as any },
} as any;
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("returns null when testimonials field items is null", () => {
const fields = {
__typename: "ComponentCarousel" as const,
sys: { id: "test" },
title: "Testimonials",
items: { items: null as any },
} as any;
const { container } = render();
expect(container.firstChild).toBeNull();
});
it("renders testimonial cards when items exist", () => {
const fields = createTestimonialFields([sampleTestimonial]);
render();
expect(screen.getByTestId("testimonial-card")).toBeInTheDocument();
});
it("renders navigation dots for each testimonial", () => {
const items = [
sampleTestimonial,
{ ...sampleTestimonial, sys: { id: "test-2" }, title: "Second" },
{ ...sampleTestimonial, sys: { id: "test-3" }, title: "Third" },
];
const fields = createTestimonialFields(items);
render();
const dots = screen.getAllByRole("button", { name: /Go to slide/ });
expect(dots).toHaveLength(3);
});
it("renders Previous and Next navigation buttons", () => {
const items = [
sampleTestimonial,
{ ...sampleTestimonial, sys: { id: "test-2" } },
];
const fields = createTestimonialFields(items);
render();
expect(screen.getByLabelText("Previous")).toBeInTheDocument();
expect(screen.getByLabelText("Next")).toBeInTheDocument();
});
it("renders dot navigation with correct aria labels", () => {
const items = [
sampleTestimonial,
{ ...sampleTestimonial, sys: { id: "test-2" } },
];
const fields = createTestimonialFields(items);
render();
expect(screen.getByLabelText("Go to slide 1")).toBeInTheDocument();
expect(screen.getByLabelText("Go to slide 2")).toBeInTheDocument();
});
it("calls goToSlide when dot is clicked", () => {
const items = [
sampleTestimonial,
{ ...sampleTestimonial, sys: { id: "test-2" } },
{ ...sampleTestimonial, sys: { id: "test-3" } },
];
const fields = createTestimonialFields(items);
render();
const dot2 = screen.getByLabelText("Go to slide 2");
fireEvent.click(dot2);
expect(dot2).toBeInTheDocument();
});
});
describe("TabSwitch", () => {
const defaultTabProps = {
tabs: ["Tab A", "Tab B", "Tab C"],
activeTab: "Tab A",
onChange: jest.fn(),
};
it("renders all tabs", () => {
render();
expect(screen.getByText("Tab A")).toBeInTheDocument();
expect(screen.getByText("Tab B")).toBeInTheDocument();
expect(screen.getByText("Tab C")).toBeInTheDocument();
});
it("calls onChange when a tab is clicked", () => {
const onChange = jest.fn();
render();
fireEvent.click(screen.getByText("Tab B"));
expect(onChange).toHaveBeenCalledWith("Tab B");
});
it("applies active style to selected tab", () => {
render();
const activeBtn = screen.getByText("Tab A");
expect(activeBtn.className).toContain("text-text-inverse");
});
it("applies inactive style to non-selected tabs", () => {
render();
const inactiveBtn = screen.getByText("Tab B");
expect(inactiveBtn.className).toContain("text-text-disabled");
});
it("applies custom className", () => {
const { container } = render(
);
expect(container.innerHTML).toContain("custom-class");
});
it("renders sliding indicator", () => {
const { container } = render();
const indicator = container.querySelector(".bg-bg-fill-brand");
expect(indicator).toBeInTheDocument();
});
});