/** * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates * SPDX-License-Identifier: MIT */ import * as React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ValidateTrigger } from '@/types'; import { toField, toFieldState } from '@/core/to-field'; import { FormModel } from '@/core/form-model'; import { FieldModel } from '@/core/field-model'; describe('toField', () => { let formModel: FormModel; let fieldModel: FieldModel; beforeEach(() => { formModel = new FormModel(); formModel.init({}); fieldModel = formModel.createField('username') as FieldModel; }); it('should convert FieldModel to Field', () => { const field = toField(fieldModel); expect(field).toBeDefined(); expect(field.name).toBe('username'); expect(field.value).toBeUndefined(); }); it('should expose name property from model', () => { const field = toField(fieldModel); expect(field.name).toBe(fieldModel.name); }); it('should expose value property from model', () => { fieldModel.value = 'John'; const field = toField(fieldModel); expect(field.value).toBe('John'); expect(field.value).toBe(fieldModel.value); }); describe('onChange', () => { it('should update model value with plain value', () => { const field = toField(fieldModel); field.onChange('Alice'); expect(fieldModel.value).toBe('Alice'); expect(field.value).toBe('Alice'); }); it('should handle React change event for input', () => { const field = toField(fieldModel); const mockEvent = { target: { value: 'Bob', }, } as React.ChangeEvent; field.onChange(mockEvent); expect(fieldModel.value).toBe('Bob'); }); it('should handle React change event for checkbox (checked)', () => { const field = toField(fieldModel); const mockEvent = { target: { type: 'checkbox', checked: true, value: 'on', }, } as React.ChangeEvent; field.onChange(mockEvent); expect(fieldModel.value).toBe(true); }); it('should handle React change event for checkbox (unchecked)', () => { const field = toField(fieldModel); const mockEvent = { target: { type: 'checkbox', checked: false, value: 'on', }, } as React.ChangeEvent; field.onChange(mockEvent); expect(fieldModel.value).toBe(false); }); it('should handle numeric value', () => { const field = toField(fieldModel); field.onChange(42); expect(fieldModel.value).toBe(42); }); it('should handle object value', () => { const field = toField(fieldModel); const objValue = { name: 'test', value: 123 }; field.onChange(objValue); expect(fieldModel.value).toEqual(objValue); }); it('should handle array value', () => { const field = toField(fieldModel); const arrValue = ['a', 'b', 'c']; field.onChange(arrValue); expect(fieldModel.value).toEqual(arrValue); }); }); describe('onBlur', () => { it('should call validate when validateTrigger is onBlur', () => { formModel.dispose(); formModel = new FormModel(); formModel.init({ validateTrigger: ValidateTrigger.onBlur }); fieldModel = formModel.createField('username') as FieldModel; const validateSpy = vi.spyOn(fieldModel, 'validate'); const field = toField(fieldModel); field.onBlur?.(); expect(validateSpy).toHaveBeenCalled(); }); it('should not trigger validation when validateTrigger is not onBlur', () => { formModel.dispose(); formModel = new FormModel(); formModel.init({ validateTrigger: ValidateTrigger.onChange }); fieldModel = formModel.createField('username') as FieldModel; const validateSpy = vi.spyOn(fieldModel, 'validate'); const field = toField(fieldModel); field.onBlur?.(); expect(validateSpy).not.toHaveBeenCalled(); }); it('should not trigger validation when validateTrigger is onSubmit', () => { formModel.dispose(); formModel = new FormModel(); formModel.init({ validateTrigger: ValidateTrigger.onSubmit }); fieldModel = formModel.createField('username') as FieldModel; const validateSpy = vi.spyOn(fieldModel, 'validate'); const field = toField(fieldModel); field.onBlur?.(); expect(validateSpy).not.toHaveBeenCalled(); }); }); describe('onFocus', () => { it('should set isTouched to true', () => { const field = toField(fieldModel); expect(fieldModel.state.isTouched).toBe(false); field.onFocus?.(); expect(fieldModel.state.isTouched).toBe(true); }); it('should set isTouched only once', () => { const field = toField(fieldModel); field.onFocus?.(); expect(fieldModel.state.isTouched).toBe(true); field.onFocus?.(); expect(fieldModel.state.isTouched).toBe(true); }); }); it('should expose key property (non-enumerable)', () => { const field = toField(fieldModel); expect((field as any).key).toBe(fieldModel.id); expect(Object.keys(field)).not.toContain('key'); }); it('should hide _fieldModel property (non-enumerable)', () => { const field = toField(fieldModel); expect((field as any)._fieldModel).toBe(fieldModel); expect(Object.keys(field)).not.toContain('_fieldModel'); }); it('should preserve reactivity through getters', () => { const field = toField(fieldModel); expect(field.name).toBe('username'); expect(field.value).toBeUndefined(); fieldModel.value = 'NewValue'; expect(field.value).toBe('NewValue'); }); }); describe('toFieldState', () => { let formModel: FormModel; let fieldModel: FieldModel; beforeEach(() => { formModel = new FormModel(); formModel.init({}); fieldModel = formModel.createField('username') as FieldModel; }); it('should convert FieldModelState to FieldState', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState).toBeDefined(); expect(fieldState.isTouched).toBe(false); expect(fieldState.isDirty).toBe(false); expect(fieldState.invalid).toBe(false); expect(fieldState.isValidating).toBe(false); }); it('should reflect isTouched state', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.isTouched).toBe(false); fieldModel.state.isTouched = true; expect(fieldState.isTouched).toBe(true); }); it('should reflect isDirty state', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.isDirty).toBe(false); // Manually set dirty state fieldModel.state.isDirty = true; expect(fieldState.isDirty).toBe(true); }); it('should reflect invalid state', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.invalid).toBe(false); fieldModel.state.invalid = true; expect(fieldState.invalid).toBe(true); }); it('should reflect isValidating state', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.isValidating).toBe(false); fieldModel.state.isValidating = true; expect(fieldState.isValidating).toBe(true); }); it('should return errors as flat array', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.errors).toBeUndefined(); fieldModel.state.errors = { validate1: ['Error 1', 'Error 2'], validate2: ['Error 3'], }; expect(fieldState.errors).toEqual(['Error 1', 'Error 2', 'Error 3']); }); it('should return warnings as flat array', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.warnings).toBeUndefined(); fieldModel.state.warnings = { validate1: ['Warning 1', 'Warning 2'], validate2: ['Warning 3'], }; expect(fieldState.warnings).toEqual(['Warning 1', 'Warning 2', 'Warning 3']); }); it('should handle empty errors object', () => { const fieldState = toFieldState(fieldModel.state); fieldModel.state.errors = {}; expect(fieldState.errors).toEqual([]); }); it('should handle empty warnings object', () => { const fieldState = toFieldState(fieldModel.state); fieldModel.state.warnings = {}; expect(fieldState.warnings).toEqual([]); }); it('should preserve reactivity through getters', () => { const fieldState = toFieldState(fieldModel.state); expect(fieldState.isTouched).toBe(false); fieldModel.state.isTouched = true; expect(fieldState.isTouched).toBe(true); }); });