import React from "react";
import { Pagination } from "./index";
import { fireEvent, render, screen } from "@testing-library/react";
// Mock dependencies
jest.mock("@shared/components/link", () => ({
Link: ({ children, href, onClick, className, ...rest }: any) => (
{children}
),
}));
jest.mock("@shared/components/material-icon", () => ({
MaterialIcon: ({ name, ...rest }: any) => (
{name}
),
}));
describe("Pagination", () => {
const mockOnPageChange = jest.fn();
const mockGetPageHref = (page: number) => `/blog?page=${page}`;
beforeEach(() => {
mockOnPageChange.mockClear();
});
describe("Rendering", () => {
it("renders the pagination nav with default aria-label", () => {
render(
);
expect(screen.getByRole("navigation")).toHaveAttribute(
"aria-label",
"Pagination"
);
});
it("renders with custom aria-label", () => {
render(
);
expect(screen.getByRole("navigation")).toHaveAttribute(
"aria-label",
"Blog pages"
);
});
it("renders previous and next navigation links", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to previous page" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to next page" })
).toBeInTheDocument();
});
it("renders chevron icons in nav links", () => {
render(
);
expect(screen.getByTestId("icon-chevron_left")).toBeInTheDocument();
expect(screen.getByTestId("icon-chevron_right")).toBeInTheDocument();
});
});
describe("Link states", () => {
it("disables previous link on first page", () => {
render(
);
const prevLink = screen.getByRole("link", { name: "Go to previous page" });
expect(prevLink).toHaveAttribute("aria-disabled", "true");
expect(prevLink).toHaveAttribute("tabindex", "-1");
});
it("enables previous link when not on first page", () => {
render(
);
const prevLink = screen.getByRole("link", { name: "Go to previous page" });
expect(prevLink).not.toHaveAttribute("aria-disabled", "true");
});
it("disables next link on last page", () => {
render(
);
const nextLink = screen.getByRole("link", { name: "Go to next page" });
expect(nextLink).toHaveAttribute("aria-disabled", "true");
expect(nextLink).toHaveAttribute("tabindex", "-1");
});
it("enables next link when not on last page", () => {
render(
);
const nextLink = screen.getByRole("link", { name: "Go to next page" });
expect(nextLink).not.toHaveAttribute("aria-disabled", "true");
});
it("marks current page with aria-current='page'", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 3" })
).toHaveAttribute("aria-current", "page");
});
it("does not set aria-current on non-current pages", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).not.toHaveAttribute("aria-current");
});
it("applies active class to current page link", () => {
render(
);
const activeLink = screen.getByRole("link", { name: "Go to page 2" });
expect(activeLink.className).toContain("bg-bg-surface-active");
expect(activeLink.className).toContain("font-bold");
});
it("does not apply active class to non-current page links", () => {
render(
);
const inactiveLink = screen.getByRole("link", {
name: "Go to page 1",
});
expect(inactiveLink.className).not.toContain("bg-bg-surface-active");
});
it("renders correct href for page links", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 3" })
).toHaveAttribute("href", "/blog?page=3");
});
});
describe("Page navigation clicks", () => {
it("calls onPageChange with previous page when previous link clicked", () => {
render(
);
fireEvent.click(
screen.getByRole("link", { name: "Go to previous page" })
);
expect(mockOnPageChange).toHaveBeenCalledWith(2);
});
it("calls onPageChange with next page when next link clicked", () => {
render(
);
fireEvent.click(screen.getByRole("link", { name: "Go to next page" }));
expect(mockOnPageChange).toHaveBeenCalledWith(4);
});
it("calls onPageChange with clicked page number", () => {
render(
);
fireEvent.click(screen.getByRole("link", { name: "Go to page 4" }));
expect(mockOnPageChange).toHaveBeenCalledWith(4);
});
it("does not call onPageChange when disabled previous link is clicked", () => {
render(
);
fireEvent.click(
screen.getByRole("link", { name: "Go to previous page" })
);
expect(mockOnPageChange).not.toHaveBeenCalled();
});
it("does not call onPageChange when disabled next link is clicked", () => {
render(
);
fireEvent.click(screen.getByRole("link", { name: "Go to next page" }));
expect(mockOnPageChange).not.toHaveBeenCalled();
});
});
describe("buildPageItems algorithm — total pages <= 7", () => {
it("renders all pages for totalPages=1", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
expect(screen.queryByText("…")).not.toBeInTheDocument();
});
it("renders all pages for totalPages=5", () => {
render(
);
for (let i = 1; i <= 5; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
expect(screen.queryByText("…")).not.toBeInTheDocument();
});
it("renders all pages for totalPages=7", () => {
render(
);
for (let i = 1; i <= 7; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
expect(screen.queryByText("…")).not.toBeInTheDocument();
});
});
describe("buildPageItems algorithm — current near start (current <= 4)", () => {
it("shows pages 1-5 and ellipsis then last page when on page 1", () => {
render(
);
for (let i = 1; i <= 5; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
expect(screen.getByText("…")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 10" })
).toBeInTheDocument();
expect(
screen.queryByRole("link", { name: "Go to page 6" })
).not.toBeInTheDocument();
});
it("shows pages 1-5, ellipsis, last for page 4 of 10", () => {
render(
);
for (let i = 1; i <= 5; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
expect(screen.getByText("…")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 10" })
).toBeInTheDocument();
});
});
describe("buildPageItems algorithm — current near end (current >= total - 3)", () => {
it("shows first page, ellipsis, last 5 pages when on last page", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
expect(screen.getByText("…")).toBeInTheDocument();
for (let i = 6; i <= 10; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
});
it("shows first, ellipsis, last 5 for page 7 of 10", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
expect(screen.getByText("…")).toBeInTheDocument();
for (let i = 6; i <= 10; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
});
});
describe("buildPageItems algorithm — current in middle", () => {
it("shows first, ellipsis, current-1/current/current+1, ellipsis, last", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 4" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 5" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 6" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 10" })
).toBeInTheDocument();
const ellipses = screen.getAllByText("…");
expect(ellipses).toHaveLength(2);
});
it("renders ellipsis as non-interactive with aria-hidden", () => {
render(
);
const ellipses = screen.getAllByText("…");
ellipses.forEach(el => {
expect(el).toHaveAttribute("aria-hidden", "true");
expect(el.tagName).toBe("SPAN");
});
});
it("shows correct pages for page 6 of 10", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 5" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 6" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 7" })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Go to page 10" })
).toBeInTheDocument();
expect(screen.getAllByText("…")).toHaveLength(2);
});
});
describe("Edge cases", () => {
it("handles single page (both nav links disabled)", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to previous page" })
).toHaveAttribute("aria-disabled", "true");
expect(
screen.getByRole("link", { name: "Go to next page" })
).toHaveAttribute("aria-disabled", "true");
});
it("handles totalPages=8 with current=4 (near-start boundary)", () => {
render(
);
for (let i = 1; i <= 5; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
expect(
screen.getByRole("link", { name: "Go to page 8" })
).toBeInTheDocument();
});
it("handles totalPages=8 with current=5 (near-end boundary)", () => {
render(
);
expect(
screen.getByRole("link", { name: "Go to page 1" })
).toBeInTheDocument();
for (let i = 4; i <= 8; i++) {
expect(
screen.getByRole("link", { name: `Go to page ${i}` })
).toBeInTheDocument();
}
});
});
});