import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { MdPagination } from '../MdPagination';
// Helper to get the desktop view container
const getDesktopView = () => {
const nav = screen.getByRole('navigation', { name: 'Pagination' });
return within(nav).getByTestId('pages-desktop');
};
describe('MdPagination', () => {
describe('rendering', () => {
it('renders with default props', () => {
render();
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument();
});
it('renders navigation buttons', () => {
render();
expect(screen.getByRole('button', { name: 'Forrige' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Neste' })).toBeInTheDocument();
});
it('renders page buttons', () => {
render();
const desktopView = getDesktopView();
expect(within(desktopView).getByRole('button', { name: 'Side 2' })).toBeInTheDocument();
expect(within(desktopView).getByRole('button', { name: 'Side 3' })).toBeInTheDocument();
});
it('marks current page with aria-current', () => {
render();
const desktopView = getDesktopView();
expect(within(desktopView).getByText('3').closest('[aria-current="page"]')).toBeInTheDocument();
});
it('returns null when totalPages is 1 or less', () => {
render();
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
it('returns null when totalPages is not finite', () => {
render();
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
it('returns null when currentPage is not finite', () => {
render();
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
});
describe('props forwarding', () => {
it('merges custom className with component classes', () => {
render();
const pagination = screen.getByRole('navigation', { name: 'Pagination' });
expect(pagination).toHaveClass('md-pagination');
expect(pagination).toHaveClass('custom-class');
});
it('applies compact class when compact is true', () => {
render();
const pagination = screen.getByRole('navigation', { name: 'Pagination' });
expect(pagination).toHaveClass('md-pagination--compact');
});
});
describe('labels', () => {
it('uses default labels', () => {
render();
expect(screen.getByRole('button', { name: 'Forrige' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Neste' })).toBeInTheDocument();
});
it('uses custom labels', () => {
render();
expect(screen.getByRole('button', { name: 'Previous' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
});
it('partially overrides labels', () => {
render();
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Neste' })).toBeInTheDocument();
});
});
describe('interactions', () => {
it('calls onPageChange when clicking a page button', async () => {
const user = userEvent.setup();
const onPageChange = vi.fn();
render();
const desktopView = getDesktopView();
await user.click(within(desktopView).getByRole('button', { name: 'Side 2' }));
expect(onPageChange).toHaveBeenCalledTimes(1);
expect(onPageChange).toHaveBeenCalledWith(2);
});
it('calls onPageChange when clicking next button', async () => {
const user = userEvent.setup();
const onPageChange = vi.fn();
render();
await user.click(screen.getByRole('button', { name: 'Neste' }));
expect(onPageChange).toHaveBeenCalledTimes(1);
expect(onPageChange).toHaveBeenCalledWith(3);
});
it('calls onPageChange when clicking previous button', async () => {
const user = userEvent.setup();
const onPageChange = vi.fn();
render();
await user.click(screen.getByRole('button', { name: 'Forrige' }));
expect(onPageChange).toHaveBeenCalledTimes(1);
expect(onPageChange).toHaveBeenCalledWith(2);
});
it('renders current page as non-interactive element', () => {
render();
const desktopView = getDesktopView();
const currentPage = within(desktopView).getByText('3');
expect(currentPage).toHaveAttribute('aria-current', 'page');
expect(currentPage.tagName).toBe('SPAN');
});
});
describe('disabled states', () => {
it('disables previous navigation on first page', () => {
render();
const prevElement = screen.getByLabelText('Forrige');
expect(prevElement).toHaveAttribute('aria-disabled', 'true');
expect(prevElement.tagName).toBe('SPAN');
});
it('disables next navigation on last page', () => {
render();
const nextElement = screen.getByLabelText('Neste');
expect(nextElement).toHaveAttribute('aria-disabled', 'true');
expect(nextElement.tagName).toBe('SPAN');
});
it('enables both navigation buttons on middle pages', () => {
render();
expect(screen.getByRole('button', { name: 'Forrige' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Neste' })).toBeInTheDocument();
});
});
describe('page numbers calculation', () => {
it('shows all pages when totalPages <= 7', () => {
render();
const desktopView = getDesktopView();
for (let i = 1; i <= 7; i++) {
if (i === 4) {
const currentPage = within(desktopView).getByText('4');
expect(currentPage).toHaveAttribute('aria-current', 'page');
} else {
expect(within(desktopView).getByRole('button', { name: `Side ${i}` })).toBeInTheDocument();
}
}
});
it('shows ellipsis when currentPage is near start', () => {
render();
const desktopView = getDesktopView();
expect(within(desktopView).getByText('...')).toBeInTheDocument();
});
it('shows ellipsis when currentPage is near end', () => {
render();
const desktopView = getDesktopView();
expect(within(desktopView).getByText('...')).toBeInTheDocument();
});
it('shows ellipsis on both sides when currentPage is in middle', () => {
render();
const desktopView = getDesktopView();
const ellipses = within(desktopView).getAllByText('...');
expect(ellipses).toHaveLength(2);
});
});
describe('compact mode', () => {
it('applies compact class', () => {
render();
const pagination = screen.getByRole('navigation', { name: 'Pagination' });
expect(pagination).toHaveClass('md-pagination--compact');
});
it('hides desktop view in compact mode', () => {
render();
expect(screen.queryByTestId('pages-desktop')).not.toBeInTheDocument();
});
});
describe('edge cases', () => {
it('clamps currentPage to valid range (too low)', () => {
render();
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument();
// Previous should be disabled when clamped to page 1
const prevElement = screen.getByLabelText('Forrige');
expect(prevElement).toHaveAttribute('aria-disabled', 'true');
});
it('clamps currentPage to valid range (too high)', () => {
render();
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument();
// Next should be disabled when clamped to last page
const nextElement = screen.getByLabelText('Neste');
expect(nextElement).toHaveAttribute('aria-disabled', 'true');
});
it('handles two pages', () => {
render();
const desktopView = getDesktopView();
expect(within(desktopView).getByRole('button', { name: 'Side 2' })).toBeInTheDocument();
});
});
describe('asChild with renderLink', () => {
it('renders custom link element for page buttons', () => {
const renderLink = (page: number, children: React.ReactNode) => {
return (
{children}
);
};
render();
const desktopView = getDesktopView();
const link = within(desktopView).getByRole('link', { name: 'Side 2' });
expect(link).toHaveAttribute('href', '/page/2');
});
it('renders custom link element for nav buttons', () => {
const renderLink = (page: number, children: React.ReactNode) => {
return (
{children}
);
};
render();
// Previous links to page 2, Next links to page 4
expect(screen.getByRole('link', { name: /Forrige/i })).toHaveAttribute('href', '/page/2');
expect(screen.getByRole('link', { name: /Neste/i })).toHaveAttribute('href', '/page/4');
});
it('calls existing onClick on custom link and onPageChange', async () => {
const user = userEvent.setup();
const onPageChange = vi.fn();
const customOnClick = vi.fn();
const renderLink = (page: number, children: React.ReactNode) => {
return (
{children}
);
};
render(
,
);
const desktopView = getDesktopView();
await user.click(within(desktopView).getByRole('link', { name: 'Side 2' }));
expect(customOnClick).toHaveBeenCalledTimes(1);
expect(onPageChange).toHaveBeenCalledTimes(1);
expect(onPageChange).toHaveBeenCalledWith(2);
});
it('preserves className from custom link element', () => {
const renderLink = (page: number, children: React.ReactNode) => {
return (
{children}
);
};
render();
const desktopView = getDesktopView();
const link = within(desktopView).getByRole('link', { name: 'Side 2' });
expect(link).toHaveClass('custom-link-class');
expect(link).toHaveClass('md-pagination__page');
});
it('falls back to buttons when asChild is true but renderLink is not provided', () => {
render();
const desktopView = getDesktopView();
const pageButton = within(desktopView).getByRole('button', { name: 'Side 2' });
expect(pageButton.tagName).toBe('BUTTON');
});
});
});