import { act, renderHook } from '@testing-library/react-native'; import { useInput } from './use-input'; import { TextInput, type NativeSyntheticEvent, type TextInputSelectionChangeEventData, } from 'react-native'; describe('useInput', () => { const defaultProps = { maxLength: 4, onChange: jest.fn(), }; beforeEach(() => { jest.clearAllMocks(); }); describe('Initialization', () => { test('initializes with default values', () => { const { result } = renderHook(() => useInput({ maxLength: 4 })); expect(result.current.value).toBe(''); expect(result.current.isFocused).toBe(false); expect(result.current.contextValue.slots).toHaveLength(4); }); test('initializes with default value', () => { const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '123' }) ); expect(result.current.value).toBe('123'); expect(result.current.contextValue.slots).toHaveLength(4); }); }); describe('Value Changes', () => { test('updates value on valid input', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, onChange })); act(() => { result.current.handlers.onChangeText('123'); }); expect(result.current.value).toBe('123'); expect(onChange).toHaveBeenCalledWith('123'); }); test('limits input to maxLength', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, onChange })); act(() => { result.current.handlers.onChangeText('12345'); }); expect(result.current.value).toBe('1234'); expect(onChange).toHaveBeenCalledWith('1234'); }); test('validates input against pattern (string)', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, pattern: '[0-9]', onChange }) ); act(() => { result.current.handlers.onChangeText('abc'); }); expect(result.current.value).toBe(''); expect(onChange).not.toHaveBeenCalled(); }); test('validates input against pattern (RegExp string)', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, pattern: '^[0-9]+$', onChange }) ); act(() => { result.current.handlers.onChangeText('123'); }); expect(result.current.value).toBe('123'); expect(onChange).toHaveBeenCalledWith('123'); }); test('calls onComplete when maxLength is reached', () => { const onComplete = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, onComplete }) ); act(() => { result.current.handlers.onChangeText('1234'); }); expect(onComplete).toHaveBeenCalledWith('1234'); }); test('applies pasteTransformer on paste operations', () => { const onChange = jest.fn(); const pasteTransformer = jest.fn((text) => text.replace(/\D/g, '')); const { result } = renderHook(() => useInput({ maxLength: 4, onChange, pasteTransformer }) ); // Simulate pasting "Code: 1234" act(() => { result.current.handlers.onChangeText('Code: 1234'); }); expect(pasteTransformer).toHaveBeenCalledWith('Code: 1234'); expect(result.current.value).toBe('1234'); expect(onChange).toHaveBeenCalledWith('1234'); }); test('onSelectionChange resets cursor when it moves from end', () => { const { result } = renderHook(() => useInput({ maxLength: 4 })); act(() => { result.current.handlers.onChangeText('12'); }); // Simulate cursor moving to position 0 (arrow key navigation) act(() => { result.current.handlers.onSelectionChange({ nativeEvent: { selection: { start: 0, end: 0 } }, } as NativeSyntheticEvent); }); // After re-render, value is unchanged (no data was lost) expect(result.current.value).toBe('12'); }); test('does not apply pasteTransformer on normal typing', () => { const onChange = jest.fn(); const pasteTransformer = jest.fn((text) => text.replace(/\D/g, '')); const { result } = renderHook(() => useInput({ maxLength: 4, onChange, pasteTransformer }) ); // Simulate typing "1" act(() => { result.current.handlers.onChangeText('1'); }); expect(pasteTransformer).not.toHaveBeenCalled(); expect(result.current.value).toBe('1'); expect(onChange).toHaveBeenCalledWith('1'); }); }); describe('Focus Management', () => { test('handles focus state', () => { const { result } = renderHook(() => useInput({ maxLength: 4 })); act(() => { result.current.handlers.onFocus(); }); expect(result.current.isFocused).toBe(true); act(() => { result.current.handlers.onBlur(); }); expect(result.current.isFocused).toBe(false); }); }); describe('Actions', () => { test('clear action resets value', () => { const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '123' }) ); act(() => { result.current.actions.clear(); }); expect(result.current.value).toBe(''); }); describe('focusSlot', () => { test('truncates value to the given index', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 6, defaultValue: '123456', onChange }) ); act(() => { result.current.actions.focusSlot(3); }); expect(result.current.value).toBe('123'); expect(onChange).toHaveBeenCalledWith('123'); }); test('focusSlot(0) clears the value entirely', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 6, defaultValue: '123456', onChange }) ); act(() => { result.current.actions.focusSlot(0); }); expect(result.current.value).toBe(''); expect(onChange).toHaveBeenCalledWith(''); }); test('clamps index above maxLength to maxLength', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '1234', onChange }) ); act(() => { result.current.actions.focusSlot(10); }); expect(result.current.value).toBe('1234'); expect(onChange).toHaveBeenCalledWith('1234'); }); test('clamps negative index to 0', () => { const onChange = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '1234', onChange }) ); act(() => { result.current.actions.focusSlot(-1); }); expect(result.current.value).toBe(''); expect(onChange).toHaveBeenCalledWith(''); }); test('focuses the input', () => { const mockFocus = jest.fn(); const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '1234' }) ); (result.current.inputRef as React.MutableRefObject).current = { focus: mockFocus, clear: jest.fn(), } as unknown as TextInput; act(() => { result.current.actions.focusSlot(2); }); expect(mockFocus).toHaveBeenCalled(); }); test('marks correct slot as active after focusSlot', () => { const { result } = renderHook(() => useInput({ maxLength: 6, defaultValue: '123456' }) ); act(() => { result.current.actions.focusSlot(3); result.current.handlers.onFocus(); }); const slots = result.current.contextValue.slots; expect(slots[3]?.isActive).toBe(true); expect(slots[2]?.isActive).toBe(false); expect(slots[4]?.isActive).toBe(false); }); }); }); describe('Slots', () => { test('generates correct slots with placeholder', () => { const { result } = renderHook(() => useInput({ maxLength: 4, placeholder: '0000' }) ); const firstSlot = result.current.contextValue.slots[0]; expect(firstSlot?.placeholderChar).toBe('0'); expect(firstSlot?.char).toBe(null); }); test('generates correct slots with value', () => { const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '12' }) ); const slots = result.current.contextValue.slots; expect(slots[0]?.char).toBe('1'); expect(slots[1]?.char).toBe('2'); expect(slots[2]?.char).toBe(null); }); test('marks active slot correctly', () => { const { result } = renderHook(() => useInput({ maxLength: 4, defaultValue: '12' }) ); act(() => { result.current.handlers.onFocus(); }); const activeSlot = result.current.contextValue.slots[2]; expect(activeSlot?.isActive).toBe(true); expect(activeSlot?.hasFakeCaret).toBe(true); }); }); describe('pattern validation', () => { test('handles undefined pattern', () => { const { result } = renderHook(() => useInput({ ...defaultProps, pattern: undefined, }) ); act(() => { result.current.handlers.onChangeText('123'); }); expect(result.current.value).toBe('123'); }); test('handles string pattern', () => { const { result } = renderHook(() => useInput({ ...defaultProps, pattern: '^[0-9]+$', }) ); // Test valid input act(() => { result.current.handlers.onChangeText('123'); }); expect(result.current.value).toBe('123'); // Test invalid input act(() => { result.current.handlers.onChangeText('abc'); }); expect(result.current.value).toBe('123'); // Value should not change }); test('handles RegExp pattern', () => { const { result } = renderHook(() => useInput({ ...defaultProps, pattern: /^[0-9]+$/, }) ); // Test valid input act(() => { result.current.handlers.onChangeText('123'); }); expect(result.current.value).toBe('123'); // Test invalid input act(() => { result.current.handlers.onChangeText('abc'); }); expect(result.current.value).toBe('123'); // Value should not change }); }); test('focus action focuses the input', () => { const mockFocus = jest.fn(); const { result } = renderHook(() => useInput({ ...defaultProps, }) ); // Mock the inputRef.current (result.current.inputRef as React.MutableRefObject).current = { focus: mockFocus, clear: jest.fn(), } as unknown as TextInput; act(() => { result.current.actions.focus(); }); expect(mockFocus).toHaveBeenCalled(); }); });