import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { DataTable, type Column } from '../DataTable' interface TestRow { id: string name: string status: string } const mockData: TestRow[] = [ { id: '1', name: 'Item 1', status: 'Active' }, { id: '2', name: 'Item 2', status: 'Inactive' }, { id: '3', name: 'Item 3', status: 'Active' }, ] const mockColumns: Column[] = [ { key: 'name', header: 'Name', cell: (row) => row.name }, { key: 'status', header: 'Status', cell: (row) => row.status }, ] const sortableColumns: Column[] = [ { key: 'name', header: 'Name', cell: (row) => row.name, sortable: true }, { key: 'status', header: 'Status', cell: (row) => row.status, sortable: true }, ] describe('DataTable', () => { it('renders correctly with data', () => { render( row.id} /> ) expect(screen.getByText('Name')).toBeInTheDocument() expect(screen.getByText('Status')).toBeInTheDocument() expect(screen.getByText('Item 1')).toBeInTheDocument() expect(screen.getByText('Item 2')).toBeInTheDocument() expect(screen.getByText('Item 3')).toBeInTheDocument() }) it('renders empty state when no data', () => { render( row.id} /> ) expect(screen.getByText('No data available')).toBeInTheDocument() }) it('renders custom empty state', () => { render( row.id} emptyState={
Custom empty message
} /> ) expect(screen.getByText('Custom empty message')).toBeInTheDocument() }) it('renders loading state', () => { render( row.id} isLoading={true} /> ) // Loading spinner should be present (animated div) const spinnerContainer = document.querySelector('.animate-spin') expect(spinnerContainer).toBeInTheDocument() }) it('handles sortable column click - ascending', () => { render( row.id} /> ) const nameHeader = screen.getByText('Name').closest('th') expect(nameHeader).toHaveAttribute('role', 'button') fireEvent.click(nameHeader!) expect(nameHeader).toHaveAttribute('aria-sort', 'ascending') }) it('handles sortable column click - descending on second click', () => { render( row.id} /> ) const nameHeader = screen.getByText('Name').closest('th') // First click - ascending fireEvent.click(nameHeader!) expect(nameHeader).toHaveAttribute('aria-sort', 'ascending') // Second click - descending fireEvent.click(nameHeader!) expect(nameHeader).toHaveAttribute('aria-sort', 'descending') }) it('handles sortable column click - resets on third click', () => { render( row.id} /> ) const nameHeader = screen.getByText('Name').closest('th') // First click - ascending fireEvent.click(nameHeader!) // Second click - descending fireEvent.click(nameHeader!) // Third click - reset fireEvent.click(nameHeader!) expect(nameHeader).not.toHaveAttribute('aria-sort') }) it('handles sortable column keyboard navigation', () => { render( row.id} /> ) const nameHeader = screen.getByText('Name').closest('th') fireEvent.keyDown(nameHeader!, { key: 'Enter' }) expect(nameHeader).toHaveAttribute('aria-sort', 'ascending') fireEvent.keyDown(nameHeader!, { key: ' ' }) expect(nameHeader).toHaveAttribute('aria-sort', 'descending') }) it('handles row selection - single row', () => { const onSelectionChange = vi.fn() render( row.id} selectable={true} selectedKeys={new Set()} onSelectionChange={onSelectionChange} /> ) const checkboxes = screen.getAllByRole('checkbox') // First checkbox is "select all", row checkboxes start at index 1 fireEvent.click(checkboxes[1]) expect(onSelectionChange).toHaveBeenCalledWith(new Set(['1'])) }) it('handles row selection - deselect row', () => { const onSelectionChange = vi.fn() render( row.id} selectable={true} selectedKeys={new Set(['1'])} onSelectionChange={onSelectionChange} /> ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) expect(onSelectionChange).toHaveBeenCalledWith(new Set()) }) it('handles row selection - select all', () => { const onSelectionChange = vi.fn() render( row.id} selectable={true} selectedKeys={new Set()} onSelectionChange={onSelectionChange} /> ) const checkboxes = screen.getAllByRole('checkbox') // First checkbox is "select all" fireEvent.click(checkboxes[0]) expect(onSelectionChange).toHaveBeenCalledWith(new Set(['1', '2', '3'])) }) it('handles row selection - deselect all when all selected', () => { const onSelectionChange = vi.fn() render( row.id} selectable={true} selectedKeys={new Set(['1', '2', '3'])} onSelectionChange={onSelectionChange} /> ) const checkboxes = screen.getAllByRole('checkbox') // First checkbox is "select all" - clicking it deselects all fireEvent.click(checkboxes[0]) expect(onSelectionChange).toHaveBeenCalledWith(new Set()) }) it('handles row click', () => { const onRowClick = vi.fn() render( row.id} onRowClick={onRowClick} /> ) const row = screen.getByText('Item 1').closest('tr') fireEvent.click(row!) expect(onRowClick).toHaveBeenCalledWith(mockData[0]) }) it('handles row keyboard navigation', () => { const onRowClick = vi.fn() render( row.id} onRowClick={onRowClick} /> ) const row = screen.getByText('Item 1').closest('tr') fireEvent.keyDown(row!, { key: 'Enter' }) expect(onRowClick).toHaveBeenCalledWith(mockData[0]) fireEvent.keyDown(row!, { key: ' ' }) expect(onRowClick).toHaveBeenCalledTimes(2) }) it('applies sticky header class when stickyHeader is true', () => { render( row.id} stickyHeader={true} /> ) const thead = document.querySelector('thead') expect(thead).toHaveClass('sticky') }) it('applies custom className', () => { const { container } = render( row.id} className="custom-class" /> ) expect(container.firstChild).toHaveClass('custom-class') }) it('highlights selected rows', () => { render( row.id} selectable={true} selectedKeys={new Set(['1'])} onSelectionChange={() => {}} /> ) const selectedRow = screen.getByText('Item 1').closest('tr') expect(selectedRow).toHaveClass('bg-brand-500/5') }) it('does not call onSelectionChange when not provided', () => { // This test ensures no error when clicking selection without handler render( row.id} selectable={true} selectedKeys={new Set()} /> ) const checkboxes = screen.getAllByRole('checkbox') // Should not throw fireEvent.click(checkboxes[0]) fireEvent.click(checkboxes[1]) }) it('applies column width when specified', () => { const columnsWithWidth: Column[] = [ { key: 'name', header: 'Name', cell: (row) => row.name, width: '200px' }, { key: 'status', header: 'Status', cell: (row) => row.status }, ] render( row.id} /> ) const nameHeader = screen.getByText('Name').closest('th') expect(nameHeader).toHaveStyle({ width: '200px' }) }) })