field-model.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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. // 暂时注释了从 parent 触发validate 的能力,所以注释这个单测
  167. // it('can trigger validate from parent', async () => {
  168. // formModel.init({
  169. // validate: {
  170. // 'parent.child1': () => ({
  171. // level: FeedbackLevel.Error,
  172. // message: 'error',
  173. // }),
  174. // 'parent.child2': () => ({
  175. // level: FeedbackLevel.Warning,
  176. // message: 'warning',
  177. // }),
  178. // },
  179. // });
  180. // const parent = formModel.createField('parent');
  181. // formModel.createField('parent.child1');
  182. // formModel.createField('parent.child2');
  183. //
  184. // await parent.validate();
  185. //
  186. // expect(formModel.state.errors?.['parent.child1'][0].message).toBe('error');
  187. // expect(formModel.state.warnings?.['parent.child2'][0].level).toBe('warning');
  188. // });
  189. });
  190. describe('onValueChange', () => {
  191. let formEffect = vi.fn();
  192. beforeEach(() => {
  193. formModel.dispose();
  194. formModel = new FormModel();
  195. formEffect = vi.fn();
  196. formModel.onFormValuesChange(formEffect);
  197. });
  198. it('should bubble value change', () => {
  199. const parent = formModel.createField('parent');
  200. const child1 = formModel.createField('parent.child1');
  201. const childOnChange = vi.fn();
  202. const parentOnChange = vi.fn();
  203. child1.onValueChange(childOnChange);
  204. parent.onValueChange(parentOnChange);
  205. child1.value = 1;
  206. expect(parentOnChange).toHaveBeenCalledTimes(1);
  207. expect(childOnChange).toHaveBeenCalledTimes(1);
  208. expect(formEffect).toHaveBeenCalledTimes(1);
  209. });
  210. it('should bubble value change in array when delete', () => {
  211. const parent = formModel.createField('parent');
  212. const arr = formModel.createFieldArray('parent.arr');
  213. const item1 = formModel.createField('parent.arr.0');
  214. const parentOnChange = vi.fn();
  215. const arrOnChange = vi.fn();
  216. const item1OnChange = vi.fn();
  217. parent.onValueChange(parentOnChange);
  218. arr.onValueChange(arrOnChange);
  219. item1.onValueChange(item1OnChange);
  220. formModel.setValueIn('parent.arr.0', 1);
  221. arr.delete(0);
  222. expect(item1OnChange).toHaveBeenCalledTimes(2);
  223. expect(arrOnChange).toHaveBeenCalledTimes(2);
  224. expect(parentOnChange).toHaveBeenCalledTimes(2);
  225. });
  226. it('should bubble value change in array when append', () => {
  227. const parent = formModel.createField('parent');
  228. const arr = formModel.createFieldArray('parent.arr');
  229. const parentOnChange = vi.fn();
  230. const arrOnChange = vi.fn();
  231. parent.onValueChange(parentOnChange);
  232. arr.onValueChange(arrOnChange);
  233. arr.append('1');
  234. expect(arrOnChange).toHaveBeenCalledTimes(1);
  235. expect(parentOnChange).toHaveBeenCalledTimes(1);
  236. expect(formEffect).toHaveBeenCalledTimes(1);
  237. });
  238. it('should not trigger child field change when array append', () => {
  239. formModel.createField('parent');
  240. const arr = formModel.createFieldArray('parent.arr');
  241. const item0 = formModel.createField('parent.arr.0');
  242. const item0x = formModel.createField('parent.arr.0.x');
  243. const item0OnChange = vi.fn();
  244. const item0xOnChange = vi.fn();
  245. item0.onValueChange(item0OnChange);
  246. item0x.onValueChange(item0xOnChange);
  247. arr.append('1');
  248. expect(item0OnChange).toHaveBeenCalledTimes(0);
  249. expect(item0xOnChange).toHaveBeenCalledTimes(0);
  250. });
  251. it('should clear and fire change', () => {
  252. const parent = formModel.createField('parent');
  253. const child1 = formModel.createField('parent.child1');
  254. const child1OnChange = vi.fn();
  255. const parentOnChange = vi.fn();
  256. child1.onValueChange(child1OnChange);
  257. parent.onValueChange(parentOnChange);
  258. formModel.setValueIn('parent.child1', 1);
  259. child1.clear();
  260. expect(child1OnChange).toHaveBeenCalledTimes(2);
  261. expect(parentOnChange).toHaveBeenCalledTimes(2);
  262. expect(formEffect).toHaveBeenCalledTimes(2);
  263. });
  264. it('should bubble change in array delete', () => {
  265. const arr = formModel.createFieldArray('arr');
  266. const child1 = formModel.createField('arr.0');
  267. const childOnChange = vi.fn();
  268. const arrOnChange = vi.fn();
  269. child1.onValueChange(childOnChange);
  270. arr.onValueChange(arrOnChange);
  271. formModel.setValueIn('arr.0', 1);
  272. arr.delete(0);
  273. expect(childOnChange).toHaveBeenCalledTimes(2);
  274. expect(arrOnChange).toHaveBeenCalledTimes(2);
  275. // formModel.setValueIn 一次,arr.delete 中 arr 本身触发一次
  276. expect(formEffect).toHaveBeenCalledTimes(2);
  277. });
  278. it('should bubble change in array append', () => {
  279. const arr = formModel.createFieldArray('arr');
  280. const item0 = formModel.createField('arr.0');
  281. const item0OnChange = vi.fn();
  282. const arrOnChange = vi.fn();
  283. item0.onValueChange(item0OnChange);
  284. arr.onValueChange(arrOnChange);
  285. formModel.setValueIn('arr.0', 'a');
  286. arr.append('b');
  287. expect(item0OnChange).toHaveBeenCalledTimes(1);
  288. });
  289. it('should ignore unchanged items when array delete', () => {
  290. const other = formModel.createField('other');
  291. const parent = formModel.createField('parent');
  292. const arr = formModel.createFieldArray('parent.arr');
  293. const item0 = formModel.createField('parent.arr.0');
  294. const item1 = formModel.createField('parent.arr.1');
  295. const item2 = formModel.createField('parent.arr.2');
  296. formModel.setValueIn('parent.arr', [1, 2, 3]);
  297. const item0OnChange = vi.fn();
  298. const item1OnChange = vi.fn();
  299. const item2OnChange = vi.fn();
  300. const arrOnChange = vi.fn();
  301. const parentOnChange = vi.fn();
  302. const otherOnChange = vi.fn();
  303. item0.onValueChange(item0OnChange);
  304. item1.onValueChange(item1OnChange);
  305. item2.onValueChange(item2OnChange);
  306. arr.onValueChange(arrOnChange);
  307. parent.onValueChange(parentOnChange);
  308. other.onValueChange(otherOnChange);
  309. arr.delete(1);
  310. expect(arrOnChange).toHaveBeenCalledTimes(1);
  311. expect(parentOnChange).toHaveBeenCalledTimes(1);
  312. expect(item0OnChange).not.toHaveBeenCalled();
  313. expect(item1OnChange).toHaveBeenCalledTimes(1);
  314. expect(item2OnChange).toHaveBeenCalledTimes(1);
  315. expect(otherOnChange).not.toHaveBeenCalled();
  316. });
  317. });
  318. describe('dispose', () => {
  319. beforeEach(() => {
  320. formModel.dispose();
  321. formModel = new FormModel();
  322. });
  323. it('should correctly cleanup when field dispose', () => {
  324. const parent = formModel.createField('parent');
  325. const child1 = formModel.createField('parent.child1');
  326. child1.state.errors = { 'parent.child1': 'errors' } as unknown as Errors;
  327. child1.bubbleState();
  328. expect(formModel.state.errors?.['parent.child1']).toEqual('errors');
  329. expect(parent.state.errors?.['parent.child1']).toEqual('errors');
  330. parent.dispose();
  331. // Ref 'dispose' method in field-model.ts
  332. // 1. expect state has been cleared
  333. // expect(child1.state.errors).toBeUndefined();
  334. // expect(parent.state.errors?.['parent.child1']).toBeUndefined();
  335. // 2. expect field model has been cleared
  336. expect(formModel.fieldMap.get('parent')).toBeUndefined();
  337. expect(formModel.fieldMap.get('parent.child1')).toBeUndefined();
  338. });
  339. });
  340. });