form-model.test.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { beforeEach, describe, expect, it, vi } from 'vitest';
  6. import { mapValues } from 'lodash-es';
  7. import { ValidateTrigger } from '@/types';
  8. import { FormModel } from '@/core/form-model';
  9. describe('FormModel', () => {
  10. let formModel = new FormModel();
  11. describe('validate trigger', () => {
  12. beforeEach(() => {
  13. formModel.dispose();
  14. formModel = new FormModel();
  15. });
  16. it('do not validate when value change if validateTrigger is onBlur', async () => {
  17. formModel.init({ validateTrigger: ValidateTrigger.onBlur });
  18. const field = formModel.createField('x');
  19. field.originalValidate = vi.fn();
  20. vi.spyOn(field, 'originalValidate');
  21. field.value = 'some value';
  22. expect(field.originalValidate).not.toHaveBeenCalledOnce();
  23. });
  24. describe('delete field', () => {
  25. beforeEach(() => {
  26. formModel.dispose();
  27. formModel = new FormModel();
  28. });
  29. it('validate onChange', async () => {
  30. formModel.init({ initialValues: { parent: { child1: 1 } } });
  31. formModel.createField('parent');
  32. formModel.createField('parent.child1');
  33. expect(formModel.values.parent?.child1).toBe(1);
  34. formModel.deleteField('parent');
  35. expect(formModel.values.parent?.child1).toBeUndefined();
  36. expect(formModel.getField('parent')).toBeUndefined();
  37. expect(formModel.getField('parent.child1')).toBeUndefined();
  38. });
  39. });
  40. });
  41. describe('FormModel.validate', () => {
  42. beforeEach(() => {
  43. formModel.dispose();
  44. formModel = new FormModel();
  45. });
  46. it('should run validate on all matched names', async () => {
  47. formModel.init({
  48. validate: {
  49. 'a.b.*': () => 'error',
  50. },
  51. });
  52. const bField = formModel.createField('a.b');
  53. const xField = formModel.createField('a.b.x');
  54. formModel.setValueIn('a.b', { x: 1, y: 2 });
  55. const results = await formModel.validate();
  56. // 1. assert validate has been executed correctly
  57. expect(results.length).toEqual(2);
  58. expect(results[0].message).toEqual('error');
  59. expect(results[0].name).toEqual('a.b.x');
  60. expect(results[1].message).toEqual('error');
  61. expect(results[1].name).toEqual('a.b.y');
  62. // 2. assert form state has been set correctly
  63. expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  64. // 3. assert field state has been set correctly
  65. expect(xField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  66. // 4. assert field state has been bubbled to its parent
  67. expect(bField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  68. });
  69. it('should run validate if multiple patterns match', async () => {
  70. const mockValidate1 = vi.fn();
  71. const mockValidate2 = vi.fn();
  72. formModel.init({
  73. validate: {
  74. 'a.b.*': mockValidate1,
  75. 'a.b.x': mockValidate2,
  76. },
  77. });
  78. const bField = formModel.createField('a.b');
  79. const xField = formModel.createField('a.b.x');
  80. formModel.setValueIn('a.b', { x: 1, y: 2 });
  81. formModel.validate();
  82. expect(mockValidate1).toHaveBeenCalledTimes(2);
  83. expect(mockValidate2).toHaveBeenCalledTimes(1);
  84. });
  85. it('should run validate correctly if multiple patterns match but multiple layer empty value exist', async () => {
  86. const mockValidate1 = vi.fn();
  87. const mockValidate2 = vi.fn();
  88. formModel.init({
  89. validate: {
  90. 'a.*.x': mockValidate1,
  91. 'a.b.x': mockValidate2,
  92. },
  93. });
  94. const bField = formModel.createField('a.b');
  95. const xField = formModel.createField('a.b.x');
  96. formModel.setValueIn('a', {});
  97. formModel.validate();
  98. expect(mockValidate1).toHaveBeenCalledTimes(0);
  99. expect(mockValidate2).toHaveBeenCalledTimes(1);
  100. });
  101. it('should correctly set form errors state when field does not exist', async () => {
  102. formModel.init({
  103. validate: {
  104. 'a.b.*': () => 'error',
  105. },
  106. });
  107. formModel.setValueIn('a.b', { x: 1, y: 2 });
  108. await formModel.validate();
  109. expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  110. });
  111. it('should set form and field state correctly when run validate twice', async () => {
  112. formModel.init({
  113. validate: {
  114. 'a.b.*': ({ value }) => (typeof value === 'string' ? undefined : 'error'),
  115. },
  116. });
  117. const bField = formModel.createField('a.b');
  118. const xField = formModel.createField('a.b.x');
  119. formModel.setValueIn('a.b', { x: 1, y: 2 });
  120. let results = await formModel.validate();
  121. // both x y is string, so 2 errors
  122. expect(results.length).toEqual(2);
  123. expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  124. expect(formModel.state?.errors?.['a.b.y']?.[0].message).toEqual('error');
  125. expect(xField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  126. expect(bField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');
  127. formModel.setValueIn('a.b', { x: '1', y: '2' });
  128. results = await formModel.validate();
  129. expect(results.length).toEqual(0);
  130. expect(formModel.state?.errors?.['a.b.x']).toEqual([]);
  131. expect(formModel.state?.errors?.['a.b.y']).toEqual([]);
  132. });
  133. it('validate as dynamic function', async () => {
  134. formModel.init({
  135. initialValues: { a: 3, b: 'str' },
  136. validate: (v, ctx) => {
  137. expect(ctx).toEqual('context');
  138. return mapValues(v, (value) => {
  139. if (typeof value === 'string') {
  140. return () => 'string error';
  141. }
  142. return () => 'num error';
  143. });
  144. },
  145. context: 'context',
  146. });
  147. const fieldResult = await formModel.validateIn('a');
  148. expect(fieldResult).toEqual(['num error']);
  149. const results = await formModel.validate();
  150. expect(results).toEqual([
  151. { name: 'a', message: 'num error', level: 'error' },
  152. { name: 'b', message: 'string error', level: 'error' },
  153. ]);
  154. });
  155. });
  156. describe('FormModel set/get values', () => {
  157. beforeEach(() => {
  158. formModel.dispose();
  159. formModel = new FormModel();
  160. vi.spyOn(formModel.onFormValuesInitEmitter, 'fire');
  161. vi.spyOn(formModel.onFormValuesChangeEmitter, 'fire');
  162. vi.spyOn(formModel.onFormValuesUpdatedEmitter, 'fire');
  163. });
  164. it('should set value for root path', () => {
  165. formModel.init({
  166. initialValues: {
  167. a: 1,
  168. },
  169. });
  170. formModel.values = { a: 2 };
  171. expect(formModel.values).toEqual({ a: 2 });
  172. });
  173. it('should set initialValues and fire init and updated events', async () => {
  174. formModel.init({
  175. initialValues: {
  176. a: 1,
  177. },
  178. });
  179. expect(formModel.values).toEqual({ a: 1 });
  180. expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledWith({
  181. values: {
  182. a: 1,
  183. },
  184. name: '',
  185. });
  186. expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({
  187. values: {
  188. a: 1,
  189. },
  190. name: '',
  191. });
  192. });
  193. it('should set initialValues in certain path and fire change', async () => {
  194. formModel.init({
  195. initialValues: {
  196. a: 1,
  197. },
  198. });
  199. formModel.setInitValueIn('b', 2);
  200. expect(formModel.values).toEqual({ a: 1, b: 2 });
  201. expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledWith({
  202. values: {
  203. a: 1,
  204. b: 2,
  205. },
  206. prevValues: {
  207. a: 1,
  208. },
  209. name: 'b',
  210. });
  211. expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({
  212. values: {
  213. a: 1,
  214. b: 2,
  215. },
  216. prevValues: {
  217. a: 1,
  218. },
  219. name: 'b',
  220. });
  221. });
  222. it('should not set initialValues in certain path if value exists', async () => {
  223. formModel.init({
  224. initialValues: {
  225. a: 1,
  226. },
  227. });
  228. formModel.setInitValueIn('a', 2);
  229. expect(formModel.values).toEqual({ a: 1 });
  230. // 仅在初始化时调用一次,setInitValueIn 没有调用
  231. expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledTimes(1);
  232. expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledTimes(1);
  233. });
  234. it('should set values in certain path and fire change and updated events', async () => {
  235. formModel.init({
  236. initialValues: {
  237. a: 1,
  238. },
  239. });
  240. formModel.setValueIn('a', 2);
  241. expect(formModel.values).toEqual({ a: 2 });
  242. // 仅在初始化时调用一次,setInitValueIn 没有调用
  243. expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledTimes(1);
  244. // 初始化一次,变更值一次,所以是两次
  245. expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledTimes(2);
  246. expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledWith({
  247. values: {
  248. a: 2,
  249. },
  250. prevValues: {
  251. a: 1,
  252. },
  253. name: 'a',
  254. });
  255. expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({
  256. values: {
  257. a: 2,
  258. },
  259. prevValues: {
  260. a: 1,
  261. },
  262. name: 'a',
  263. });
  264. });
  265. });
  266. });