/** * Tests for getFirstValidationError utility */ import { getFirstValidationError, getAllValidationErrors, } from '@/lib/forms/utils/getFirstValidationError'; import type { FieldErrors } from 'react-hook-form'; describe('getFirstValidationError', () => { it('returns null for empty errors object', () => { const result = getFirstValidationError({}); expect(result).toBeNull(); }); it('extracts direct error message', () => { const errors: FieldErrors = { name: { message: 'Name is required', type: 'required' }, }; const result = getFirstValidationError(errors); expect(result).toEqual({ field: 'name', message: 'Name is required' }); }); it('extracts nested error message', () => { const errors: FieldErrors = { model_params: { temperature: { message: 'Temperature must be a number', type: 'type' }, }, }; const result = getFirstValidationError(errors); expect(result).toEqual({ field: 'model_params.temperature', message: 'Temperature must be a number', }); }); it('returns first error when multiple fields have errors', () => { const errors: FieldErrors = { name: { message: 'Name is required', type: 'required' }, slug: { message: 'Slug is required', type: 'required' }, }; const result = getFirstValidationError(errors); // Object.keys order is insertion order, so 'name' comes first expect(result?.field).toBe('name'); expect(result?.message).toBe('Name is required'); }); it('handles deeply nested errors', () => { const errors: FieldErrors = { config: { nested: { deep: { message: 'Deep error', type: 'validation' }, }, }, }; const result = getFirstValidationError(errors); expect(result).toEqual({ field: 'config.nested.deep', message: 'Deep error' }); }); it('skips null error entries', () => { const errors: FieldErrors = { name: null as unknown as undefined, slug: { message: 'Slug is required', type: 'required' }, }; const result = getFirstValidationError(errors); expect(result).toEqual({ field: 'slug', message: 'Slug is required' }); }); it('handles error object with ref but no message', () => { // react-hook-form errors may have 'ref' property but no 'message' // We cast to FieldErrors to simulate edge cases const errors = { name: { type: 'required', ref: { current: null } }, slug: { message: 'Slug is required', type: 'required' }, } as unknown as FieldErrors; const result = getFirstValidationError(errors); // Should skip name (no message) and find slug expect(result).toEqual({ field: 'slug', message: 'Slug is required' }); }); }); describe('getAllValidationErrors', () => { it('returns empty array for empty errors object', () => { const result = getAllValidationErrors({}); expect(result).toEqual([]); }); it('returns all errors as flat array', () => { const errors: FieldErrors = { name: { message: 'Name is required', type: 'required' }, slug: { message: 'Slug is required', type: 'required' }, }; const result = getAllValidationErrors(errors); expect(result).toHaveLength(2); expect(result).toContainEqual({ field: 'name', message: 'Name is required' }); expect(result).toContainEqual({ field: 'slug', message: 'Slug is required' }); }); it('flattens nested errors', () => { const errors: FieldErrors = { model_params: { temperature: { message: 'Invalid temperature', type: 'type' }, max_tokens: { message: 'Invalid max tokens', type: 'type' }, }, }; const result = getAllValidationErrors(errors); expect(result).toHaveLength(2); expect(result).toContainEqual({ field: 'model_params.temperature', message: 'Invalid temperature', }); expect(result).toContainEqual({ field: 'model_params.max_tokens', message: 'Invalid max tokens', }); }); it('combines direct and nested errors', () => { const errors: FieldErrors = { name: { message: 'Name is required', type: 'required' }, model_params: { temperature: { message: 'Invalid temperature', type: 'type' }, }, }; const result = getAllValidationErrors(errors); expect(result).toHaveLength(2); expect(result).toContainEqual({ field: 'name', message: 'Name is required' }); expect(result).toContainEqual({ field: 'model_params.temperature', message: 'Invalid temperature', }); }); });