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