import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderHook, act } from '@testing-library/react' import { useSelection } from './use-selection' import { useWidgetStore } from '../../stores/widget-store' import type { TableRow } from '../types' describe('useSelection', () => { const testRows: TableRow[] = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, { id: 4, name: 'David' }, ] // Clear widget store before each test beforeEach(() => { useWidgetStore.getState().clearWidgets() }) describe('initialization', () => { it('should initialize with empty selection', () => { const { result } = renderHook(() => useSelection('test-widget')) expect(result.current.selectedIds.length).toBe(0) }) it('should initialize with provided selection', () => { const initialIds = new Set([1, 2]) const { result } = renderHook(() => useSelection('test-widget')) // Set initial selection using selectRows act(() => { result.current.selectRows(Array.from(initialIds)) }) expect(result.current.selectedIds.length).toBe(2) expect(result.current.isSelected(1)).toBe(true) expect(result.current.isSelected(2)).toBe(true) expect(result.current.isSelected(3)).toBe(false) }) }) describe('isSelected', () => { it('should return true for selected row', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) expect(result.current.isSelected(1)).toBe(true) }) it('should return false for non-selected row', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) expect(result.current.isSelected(2)).toBe(false) }) it('should handle string IDs', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows(['row-1', 'row-2']) }) expect(result.current.isSelected('row-1')).toBe(true) expect(result.current.isSelected('row-3')).toBe(false) }) }) describe('isAllSelected', () => { it('should return true when all rows are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3, 4]) }) expect(result.current.isAllSelected(testRows)).toBe(true) }) it('should return false when some rows are not selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2]) }) expect(result.current.isAllSelected(testRows)).toBe(false) }) it('should return false when no rows are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) expect(result.current.isAllSelected(testRows)).toBe(false) }) it('should return false for empty rows array', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) expect(result.current.isAllSelected([])).toBe(false) }) }) describe('isIndeterminate', () => { it('should return true when some but not all rows are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2]) }) expect(result.current.isIndeterminate(testRows)).toBe(true) }) it('should return false when all rows are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3, 4]) }) expect(result.current.isIndeterminate(testRows)).toBe(false) }) it('should return false when no rows are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) expect(result.current.isIndeterminate(testRows)).toBe(false) }) it('should return false for empty rows array', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) expect(result.current.isIndeterminate([])).toBe(false) }) }) describe('toggleRow', () => { it('should select unselected row', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.toggleRow(1) }) expect(result.current.isSelected(1)).toBe(true) }) it('should deselect selected row', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) act(() => { result.current.toggleRow(1) }) expect(result.current.isSelected(1)).toBe(false) }) it('should call onSelectionChange callback', () => { const onSelectionChange = vi.fn() const { result } = renderHook(() => useSelection('test-widget', { onSelectionChange }), ) act(() => { result.current.toggleRow(1) }) expect(onSelectionChange).toHaveBeenCalledWith([1]) }) it('should preserve other selections when toggling', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2]) }) act(() => { result.current.toggleRow(3) }) expect(result.current.isSelected(1)).toBe(true) expect(result.current.isSelected(2)).toBe(true) expect(result.current.isSelected(3)).toBe(true) }) }) describe('toggleAll', () => { it('should select all rows when none are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.toggleAll(testRows) }) expect(result.current.isAllSelected(testRows)).toBe(true) }) it('should select all rows when some are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) act(() => { result.current.toggleAll(testRows) }) expect(result.current.isAllSelected(testRows)).toBe(true) }) it('should deselect all rows when all are selected', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3, 4]) }) act(() => { result.current.toggleAll(testRows) }) testRows.forEach((row) => { expect(result.current.isSelected(row.id)).toBe(false) }) }) it('should preserve selections not in provided rows', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([5, 6]) }) act(() => { result.current.toggleAll(testRows) }) // Rows 5 and 6 should still be selected expect(result.current.isSelected(5)).toBe(true) expect(result.current.isSelected(6)).toBe(true) // Test rows should also be selected expect(result.current.isAllSelected(testRows)).toBe(true) }) it('should call onSelectionChange callback', () => { const onSelectionChange = vi.fn() const { result } = renderHook(() => useSelection('test-widget', { onSelectionChange }), ) act(() => { result.current.toggleAll(testRows) }) expect(onSelectionChange).toHaveBeenCalled() }) }) describe('selectRows', () => { it('should select specified rows', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2]) }) expect(result.current.isSelected(1)).toBe(true) expect(result.current.isSelected(2)).toBe(true) expect(result.current.isSelected(3)).toBe(false) }) it('should add to existing selection', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) act(() => { result.current.selectRows([2, 3]) }) expect(result.current.isSelected(1)).toBe(true) expect(result.current.isSelected(2)).toBe(true) expect(result.current.isSelected(3)).toBe(true) }) it('should call onSelectionChange callback', () => { const onSelectionChange = vi.fn() const { result } = renderHook(() => useSelection('test-widget', { onSelectionChange }), ) act(() => { result.current.selectRows([1, 2]) }) expect(onSelectionChange).toHaveBeenCalledWith([1, 2]) }) }) describe('deselectRows', () => { it('should deselect specified rows', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3]) }) act(() => { result.current.deselectRows([1, 2]) }) expect(result.current.isSelected(1)).toBe(false) expect(result.current.isSelected(2)).toBe(false) expect(result.current.isSelected(3)).toBe(true) }) it('should handle deselecting non-selected rows', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1]) }) act(() => { result.current.deselectRows([2, 3]) }) expect(result.current.isSelected(1)).toBe(true) expect(result.current.selectedIds.length).toBe(1) }) it('should call onSelectionChange callback', () => { const onSelectionChange = vi.fn() const { result } = renderHook(() => useSelection('test-widget', { onSelectionChange }), ) act(() => { result.current.selectRows([1, 2]) }) act(() => { result.current.deselectRows([1]) }) expect(onSelectionChange).toHaveBeenCalledWith([2]) }) }) describe('clearSelection', () => { it('should clear all selections', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3]) }) act(() => { result.current.clearSelection() }) expect(result.current.selectedIds.length).toBe(0) }) it('should handle clearing empty selection', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.clearSelection() }) expect(result.current.selectedIds.length).toBe(0) }) it('should call onSelectionChange callback', () => { const onSelectionChange = vi.fn() const { result } = renderHook(() => useSelection('test-widget', { onSelectionChange }), ) act(() => { result.current.selectRows([1, 2]) }) act(() => { result.current.clearSelection() }) expect(onSelectionChange).toHaveBeenCalledWith([]) }) }) describe('edge cases', () => { it('should handle mixed string and number IDs', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 'row-2', 3, 'row-4']) }) expect(result.current.isSelected(1)).toBe(true) expect(result.current.isSelected('row-2')).toBe(true) expect(result.current.isSelected(3)).toBe(true) expect(result.current.isSelected('row-4')).toBe(true) }) it('should handle multiple toggle operations', () => { const { result } = renderHook(() => useSelection('test-widget')) act(() => { result.current.toggleRow(1) }) expect(result.current.isSelected(1)).toBe(true) act(() => { result.current.toggleRow(1) }) expect(result.current.isSelected(1)).toBe(false) act(() => { result.current.toggleRow(1) }) expect(result.current.isSelected(1)).toBe(true) }) it('should maintain selection across page changes', () => { const page1Rows: TableRow[] = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ] const page2Rows: TableRow[] = [ { id: 3, name: 'Charlie' }, { id: 4, name: 'David' }, ] const { result } = renderHook(() => useSelection('test-widget')) // Select on page 1 act(() => { result.current.selectRows([1, 2]) }) // Check selections still valid for page 2 rows expect(result.current.isAllSelected(page2Rows)).toBe(false) expect(result.current.isIndeterminate(page2Rows)).toBe(false) // Select all on page 2 act(() => { result.current.toggleAll(page2Rows) }) // All 4 rows should be selected expect(result.current.selectedIds.length).toBe(4) expect(result.current.isAllSelected([...page1Rows, ...page2Rows])).toBe( true, ) }) it('should persist state in widget store', () => { const { result, unmount } = renderHook(() => useSelection('test-widget')) act(() => { result.current.selectRows([1, 2, 3]) }) expect(result.current.selectedIds.length).toBe(3) // Unmount and remount - state should persist unmount() const { result: newResult } = renderHook(() => useSelection('test-widget'), ) expect(newResult.current.selectedIds.length).toBe(3) expect(newResult.current.isSelected(1)).toBe(true) expect(newResult.current.isSelected(2)).toBe(true) expect(newResult.current.isSelected(3)).toBe(true) }) }) })