import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Errors, FeedbackLevel, ValidateTrigger } from '@/types'; import { FormModel } from '@/core/form-model'; describe('FieldModel', () => { let formModel = new FormModel(); describe('state', () => { beforeEach(() => { formModel.dispose(); formModel = new FormModel(); }); it('can bubble', () => { formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; const parentField = formModel.getField('parent')!; childField.value = 1; expect(childField.state.isTouched).toBe(true); expect(parentField.state.isTouched).toBe(true); expect(formModel.state.isTouched).toBe(true); }); it('can bubble with array', () => { formModel.createField('parent'); formModel.createField('parent.arr'); formModel.createField('parent.arr.1'); const arrChild = formModel.getField('parent.arr.1')!; const arrField = formModel.getField('parent.arr')!; const parentField = formModel.getField('parent')!; arrChild.value = 1; expect(arrChild.state.isTouched).toBe(true); expect(arrField.state.isTouched).toBe(true); expect(parentField.state.isTouched).toBe(true); expect(formModel.state.isTouched).toBe(true); }); it('do not set isTouched for init value set', () => { formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; const parentField = formModel.getField('parent')!; expect(childField.state.isTouched).toBe(false); expect(parentField.state.isTouched).toBe(false); expect(formModel.state.isTouched).toBe(false); }); }); describe('validate', () => { beforeEach(() => { formModel.dispose(); formModel = new FormModel(); }); it('when validate func return only a message', async () => { formModel.init({ validate: { 'parent.*': () => 'some message' } }); formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; expect(childField.state.errors).toBeUndefined(); await childField.validate(); expect(childField.state.errors?.['parent.child'][0].message).toBe('some message'); expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error); }); it('when validate func return a FieldWarning', async () => { formModel.init({ validate: { 'parent.*': () => ({ level: FeedbackLevel.Warning, message: 'some message', }), }, }); formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; expect(childField.state.errors).toBeUndefined(); await childField.validate(); expect(childField.state.warnings?.['parent.child'][0].message).toBe('some message'); expect(childField.state.warnings?.['parent.child'][0].level).toBe(FeedbackLevel.Warning); }); it('when validate return a FormError', async () => { formModel.init({ validate: { 'parent.*': () => ({ level: FeedbackLevel.Error, message: 'some message', }), }, }); formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; expect(childField.state.errors?.length).toBeUndefined(); await childField.validate(); expect(childField.state.errors?.['parent.child'][0].message).toBe('some message'); expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error); }); it('should bubble errors to parent field', async () => { formModel.init({ validate: { 'parent.*': () => ({ level: FeedbackLevel.Error, message: 'some message', }), }, }); formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; const parentField = formModel.getField('parent')!; await childField.validate(); expect(parentField.state.errors?.['parent.child'][0].message).toBe('some message'); expect(parentField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error); }); it('should bubble errors to form', async () => { formModel.init({ validate: { 'parent.*': () => ({ level: FeedbackLevel.Error, message: 'some message', }), }, }); formModel.createField('parent'); formModel.createField('parent.child'); const childField = formModel.getField('parent.child')!; await childField.validate(); expect(formModel.state.errors?.['parent.child'][0].message).toBe('some message'); expect(formModel.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error); }); it('should correctly set and bubble invalid', async () => { formModel.init({ validate: { 'parent.*': () => ({ level: FeedbackLevel.Error, message: 'some message', }), }, }); const parent = formModel.createField('parent'); const child = formModel.createField('parent.child'); await child.validate(); expect(child.state.invalid).toBe(true); expect(parent.state.invalid).toBe(true); expect(formModel.state.invalid).toBe(true); }); it('should validate self ancestors and child', async () => { formModel.init({ validateTrigger: ValidateTrigger.onChange, }); const root = formModel.createField('root'); const l1 = formModel.createField('root.l1'); const l2 = formModel.createField('root.l1.l2'); const l3 = formModel.createField('root.l1.l2.l3'); const l4 = formModel.createField('root.l1.l2.l3.l4'); const other = formModel.createField('root.other'); vi.spyOn(root, 'validate'); vi.spyOn(l1, 'validate'); vi.spyOn(l2, 'validate'); vi.spyOn(l3, 'validate'); vi.spyOn(l4, 'validate'); vi.spyOn(other, 'validate'); formModel.setValueIn('root.l1.l2', 1); expect(root.validate).toHaveBeenCalledTimes(1); expect(l1.validate).toHaveBeenCalledTimes(1); expect(l2.validate).toHaveBeenCalledTimes(1); expect(l3.validate).toHaveBeenCalledTimes(1); expect(l4.validate).toHaveBeenCalledTimes(1); expect(other.validate).toHaveBeenCalledTimes(0); }); it('should validate when multiple pattern match ', async () => { const validate1 = vi.fn(); const validate2 = vi.fn(); formModel.init({ validateTrigger: ValidateTrigger.onChange, validate: { 'a.*.input': validate1, 'a.1.input': validate2, }, initialValues: { a: [{ input: '0' }, { input: '1' }], }, }); const root = formModel.createField('a'); const i0 = formModel.createField('a.0.input'); const i1 = formModel.createField('a.1.input'); formModel.setValueIn('a.1.input', 'xxx'); expect(validate1).toHaveBeenCalledTimes(1); expect(validate2).toHaveBeenCalledTimes(1); }); // 暂时注释了从 parent 触发validate 的能力,所以注释这个单测 // it('can trigger validate from parent', async () => { // formModel.init({ // validate: { // 'parent.child1': () => ({ // level: FeedbackLevel.Error, // message: 'error', // }), // 'parent.child2': () => ({ // level: FeedbackLevel.Warning, // message: 'warning', // }), // }, // }); // const parent = formModel.createField('parent'); // formModel.createField('parent.child1'); // formModel.createField('parent.child2'); // // await parent.validate(); // // expect(formModel.state.errors?.['parent.child1'][0].message).toBe('error'); // expect(formModel.state.warnings?.['parent.child2'][0].level).toBe('warning'); // }); }); describe('onValueChange', () => { let formEffect = vi.fn(); beforeEach(() => { formModel.dispose(); formModel = new FormModel(); formEffect = vi.fn(); formModel.onFormValuesChange(formEffect); }); it('should bubble value change', () => { const parent = formModel.createField('parent'); const child1 = formModel.createField('parent.child1'); const childOnChange = vi.fn(); const parentOnChange = vi.fn(); child1.onValueChange(childOnChange); parent.onValueChange(parentOnChange); child1.value = 1; expect(parentOnChange).toHaveBeenCalledTimes(1); expect(childOnChange).toHaveBeenCalledTimes(1); expect(formEffect).toHaveBeenCalledTimes(1); }); it('should bubble value change in array when delete', () => { const parent = formModel.createField('parent'); const arr = formModel.createFieldArray('parent.arr'); const item1 = formModel.createField('parent.arr.0'); const parentOnChange = vi.fn(); const arrOnChange = vi.fn(); const item1OnChange = vi.fn(); parent.onValueChange(parentOnChange); arr.onValueChange(arrOnChange); item1.onValueChange(item1OnChange); formModel.setValueIn('parent.arr.0', 1); arr.delete(0); expect(item1OnChange).toHaveBeenCalledTimes(2); expect(arrOnChange).toHaveBeenCalledTimes(2); expect(parentOnChange).toHaveBeenCalledTimes(2); }); it('should bubble value change in array when append', () => { const parent = formModel.createField('parent'); const arr = formModel.createFieldArray('parent.arr'); const parentOnChange = vi.fn(); const arrOnChange = vi.fn(); parent.onValueChange(parentOnChange); arr.onValueChange(arrOnChange); arr.append('1'); expect(arrOnChange).toHaveBeenCalledTimes(1); expect(parentOnChange).toHaveBeenCalledTimes(1); expect(formEffect).toHaveBeenCalledTimes(1); }); it('should not trigger child field change when array append', () => { formModel.createField('parent'); const arr = formModel.createFieldArray('parent.arr'); const item0 = formModel.createField('parent.arr.0'); const item0x = formModel.createField('parent.arr.0.x'); const item0OnChange = vi.fn(); const item0xOnChange = vi.fn(); item0.onValueChange(item0OnChange); item0x.onValueChange(item0xOnChange); arr.append('1'); expect(item0OnChange).toHaveBeenCalledTimes(0); expect(item0xOnChange).toHaveBeenCalledTimes(0); }); it('should clear and fire change', () => { const parent = formModel.createField('parent'); const child1 = formModel.createField('parent.child1'); const child1OnChange = vi.fn(); const parentOnChange = vi.fn(); child1.onValueChange(child1OnChange); parent.onValueChange(parentOnChange); formModel.setValueIn('parent.child1', 1); child1.clear(); expect(child1OnChange).toHaveBeenCalledTimes(2); expect(parentOnChange).toHaveBeenCalledTimes(2); expect(formEffect).toHaveBeenCalledTimes(2); }); it('should bubble change in array delete', () => { const arr = formModel.createFieldArray('arr'); const child1 = formModel.createField('arr.0'); const childOnChange = vi.fn(); const arrOnChange = vi.fn(); child1.onValueChange(childOnChange); arr.onValueChange(arrOnChange); formModel.setValueIn('arr.0', 1); arr.delete(0); expect(childOnChange).toHaveBeenCalledTimes(2); expect(arrOnChange).toHaveBeenCalledTimes(2); // formModel.setValueIn 一次,arr.delete 中 arr 本身触发一次 expect(formEffect).toHaveBeenCalledTimes(2); }); it('should bubble change in array append', () => { const arr = formModel.createFieldArray('arr'); const item0 = formModel.createField('arr.0'); const item0OnChange = vi.fn(); const arrOnChange = vi.fn(); item0.onValueChange(item0OnChange); arr.onValueChange(arrOnChange); formModel.setValueIn('arr.0', 'a'); arr.append('b'); expect(item0OnChange).toHaveBeenCalledTimes(1); }); it('should ignore unchanged items when array delete', () => { const other = formModel.createField('other'); const parent = formModel.createField('parent'); const arr = formModel.createFieldArray('parent.arr'); const item0 = formModel.createField('parent.arr.0'); const item1 = formModel.createField('parent.arr.1'); const item2 = formModel.createField('parent.arr.2'); formModel.setValueIn('parent.arr', [1, 2, 3]); const item0OnChange = vi.fn(); const item1OnChange = vi.fn(); const item2OnChange = vi.fn(); const arrOnChange = vi.fn(); const parentOnChange = vi.fn(); const otherOnChange = vi.fn(); item0.onValueChange(item0OnChange); item1.onValueChange(item1OnChange); item2.onValueChange(item2OnChange); arr.onValueChange(arrOnChange); parent.onValueChange(parentOnChange); other.onValueChange(otherOnChange); arr.delete(1); expect(arrOnChange).toHaveBeenCalledTimes(1); expect(parentOnChange).toHaveBeenCalledTimes(1); expect(item0OnChange).not.toHaveBeenCalled(); expect(item1OnChange).toHaveBeenCalledTimes(1); expect(item2OnChange).toHaveBeenCalledTimes(1); expect(otherOnChange).not.toHaveBeenCalled(); }); }); describe('dispose', () => { beforeEach(() => { formModel.dispose(); formModel = new FormModel(); }); it('should correctly cleanup when field dispose', () => { const parent = formModel.createField('parent'); const child1 = formModel.createField('parent.child1'); child1.state.errors = { 'parent.child1': 'errors' } as unknown as Errors; child1.bubbleState(); expect(formModel.state.errors?.['parent.child1']).toEqual('errors'); expect(parent.state.errors?.['parent.child1']).toEqual('errors'); parent.dispose(); // Ref 'dispose' method in field-model.ts // 1. expect state has been cleared // expect(child1.state.errors).toBeUndefined(); // expect(parent.state.errors?.['parent.child1']).toBeUndefined(); // 2. expect field model has been cleared expect(formModel.fieldMap.get('parent')).toBeUndefined(); expect(formModel.fieldMap.get('parent.child1')).toBeUndefined(); }); }); });