import { describe, it, expect, vi } from 'vitest' import { nextTick, reactive } from 'vue' // Mock vue lifecycle hooks since we're not in a component vi.mock('vue', async () => { const actual = await vi.importActual('vue') return { ...actual, onMounted: vi.fn(), onUnmounted: vi.fn(), } }) import { useForm } from '../../composables/useForm' describe('useForm', () => { describe('initialization', () => { it('should initialize with provided values', () => { const { data } = useForm({ name: '', email: '' }) expect(data.name).toBe('') expect(data.email).toBe('') }) it('should unwrap nested reactive initial values before cloning', () => { const initial = reactive({ sampleNames: ['Vehicle', 'Drug'], metadata: { groups: ['Control'] }, }) const { data, reset } = useForm(initial) expect(data.sampleNames).toEqual(['Vehicle', 'Drug']) expect(data.metadata.groups).toEqual(['Control']) data.sampleNames.push('Rescue') data.metadata.groups.push('Treatment') reset() expect(data.sampleNames).toEqual(['Vehicle', 'Drug']) expect(data.metadata.groups).toEqual(['Control']) }) it('should start with no errors', () => { const { errors } = useForm({ name: '' }) expect(errors.name).toBeNull() }) it('should start as not dirty', () => { const { isDirty } = useForm({ name: '' }) expect(isDirty.value).toBe(false) }) }) describe('validation - required', () => { it('should fail for empty required field', () => { const { validateField, errors } = useForm( { name: '' }, { name: { required: true } } ) const valid = validateField('name') expect(valid).toBe(false) expect(errors.name).toBe('This field is required') }) it('should pass for filled required field', () => { const { data, validateField, errors } = useForm( { name: '' }, { name: { required: true } } ) data.name = 'Alice' const valid = validateField('name') expect(valid).toBe(true) expect(errors.name).toBeNull() }) it('should use custom required message', () => { const { validateField, errors } = useForm( { name: '' }, { name: { required: 'Name is required' } } ) validateField('name') expect(errors.name).toBe('Name is required') }) }) describe('validation - email', () => { it('should reject invalid email', () => { const { data, validateField, errors } = useForm( { email: '' }, { email: { email: true } } ) data.email = 'notanemail' const valid = validateField('email') expect(valid).toBe(false) expect(errors.email).toBe('Invalid email address') }) it('should accept valid email', () => { const { data, validateField, errors } = useForm( { email: '' }, { email: { email: true } } ) data.email = 'user@example.com' const valid = validateField('email') expect(valid).toBe(true) expect(errors.email).toBeNull() }) }) describe('validation - minLength', () => { it('should reject short strings', () => { const { data, validateField, errors } = useForm( { password: '' }, { password: { minLength: 8 } } ) data.password = 'abc' const valid = validateField('password') expect(valid).toBe(false) expect(errors.password).toBe('Must be at least 8 characters') }) }) describe('validation - pattern', () => { it('should validate with regex', () => { const { data, validateField, errors } = useForm( { code: '' }, { code: { pattern: /^[A-Z]{3}$/ } } ) data.code = 'ab' expect(validateField('code')).toBe(false) expect(errors.code).toBe('Invalid format') data.code = 'ABC' expect(validateField('code')).toBe(true) }) }) describe('validation - custom', () => { it('should support custom validator', () => { const { data, validateField, errors } = useForm( { password: '', confirm: '' }, { confirm: { custom: (value, formData) => value !== formData.password ? 'Passwords must match' : undefined, }, } ) data.password = 'secret123' data.confirm = 'different' expect(validateField('confirm')).toBe(false) expect(errors.confirm).toBe('Passwords must match') data.confirm = 'secret123' expect(validateField('confirm')).toBe(true) }) }) describe('validate (all fields)', () => { it('should validate all fields at once', () => { const { validate, errors } = useForm( { name: '', email: '' }, { name: { required: true }, email: { required: true } } ) const valid = validate() expect(valid).toBe(false) expect(errors.name).not.toBeNull() expect(errors.email).not.toBeNull() }) }) describe('setFieldValue', () => { it('should update field value', () => { const { data, setFieldValue } = useForm({ name: '' }) setFieldValue('name', 'Bob') expect(data.name).toBe('Bob') }) }) describe('reset', () => { it('should reset to initial values', () => { const { data, errors, touched, dirty, reset, setFieldTouched } = useForm( { name: 'initial' }, { name: { required: true } } ) data.name = 'changed' setFieldTouched('name') reset() expect(data.name).toBe('initial') expect(errors.name).toBeNull() expect(touched.name).toBe(false) expect(dirty.name).toBe(false) }) }) describe('replaceState', () => { it('should replace values and clear field state', async () => { const { data, errors, touched, dirty, isDirty, replaceState, setFieldTouched } = useForm>( { name: 'initial', stale: 'remove' }, { name: { required: true } } ) data.name = '' setFieldTouched('name') expect(errors.name).toBe('This field is required') replaceState({ name: 'server', added: 3 }) await nextTick() expect(data.name).toBe('server') expect(data.stale).toBeUndefined() expect(data.added).toBe(3) expect(errors.name).toBeNull() expect(errors.stale).toBeUndefined() expect(touched.name).toBe(false) expect(dirty.name).toBe(false) expect(isDirty.value).toBe(false) }) it('should use replaced values as the next reset baseline', () => { const { data, replaceState, reset } = useForm>({ samples: ['Vehicle'], }) const loaded = reactive({ samples: ['Drug'] }) replaceState(loaded) ;(data.samples as string[]).push('Rescue') reset() expect(data.samples).toEqual(['Drug']) }) }) describe('dirty tracking', () => { it('should track dirty state on setFieldValue', () => { const { setFieldValue } = useForm({ name: 'original' }) setFieldValue('name', 'changed') // Note: dirty tracking happens via watch, which may be async in tests // setFieldValue directly mutates data, watch triggers next tick }) }) describe('handleSubmit', () => { it('should call onSubmit when valid', async () => { const onSubmit = vi.fn() const { data, handleSubmit } = useForm({ name: 'Alice' }) const handler = handleSubmit(onSubmit) await handler() expect(onSubmit).toHaveBeenCalledWith(data) }) it('should not call onSubmit when invalid', async () => { const onSubmit = vi.fn() const { handleSubmit } = useForm( { name: '' }, { name: { required: true } } ) const handler = handleSubmit(onSubmit) await handler() expect(onSubmit).not.toHaveBeenCalled() }) }) })