import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { UnifiedTable } from '../../index' import { ColumnConfig, BulkAction } from '../../types' import { Mail, Trash } from 'lucide-react' import { useState } from 'react' interface TestData { id: string name: string email: string status: string } describe('UnifiedTable - Integration Tests with Selection', () => { const mockData: TestData[] = Array.from({ length: 50 }, (_, i) => ({ id: `${i + 1}`, name: `User ${i + 1}`, email: `user${i + 1}@example.com`, status: i % 2 === 0 ? 'active' : 'inactive', })) const columns: ColumnConfig[] = [ { id: 'name', header: 'Name', accessorKey: 'name', sortable: true, }, { id: 'email', header: 'Email', accessorKey: 'email', sortable: true, }, { id: 'status', header: 'Status', accessorKey: 'status', sortable: true, }, ] const getRowId = (row: TestData) => row.id describe('Basic Selection Functionality', () => { it('should render table with selection enabled', () => { render( ) // Should have checkboxes const checkboxes = screen.getAllByRole('checkbox') expect(checkboxes.length).toBeGreaterThan(0) }) it('should select individual rows', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') // First checkbox is "select all", second is first row fireEvent.click(checkboxes[1]) expect(screen.getByText(/1 item selected/)).toBeInTheDocument() }) it('should select multiple rows individually', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) fireEvent.click(checkboxes[2]) fireEvent.click(checkboxes[3]) expect(screen.getByText(/3 items selected/)).toBeInTheDocument() }) it('should deselect rows', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) expect(screen.getByText(/1 item selected/)).toBeInTheDocument() fireEvent.click(checkboxes[1]) expect(screen.queryByText(/item selected/)).not.toBeInTheDocument() }) }) describe('Select All on Current Page', () => { it('should select all rows on current page', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') // First checkbox is "select all" fireEvent.click(checkboxes[0]) expect(screen.getByText(/10 items selected on this page/)).toBeInTheDocument() }) it('should deselect all rows when clicking select all again', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[0]) expect(screen.getByText(/10 items selected/)).toBeInTheDocument() fireEvent.click(checkboxes[0]) expect(screen.queryByText(/items selected/)).not.toBeInTheDocument() }) }) describe('Select All Pages', () => { it('should show select all pages option when pagination enabled', () => { render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[0]) // Check that the select all button is present (there's a span and button, target the button) expect(screen.getByRole('button', { name: /Select all 50 items/ })).toBeInTheDocument() }) it('should select all pages when button clicked', async () => { render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[0]) const selectAllButton = screen.getByRole('button', { name: /Select all 50 items/ }) fireEvent.click(selectAllButton) await waitFor(() => { // After selecting all pages, the bulk action bar should show the total count expect(screen.getByText((content, element) => { const tagName = element?.tagName.toLowerCase() || '' const className = element?.className || '' const textContent = element?.textContent || '' return tagName === 'span' && typeof className === 'string' && className.includes('text-blue-600') && textContent.includes('All') && textContent.includes('50') && textContent.includes('items selected') })).toBeInTheDocument() }) }) }) describe('Bulk Actions with Selection', () => { it('should show bulk action bar when items selected', () => { const bulkActions: BulkAction[] = [ { id: 'email', label: 'Send Email', icon: Mail, variant: 'gradient-purple', onClick: jest.fn(), }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) expect(screen.getByText('Send Email')).toBeInTheDocument() }) it('should execute bulk action on selected items', async () => { const onBulkAction = jest.fn() const bulkActions: BulkAction[] = [ { id: 'email', label: 'Send Email', icon: Mail, variant: 'gradient-purple', onClick: onBulkAction, }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) fireEvent.click(checkboxes[2]) const emailButton = screen.getByText('Send Email') fireEvent.click(emailButton) await waitFor(() => { expect(onBulkAction).toHaveBeenCalledWith(expect.any(Set)) const selectedIds = onBulkAction.mock.calls[0][0] expect(selectedIds.size).toBe(2) }) }) it('should clear selection after bulk action', async () => { const onBulkAction = jest.fn().mockResolvedValue(undefined) const bulkActions: BulkAction[] = [ { id: 'email', label: 'Send Email', icon: Mail, variant: 'gradient-purple', onClick: onBulkAction, }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) const emailButton = screen.getByText('Send Email') fireEvent.click(emailButton) await waitFor(() => { expect(screen.queryByText(/items selected/)).not.toBeInTheDocument() }) }) it('should show confirmation dialog for actions with confirmMessage', () => { const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(true) const onBulkAction = jest.fn() const bulkActions: BulkAction[] = [ { id: 'delete', label: 'Delete', icon: Trash, variant: 'gradient-orange', onClick: onBulkAction, confirmMessage: 'Are you sure you want to delete {count} items?', }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) fireEvent.click(checkboxes[2]) const deleteButton = screen.getByText('Delete') fireEvent.click(deleteButton) expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete 2 items?') confirmSpy.mockRestore() }) it('should not execute action when confirmation cancelled', () => { const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(false) const onBulkAction = jest.fn() const bulkActions: BulkAction[] = [ { id: 'delete', label: 'Delete', icon: Trash, variant: 'gradient-orange', onClick: onBulkAction, confirmMessage: 'Are you sure?', }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) const deleteButton = screen.getByText('Delete') fireEvent.click(deleteButton) expect(onBulkAction).not.toHaveBeenCalled() confirmSpy.mockRestore() }) it('should enforce maxSelection limit', () => { const onBulkAction = jest.fn() const bulkActions: BulkAction[] = [ { id: 'limited', label: 'Limited Action', icon: Mail, variant: 'default', onClick: onBulkAction, maxSelection: 2, }, ] render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) fireEvent.click(checkboxes[2]) fireEvent.click(checkboxes[3]) const actionButton = screen.getByText('Limited Action').closest('button') expect(actionButton).toBeDisabled() expect(actionButton).toHaveAttribute('title', 'Maximum 2 items can be selected for this action') expect(onBulkAction).not.toHaveBeenCalled() }) }) describe('Clear Selection', () => { it('should clear selection when clear button clicked', () => { const bulkActions: BulkAction[] = [ { id: 'email', label: 'Send Email', icon: Mail, variant: 'default', onClick: jest.fn(), }, ] const { container } = render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) fireEvent.click(checkboxes[2]) expect(screen.getByText(/2 items selected/)).toBeInTheDocument() const clearButton = screen.getByText('Clear') fireEvent.click(clearButton) // After clearing, the bulk action bar is hidden with CSS (not removed from DOM) // Check that it's hidden via aria-hidden attribute const bulkActionBar = container.querySelector('[aria-hidden="true"]') expect(bulkActionBar).toBeInTheDocument() }) }) describe('Selection with Pagination', () => { it('should maintain selection when changing pages', async () => { const TestComponent = () => { const [currentPage, setCurrentPage] = useState(1) const pageSize = 10 const startIndex = (currentPage - 1) * pageSize const currentData = mockData.slice(startIndex, startIndex + pageSize) return ( ) } render() // Select items on first page const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) expect(screen.getByText(/1 item selected/)).toBeInTheDocument() // Go to next page const nextButton = screen.getByTitle('Next page') fireEvent.click(nextButton) await waitFor(() => { expect(screen.getByText(/Page 2/)).toBeInTheDocument() }) // Selection should still show expect(screen.getByText(/1 item selected/)).toBeInTheDocument() }) }) describe('External Selection Control', () => { it('should call onSelectionChange callback', () => { const onSelectionChange = jest.fn() render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[1]) expect(onSelectionChange).toHaveBeenCalled() const selectedIds = onSelectionChange.mock.calls[0][0] expect(selectedIds.size).toBe(1) }) it('should call onSelectAllPages callback', async () => { const onSelectAllPages = jest.fn() render( ) const checkboxes = screen.getAllByRole('checkbox') fireEvent.click(checkboxes[0]) const selectAllButton = screen.getByRole('button', { name: /Select all 50 items/ }) fireEvent.click(selectAllButton) await waitFor(() => { expect(onSelectAllPages).toHaveBeenCalledWith(true) }) }) }) describe('Edge Cases', () => { it('should handle empty data with selection enabled', () => { render( ) expect(screen.getByText('No results found.')).toBeInTheDocument() }) it('should not show bulk actions bar when no items selected', () => { const bulkActions: BulkAction[] = [ { id: 'email', label: 'Send Email', icon: Mail, variant: 'default', onClick: jest.fn(), }, ] const { container } = render( ) // Bulk action bar is hidden via CSS, not removed from DOM // Check that it has aria-hidden="true" and pointer-events-none const bulkActionBar = container.querySelector('[aria-hidden="true"]') expect(bulkActionBar).toBeInTheDocument() expect(bulkActionBar).toHaveClass('pointer-events-none') }) }) })