import React from "react";
import { DynamicTabs } from "./index";
import { act, fireEvent, render, screen } from "@testing-library/react";
jest.mock("next/link", () => {
const MockNextLink = ({ children, href, className, ...props }: any) => (
{children}
);
MockNextLink.displayName = "MockNextLink";
return MockNextLink;
});
jest.mock("framer-motion", () => ({
AnimatePresence: ({ children }: any) => <>{children}>,
motion: {
div: ({ children, animate }: any) => (
{children}
),
},
}));
jest.mock("@shared/components/collapse", () => ({
Collapse: ({ open, children }: any) => (
{open ? children : null}
),
}));
jest.mock("@shared/components/link", () => ({
Link: ({ children, href, className }: any) => (
{children}
),
}));
jest.mock("@shared/components/material-icon", () => ({
MaterialIcon: ({ name }: any) => (
{name}
),
}));
jest.mock("@shared/components/text", () => ({
Text: ({ children, className }: any) => (
{children}
),
}));
jest.mock("@shared/contentful/blocks/button", () => ({
Button: ({ children, onClick, buttonClassName }: any) => (
),
}));
jest.mock("@shared/utils", () => ({
cx: (...args: any[]) => args.filter(Boolean).join(" "),
}));
describe("DynamicTabs", () => {
const defaultProps = {
tabs: {
items: [
{
tabName: "Tab 1",
anchorId: "tab-1",
tabContent: Content 1
,
},
{
tabName: "Tab 2",
anchorId: "tab-2",
tabContent: Content 2
,
},
{
tabName: "Tab 3",
anchorId: "tab-3",
tabContent: Content 3
,
},
],
},
};
beforeEach(() => {
jest.clearAllMocks();
window.location.hash = "";
});
describe("Rendering", () => {
it("renders all tab buttons", () => {
render();
expect(screen.getAllByText("Tab 1").length).toBeGreaterThan(0);
expect(screen.getAllByText("Tab 2").length).toBeGreaterThan(0);
expect(screen.getAllByText("Tab 3").length).toBeGreaterThan(0);
});
it("renders tab content", () => {
render();
expect(screen.getAllByText("Content 1").length).toBeGreaterThan(0);
});
it("renders title when provided", () => {
render();
expect(screen.getByText("My Tabs")).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render();
expect(screen.getByText("A subtitle")).toBeInTheDocument();
});
it("does not render title section when no title or subtitle", () => {
const { container } = render();
expect(container.querySelector(".mb-8.mt-5")).not.toBeInTheDocument();
});
});
describe("Tab switching", () => {
it("switches tab content on click", () => {
render();
const tabButtons = screen.getAllByTestId("tab-button");
// Find a Tab 2 button and click it
const tab2Buttons = tabButtons.filter(btn => btn.textContent === "Tab 2");
fireEvent.click(tab2Buttons[0]);
// After clicking Tab 2, the motion-div for tab 2 should show opacity 1
const motionDivs = screen.getAllByTestId("motion-div");
expect(motionDivs.length).toBeGreaterThan(0);
});
it("first tab is active by default", () => {
render();
const tabButtons = screen.getAllByTestId("tab-button");
const firstTabBtn = tabButtons.find(btn => btn.textContent === "Tab 1");
expect(firstTabBtn?.className).toContain("border-b-2");
});
});
describe("Navigation type", () => {
it("renders tab view on desktop when navigationType is true", () => {
const { container } = render(
);
// Desktop view should have the overflow-x-auto div in "hidden md:block"
const desktopBlock = container.querySelector(".hidden.md\\:block");
expect(desktopBlock).toBeInTheDocument();
});
it("renders sidebar view on desktop when navigationType is false", () => {
const { container } = render(
);
const desktopBlock = container.querySelector(".hidden.md\\:block");
expect(desktopBlock).toBeInTheDocument();
});
});
describe("Mobile view", () => {
it("renders tab view on mobile when mobileSidebarTabType is true", () => {
const { container } = render(
);
const mobileBlock = container.querySelector(".md\\:hidden");
expect(mobileBlock).toBeInTheDocument();
});
it("renders accordion view on mobile when mobileSidebarTabType is false", () => {
render();
// Accordion uses Collapse component
const collapses = screen.getAllByTestId("collapse");
expect(collapses.length).toBeGreaterThan(0);
});
it("clicks accordion button to switch tabs", () => {
window.location.hash = "#tab-1";
const { container } = render(
);
// Accordion buttons are inside the mobile container (.md\:hidden)
const mobileContainer = container.querySelector(".md\\:hidden");
const accordionButtons = mobileContainer!.querySelectorAll(
"[data-testid='tab-button']"
);
// Click the second accordion button (Tab 2)
fireEvent.click(accordionButtons[1]);
expect(window.location.hash).toBe("#tab-2");
});
});
describe("Sidebar view", () => {
it("clicks sidebar button to switch tabs", () => {
window.location.hash = "#tab-1";
const { container } = render(
);
// Sidebar buttons are inside the desktop container (.hidden.md\:block)
const desktopContainer = container.querySelector(".hidden.md\\:block");
const sidebarButtons = desktopContainer!.querySelectorAll(
"[data-testid='tab-button']"
);
// Click the third sidebar button (Tab 3)
fireEvent.click(sidebarButtons[2]);
expect(window.location.hash).toBe("#tab-3");
});
});
describe("Hash navigation", () => {
it("sets tab based on URL hash", () => {
window.location.hash = "#tab-2";
render();
// After useEffect runs, tab 2 should be active
const tabButtons = screen.getAllByTestId("tab-button");
const tab2Btns = tabButtons.filter(btn => btn.textContent === "Tab 2");
// At least one Tab 2 button should have the active style
const hasActiveTab2 = tab2Btns.some(btn =>
btn.className.includes("border-b-2")
);
expect(hasActiveTab2).toBe(true);
});
it("updates hash on tab click when fragment exists", () => {
window.location.hash = "#tab-1";
render();
const tabButtons = screen.getAllByTestId("tab-button");
const tab2Btns = tabButtons.filter(btn => btn.textContent === "Tab 2");
fireEvent.click(tab2Btns[0]);
expect(window.location.hash).toBe("#tab-2");
});
it("listens to hashchange events", () => {
render();
act(() => {
window.location.hash = "#tab-3";
window.dispatchEvent(new HashChangeEvent("hashchange"));
});
const tabButtons = screen.getAllByTestId("tab-button");
const tab3Btns = tabButtons.filter(btn => btn.textContent === "Tab 3");
const hasActiveTab3 = tab3Btns.some(btn =>
btn.className.includes("border-b-2")
);
expect(hasActiveTab3).toBe(true);
});
});
describe("Empty state", () => {
it("renders without tabs", () => {
const { container } = render();
expect(container.firstChild).toBeInTheDocument();
});
it("renders without tabsContent", () => {
render();
expect(screen.getAllByTestId("tab-button").length).toBeGreaterThan(0);
});
});
describe("Ordered list mode", () => {
const orderedProps = {
dynamic: true,
isOrderedList: true,
tabs: {
items: [
{
tabName: "Group A",
anchorId: "tab-0",
tabContent: [
Item A1,
Item A2,
],
},
{
tabName: "Group B",
anchorId: "tab-1",
tabContent: [Item B1],
},
],
},
};
it("renders ordered tab names from the tabs prop", () => {
render();
expect(screen.getAllByText("Group A").length).toBeGreaterThan(0);
expect(screen.getAllByText("Group B").length).toBeGreaterThan(0);
});
it("renders each tab's tabContent children", () => {
render();
expect(screen.getAllByText("Item A1").length).toBeGreaterThan(0);
expect(screen.getAllByText("Item A2").length).toBeGreaterThan(0);
});
it("updates hash when an ordered tab is clicked", () => {
window.location.hash = "#tab-0";
render();
const tabButtons = screen.getAllByTestId("tab-button");
const groupB = tabButtons.filter(btn => btn.textContent === "Group B");
fireEvent.click(groupB[0]);
expect(window.location.hash).toBe("#tab-1");
});
it("renders sidebar layout when navigationType is false", () => {
const { container } = render(
);
expect(container.querySelector(".hidden.lg\\:block")).toBeInTheDocument();
expect(screen.getAllByText("Group A").length).toBeGreaterThan(0);
});
it("renders accordion layout when mobileSidebarTabType is false", () => {
const { container } = render(
);
expect(container.querySelector(".lg\\:hidden")).toBeInTheDocument();
expect(screen.getAllByText("Item B1").length).toBeGreaterThan(0);
});
});
});