to-field.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import * as React from 'react';
  6. import { beforeEach, describe, expect, it, vi } from 'vitest';
  7. import { ValidateTrigger } from '@/types';
  8. import { toField, toFieldState } from '@/core/to-field';
  9. import { FormModel } from '@/core/form-model';
  10. import { FieldModel } from '@/core/field-model';
  11. describe('toField', () => {
  12. let formModel: FormModel;
  13. let fieldModel: FieldModel;
  14. beforeEach(() => {
  15. formModel = new FormModel();
  16. formModel.init({});
  17. fieldModel = formModel.createField('username') as FieldModel;
  18. });
  19. it('should convert FieldModel to Field', () => {
  20. const field = toField(fieldModel);
  21. expect(field).toBeDefined();
  22. expect(field.name).toBe('username');
  23. expect(field.value).toBeUndefined();
  24. });
  25. it('should expose name property from model', () => {
  26. const field = toField(fieldModel);
  27. expect(field.name).toBe(fieldModel.name);
  28. });
  29. it('should expose value property from model', () => {
  30. fieldModel.value = 'John';
  31. const field = toField(fieldModel);
  32. expect(field.value).toBe('John');
  33. expect(field.value).toBe(fieldModel.value);
  34. });
  35. describe('onChange', () => {
  36. it('should update model value with plain value', () => {
  37. const field = toField(fieldModel);
  38. field.onChange('Alice');
  39. expect(fieldModel.value).toBe('Alice');
  40. expect(field.value).toBe('Alice');
  41. });
  42. it('should handle React change event for input', () => {
  43. const field = toField(fieldModel);
  44. const mockEvent = {
  45. target: {
  46. value: 'Bob',
  47. },
  48. } as React.ChangeEvent<HTMLInputElement>;
  49. field.onChange(mockEvent);
  50. expect(fieldModel.value).toBe('Bob');
  51. });
  52. it('should handle React change event for checkbox (checked)', () => {
  53. const field = toField(fieldModel);
  54. const mockEvent = {
  55. target: {
  56. type: 'checkbox',
  57. checked: true,
  58. value: 'on',
  59. },
  60. } as React.ChangeEvent<HTMLInputElement>;
  61. field.onChange(mockEvent);
  62. expect(fieldModel.value).toBe(true);
  63. });
  64. it('should handle React change event for checkbox (unchecked)', () => {
  65. const field = toField(fieldModel);
  66. const mockEvent = {
  67. target: {
  68. type: 'checkbox',
  69. checked: false,
  70. value: 'on',
  71. },
  72. } as React.ChangeEvent<HTMLInputElement>;
  73. field.onChange(mockEvent);
  74. expect(fieldModel.value).toBe(false);
  75. });
  76. it('should handle numeric value', () => {
  77. const field = toField(fieldModel);
  78. field.onChange(42);
  79. expect(fieldModel.value).toBe(42);
  80. });
  81. it('should handle object value', () => {
  82. const field = toField(fieldModel);
  83. const objValue = { name: 'test', value: 123 };
  84. field.onChange(objValue);
  85. expect(fieldModel.value).toEqual(objValue);
  86. });
  87. it('should handle array value', () => {
  88. const field = toField(fieldModel);
  89. const arrValue = ['a', 'b', 'c'];
  90. field.onChange(arrValue);
  91. expect(fieldModel.value).toEqual(arrValue);
  92. });
  93. });
  94. describe('onBlur', () => {
  95. it('should call validate when validateTrigger is onBlur', () => {
  96. formModel.dispose();
  97. formModel = new FormModel();
  98. formModel.init({ validateTrigger: ValidateTrigger.onBlur });
  99. fieldModel = formModel.createField('username') as FieldModel;
  100. const validateSpy = vi.spyOn(fieldModel, 'validate');
  101. const field = toField(fieldModel);
  102. field.onBlur?.();
  103. expect(validateSpy).toHaveBeenCalled();
  104. });
  105. it('should not trigger validation when validateTrigger is not onBlur', () => {
  106. formModel.dispose();
  107. formModel = new FormModel();
  108. formModel.init({ validateTrigger: ValidateTrigger.onChange });
  109. fieldModel = formModel.createField('username') as FieldModel;
  110. const validateSpy = vi.spyOn(fieldModel, 'validate');
  111. const field = toField(fieldModel);
  112. field.onBlur?.();
  113. expect(validateSpy).not.toHaveBeenCalled();
  114. });
  115. it('should not trigger validation when validateTrigger is onSubmit', () => {
  116. formModel.dispose();
  117. formModel = new FormModel();
  118. formModel.init({ validateTrigger: ValidateTrigger.onSubmit });
  119. fieldModel = formModel.createField('username') as FieldModel;
  120. const validateSpy = vi.spyOn(fieldModel, 'validate');
  121. const field = toField(fieldModel);
  122. field.onBlur?.();
  123. expect(validateSpy).not.toHaveBeenCalled();
  124. });
  125. });
  126. describe('onFocus', () => {
  127. it('should set isTouched to true', () => {
  128. const field = toField(fieldModel);
  129. expect(fieldModel.state.isTouched).toBe(false);
  130. field.onFocus?.();
  131. expect(fieldModel.state.isTouched).toBe(true);
  132. });
  133. it('should set isTouched only once', () => {
  134. const field = toField(fieldModel);
  135. field.onFocus?.();
  136. expect(fieldModel.state.isTouched).toBe(true);
  137. field.onFocus?.();
  138. expect(fieldModel.state.isTouched).toBe(true);
  139. });
  140. });
  141. it('should expose key property (non-enumerable)', () => {
  142. const field = toField(fieldModel);
  143. expect((field as any).key).toBe(fieldModel.id);
  144. expect(Object.keys(field)).not.toContain('key');
  145. });
  146. it('should hide _fieldModel property (non-enumerable)', () => {
  147. const field = toField(fieldModel);
  148. expect((field as any)._fieldModel).toBe(fieldModel);
  149. expect(Object.keys(field)).not.toContain('_fieldModel');
  150. });
  151. it('should preserve reactivity through getters', () => {
  152. const field = toField(fieldModel);
  153. expect(field.name).toBe('username');
  154. expect(field.value).toBeUndefined();
  155. fieldModel.value = 'NewValue';
  156. expect(field.value).toBe('NewValue');
  157. });
  158. });
  159. describe('toFieldState', () => {
  160. let formModel: FormModel;
  161. let fieldModel: FieldModel;
  162. beforeEach(() => {
  163. formModel = new FormModel();
  164. formModel.init({});
  165. fieldModel = formModel.createField('username') as FieldModel;
  166. });
  167. it('should convert FieldModelState to FieldState', () => {
  168. const fieldState = toFieldState(fieldModel.state);
  169. expect(fieldState).toBeDefined();
  170. expect(fieldState.isTouched).toBe(false);
  171. expect(fieldState.isDirty).toBe(false);
  172. expect(fieldState.invalid).toBe(false);
  173. expect(fieldState.isValidating).toBe(false);
  174. });
  175. it('should reflect isTouched state', () => {
  176. const fieldState = toFieldState(fieldModel.state);
  177. expect(fieldState.isTouched).toBe(false);
  178. fieldModel.state.isTouched = true;
  179. expect(fieldState.isTouched).toBe(true);
  180. });
  181. it('should reflect isDirty state', () => {
  182. const fieldState = toFieldState(fieldModel.state);
  183. expect(fieldState.isDirty).toBe(false);
  184. // Manually set dirty state
  185. fieldModel.state.isDirty = true;
  186. expect(fieldState.isDirty).toBe(true);
  187. });
  188. it('should reflect invalid state', () => {
  189. const fieldState = toFieldState(fieldModel.state);
  190. expect(fieldState.invalid).toBe(false);
  191. fieldModel.state.invalid = true;
  192. expect(fieldState.invalid).toBe(true);
  193. });
  194. it('should reflect isValidating state', () => {
  195. const fieldState = toFieldState(fieldModel.state);
  196. expect(fieldState.isValidating).toBe(false);
  197. fieldModel.state.isValidating = true;
  198. expect(fieldState.isValidating).toBe(true);
  199. });
  200. it('should return errors as flat array', () => {
  201. const fieldState = toFieldState(fieldModel.state);
  202. expect(fieldState.errors).toBeUndefined();
  203. fieldModel.state.errors = {
  204. validate1: ['Error 1', 'Error 2'],
  205. validate2: ['Error 3'],
  206. };
  207. expect(fieldState.errors).toEqual(['Error 1', 'Error 2', 'Error 3']);
  208. });
  209. it('should return warnings as flat array', () => {
  210. const fieldState = toFieldState(fieldModel.state);
  211. expect(fieldState.warnings).toBeUndefined();
  212. fieldModel.state.warnings = {
  213. validate1: ['Warning 1', 'Warning 2'],
  214. validate2: ['Warning 3'],
  215. };
  216. expect(fieldState.warnings).toEqual(['Warning 1', 'Warning 2', 'Warning 3']);
  217. });
  218. it('should handle empty errors object', () => {
  219. const fieldState = toFieldState(fieldModel.state);
  220. fieldModel.state.errors = {};
  221. expect(fieldState.errors).toEqual([]);
  222. });
  223. it('should handle empty warnings object', () => {
  224. const fieldState = toFieldState(fieldModel.state);
  225. fieldModel.state.warnings = {};
  226. expect(fieldState.warnings).toEqual([]);
  227. });
  228. it('should preserve reactivity through getters', () => {
  229. const fieldState = toFieldState(fieldModel.state);
  230. expect(fieldState.isTouched).toBe(false);
  231. fieldModel.state.isTouched = true;
  232. expect(fieldState.isTouched).toBe(true);
  233. });
  234. });