mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
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();
|
||
});
|
||
});
|
||
});
|