Files
syndarix/frontend/tests/lib/forms/utils/getFirstValidationError.test.ts
Felipe Cardoso 3c6b14d2bf refactor(forms): extract reusable form utilities and components
- Add getFirstValidationError utility for nested FieldErrors extraction
- Add mergeWithDefaults utilities (deepMergeWithDefaults, type guards)
- Add useValidationErrorHandler hook for toast + tab navigation
- Add FormSelect component with Controller integration
- Add FormTextarea component with register integration
- Refactor AgentTypeForm to use new utilities
- Remove verbose debug logging (now handled by hook)
- Add comprehensive tests (53 new tests, 100 total)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 13:50:36 +01:00

135 lines
4.5 KiB
TypeScript

/**
* 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',
});
});
});