/** * Tests for useValidationErrorHandler hook */ import { renderHook } from '@testing-library/react'; import { toast } from 'sonner'; import { useValidationErrorHandler } from '@/lib/forms/hooks/useValidationErrorHandler'; import type { FieldErrors } from 'react-hook-form'; // Mock sonner toast jest.mock('sonner', () => ({ toast: { error: jest.fn(), }, })); // Mock console.error to track debug logging const originalConsoleError = console.error; let consoleErrorMock: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { consoleErrorMock.mockRestore(); console.error = originalConsoleError; }); describe('useValidationErrorHandler', () => { describe('basic functionality', () => { it('shows toast with first error message', () => { const { result } = renderHook(() => useValidationErrorHandler({ debug: false })); const errors: FieldErrors = { name: { message: 'Name is required', type: 'required' }, }; result.current.onValidationError(errors); expect(toast.error).toHaveBeenCalledWith('Please fix form errors', { description: 'name: Name is required', }); }); it('uses custom toast title when provided', () => { const { result } = renderHook(() => useValidationErrorHandler({ toastTitle: 'Validation Failed', debug: false, }) ); const errors: FieldErrors = { email: { message: 'Invalid email', type: 'pattern' }, }; result.current.onValidationError(errors); expect(toast.error).toHaveBeenCalledWith('Validation Failed', { description: 'email: Invalid email', }); }); it('does nothing when no errors', () => { const { result } = renderHook(() => useValidationErrorHandler({ debug: false })); result.current.onValidationError({}); expect(toast.error).not.toHaveBeenCalled(); }); }); describe('nested errors', () => { it('handles nested field errors', () => { const { result } = renderHook(() => useValidationErrorHandler({ debug: false })); const errors: FieldErrors = { model_params: { temperature: { message: 'Temperature must be between 0 and 2', type: 'max' }, }, }; result.current.onValidationError(errors); expect(toast.error).toHaveBeenCalledWith('Please fix form errors', { description: 'model_params.temperature: Temperature must be between 0 and 2', }); }); }); describe('tab navigation', () => { it('navigates to correct tab when mapping provided', () => { const setActiveTab = jest.fn(); const tabMapping = { name: 'basic', model_params: 'model', }; const { result } = renderHook(() => useValidationErrorHandler({ tabMapping, setActiveTab, debug: false, }) ); const errors: FieldErrors = { model_params: { temperature: { message: 'Invalid', type: 'type' }, }, }; result.current.onValidationError(errors); expect(setActiveTab).toHaveBeenCalledWith('model'); }); it('does not navigate if field not in mapping', () => { const setActiveTab = jest.fn(); const tabMapping = { name: 'basic', }; const { result } = renderHook(() => useValidationErrorHandler({ tabMapping, setActiveTab, debug: false, }) ); const errors: FieldErrors = { unknown_field: { message: 'Error', type: 'validation' }, }; result.current.onValidationError(errors); expect(setActiveTab).not.toHaveBeenCalled(); }); it('does not crash when setActiveTab not provided', () => { const tabMapping = { name: 'basic' }; const { result } = renderHook(() => useValidationErrorHandler({ tabMapping, // setActiveTab not provided debug: false, }) ); const errors: FieldErrors = { name: { message: 'Required', type: 'required' }, }; expect(() => result.current.onValidationError(errors)).not.toThrow(); }); }); describe('debug logging', () => { it('logs errors when debug is true', () => { const { result } = renderHook(() => useValidationErrorHandler({ debug: true })); const errors: FieldErrors = { name: { message: 'Required', type: 'required' }, }; result.current.onValidationError(errors); expect(consoleErrorMock).toHaveBeenCalledWith('[Form Validation] Errors:', errors); }); it('does not log errors when debug is false', () => { const { result } = renderHook(() => useValidationErrorHandler({ debug: false })); const errors: FieldErrors = { name: { message: 'Required', type: 'required' }, }; result.current.onValidationError(errors); expect(consoleErrorMock).not.toHaveBeenCalled(); }); }); describe('memoization', () => { it('returns stable callback reference', () => { const { result, rerender } = renderHook(() => useValidationErrorHandler({ debug: false })); const firstCallback = result.current.onValidationError; rerender(); const secondCallback = result.current.onValidationError; expect(firstCallback).toBe(secondCallback); }); it('returns new callback when options change', () => { const { result, rerender } = renderHook( ({ title }) => useValidationErrorHandler({ toastTitle: title, debug: false }), { initialProps: { title: 'Error A' } } ); const firstCallback = result.current.onValidationError; rerender({ title: 'Error B' }); const secondCallback = result.current.onValidationError; expect(firstCallback).not.toBe(secondCallback); }); }); });