forked from cardosofelipe/fast-next-template
- 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>
135 lines
4.5 KiB
TypeScript
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',
|
|
});
|
|
});
|
|
});
|