import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { Table, type TableHeader } from '../../src/components/Table'; import type { SortState } from '../../src/types/sort'; type Row = { id: string; name: string; email: string; }; const headers: TableHeader[] = [ { label: 'Name', id: 'name', value: (row) => row.name, headerClassName: 'head-name' }, { label: 'Email', id: 'email', value: (row) => row.email, cellClassName: 'cell-email' }, { label: 'Static', id: 'static', value: 'Always', sortable: false }, ]; const data: Row[] = [{ id: '1', name: 'Jane', email: 'jane@example.com' }]; describe('Table', () => { it('renders loading state', () => { render( row.id} />); expect(document.querySelector('.animate-spin')).toBeTruthy(); }); it('renders empty state message when no rows are available', () => { render(
row.id} />, ); expect(screen.getByText('Nothing here')).toBeInTheDocument(); }); it('renders row values from function and static header values', () => { const rowKey = vi.fn((row: Row) => row.id); render(
); expect(screen.getByText('Jane')).toBeInTheDocument(); expect(screen.getByText('jane@example.com')).toBeInTheDocument(); expect(screen.getByText('Always')).toBeInTheDocument(); expect(screen.getByText('Name').closest('th')).toHaveClass('head-name'); expect(screen.getByText('jane@example.com').closest('td')).toHaveClass('cell-email'); expect(rowKey).toHaveBeenCalledWith(data[0], 0); }); it('shows sortable buttons only when sort config is complete', () => { const onSortChange = vi.fn(); const sortableHeaders: TableHeader[] = [ { label: 'No field', id: 'a', sortable: true, value: (row) => row.name }, { label: 'Empty field', id: 'b', sortable: true, sortField: '', value: (row) => row.name }, { label: 'Name', id: 'c', sortable: true, sortField: 'name', value: (row) => row.name }, ]; const { rerender } = render(
row.id} onSortChange={onSortChange} />, ); expect(screen.queryByRole('button', { name: 'Sort by No field' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Sort by Empty field' })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Sort by Name' })).toBeInTheDocument(); rerender(
row.id} />); expect(screen.queryByRole('button', { name: 'Sort by Name' })).not.toBeInTheDocument(); }); it('renders sort states and notifies callback on header click', () => { const onSortChange = vi.fn(); const sortableHeaders: TableHeader[] = [ { label: 'Name', id: 'name', sortable: true, sortField: 'name', value: (row) => row.name }, { label: 'Email', id: 'email', sortable: true, sortField: 'email', value: (row) => row.email }, ]; const sorting: SortState = { field: 'name', direction: 'asc' }; const { rerender } = render(
row.id} sorting={sorting} onSortChange={onSortChange} />, ); const nameSort = screen.getByRole('button', { name: 'Sort by Name' }); const emailSort = screen.getByRole('button', { name: 'Sort by Email' }); expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'asc'); expect(emailSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'none'); fireEvent.click(nameSort); expect(onSortChange).toHaveBeenCalledWith('name'); rerender(
row.id} sorting={{ field: 'name', direction: 'desc' }} onSortChange={onSortChange} />, ); expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'desc'); }); it('supports pagination controls and page-size changes', () => { const onPageChange = vi.fn(); const onPageSizeChange = vi.fn(); render(
row.id} pagination={{ page: 2, pageSize: 10, total: 21, totalPages: 3, onPageChange, onPageSizeChange, }} />, ); fireEvent.click(screen.getByRole('button', { name: 'Previous page' })); fireEvent.click(screen.getByRole('button', { name: 'Next page' })); fireEvent.change(screen.getByLabelText('Rows'), { target: { value: '20' } }); expect(onPageChange).toHaveBeenNthCalledWith(1, 1); expect(onPageChange).toHaveBeenNthCalledWith(2, 3); expect(onPageSizeChange).toHaveBeenCalledWith(20); }); it('hides rows selector when onPageSizeChange is absent and clamps page count display', () => { render(
row.id} pagination={{ page: 1, pageSize: 10, total: 1, totalPages: 0, onPageChange: vi.fn(), }} />, ); expect(screen.queryByLabelText('Rows')).not.toBeInTheDocument(); expect(screen.getByText('Page 1 of 1')).toBeInTheDocument(); }); it('disables prev/next at bounds or while loading', () => { const { rerender } = render(
row.id} pagination={{ page: 1, pageSize: 10, total: 10, totalPages: 1, onPageChange: vi.fn(), }} />, ); expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); rerender(
row.id} isLoading pagination={{ page: 2, pageSize: 10, total: 100, totalPages: 10, onPageChange: vi.fn(), onPageSizeChange: vi.fn(), }} />, ); expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); expect(screen.getByLabelText('Rows')).toBeDisabled(); }); });