import { act, fireEvent, render, screen } from '@testing-library/react' import CustomizedPagination from '../index' describe('CustomizedPagination', () => { const defaultProps = { current: 0, onChange: vi.fn(), total: 100, } beforeEach(() => { vi.clearAllMocks() vi.useRealTimers() }) describe('Rendering', () => { it('should render without crashing', () => { const { container } = render() expect(container).toBeInTheDocument() }) it('should display current page and total pages', () => { render() // current + 1 = 1, totalPages = 10 // The page info display shows "1 / 10" and page buttons also show numbers expect(screen.getByText('/')).toBeInTheDocument() expect(screen.getAllByText('1').length).toBeGreaterThanOrEqual(1) }) it('should render prev and next buttons', () => { render() const buttons = screen.getAllByRole('button') expect(buttons.length).toBeGreaterThanOrEqual(2) }) it('should render page number buttons', () => { render() // 5 pages total, should see page numbers expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() }) it('should display slash separator between current page and total', () => { render() expect(screen.getByText('/')).toBeInTheDocument() }) }) describe('Props', () => { it('should apply custom className', () => { const { container } = render() const wrapper = container.firstChild as HTMLElement expect(wrapper).toHaveClass('my-custom') }) it('should default limit to 10', () => { render() // totalPages = 100 / 10 = 10, displayed in the page info area expect(screen.getAllByText('10').length).toBeGreaterThanOrEqual(1) }) it('should calculate total pages based on custom limit', () => { render() // totalPages = 100 / 25 = 4, displayed in the page info area expect(screen.getAllByText('4').length).toBeGreaterThanOrEqual(1) }) it('should disable prev button on first page', () => { render() const buttons = screen.getAllByRole('button') // First button is prev expect(buttons[0]).toBeDisabled() }) it('should disable next button on last page', () => { render() const buttons = screen.getAllByRole('button') // Last button is next expect(buttons[buttons.length - 1]).toBeDisabled() }) it('should not render limit selector when onLimitChange is not provided', () => { render() expect(screen.queryByText(/common\.pagination\.perPage/i)).not.toBeInTheDocument() }) it('should render limit selector when onLimitChange is provided', () => { const onLimitChange = vi.fn() render() // Should show limit options 10, 25, 50 expect(screen.getByText('25')).toBeInTheDocument() expect(screen.getByText('50')).toBeInTheDocument() }) }) describe('User Interactions', () => { it('should call onChange when next button is clicked', () => { const onChange = vi.fn() render() const buttons = screen.getAllByRole('button') const nextButton = buttons[buttons.length - 1] fireEvent.click(nextButton) expect(onChange).toHaveBeenCalledWith(1) }) it('should call onChange when prev button is clicked', () => { const onChange = vi.fn() render() const buttons = screen.getAllByRole('button') fireEvent.click(buttons[0]) expect(onChange).toHaveBeenCalledWith(4) }) it('should show input when page display is clicked', () => { render() // Click the current page display (the div containing "1 / 10") fireEvent.click(screen.getByText('/')) // Input should appear expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should navigate to entered page on Enter key', () => { vi.useFakeTimers() const onChange = vi.fn() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: '5' } }) fireEvent.keyDown(input, { key: 'Enter' }) act(() => { vi.advanceTimersByTime(500) }) expect(onChange).toHaveBeenCalledWith(4) // 0-indexed }) it('should cancel input on Escape key', () => { render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.keyDown(input, { key: 'Escape' }) // Input should be hidden and page display should return expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(screen.getByText('/')).toBeInTheDocument() }) it('should confirm input on blur', () => { vi.useFakeTimers() const onChange = vi.fn() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: '3' } }) fireEvent.blur(input) act(() => { vi.advanceTimersByTime(500) }) expect(onChange).toHaveBeenCalledWith(2) // 0-indexed }) it('should clamp page to max when input exceeds total pages', () => { vi.useFakeTimers() const onChange = vi.fn() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: '999' } }) fireEvent.keyDown(input, { key: 'Enter' }) act(() => { vi.advanceTimersByTime(500) }) expect(onChange).toHaveBeenCalledWith(9) // last page (0-indexed) }) it('should clamp page to min when input is less than 1', () => { vi.useFakeTimers() const onChange = vi.fn() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: '0' } }) fireEvent.keyDown(input, { key: 'Enter' }) act(() => { vi.advanceTimersByTime(500) }) expect(onChange).toHaveBeenCalledWith(0) }) it('should ignore non-numeric input and empty input', () => { render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: 'abc' } }) expect(input).toHaveValue('') fireEvent.change(input, { target: { value: '' } }) expect(input).toHaveValue('') }) it('should show per page tip on hover and hide on leave', () => { const onLimitChange = vi.fn() render() const container = screen.getByText('25').closest('div.flex.items-center.gap-\\[1px\\]')! fireEvent.mouseEnter(container) // I18n mock returns ns.key expect(screen.getByText('common.pagination.perPage')).toBeInTheDocument() fireEvent.mouseLeave(container) expect(screen.queryByText('common.pagination.perPage')).not.toBeInTheDocument() }) it('should call onLimitChange when limit option is clicked', () => { const onLimitChange = vi.fn() render() fireEvent.click(screen.getByText('25')) expect(onLimitChange).toHaveBeenCalledWith(25) }) it('should call onLimitChange with 10 when 10 option is clicked', () => { const onLimitChange = vi.fn() render() // The limit selector contains options 10, 25, 50. // Query specifically within the limit container const container = screen.getByText('25').closest('div.flex.items-center.gap-\\[1px\\]')! const option10 = Array.from(container.children).find(el => el.textContent === '10')! fireEvent.click(option10) expect(onLimitChange).toHaveBeenCalledWith(10) }) it('should call onLimitChange with 50 when 50 option is clicked', () => { const onLimitChange = vi.fn() render() fireEvent.click(screen.getByText('50')) expect(onLimitChange).toHaveBeenCalledWith(50) }) it('should call onChange when a page button is clicked', () => { const onChange = vi.fn() render() fireEvent.click(screen.getByText('3')) expect(onChange).toHaveBeenCalledWith(2) // 0-indexed }) it('should correctly select active limit style for 25 and 50', () => { // Test limit 25 const { container: containerA } = render() const wrapper25 = Array.from(containerA.querySelectorAll('div.system-sm-medium')).find(el => el.textContent === '25')! expect(wrapper25).toHaveClass('bg-components-segmented-control-item-active-bg') // Test limit 50 const { container: containerB } = render() const wrapper50 = Array.from(containerB.querySelectorAll('div.system-sm-medium')).find(el => el.textContent === '50')! expect(wrapper50).toHaveClass('bg-components-segmented-control-item-active-bg') }) }) describe('Edge Cases', () => { it('should handle total of 0', () => { const { container } = render() expect(container).toBeInTheDocument() }) it('should handle confirm when input value is unchanged (covers false branch of empty string check)', () => { vi.useFakeTimers() const onChange = vi.fn() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') // Blur without changing anything fireEvent.blur(input) act(() => { vi.advanceTimersByTime(500) }) // onChange should NOT be called expect(onChange).not.toHaveBeenCalled() expect(screen.queryByRole('textbox')).not.toBeInTheDocument() }) it('should ignore other keys in handleInputKeyDown (covers false branch of Escape check)', () => { render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.keyDown(input, { key: 'a' }) expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should trigger handleInputConfirm with empty string specifically on keydown Enter', async () => { const { userEvent } = await import('@testing-library/user-event') const user = userEvent.setup() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') await user.clear(input) await user.type(input, '{Enter}') // Wait for debounce 500ms await new Promise(r => setTimeout(r, 600)) // Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter expect(screen.queryByRole('textbox')).not.toBeInTheDocument() }) it('should explicitly trigger Escape key logic in handleInputKeyDown', async () => { const { userEvent } = await import('@testing-library/user-event') const user = userEvent.setup() render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') await user.type(input, '{Escape}') // Wait for debounce 500ms await new Promise(r => setTimeout(r, 600)) expect(screen.queryByRole('textbox')).not.toBeInTheDocument() }) it('should handle single page', () => { render() // totalPages = 1, both buttons should be disabled const buttons = screen.getAllByRole('button') expect(buttons[0]).toBeDisabled() expect(buttons[buttons.length - 1]).toBeDisabled() }) it('should restore input value when blurred with empty value', () => { render() fireEvent.click(screen.getByText('/')) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: '' } }) fireEvent.blur(input) // Should close input without calling onChange, restoring to current + 1 expect(screen.queryByRole('textbox')).not.toBeInTheDocument() }) }) })