Jelajahi Sumber

chore: add ut to form core

YuanHeDx 3 minggu lalu
induk
melakukan
709bff51a6

+ 195 - 0
packages/node-engine/form/__tests__/create-form.test.ts

@@ -5,9 +5,19 @@
 
 
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 
+import { FieldModel } from '@/core/field-model';
+import { FieldArrayModel } from '@/core/field-array-model';
 import { createForm } from '@/core/create-form';
 import { createForm } from '@/core/create-form';
 
 
 describe('createForm', () => {
 describe('createForm', () => {
+  it('should create form with auto initialization by default', () => {
+    const { form, control } = createForm();
+
+    expect(form).toBeDefined();
+    expect(control).toBeDefined();
+    expect(control._formModel.initialized).toBe(true);
+  });
+
   it('should disableAutoInit work', async () => {
   it('should disableAutoInit work', async () => {
     const { control } = createForm({ disableAutoInit: true });
     const { control } = createForm({ disableAutoInit: true });
 
 
@@ -16,4 +26,189 @@ describe('createForm', () => {
     control.init();
     control.init();
     expect(control._formModel.initialized).toBe(true);
     expect(control._formModel.initialized).toBe(true);
   });
   });
+
+  it('should create form with initial values', () => {
+    const initialValues = {
+      username: 'John',
+      email: 'john@example.com',
+      age: 30,
+    };
+    const { form } = createForm({ initialValues });
+
+    expect(form.initialValues).toEqual(initialValues);
+    expect(form.values).toEqual(initialValues);
+  });
+
+  it('should create form with validation', async () => {
+    const { form } = createForm({
+      initialValues: { username: '' },
+    });
+
+    // Validation should be callable
+    const errors = await form.validate();
+
+    // Without explicit validators, errors should be empty or undefined
+    expect(errors === undefined || Object.keys(errors).length === 0).toBe(true);
+  });
+
+  it('should create form with empty options', () => {
+    const { form, control } = createForm({});
+
+    expect(form).toBeDefined();
+    expect(control).toBeDefined();
+    expect(control._formModel.initialized).toBe(true);
+  });
+
+  it('should create form without options', () => {
+    const { form, control } = createForm();
+
+    expect(form).toBeDefined();
+    expect(control).toBeDefined();
+    expect(control._formModel.initialized).toBe(true);
+  });
+
+  describe('control.getField', () => {
+    it('should get field by name', () => {
+      const { form, control } = createForm({
+        initialValues: {
+          username: 'John',
+        },
+      });
+
+      // Create field first
+      control._formModel.createField('username');
+      const field = control.getField('username');
+
+      expect(field).toBeDefined();
+      expect(field!.name).toBe('username');
+      expect(field!.value).toBe('John');
+    });
+
+    it('should return undefined for non-existent field', () => {
+      const { control } = createForm();
+
+      const field = control.getField('nonexistent');
+
+      expect(field).toBeUndefined();
+    });
+
+    it('should get FieldArray when field is array', () => {
+      const { control } = createForm({
+        initialValues: {
+          users: [{ name: 'Alice' }, { name: 'Bob' }],
+        },
+      });
+
+      // Create field array
+      control._formModel.createFieldArray('users');
+      const fieldArray = control.getField('users');
+
+      expect(fieldArray).toBeDefined();
+      expect(fieldArray!.name).toBe('users');
+      expect(Array.isArray(fieldArray!.value)).toBe(true);
+    });
+
+    it('should return Field for regular field', () => {
+      const { control } = createForm({
+        initialValues: {
+          username: 'John',
+        },
+      });
+
+      control._formModel.createField('username');
+      const field = control.getField('username');
+
+      expect(field).toBeDefined();
+      expect(field!.name).toBe('username');
+      expect((field as any).onChange).toBeDefined();
+    });
+
+    it('should return FieldArray for array field with array methods', () => {
+      const { control } = createForm({
+        initialValues: {
+          users: [{ name: 'Alice' }],
+        },
+      });
+
+      control._formModel.createFieldArray('users');
+      const fieldArrayModel = control._formModel.getField('users');
+      expect(fieldArrayModel).toBeInstanceOf(FieldArrayModel);
+
+      const fieldArray = control.getField('users');
+
+      expect(fieldArray).toBeDefined();
+      expect((fieldArray as any).append).toBeDefined();
+      expect((fieldArray as any).remove).toBeDefined();
+      expect((fieldArray as any).swap).toBeDefined();
+      expect((fieldArray as any).move).toBeDefined();
+    });
+
+    it('should handle nested field names', () => {
+      const { control } = createForm({
+        initialValues: {
+          user: {
+            profile: {
+              name: 'Alice',
+            },
+          },
+        },
+      });
+
+      control._formModel.createField('user.profile.name');
+      const field = control.getField('user.profile.name');
+
+      expect(field).toBeDefined();
+      expect(field!.name).toBe('user.profile.name');
+      expect(field!.value).toBe('Alice');
+    });
+
+    it('should handle array index in field names', () => {
+      const { control } = createForm({
+        initialValues: {
+          users: [{ name: 'Alice' }, { name: 'Bob' }],
+        },
+      });
+
+      control._formModel.createField('users.0.name');
+      const field = control.getField('users.0.name');
+
+      expect(field).toBeDefined();
+      expect(field!.name).toBe('users.0.name');
+      expect(field!.value).toBe('Alice');
+    });
+  });
+
+  describe('control.init', () => {
+    it('should initialize form with new options', () => {
+      const { control } = createForm({ disableAutoInit: true });
+
+      expect(control._formModel.initialized).toBe(false);
+
+      control.init();
+
+      expect(control._formModel.initialized).toBe(true);
+    });
+
+    it('should reinitialize form', () => {
+      const { control } = createForm({
+        initialValues: { username: 'John' },
+      });
+
+      expect(control._formModel.initialized).toBe(true);
+      expect(control._formModel.initialValues).toEqual({ username: 'John' });
+
+      control._formModel.dispose();
+      control.init();
+
+      expect(control._formModel.initialized).toBe(true);
+    });
+  });
+
+  it('should expose _formModel on control', () => {
+    const { control } = createForm();
+
+    expect(control._formModel).toBeDefined();
+    expect(control._formModel.init).toBeDefined();
+    expect(control._formModel.createField).toBeDefined();
+  });
 });
 });

+ 202 - 0
packages/node-engine/form/__tests__/to-field-array.test.ts

@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { beforeEach, describe, expect, it } from 'vitest';
+
+import { toFieldArray } from '@/core/to-field-array';
+import { FormModel } from '@/core/form-model';
+import { FieldArrayModel } from '@/core/field-array-model';
+
+describe('toFieldArray', () => {
+  let formModel: FormModel;
+  let fieldArrayModel: FieldArrayModel;
+
+  beforeEach(() => {
+    formModel = new FormModel();
+    formModel.init({});
+    formModel.createFieldArray('users');
+    fieldArrayModel = formModel.getField<FieldArrayModel>('users')!;
+  });
+
+  it('should convert FieldArrayModel to FieldArray', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    expect(fieldArray).toBeDefined();
+    expect(fieldArray.name).toBe('users');
+    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);
+  });
+
+  it('should expose key property from model id', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    expect(fieldArray.key).toBe(fieldArrayModel.id);
+  });
+
+  it('should expose name property from model path', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    expect(fieldArray.name).toBe('users');
+    expect(fieldArray.name).toBe(fieldArrayModel.path.toString());
+  });
+
+  it('should expose value property from model value', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);
+    expect(fieldArray.value).toBe(fieldArrayModel.value);
+  });
+
+  it('should update model value via onChange', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+    const newValue = [{ name: 'Charlie' }];
+
+    fieldArray.onChange(newValue);
+
+    expect(fieldArrayModel.value).toEqual(newValue);
+    expect(fieldArray.value).toEqual(newValue);
+  });
+
+  it('should map over array elements correctly', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    const mapped = fieldArray.map((field, index) => {
+      expect(field).toBeDefined();
+      expect(field.name).toBe(`users.${index}`);
+      return field.value;
+    });
+
+    expect(mapped).toHaveLength(2);
+    expect(mapped).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);
+  });
+
+  it('should append new item and return Field', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+    const newItem = { name: 'Charlie' };
+
+    const newField = fieldArray.append(newItem);
+
+    expect(newField).toBeDefined();
+    expect(newField.name).toBe('users.0');
+    expect(newField.value).toEqual(newItem);
+    expect(fieldArrayModel.value).toEqual([newItem]);
+  });
+
+  it('should delete item by index', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    fieldArray.delete(1);
+
+    expect(fieldArrayModel.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);
+    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);
+  });
+
+  it('should remove item by index (same as delete)', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    fieldArray.remove(1);
+
+    expect(fieldArrayModel.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);
+    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);
+  });
+
+  it('should swap items at two indices', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    fieldArray.swap(0, 2);
+
+    expect(fieldArrayModel.value).toEqual([
+      { name: 'Charlie' },
+      { name: 'Bob' },
+      { name: 'Alice' },
+    ]);
+    expect(fieldArray.value).toEqual([{ name: 'Charlie' }, { name: 'Bob' }, { name: 'Alice' }]);
+  });
+
+  it('should move item from one index to another', () => {
+    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    fieldArray.move(0, 2);
+
+    expect(fieldArrayModel.value).toEqual([
+      { name: 'Bob' },
+      { name: 'Charlie' },
+      { name: 'Alice' },
+    ]);
+    expect(fieldArray.value).toEqual([{ name: 'Bob' }, { name: 'Charlie' }, { name: 'Alice' }]);
+  });
+
+  it('should hide _fieldModel property (non-enumerable)', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    // _fieldModel should exist but not be enumerable
+    expect((fieldArray as any)._fieldModel).toBe(fieldArrayModel);
+    expect(Object.keys(fieldArray)).not.toContain('_fieldModel');
+  });
+
+  it('should support complex nested operations', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    // Append multiple items
+    fieldArray.append({ name: 'Alice', age: 30 });
+    fieldArray.append({ name: 'Bob', age: 25 });
+    fieldArray.append({ name: 'Charlie', age: 35 });
+
+    expect(fieldArray.value).toHaveLength(3);
+
+    // Map and modify
+    const names = fieldArray.map((field) => field.value.name);
+    expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
+
+    // Swap
+    fieldArray.swap(0, 1);
+    expect(fieldArray.value[0].name).toBe('Bob');
+    expect(fieldArray.value[1].name).toBe('Alice');
+
+    // Remove
+    fieldArray.remove(2);
+    expect(fieldArray.value).toHaveLength(2);
+
+    // Move
+    fieldArray.move(1, 0);
+    expect(fieldArray.value[0].name).toBe('Alice');
+    expect(fieldArray.value[1].name).toBe('Bob');
+  });
+
+  it('should work with empty array', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    // Value might be undefined or empty array initially
+    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);
+
+    const mapped = fieldArray.map((field) => field.value);
+    expect(Array.isArray(mapped)).toBe(true);
+    expect(mapped.length === 0 || mapped.length > 0).toBe(true);
+  });
+
+  it('should preserve reactivity through getters', () => {
+    const fieldArray = toFieldArray(fieldArrayModel);
+
+    // Initial value might be undefined or empty array
+    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);
+
+    // Modify through model
+    fieldArrayModel.value = [{ name: 'Alice' }];
+
+    // Should reflect in fieldArray (getter)
+    expect(fieldArray.value).toEqual([{ name: 'Alice' }]);
+
+    // Modify through fieldArray
+    fieldArray.onChange([{ name: 'Bob' }]);
+
+    // Should reflect in model
+    expect(fieldArrayModel.value).toEqual([{ name: 'Bob' }]);
+  });
+});

