| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- /**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
- 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();
- });
- });
- });
|