import { renderHook, act } from '@testing-library/react' import { useTableKeyboard } from '../../hooks/useTableKeyboard' import { MutableRefObject } from 'react' interface TestData { id: string name: string } describe('useTableKeyboard', () => { const mockData: TestData[] = [ { id: '1', name: 'Item 1' }, { id: '2', name: 'Item 2' }, { id: '3', name: 'Item 3' }, ] const getRowId = (row: TestData) => row.id let mockTableRef: MutableRefObject = { current: null } let mockTableElement: HTMLDivElement let mockRowElements: HTMLTableRowElement[] beforeEach(() => { // Create mock table structure mockTableElement = document.createElement('div') // Create a proper table structure const table = document.createElement('table') const tbody = document.createElement('tbody') mockRowElements = mockData.map((_, index) => { const row = document.createElement('tr') as HTMLTableRowElement row.setAttribute('data-row-index', String(index)) row.tabIndex = 0 // Create a proper jest mock function const focusMock = jest.fn() Object.defineProperty(row, 'focus', { value: focusMock, writable: true, configurable: true, }) tbody.appendChild(row) return row }) table.appendChild(tbody) mockTableElement.appendChild(table) // Update the ref's current property instead of creating a new ref object mockTableRef.current = mockTableElement document.body.appendChild(mockTableElement) }) afterEach(() => { document.body.removeChild(mockTableElement) mockTableRef.current = null }) const defaultProps = { data: mockData, getRowId, tableRef: mockTableRef, enabled: true, } describe('Initialization', () => { it('should initialize with focusedRowIndex of -1', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) expect(result.current.focusedRowIndex).toBe(-1) }) it('should provide setFocusedRowIndex function', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) expect(typeof result.current.setFocusedRowIndex).toBe('function') }) it('should provide handleKeyDown function', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) expect(typeof result.current.handleKeyDown).toBe('function') }) }) describe('Arrow Down Navigation', () => { it('should move focus to next row on ArrowDown', () => { const { result, rerender } = renderHook(() => useTableKeyboard(defaultProps)) // Set initial focused row act(() => { result.current.setFocusedRowIndex(0) }) // Force a rerender to ensure the ref update is captured rerender() const mockEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() // Verify the element was found and focus was called const focusedElement = mockTableElement.querySelector('[data-row-index="1"]') expect(focusedElement).toBeTruthy() expect((mockRowElements[1].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) it('should not move past last row on ArrowDown', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(2) // Last row }) const mockEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() // Should stay on last row expect((mockRowElements[2].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) }) describe('Arrow Up Navigation', () => { it('should move focus to previous row on ArrowUp', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(2) }) const mockEvent = { key: 'ArrowUp', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect((mockRowElements[1].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) it('should not move past first row on ArrowUp', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(0) }) const mockEvent = { key: 'ArrowUp', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() // Should stay on first row expect((mockRowElements[0].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) }) describe('Home/End Navigation', () => { it('should move to first row on Home key', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(2) }) const mockEvent = { key: 'Home', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect((mockRowElements[0].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) it('should move to last row on End key', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(0) }) const mockEvent = { key: 'End', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect((mockRowElements[2].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0) }) }) describe('Enter Key - Row Click', () => { it('should trigger onRowClick when Enter is pressed', () => { const onRowClick = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onRowClick }) ) act(() => { result.current.setFocusedRowIndex(1) }) const mockEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect(onRowClick).toHaveBeenCalledWith(mockData[1]) }) it('should not trigger onRowClick when no row is focused', () => { const onRowClick = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onRowClick }) ) const mockEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(onRowClick).not.toHaveBeenCalled() }) it('should not trigger onRowClick when onRowClick is not provided', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(1) }) const mockEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not throw error expect(mockEvent.preventDefault).not.toHaveBeenCalled() }) }) describe('Space Key - Row Selection', () => { it('should toggle row selection when Space is pressed', () => { const onToggleRow = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onToggleRow }) ) act(() => { result.current.setFocusedRowIndex(1) }) const mockEvent = { key: ' ', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect(onToggleRow).toHaveBeenCalledWith('2') }) it('should not toggle selection when no row is focused', () => { const onToggleRow = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onToggleRow }) ) const mockEvent = { key: ' ', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(onToggleRow).not.toHaveBeenCalled() }) }) describe('Ctrl/Cmd + A - Select All', () => { it('should select all rows on Ctrl+A', () => { const onToggleAll = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onToggleAll }) ) const mockEvent = { key: 'a', ctrlKey: true, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect(onToggleAll).toHaveBeenCalled() }) it('should select all rows on Cmd+A (Mac)', () => { const onToggleAll = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onToggleAll }) ) const mockEvent = { key: 'a', ctrlKey: false, metaKey: true, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect(onToggleAll).toHaveBeenCalled() }) it('should not select all when onToggleAll is not provided', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) const mockEvent = { key: 'a', ctrlKey: true, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not throw error expect(mockEvent.preventDefault).not.toHaveBeenCalled() }) }) describe('Delete Key', () => { it('should trigger delete action when Delete is pressed with selection', () => { const onDelete = jest.fn() const selectedIds = new Set(['1', '2']) const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onDelete, selectedIds }) ) const mockEvent = { key: 'Delete', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).toHaveBeenCalled() expect(onDelete).toHaveBeenCalledWith(selectedIds) }) it('should not trigger delete when no items are selected', () => { const onDelete = jest.fn() const selectedIds = new Set() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onDelete, selectedIds }) ) const mockEvent = { key: 'Delete', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(onDelete).not.toHaveBeenCalled() }) it('should not trigger delete when onDelete is not provided', () => { const selectedIds = new Set(['1', '2']) const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, selectedIds }) ) const mockEvent = { key: 'Delete', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not throw error expect(mockEvent.preventDefault).not.toHaveBeenCalled() }) }) describe('Disabled State', () => { it('should not handle keyboard events when disabled', () => { const onRowClick = jest.fn() const onToggleRow = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onRowClick, onToggleRow, enabled: false, }) ) act(() => { result.current.setFocusedRowIndex(1) }) const mockEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(onRowClick).not.toHaveBeenCalled() expect(mockEvent.preventDefault).not.toHaveBeenCalled() }) }) describe('Empty Data', () => { it('should not handle keyboard events when data is empty', () => { const onRowClick = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, data: [], onRowClick, }) ) const mockEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) expect(mockEvent.preventDefault).not.toHaveBeenCalled() }) }) describe('Edge Cases', () => { it('should handle missing table ref gracefully', () => { const nullRef: MutableRefObject = { current: null } const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, tableRef: nullRef, }) ) const mockEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not throw error expect(mockEvent.preventDefault).toHaveBeenCalled() }) it('should handle missing row element gracefully', () => { // Create empty table const emptyTableElement = document.createElement('div') const emptyTableRef = { current: emptyTableElement } const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, tableRef: emptyTableRef, }) ) const mockEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not throw error expect(mockEvent.preventDefault).toHaveBeenCalled() }) it('should handle invalid focusedRowIndex gracefully', () => { const onRowClick = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onRowClick, }) ) act(() => { result.current.setFocusedRowIndex(999) // Invalid index }) const mockEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEvent) }) // Should not call onRowClick for invalid index expect(onRowClick).not.toHaveBeenCalled() }) }) describe('Integration Scenarios', () => { it('should handle sequential arrow key navigation', () => { const { result } = renderHook(() => useTableKeyboard(defaultProps)) act(() => { result.current.setFocusedRowIndex(0) }) // Press ArrowDown multiple times const mockEventDown = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(mockEventDown) }) const firstFocused = mockTableElement.querySelector('[data-row-index="1"]') expect((firstFocused as any).focus).toHaveBeenCalled() act(() => { result.current.handleKeyDown(mockEventDown) }) const secondFocused = mockTableElement.querySelector('[data-row-index="2"]') expect((secondFocused as any).focus).toHaveBeenCalled() }) it('should handle selection and navigation together', () => { const onToggleRow = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onToggleRow }) ) act(() => { result.current.setFocusedRowIndex(0) }) // Select row const spaceEvent = { key: ' ', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(spaceEvent) }) expect(onToggleRow).toHaveBeenCalledWith('1') // Navigate down const downEvent = { key: 'ArrowDown', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(downEvent) }) const focusedElement = mockTableElement.querySelector('[data-row-index="1"]') expect((focusedElement as any).focus).toHaveBeenCalled() // Select another row act(() => { result.current.handleKeyDown(spaceEvent) }) expect(onToggleRow).toHaveBeenCalledWith('2') }) it('should handle navigation to end and then click', () => { const onRowClick = jest.fn() const { result } = renderHook(() => useTableKeyboard({ ...defaultProps, onRowClick }) ) // Press End const endEvent = { key: 'End', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(endEvent) }) const focusedElement = mockTableElement.querySelector('[data-row-index="2"]') expect((focusedElement as any).focus).toHaveBeenCalled() // Press Enter const enterEvent = { key: 'Enter', ctrlKey: false, metaKey: false, shiftKey: false, preventDefault: jest.fn(), stopPropagation: jest.fn(), } as any act(() => { result.current.handleKeyDown(enterEvent) }) expect(onRowClick).toHaveBeenCalledWith(mockData[2]) }) }) describe('Accessibility', () => { it('should set tabIndex on row elements for keyboard navigation', () => { renderHook(() => useTableKeyboard(defaultProps)) mockRowElements.forEach((row) => { expect(row.tabIndex).toBe(0) }) }) it('should provide data-row-index attributes for screen readers', () => { renderHook(() => useTableKeyboard(defaultProps)) mockRowElements.forEach((row, index) => { expect(row.getAttribute('data-row-index')).toBe(String(index)) }) }) }) })