+ 331 - 0
packages/node-engine/form/__tests__/to-field.test.ts

@@ -0,0 +1,331 @@
+/**
+ * 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<HTMLInputElement>;
+
+      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<HTMLInputElement>;
+
+      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<HTMLInputElement>;
+
+      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);
+  });
+});

+ 316 - 0
packages/node-engine/form/__tests__/to-form.test.ts

@@ -0,0 +1,316 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { toForm, toFormState } from '@/core/to-form';
+import { FormModel } from '@/core/form-model';
+
+describe('toForm', () => {
+  let formModel: FormModel;
+
+  beforeEach(() => {
+    formModel = new FormModel();
+    formModel.init({
+      initialValues: {
+        username: 'John',
+        email: 'john@example.com',
+        age: 30,
+      },
+    });
+  });
+
+  it('should convert FormModel to Form', () => {
+    const form = toForm(formModel);
+
+    expect(form).toBeDefined();
+    expect(form.initialValues).toEqual({
+      username: 'John',
+      email: 'john@example.com',
+      age: 30,
+    });
+  });
+
+  it('should expose initialValues from model', () => {
+    const form = toForm(formModel);
+
+    expect(form.initialValues).toBe(formModel.initialValues);
+  });
+
+  it('should expose values getter from model', () => {
+    const form = toForm(formModel);
+
+    expect(form.values).toEqual({
+      username: 'John',
+      email: 'john@example.com',
+      age: 30,
+    });
+    expect(form.values).toEqual(formModel.values);
+  });
+
+  it('should expose values setter to update model', () => {
+    const form = toForm(formModel);
+    const newValues = {
+      username: 'Alice',
+      email: 'alice@example.com',
+      age: 25,
+    };
+
+    form.values = newValues;
+
+    expect(formModel.values).toEqual(newValues);
+    expect(form.values).toEqual(newValues);
+  });
+
+  it('should expose state as FormState', () => {
+    const form = toForm(formModel);
+
+    expect(form.state).toBeDefined();
+    expect(form.state.isTouched).toBe(false);
+    expect(form.state.isDirty).toBe(false);
+    expect(form.state.invalid).toBe(false);
+    expect(form.state.isValidating).toBe(false);
+  });
+
+  describe('getValueIn', () => {
+    it('should get value by field name', () => {
+      const form = toForm(formModel);
+
+      expect(form.getValueIn('username')).toBe('John');
+      expect(form.getValueIn('email')).toBe('john@example.com');
+      expect(form.getValueIn('age')).toBe(30);
+    });
+
+    it('should get nested value by path', () => {
+      formModel.values = {
+        user: {
+          profile: {
+            name: 'Alice',
+            age: 25,
+          },
+        },
+      };
+      const form = toForm(formModel);
+
+      expect(form.getValueIn('user.profile.name')).toBe('Alice');
+      expect(form.getValueIn('user.profile.age')).toBe(25);
+    });
+
+    it('should get array value by index', () => {
+      formModel.values = {
+        users: [{ name: 'Alice' }, { name: 'Bob' }],
+      };
+      const form = toForm(formModel);
+
+      expect(form.getValueIn('users.0.name')).toBe('Alice');
+      expect(form.getValueIn('users.1.name')).toBe('Bob');
+    });
+
+    it('should return undefined for non-existent path', () => {
+      const form = toForm(formModel);
+
+      expect(form.getValueIn('nonexistent')).toBeUndefined();
+    });
+  });
+
+  describe('setValueIn', () => {
+    it('should set value by field name', () => {
+      const form = toForm(formModel);
+
+      form.setValueIn('username', 'Bob');
+
+      expect(formModel.values.username).toBe('Bob');
+      expect(form.values.username).toBe('Bob');
+    });
+
+    it('should set nested value by path', () => {
+      formModel.values = {
+        user: {
+          profile: {
+            name: 'Alice',
+            age: 25,
+          },
+        },
+      };
+      const form = toForm(formModel);
+
+      form.setValueIn('user.profile.name', 'Charlie');
+
+      expect(formModel.values.user.profile.name).toBe('Charlie');
+    });
+
+    it('should set array value by index', () => {
+      formModel.values = {
+        users: [{ name: 'Alice' }, { name: 'Bob' }],
+      };
+      const form = toForm(formModel);
+
+      form.setValueIn('users.0.name', 'Charlie');
+
+      expect(formModel.values.users[0].name).toBe('Charlie');
+    });
+
+    it('should create nested structure if not exists', () => {
+      formModel.values = {};
+      const form = toForm(formModel);
+
+      form.setValueIn('user.profile.name', 'Alice');
+
+      expect(formModel.values.user.profile.name).toBe('Alice');
+    });
+  });
+
+  describe('validate', () => {
+    it('should bind model validate method', () => {
+      const form = toForm(formModel);
+
+      expect(form.validate).toBeDefined();
+      expect(typeof form.validate).toBe('function');
+    });
+
+    it('should call form validate method', async () => {
+      const form = toForm(formModel);
+
+      // Validate should be callable
+      const result = await form.validate();
+
+      // Without validators, should return empty object or undefined
+      expect(result === undefined || Object.keys(result || {}).length === 0).toBe(true);
+    });
+  });
+
+  it('should hide _formModel property (non-enumerable)', () => {
+    const form = toForm(formModel);
+
+    expect((form as any)._formModel).toBe(formModel);
+    expect(Object.keys(form)).not.toContain('_formModel');
+  });
+
+  it('should preserve reactivity through getters', () => {
+    const form = toForm(formModel);
+
+    expect(form.values.username).toBe('John');
+
+    formModel.values = { username: 'Alice' };
+
+    expect(form.values.username).toBe('Alice');
+  });
+
+  it('should work with empty initialValues', () => {
+    const emptyFormModel = new FormModel();
+    emptyFormModel.init({});
+    const form = toForm(emptyFormModel);
+
+    expect(form.initialValues).toBeUndefined();
+    expect(form.values).toBeUndefined();
+  });
+});
+
+describe('toFormState', () => {
+  let formModel: FormModel;
+
+  beforeEach(() => {
+    formModel = new FormModel();
+    formModel.init({
+      initialValues: {
+        username: 'John',
+        email: 'john@example.com',
+      },
+    });
+  });
+
+  it('should convert FormModelState to FormState', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState).toBeDefined();
+    expect(formState.isTouched).toBe(false);
+    expect(formState.isDirty).toBe(false);
+    expect(formState.invalid).toBe(false);
+    expect(formState.isValidating).toBe(false);
+  });
+
+  it('should reflect isTouched state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.isTouched).toBe(false);
+
+    formModel.state.isTouched = true;
+
+    expect(formState.isTouched).toBe(true);
+  });
+
+  it('should reflect isDirty state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.isDirty).toBe(false);
+
+    // Manually set dirty state
+    formModel.state.isDirty = true;
+
+    expect(formState.isDirty).toBe(true);
+  });
+
+  it('should reflect invalid state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.invalid).toBe(false);
+
+    formModel.state.invalid = true;
+
+    expect(formState.invalid).toBe(true);
+  });
+
+  it('should reflect isValidating state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.isValidating).toBe(false);
+
+    formModel.state.isValidating = true;
+
+    expect(formState.isValidating).toBe(true);
+  });
+
+  it('should expose errors from model state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.errors).toBeUndefined();
+
+    formModel.state.errors = {
+      username: 'Username is required',
+      email: 'Invalid email format',
+    };
+
+    expect(formState.errors).toEqual({
+      username: 'Username is required',
+      email: 'Invalid email format',
+    });
+  });
+
+  it('should expose warnings from model state', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.warnings).toBeUndefined();
+
+    formModel.state.warnings = {
+      username: 'Username should be longer',
+      email: 'Consider using a different email',
+    };
+
+    expect(formState.warnings).toEqual({
+      username: 'Username should be longer',
+      email: 'Consider using a different email',
+    });
+  });
+
+  it('should preserve reactivity through getters', () => {
+    const formState = toFormState(formModel.state);
+
+    expect(formState.isTouched).toBe(false);
+
+    formModel.state.isTouched = true;
+
+    expect(formState.isTouched).toBe(true);
+  });
+});