field-model.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import { beforeEach, describe, expect, it, vi } from 'vitest';
  2. import { Errors, FeedbackLevel, ValidateTrigger } from '@/types';
  3. import { FormModel } from '@/core/form-model';
  4. describe('FieldModel', () => {
  5. let formModel = new FormModel();
  6. describe('state', () => {
  7. beforeEach(() => {
  8. formModel.dispose();
  9. formModel = new FormModel();
  10. });
  11. it('can bubble', () => {
  12. formModel.createField('parent');
  13. formModel.createField('parent.child');
  14. const childField = formModel.getField('parent.child')!;
  15. const parentField = formModel.getField('parent')!;
  16. childField.value = 1;
  17. expect(childField.state.isTouched).toBe(true);
  18. expect(parentField.state.isTouched).toBe(true);
  19. expect(formModel.state.isTouched).toBe(true);
  20. });
  21. it('can bubble with array', () => {
  22. formModel.createField('parent');
  23. formModel.createField('parent.arr');
  24. formModel.createField('parent.arr.1');
  25. const arrChild = formModel.getField('parent.arr.1')!;
  26. const arrField = formModel.getField('parent.arr')!;
  27. const parentField = formModel.getField('parent')!;
  28. arrChild.value = 1;
  29. expect(arrChild.state.isTouched).toBe(true);
  30. expect(arrField.state.isTouched).toBe(true);
  31. expect(parentField.state.isTouched).toBe(true);
  32. expect(formModel.state.isTouched).toBe(true);
  33. });
  34. it('do not set isTouched for init value set', () => {
  35. formModel.createField('parent');
  36. formModel.createField('parent.child');
  37. const childField = formModel.getField('parent.child')!;
  38. const parentField = formModel.getField('parent')!;
  39. expect(childField.state.isTouched).toBe(false);
  40. expect(parentField.state.isTouched).toBe(false);
  41. expect(formModel.state.isTouched).toBe(false);
  42. });
  43. });
  44. describe('validate', () => {
  45. beforeEach(() => {
  46. formModel.dispose();
  47. formModel = new FormModel();
  48. });
  49. it('when validate func return only a message', async () => {
  50. formModel.init({ validate: { 'parent.*': () => 'some message' } });
  51. formModel.createField('parent');
  52. formModel.createField('parent.child');
  53. const childField = formModel.getField('parent.child')!;
  54. expect(childField.state.errors).toBeUndefined();
  55. await childField.validate();
  56. expect(childField.state.errors?.['parent.child'][0].message).toBe('some message');
  57. expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);
  58. });
  59. it('when validate func return a FieldWarning', async () => {
  60. formModel.init({
  61. validate: {
  62. 'parent.*': () => ({
  63. level: FeedbackLevel.Warning,
  64. message: 'some message',
  65. }),
  66. },
  67. });
  68. formModel.createField('parent');
  69. formModel.createField('parent.child');
  70. const childField = formModel.getField('parent.child')!;
  71. expect(childField.state.errors).toBeUndefined();
  72. await childField.validate();
  73. expect(childField.state.warnings?.['parent.child'][0].message).toBe('some message');
  74. expect(childField.state.warnings?.['parent.child'][0].level).toBe(FeedbackLevel.Warning);
  75. });
  76. it('when validate return a FormError', async () => {
  77. formModel.init({
  78. validate: {
  79. 'parent.*': () => ({
  80. level: FeedbackLevel.Error,
  81. message: 'some message',
  82. }),
  83. },
  84. });
  85. formModel.createField('parent');
  86. formModel.createField('parent.child');
  87. const childField = formModel.getField('parent.child')!;
  88. expect(childField.state.errors?.length).toBeUndefined();
  89. await childField.validate();
  90. expect(childField.state.errors?.['parent.child'][0].message).toBe('some message');
  91. expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);
  92. });
  93. it('should bubble errors to parent field', async () => {
  94. formModel.init({
  95. validate: {
  96. 'parent.*': () => ({
  97. level: FeedbackLevel.Error,
  98. message: 'some message',
  99. }),
  100. },
  101. });
  102. formModel.createField('parent');
  103. formModel.createField('parent.child');
  104. const childField = formModel.getField('parent.child')!;
  105. const parentField = formModel.getField('parent')!;
  106. await childField.validate();
  107. expect(parentField.state.errors?.['parent.child'][0].message).toBe('some message');
  108. expect(parentField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);
  109. });
  110. it('should bubble errors to form', async () => {
  111. formModel.init({
  112. validate: {
  113. 'parent.*': () => ({
  114. level: FeedbackLevel.Error,
  115. message: 'some message',
  116. }),
  117. },
  118. });
  119. formModel.createField('parent');
  120. formModel.createField('parent.child');
  121. const childField = formModel.getField('parent.child')!;
  122. await childField.validate();
  123. expect(formModel.state.errors?.['parent.child'][0].message).toBe('some message');
  124. expect(formModel.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);
  125. });
  126. it('should correctly set and bubble invalid', async () => {
  127. formModel.init({
  128. validate: {
  129. 'parent.*': () => ({
  130. level: FeedbackLevel.Error,
  131. message: 'some message',
  132. }),
  133. },
  134. });
  135. const parent = formModel.createField('parent');
  136. const child = formModel.createField('parent.child');
  137. await child.validate();
  138. expect(child.state.invalid).toBe(true);
  139. expect(parent.state.invalid).toBe(true);
  140. expect(formModel.state.invalid).toBe(true);
  141. });
  142. it('should validate self ancestors and child', async () => {
  143. formModel.init({
  144. validateTrigger: ValidateTrigger.onChange,
  145. });
  146. const root = formModel.createField('root');
  147. const l1 = formModel.createField('root.l1');
  148. const l2 = formModel.createField('root.l1.l2');
  149. const l3 = formModel.createField('root.l1.l2.l3');
  150. const l4 = formModel.createField('root.l1.l2.l3.l4');
  151. const other = formModel.createField('root.other');
  152. vi.spyOn(root, 'validate');
  153. vi.spyOn(l1, 'validate');
  154. vi.spyOn(l2, 'validate');
  155. vi.spyOn(l3, 'validate');
  156. vi.spyOn(l4, 'validate');
  157. vi.spyOn(other, 'validate');
  158. formModel.setValueIn('root.l1.l2', 1);
  159. expect(root.validate).toHaveBeenCalledTimes(1);
  160. expect(l1.validate).toHaveBeenCalledTimes(1);
  161. expect(l2.validate).toHaveBeenCalledTimes(1);
  162. expect(l3.validate).toHaveBeenCalledTimes(1);
  163. expect(l4.validate).toHaveBeenCalledTimes(1);
  164. expect(other.validate).toHaveBeenCalledTimes(0);
  165. });
  166. it('should validate when multiple pattern match ', async () => {
  167. const validate1 = vi.fn();
  168. const validate2 = vi.fn();
  169. formModel.init({
  170. validateTrigger: ValidateTrigger.onChange,
  171. validate: {
  172. 'a.*.input': validate1,
  173. 'a.1.input': validate2,
  174. },
  175. initialValues: {
  176. a: [{ input: '0' }, { input: '1' }],
  177. },
  178. });
  179. const root = formModel.createField('a');
  180. const i0 = formModel.createField('a.0.input');
  181. const i1 = formModel.createField('a.1.input');
  182. formModel.setValueIn('a.1.input', 'xxx');
  183. expect(validate1).toHaveBeenCalledTimes(1);
  184. expect(validate2).toHaveBeenCalledTimes(1);
  185. });
  186. // 暂时注释了从 parent 触发validate 的能力,所以注释这个单测
  187. // it('can trigger validate from parent', async () => {
  188. // formModel.init({
  189. // validate: {
  190. // 'parent.child1': () => ({
  191. // level: FeedbackLevel.Error,
  192. // message: 'error',
  193. // }),
  194. // 'parent.child2': () => ({
  195. // level: FeedbackLevel.Warning,
  196. // message: 'warning',
  197. // }),
  198. // },
  199. // });
  200. // const parent = formModel.createField('parent');
  201. // formModel.createField('parent.child1');
  202. // formModel.createField('parent.child2');
  203. //
  204. // await parent.validate();
  205. //
  206. // expect(formModel.state.errors?.['parent.child1'][0].message).toBe('error');
  207. // expect(formModel.state.warnings?.['parent.child2'][0].level).toBe('warning');
  208. // });
  209. });
  210. describe('onValueChange', () => {
  211. let formEffect = vi.fn();
  212. beforeEach(() => {
  213. formModel.dispose();
  214. formModel = new FormModel();
  215. formEffect = vi.fn();
  216. formModel.onFormValuesChange(formEffect);
  217. });
  218. it('should bubble value change', () => {
  219. const parent = formModel.createField('parent');
  220. const child1 = formModel.createField('parent.child1');
  221. const childOnChange = vi.fn();
  222. const parentOnChange = vi.fn();
  223. child1.onValueChange(childOnChange);
  224. parent.onValueChange(parentOnChange);
  225. child1.value = 1;
  226. expect(parentOnChange).toHaveBeenCalledTimes(1);
  227. expect(childOnChange).toHaveBeenCalledTimes(1);
  228. expect(formEffect).toHaveBeenCalledTimes(1);
  229. });
  230. it('should bubble value change in array when delete', () => {
  231. const parent = formModel.createField('parent');
  232. const arr = formModel.createFieldArray('parent.arr');
  233. const item1 = formModel.createField('parent.arr.0');
  234. const parentOnChange = vi.fn();
  235. const arrOnChange = vi.fn();
  236. const item1OnChange = vi.fn();
  237. parent.onValueChange(parentOnChange);
  238. arr.onValueChange(arrOnChange);
  239. item1.onValueChange(item1OnChange);
  240. formModel.setValueIn('parent.arr.0', 1);
  241. arr.delete(0);
  242. expect(item1OnChange).toHaveBeenCalledTimes(2);
  243. expect(arrOnChange).toHaveBeenCalledTimes(2);
  244. expect(parentOnChange).toHaveBeenCalledTimes(2);
  245. });
  246. it('should bubble value change in array when append', () => {
  247. const parent = formModel.createField('parent');
  248. const arr = formModel.createFieldArray('parent.arr');
  249. const parentOnChange = vi.fn();
  250. const arrOnChange = vi.fn();
  251. parent.onValueChange(parentOnChange);
  252. arr.onValueChange(arrOnChange);
  253. arr.append('1');
  254. expect(arrOnChange).toHaveBeenCalledTimes(1);
  255. expect(parentOnChange).toHaveBeenCalledTimes(1);
  256. expect(formEffect).toHaveBeenCalledTimes(1);
  257. });
  258. it('should not trigger child field change when array append', () => {
  259. formModel.createField('parent');
  260. const arr = formModel.createFieldArray('parent.arr');
  261. const item0 = formModel.createField('parent.arr.0');
  262. const item0x = formModel.createField('parent.arr.0.x');
  263. const item0OnChange = vi.fn();
  264. const item0xOnChange = vi.fn();
  265. item0.onValueChange(item0OnChange);
  266. item0x.onValueChange(item0xOnChange);
  267. arr.append('1');
  268. expect(item0OnChange).toHaveBeenCalledTimes(0);
  269. expect(item0xOnChange).toHaveBeenCalledTimes(0);
  270. });
  271. it('should clear and fire change', () => {
  272. const parent = formModel.createField('parent');
  273. const child1 = formModel.createField('parent.child1');
  274. const child1OnChange = vi.fn();
  275. const parentOnChange = vi.fn();
  276. child1.onValueChange(child1OnChange);
  277. parent.onValueChange(parentOnChange);
  278. formModel.setValueIn('parent.child1', 1);
  279. child1.clear();
  280. expect(child1OnChange).toHaveBeenCalledTimes(2);
  281. expect(parentOnChange).toHaveBeenCalledTimes(2);
  282. expect(formEffect).toHaveBeenCalledTimes(2);
  283. });
  284. it('should bubble change in array delete', () => {
  285. const arr = formModel.createFieldArray('arr');
  286. const child1 = formModel.createField('arr.0');
  287. const childOnChange = vi.fn();
  288. const arrOnChange = vi.fn();
  289. child1.onValueChange(childOnChange);
  290. arr.onValueChange(arrOnChange);
  291. formModel.setValueIn('arr.0', 1);
  292. arr.delete(0);
  293. expect(childOnChange).toHaveBeenCalledTimes(2);
  294. expect(arrOnChange).toHaveBeenCalledTimes(2);
  295. // formModel.setValueIn 一次,arr.delete 中 arr 本身触发一次
  296. expect(formEffect).toHaveBeenCalledTimes(2);
  297. });
  298. it('should bubble change in array append', () => {
  299. const arr = formModel.createFieldArray('arr');
  300. const item0 = formModel.createField('arr.0');
  301. const item0OnChange = vi.fn();
  302. const arrOnChange = vi.fn();
  303. item0.onValueChange(item0OnChange);
  304. arr.onValueChange(arrOnChange);
  305. formModel.setValueIn('arr.0', 'a');
  306. arr.append('b');
  307. expect(item0OnChange).toHaveBeenCalledTimes(1);
  308. });
  309. it('should ignore unchanged items when array delete', () => {
  310. const other = formModel.createField('other');
  311. const parent = formModel.createField('parent');
  312. const arr = formModel.createFieldArray('parent.arr');
  313. const item0 = formModel.createField('parent.arr.0');
  314. const item1 = formModel.createField('parent.arr.1');
  315. const item2 = formModel.createField('parent.arr.2');
  316. formModel.setValueIn('parent.arr', [1, 2, 3]);
  317. const item0OnChange = vi.fn();
  318. const item1OnChange = vi.fn();
  319. const item2OnChange = vi.fn();
  320. const arrOnChange = vi.fn();
  321. const parentOnChange = vi.fn();
  322. const otherOnChange = vi.fn();
  323. item0.onValueChange(item0OnChange);
  324. item1.onValueChange(item1OnChange);
  325. item2.onValueChange(item2OnChange);
  326. arr.onValueChange(arrOnChange);
  327. parent.onValueChange(parentOnChange);
  328. other.onValueChange(otherOnChange);
  329. arr.delete(1);
  330. expect(arrOnChange).toHaveBeenCalledTimes(1);
  331. expect(parentOnChange).toHaveBeenCalledTimes(1);
  332. expect(item0OnChange).not.toHaveBeenCalled();
  333. expect(item1OnChange).toHaveBeenCalledTimes(1);
  334. expect(item2OnChange).toHaveBeenCalledTimes(1);
  335. expect(otherOnChange).not.toHaveBeenCalled();
  336. });
  337. });
  338. describe('dispose', () => {
  339. beforeEach(() => {
  340. formModel.dispose();
  341. formModel = new FormModel();
  342. });
  343. it('should correctly cleanup when field dispose', () => {
  344. const parent = formModel.createField('parent');
  345. const child1 = formModel.createField('parent.child1');
  346. child1.state.errors = { 'parent.child1': 'errors' } as unknown as Errors;
  347. child1.bubbleState();
  348. expect(formModel.state.errors?.['parent.child1']).toEqual('errors');
  349. expect(parent.state.errors?.['parent.child1']).toEqual('errors');
  350. parent.dispose();
  351. // Ref 'dispose' method in field-model.ts
  352. // 1. expect state has been cleared
  353. // expect(child1.state.errors).toBeUndefined();
  354. // expect(parent.state.errors?.['parent.child1']).toBeUndefined();
  355. // 2. expect field model has been cleared
  356. expect(formModel.fieldMap.get('parent')).toBeUndefined();
  357. expect(formModel.fieldMap.get('parent.child1')).toBeUndefined();
  358. });
  359. });
  360. });