Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions frontend/__tests__/unit/components/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { within, render, fireEvent, screen, cleanup } from '@testing-library/react'
import '@testing-library/jest-dom'
import Pagination from 'components/Pagination'

afterEach(cleanup)

describe('<Pagination />', () => {
const onPageChange = jest.fn()

const renderComponent = (
overrides: {
currentPage?: number
totalPages?: number
isLoaded?: boolean
} = {}
) => {
const props = {
currentPage: overrides.currentPage ?? 1,
totalPages: overrides.totalPages ?? 5,
isLoaded: overrides.isLoaded ?? true,
onPageChange,
}
return render(<Pagination {...props} />)
}

beforeEach(() => {
onPageChange.mockClear()
})

it('does not render when isLoaded is false', () => {
const { container } = renderComponent({ isLoaded: false })
expect(container.firstChild).toBeNull()
})

it('does not render when totalPages ≤ 1', () => {
const { container } = renderComponent({ totalPages: 1 })
expect(container.firstChild).toBeNull()
})

it('renders Prev and Next buttons and page numbers for small totalPages', () => {
renderComponent({ currentPage: 2, totalPages: 4 })

// Prev / Next
const prev = screen.getByRole('button', { name: 'Prev' })
const next = screen.getByRole('button', { name: 'Next' })

expect(prev).toBeEnabled()
expect(next).toBeEnabled()

// Pages 1–4
for (let n = 1; n <= 4; n++) {
expect(screen.getByRole('button', { name: String(n) })).toBeInTheDocument()
}
})

it('disables Prev on first page', () => {
const { container } = renderComponent({ currentPage: 1, totalPages: 3 })
const prevBtn = within(container).getByRole('button', { name: 'Prev' })
expect(prevBtn).toBeDisabled()
})

it('disables Next on last page', () => {
const { container } = renderComponent({ currentPage: 3, totalPages: 3 })
const nextBtn = within(container).getByRole('button', { name: 'Next' })
expect(nextBtn).toBeDisabled()
})

it('calls onPageChange with correct page number on button clicks', () => {
renderComponent({ currentPage: 2, totalPages: 5 })

fireEvent.click(screen.getByRole('button', { name: 'Prev' }))
expect(onPageChange).toHaveBeenCalledWith(1)

fireEvent.click(screen.getByRole('button', { name: 'Next' }))
expect(onPageChange).toHaveBeenCalledWith(3)

fireEvent.click(screen.getByRole('button', { name: '4' }))
expect(onPageChange).toHaveBeenCalledWith(4)
})

it('renders ellipses and correct pages for large totalPages', () => {
renderComponent({ currentPage: 10, totalPages: 20 })

// Should show first 3 pages
;[1, 2, 3].forEach((n) => {
expect(screen.getByRole('button', { name: String(n) })).toBeInTheDocument()
})

// Should show exactly two “More pages” indicators
const ellipses = screen.getAllByLabelText('More pages')
expect(ellipses).toHaveLength(2)

// Should show pages around currentPage: 9, 10, 11
;[9, 10, 11].forEach((n) =>
expect(screen.getByRole('button', { name: String(n) })).toBeInTheDocument()
)

// Last page
expect(screen.getByRole('button', { name: '20' })).toBeInTheDocument()
})

it('applies active styles and aria-current on the selected page', () => {
renderComponent({ currentPage: 3, totalPages: 5 })

const activeBtn = screen.getByRole('button', { name: '3' })
// check for class used in active state
expect(activeBtn).toHaveClass('bg-[#83a6cc]')
// accessibility: mark current page
expect(activeBtn).toHaveAttribute('aria-current', 'page')
})

it('uses default values when props are missing', () => {
// No overrides → currentPage=1, totalPages=5, isLoaded=true
renderComponent()

expect(screen.getByRole('button', { name: 'Prev' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'Next' })).toBeEnabled()
expect(screen.getByRole('button', { name: '5' })).toBeInTheDocument()
})

// Edge-case: currentPage near the start of a large set
it('shows correct pages when currentPage = 4 of 10', () => {
renderComponent({ currentPage: 4, totalPages: 10 })
// Should show 1,2,3,4,5 then ellipsis and 10
;[1, 2, 3, 4, 5].forEach((n) =>
expect(screen.getByRole('button', { name: String(n) })).toBeInTheDocument()
)
expect(screen.getByRole('button', { name: '10' })).toBeInTheDocument()
})

// Edge-case: very small totalPages (2)
it('renders exactly pages [1, 2] when totalPages = 2', () => {
renderComponent({ totalPages: 2, currentPage: 2 })
expect(screen.getAllByRole('button', { name: /^(1|2)$/ })).toHaveLength(2)
})
})
10 changes: 9 additions & 1 deletion frontend/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const Pagination: React.FC<PaginationProps> = ({
<div className="mt-8 flex flex-col items-center justify-center space-y-3">
<div className="flex flex-wrap items-center justify-center gap-2">
<Button
type="button"
className="flex h-10 min-w-[2.5rem] items-center justify-center rounded-md border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
onPress={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
Expand All @@ -70,11 +71,17 @@ const Pagination: React.FC<PaginationProps> = ({
{pageNumbers.map((number, index) => (
<React.Fragment key={index}>
{number === '...' ? (
<span className="flex h-10 w-10 items-center justify-center text-gray-600 dark:text-gray-400">
<span
className="flex h-10 w-10 items-center justify-center text-gray-600 dark:text-gray-400"
role="presentation"
aria-label="More pages"
>
<FontAwesomeIcon icon={faEllipsisH} className="h-5 w-5"></FontAwesomeIcon>
</span>
) : (
<Button
type="button"
aria-current={currentPage === number ? 'page' : undefined}
className={`flex h-10 min-w-[2.5rem] items-center justify-center rounded-md px-3 text-sm font-medium ${
currentPage === number
? 'bg-[#83a6cc] text-white dark:bg-white dark:text-black'
Expand All @@ -88,6 +95,7 @@ const Pagination: React.FC<PaginationProps> = ({
</React.Fragment>
))}
<Button
type="button"
className="flex h-10 min-w-[2.5rem] items-center justify-center rounded-md border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
onPress={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
Expand Down