@teamnovu/kit-vue-forms 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLAN.md +209 -0
- package/dist/composables/useField.d.ts +12 -0
- package/dist/composables/useFieldRegistry.d.ts +15 -0
- package/dist/composables/useForm.d.ts +10 -0
- package/dist/composables/useFormState.d.ts +7 -0
- package/dist/composables/useSubform.d.ts +5 -0
- package/dist/composables/useValidation.d.ts +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +414 -0
- package/dist/types/form.d.ts +41 -0
- package/dist/types/util.d.ts +26 -0
- package/dist/types/validation.d.ts +16 -0
- package/dist/utils/general.d.ts +2 -0
- package/dist/utils/path.d.ts +11 -0
- package/dist/utils/type-helpers.d.ts +3 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/zod.d.ts +3 -0
- package/package.json +41 -0
- package/src/composables/useField.ts +74 -0
- package/src/composables/useFieldRegistry.ts +53 -0
- package/src/composables/useForm.ts +54 -0
- package/src/composables/useFormData.ts +16 -0
- package/src/composables/useFormState.ts +21 -0
- package/src/composables/useSubform.ts +173 -0
- package/src/composables/useValidation.ts +227 -0
- package/src/index.ts +11 -0
- package/src/types/form.ts +58 -0
- package/src/types/util.ts +73 -0
- package/src/types/validation.ts +22 -0
- package/src/utils/general.ts +7 -0
- package/src/utils/path.ts +87 -0
- package/src/utils/type-helpers.ts +3 -0
- package/src/utils/validation.ts +66 -0
- package/src/utils/zod.ts +24 -0
- package/tests/formState.test.ts +138 -0
- package/tests/integration.test.ts +200 -0
- package/tests/nestedPath.test.ts +651 -0
- package/tests/path-utils.test.ts +159 -0
- package/tests/subform.test.ts +1348 -0
- package/tests/useField.test.ts +147 -0
- package/tests/useForm.test.ts +178 -0
- package/tests/useValidation.test.ts +216 -0
- package/tsconfig.json +18 -0
- package/vite.config.js +39 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { useForm } from '../src/composables/useForm'
|
|
5
|
+
import { Form } from '../src/types/form'
|
|
6
|
+
|
|
7
|
+
describe('Subform Implementation', () => {
|
|
8
|
+
describe('Basic Functionality', () => {
|
|
9
|
+
describe('Subform Creation', () => {
|
|
10
|
+
it('should create subform with correct data scoping', () => {
|
|
11
|
+
const form = useForm({
|
|
12
|
+
initialData: {
|
|
13
|
+
user: {
|
|
14
|
+
name: 'John',
|
|
15
|
+
email: 'john@example.com',
|
|
16
|
+
},
|
|
17
|
+
company: { name: 'Tech Corp' },
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const userForm = form.getSubForm('user')
|
|
22
|
+
|
|
23
|
+
expect(userForm.formData.value).toEqual({
|
|
24
|
+
name: 'John',
|
|
25
|
+
email: 'john@example.com',
|
|
26
|
+
})
|
|
27
|
+
expect(userForm.initialData.value).toEqual({
|
|
28
|
+
name: 'John',
|
|
29
|
+
email: 'john@example.com',
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should create array subforms', () => {
|
|
34
|
+
const form = useForm({
|
|
35
|
+
initialData: {
|
|
36
|
+
users: [
|
|
37
|
+
{
|
|
38
|
+
name: 'John',
|
|
39
|
+
email: 'john@example.com',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Jane',
|
|
43
|
+
email: 'jane@example.com',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const firstUserForm = form.getSubForm('users.0')
|
|
50
|
+
const secondUserForm = form.getSubForm('users.1')
|
|
51
|
+
|
|
52
|
+
expect(firstUserForm.formData.value).toEqual({
|
|
53
|
+
name: 'John',
|
|
54
|
+
email: 'john@example.com',
|
|
55
|
+
})
|
|
56
|
+
expect(secondUserForm.formData.value).toEqual({
|
|
57
|
+
name: 'Jane',
|
|
58
|
+
email: 'jane@example.com',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should handle deeply nested subforms', () => {
|
|
63
|
+
const form = useForm({
|
|
64
|
+
initialData: {
|
|
65
|
+
user: {
|
|
66
|
+
profile: {
|
|
67
|
+
personal: {
|
|
68
|
+
name: 'John',
|
|
69
|
+
age: 30,
|
|
70
|
+
},
|
|
71
|
+
work: {
|
|
72
|
+
title: 'Developer',
|
|
73
|
+
company: 'Tech Corp',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const userForm = form.getSubForm('user')
|
|
81
|
+
const profileForm = userForm.getSubForm('profile')
|
|
82
|
+
const personalForm = profileForm.getSubForm('personal')
|
|
83
|
+
|
|
84
|
+
expect(personalForm.formData.value).toEqual({
|
|
85
|
+
name: 'John',
|
|
86
|
+
age: 30,
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('Field Operations', () => {
|
|
92
|
+
it('should register fields with correct paths in main form', () => {
|
|
93
|
+
const form = useForm({
|
|
94
|
+
initialData: {
|
|
95
|
+
user: { name: 'John' },
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const userForm = form.getSubForm('user')
|
|
100
|
+
const nameField = userForm.defineField({ path: 'name' })
|
|
101
|
+
|
|
102
|
+
expect(nameField.path.value).toBe('name')
|
|
103
|
+
expect(nameField.value.value).toBe('John')
|
|
104
|
+
expect(form.getField('user.name')).toBeDefined()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should retrieve fields with path transformation', () => {
|
|
108
|
+
const form = useForm({
|
|
109
|
+
initialData: {
|
|
110
|
+
user: {
|
|
111
|
+
name: 'John',
|
|
112
|
+
email: 'john@example.com',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const userForm = form.getSubForm('user')
|
|
118
|
+
userForm.defineField({ path: 'name' })
|
|
119
|
+
userForm.defineField({ path: 'email' })
|
|
120
|
+
|
|
121
|
+
const nameField = userForm.getField('name')
|
|
122
|
+
const emailField = userForm.getField('email')
|
|
123
|
+
|
|
124
|
+
expect(nameField).toBeDefined()
|
|
125
|
+
expect(emailField).toBeDefined()
|
|
126
|
+
expect(nameField?.path.value).toBe('name')
|
|
127
|
+
expect(emailField?.path.value).toBe('email')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should handle field operations on nested subforms', () => {
|
|
131
|
+
const form = useForm({
|
|
132
|
+
initialData: {
|
|
133
|
+
user: {
|
|
134
|
+
profile: {
|
|
135
|
+
name: 'John',
|
|
136
|
+
bio: 'Developer',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const userForm = form.getSubForm('user')
|
|
143
|
+
const profileForm = userForm.getSubForm('profile')
|
|
144
|
+
const nameField = profileForm.defineField({ path: 'name' })
|
|
145
|
+
|
|
146
|
+
expect(nameField.path.value).toBe('name')
|
|
147
|
+
expect(nameField.value.value).toBe('John')
|
|
148
|
+
|
|
149
|
+
// Should be registered in main form with full path
|
|
150
|
+
expect(form.getField('user.profile.name')).toBeDefined()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle field value updates correctly', async () => {
|
|
154
|
+
const form = useForm({
|
|
155
|
+
initialData: {
|
|
156
|
+
user: { name: 'John' },
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const userForm = form.getSubForm('user')
|
|
161
|
+
const nameField = userForm.defineField({ path: 'name' })
|
|
162
|
+
|
|
163
|
+
nameField.setValue('Jane')
|
|
164
|
+
await nextTick()
|
|
165
|
+
|
|
166
|
+
expect(nameField.value.value).toBe('Jane')
|
|
167
|
+
expect(userForm.formData.value.name).toBe('Jane')
|
|
168
|
+
expect(form.formData.value.user.name).toBe('Jane')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('Nested Subforms', () => {
|
|
173
|
+
it('should create nested subforms from subforms', () => {
|
|
174
|
+
const form = useForm({
|
|
175
|
+
initialData: {
|
|
176
|
+
user: {
|
|
177
|
+
profile: {
|
|
178
|
+
name: 'John',
|
|
179
|
+
settings: {
|
|
180
|
+
theme: 'dark',
|
|
181
|
+
notifications: true,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const userForm = form.getSubForm('user')
|
|
189
|
+
const profileForm = userForm.getSubForm('profile')
|
|
190
|
+
const settingsForm = profileForm.getSubForm('settings')
|
|
191
|
+
|
|
192
|
+
expect(settingsForm.formData.value).toEqual({
|
|
193
|
+
theme: 'dark',
|
|
194
|
+
notifications: true,
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should handle infinite nesting levels', () => {
|
|
199
|
+
const form = useForm({
|
|
200
|
+
initialData: {
|
|
201
|
+
level1: {
|
|
202
|
+
level2: {
|
|
203
|
+
level3: {
|
|
204
|
+
level4: {
|
|
205
|
+
level5: {
|
|
206
|
+
name: 'Deep Value',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const level1Form = form.getSubForm('level1')
|
|
216
|
+
const level2Form = level1Form.getSubForm('level2')
|
|
217
|
+
const level3Form = level2Form.getSubForm('level3')
|
|
218
|
+
const level4Form = level3Form.getSubForm('level4')
|
|
219
|
+
const level5Form = level4Form.getSubForm('level5')
|
|
220
|
+
|
|
221
|
+
expect(level5Form.formData.value).toEqual({
|
|
222
|
+
name: 'Deep Value',
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const nameField = level5Form.defineField({ path: 'name' })
|
|
226
|
+
expect(form.getField('level1.level2.level3.level4.level5.name')).toBeDefined()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should handle mixed object and array nesting', () => {
|
|
230
|
+
const form = useForm({
|
|
231
|
+
initialData: {
|
|
232
|
+
teams: [
|
|
233
|
+
{
|
|
234
|
+
name: 'Team A',
|
|
235
|
+
members: [
|
|
236
|
+
{
|
|
237
|
+
name: 'John',
|
|
238
|
+
role: 'lead',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'Jane',
|
|
242
|
+
role: 'dev',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const teamForm = form.getSubForm('teams.0')
|
|
251
|
+
const memberForm = teamForm.getSubForm('members.0')
|
|
252
|
+
|
|
253
|
+
expect(memberForm.formData.value).toEqual({
|
|
254
|
+
name: 'John',
|
|
255
|
+
role: 'lead',
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('Validation Integration', () => {
|
|
262
|
+
describe('Schema Validation', () => {
|
|
263
|
+
it('should validate subform with schema using defineValidator', async () => {
|
|
264
|
+
const form = useForm({
|
|
265
|
+
initialData: {
|
|
266
|
+
user: {
|
|
267
|
+
name: '',
|
|
268
|
+
email: '',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const userForm = form.getSubForm('user')
|
|
274
|
+
userForm.defineValidator({
|
|
275
|
+
schema: z.object({
|
|
276
|
+
name: z.string().min(1, 'Name required'),
|
|
277
|
+
email: z.string().email('Invalid email'),
|
|
278
|
+
}),
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Set invalid data
|
|
282
|
+
userForm.formData.value.name = ''
|
|
283
|
+
userForm.formData.value.email = 'invalid'
|
|
284
|
+
|
|
285
|
+
const result = await form.validateForm()
|
|
286
|
+
|
|
287
|
+
expect(result.isValid).toBe(false)
|
|
288
|
+
expect(result.errors.propertyErrors['user.name']).toContain('Name required')
|
|
289
|
+
expect(result.errors.propertyErrors['user.email']).toContain('Invalid email')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should validate nested subforms with schemas', async () => {
|
|
293
|
+
const form = useForm({
|
|
294
|
+
initialData: {
|
|
295
|
+
user: {
|
|
296
|
+
profile: {
|
|
297
|
+
name: '',
|
|
298
|
+
bio: '',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const userForm = form.getSubForm('user')
|
|
305
|
+
const profileForm = userForm.getSubForm('profile')
|
|
306
|
+
|
|
307
|
+
profileForm.defineValidator({
|
|
308
|
+
schema: z.object({
|
|
309
|
+
name: z.string().min(1, 'Name required'),
|
|
310
|
+
bio: z.string().min(10, 'Bio too short'),
|
|
311
|
+
}),
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Set invalid data
|
|
315
|
+
profileForm.formData.value.name = ''
|
|
316
|
+
profileForm.formData.value.bio = 'Short'
|
|
317
|
+
|
|
318
|
+
const result = await form.validateForm()
|
|
319
|
+
|
|
320
|
+
expect(result.isValid).toBe(false)
|
|
321
|
+
expect(result.errors.propertyErrors['user.profile.name']).toContain('Name required')
|
|
322
|
+
expect(result.errors.propertyErrors['user.profile.bio']).toContain('Bio too short')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should handle multiple subform validations at same level', async () => {
|
|
326
|
+
const form = useForm({
|
|
327
|
+
initialData: {
|
|
328
|
+
user: { name: '' },
|
|
329
|
+
company: { name: '' },
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const userForm = form.getSubForm('user')
|
|
334
|
+
const companyForm = form.getSubForm('company')
|
|
335
|
+
|
|
336
|
+
userForm.defineValidator({
|
|
337
|
+
schema: z.object({
|
|
338
|
+
name: z.string().min(1, 'User name required'),
|
|
339
|
+
}),
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
companyForm.defineValidator({
|
|
343
|
+
schema: z.object({
|
|
344
|
+
name: z.string().min(1, 'Company name required'),
|
|
345
|
+
}),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const result = await form.validateForm()
|
|
349
|
+
|
|
350
|
+
expect(result.isValid).toBe(false)
|
|
351
|
+
expect(result.errors.propertyErrors['user.name']).toContain('User name required')
|
|
352
|
+
expect(result.errors.propertyErrors['company.name']).toContain('Company name required')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('Custom Validation Functions', () => {
|
|
357
|
+
it('should validate subform with custom validation function', async () => {
|
|
358
|
+
const form = useForm({
|
|
359
|
+
initialData: {
|
|
360
|
+
user: {
|
|
361
|
+
name: 'admin',
|
|
362
|
+
email: 'admin@example.com',
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const userForm = form.getSubForm('user')
|
|
368
|
+
userForm.defineValidator({
|
|
369
|
+
validateFn: async (data) => {
|
|
370
|
+
const errors: Record<string, string[]> = {}
|
|
371
|
+
|
|
372
|
+
if (data.name === 'admin') {
|
|
373
|
+
errors.name = ['Admin name not allowed']
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
isValid: Object.keys(errors).length === 0,
|
|
378
|
+
errors: {
|
|
379
|
+
general: [],
|
|
380
|
+
propertyErrors: errors,
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const result = await form.validateForm()
|
|
387
|
+
|
|
388
|
+
expect(result.isValid).toBe(false)
|
|
389
|
+
expect(result.errors.propertyErrors['user.name']).toContain('Admin name not allowed')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should handle both schema and custom validation', async () => {
|
|
393
|
+
const form = useForm({
|
|
394
|
+
initialData: {
|
|
395
|
+
user: {
|
|
396
|
+
name: '',
|
|
397
|
+
email: 'test@example.com',
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
const userForm = form.getSubForm('user')
|
|
403
|
+
userForm.defineValidator({
|
|
404
|
+
schema: z.object({
|
|
405
|
+
name: z.string().min(1, 'Name required'),
|
|
406
|
+
}),
|
|
407
|
+
validateFn: async (data) => {
|
|
408
|
+
const errors: Record<string, string[]> = {}
|
|
409
|
+
|
|
410
|
+
if (data.email === 'test@example.com') {
|
|
411
|
+
errors.email = ['Test email not allowed']
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
isValid: Object.keys(errors).length === 0,
|
|
416
|
+
errors: {
|
|
417
|
+
general: [],
|
|
418
|
+
propertyErrors: errors,
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const result = await form.validateForm()
|
|
425
|
+
|
|
426
|
+
expect(result.isValid).toBe(false)
|
|
427
|
+
expect(result.errors.propertyErrors['user.name']).toContain('Name required')
|
|
428
|
+
expect(result.errors.propertyErrors['user.email']).toContain('Test email not allowed')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should handle validation with nested custom functions', async () => {
|
|
432
|
+
const form = useForm({
|
|
433
|
+
initialData: {
|
|
434
|
+
user: {
|
|
435
|
+
profile: {
|
|
436
|
+
name: 'admin',
|
|
437
|
+
bio: 'Test bio',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const userForm = form.getSubForm('user')
|
|
444
|
+
const profileForm = userForm.getSubForm('profile')
|
|
445
|
+
|
|
446
|
+
profileForm.defineValidator({
|
|
447
|
+
validateFn: async (data) => {
|
|
448
|
+
const errors: Record<string, string[]> = {}
|
|
449
|
+
|
|
450
|
+
if (data.name === 'admin') {
|
|
451
|
+
errors.name = ['Admin profile name not allowed']
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
isValid: Object.keys(errors).length === 0,
|
|
456
|
+
errors: {
|
|
457
|
+
general: [],
|
|
458
|
+
propertyErrors: errors,
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const result = await form.validateForm()
|
|
465
|
+
|
|
466
|
+
expect(result.isValid).toBe(false)
|
|
467
|
+
expect(result.errors.propertyErrors['user.profile.name']).toContain('Admin profile name not allowed')
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
describe('Main Form and Subform Validation Integration', () => {
|
|
472
|
+
it('should integrate main form and subform validation', async () => {
|
|
473
|
+
const form = useForm({
|
|
474
|
+
initialData: {
|
|
475
|
+
globalSetting: 'abc',
|
|
476
|
+
user: {
|
|
477
|
+
name: 'admin',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
// Main form validation
|
|
483
|
+
form.defineValidator({
|
|
484
|
+
schema: z.object({
|
|
485
|
+
globalSetting: z.string().min(5, 'Global setting too short'),
|
|
486
|
+
}),
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// Subform validation
|
|
490
|
+
const userForm = form.getSubForm('user')
|
|
491
|
+
userForm.defineValidator({
|
|
492
|
+
validateFn: async (data) => {
|
|
493
|
+
const errors: Record<string, string[]> = {}
|
|
494
|
+
|
|
495
|
+
if (data.name === 'admin') {
|
|
496
|
+
errors.name = ['Admin name not allowed']
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
isValid: Object.keys(errors).length === 0,
|
|
501
|
+
errors: {
|
|
502
|
+
general: [],
|
|
503
|
+
propertyErrors: errors,
|
|
504
|
+
},
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const result = await form.validateForm()
|
|
510
|
+
|
|
511
|
+
expect(result.isValid).toBe(false)
|
|
512
|
+
expect(result.errors.propertyErrors['globalSetting']).toContain('Global setting too short')
|
|
513
|
+
expect(result.errors.propertyErrors['user.name']).toContain('Admin name not allowed')
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
describe('State Management', () => {
|
|
519
|
+
describe('Dirty State', () => {
|
|
520
|
+
it('should compute isDirty for subform scope only', async () => {
|
|
521
|
+
const form = useForm({
|
|
522
|
+
initialData: {
|
|
523
|
+
user: { name: 'John' },
|
|
524
|
+
company: { name: 'Tech Corp' },
|
|
525
|
+
},
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const userForm = form.getSubForm('user')
|
|
529
|
+
const companyForm = form.getSubForm('company')
|
|
530
|
+
|
|
531
|
+
// Define fields to enable dirty state computation
|
|
532
|
+
const userNameField = userForm.defineField({ path: 'name' })
|
|
533
|
+
const companyNameField = companyForm.defineField({ path: 'name' })
|
|
534
|
+
|
|
535
|
+
expect(userForm.isDirty.value).toBe(false)
|
|
536
|
+
expect(companyForm.isDirty.value).toBe(false)
|
|
537
|
+
|
|
538
|
+
userNameField.setValue('Jane')
|
|
539
|
+
await nextTick()
|
|
540
|
+
|
|
541
|
+
expect(userForm.isDirty.value).toBe(true)
|
|
542
|
+
expect(companyForm.isDirty.value).toBe(false)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('should handle nested isDirty computation', async () => {
|
|
546
|
+
const form = useForm({
|
|
547
|
+
initialData: {
|
|
548
|
+
user: {
|
|
549
|
+
profile: {
|
|
550
|
+
name: 'John',
|
|
551
|
+
bio: 'Developer',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const userForm = form.getSubForm('user')
|
|
558
|
+
const profileForm = userForm.getSubForm('profile')
|
|
559
|
+
|
|
560
|
+
// Define fields to enable dirty state computation
|
|
561
|
+
const nameField = profileForm.defineField({ path: 'name' })
|
|
562
|
+
|
|
563
|
+
expect(profileForm.isDirty.value).toBe(false)
|
|
564
|
+
|
|
565
|
+
nameField.setValue('Jane')
|
|
566
|
+
await nextTick()
|
|
567
|
+
|
|
568
|
+
expect(profileForm.isDirty.value).toBe(true)
|
|
569
|
+
expect(userForm.isDirty.value).toBe(true)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('should handle array subform isDirty', async () => {
|
|
573
|
+
const form = useForm({
|
|
574
|
+
initialData: {
|
|
575
|
+
users: [
|
|
576
|
+
{ name: 'John' },
|
|
577
|
+
{ name: 'Jane' },
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const firstUserForm = form.getSubForm('users.0')
|
|
583
|
+
const secondUserForm = form.getSubForm('users.1')
|
|
584
|
+
|
|
585
|
+
// Define fields to enable dirty state computation
|
|
586
|
+
const firstNameField = firstUserForm.defineField({ path: 'name' })
|
|
587
|
+
const secondNameField = secondUserForm.defineField({ path: 'name' })
|
|
588
|
+
|
|
589
|
+
expect(firstUserForm.isDirty.value).toBe(false)
|
|
590
|
+
expect(secondUserForm.isDirty.value).toBe(false)
|
|
591
|
+
|
|
592
|
+
firstNameField.setValue('Johnny')
|
|
593
|
+
await nextTick()
|
|
594
|
+
|
|
595
|
+
expect(firstUserForm.isDirty.value).toBe(true)
|
|
596
|
+
expect(secondUserForm.isDirty.value).toBe(false)
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
describe('Touched State', () => {
|
|
601
|
+
it('should compute isTouched for subform scope only', async () => {
|
|
602
|
+
const form = useForm({
|
|
603
|
+
initialData: {
|
|
604
|
+
user: { name: 'John' },
|
|
605
|
+
company: { name: 'Tech Corp' },
|
|
606
|
+
},
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const userForm = form.getSubForm('user')
|
|
610
|
+
const companyForm = form.getSubForm('company')
|
|
611
|
+
|
|
612
|
+
const userNameField = userForm.defineField({ path: 'name' })
|
|
613
|
+
const companyNameField = companyForm.defineField({ path: 'name' })
|
|
614
|
+
|
|
615
|
+
expect(userForm.isTouched.value).toBe(false)
|
|
616
|
+
expect(companyForm.isTouched.value).toBe(false)
|
|
617
|
+
|
|
618
|
+
userNameField.onBlur()
|
|
619
|
+
await nextTick()
|
|
620
|
+
|
|
621
|
+
expect(userForm.isTouched.value).toBe(true)
|
|
622
|
+
expect(companyForm.isTouched.value).toBe(false)
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('should handle nested isTouched computation', async () => {
|
|
626
|
+
const form = useForm({
|
|
627
|
+
initialData: {
|
|
628
|
+
user: {
|
|
629
|
+
profile: {
|
|
630
|
+
name: 'John',
|
|
631
|
+
bio: 'Developer',
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const userForm = form.getSubForm('user')
|
|
638
|
+
const profileForm = userForm.getSubForm('profile')
|
|
639
|
+
|
|
640
|
+
const nameField = profileForm.defineField({ path: 'name' })
|
|
641
|
+
|
|
642
|
+
expect(profileForm.isTouched.value).toBe(false)
|
|
643
|
+
expect(userForm.isTouched.value).toBe(false)
|
|
644
|
+
|
|
645
|
+
nameField.onBlur()
|
|
646
|
+
await nextTick()
|
|
647
|
+
|
|
648
|
+
expect(profileForm.isTouched.value).toBe(true)
|
|
649
|
+
expect(userForm.isTouched.value).toBe(true)
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
it('should handle multiple fields touched in same subform', async () => {
|
|
653
|
+
const form = useForm({
|
|
654
|
+
initialData: {
|
|
655
|
+
user: {
|
|
656
|
+
name: 'John',
|
|
657
|
+
email: 'john@example.com',
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
const userForm = form.getSubForm('user')
|
|
663
|
+
const nameField = userForm.defineField({ path: 'name' })
|
|
664
|
+
const emailField = userForm.defineField({ path: 'email' })
|
|
665
|
+
|
|
666
|
+
expect(userForm.isTouched.value).toBe(false)
|
|
667
|
+
|
|
668
|
+
nameField.onBlur()
|
|
669
|
+
await nextTick()
|
|
670
|
+
|
|
671
|
+
expect(userForm.isTouched.value).toBe(true)
|
|
672
|
+
|
|
673
|
+
emailField.onBlur()
|
|
674
|
+
await nextTick()
|
|
675
|
+
|
|
676
|
+
expect(userForm.isTouched.value).toBe(true)
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
describe('Error State', () => {
|
|
681
|
+
it('should filter errors to subform scope', async () => {
|
|
682
|
+
const form = useForm({
|
|
683
|
+
initialData: {
|
|
684
|
+
user: { name: '' },
|
|
685
|
+
company: { name: '' },
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
const userForm = form.getSubForm('user')
|
|
690
|
+
const companyForm = form.getSubForm('company')
|
|
691
|
+
|
|
692
|
+
userForm.defineValidator({
|
|
693
|
+
schema: z.object({
|
|
694
|
+
name: z.string().min(1, 'User name required'),
|
|
695
|
+
}),
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
companyForm.defineValidator({
|
|
699
|
+
schema: z.object({
|
|
700
|
+
name: z.string().min(1, 'Company name required'),
|
|
701
|
+
}),
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
await form.validateForm()
|
|
705
|
+
|
|
706
|
+
// User form should only see user errors
|
|
707
|
+
expect(userForm.errors.value.propertyErrors['name']).toContain('User name required')
|
|
708
|
+
expect(userForm.errors.value.propertyErrors['company.name']).toBeUndefined()
|
|
709
|
+
|
|
710
|
+
// Company form should only see company errors
|
|
711
|
+
expect(companyForm.errors.value.propertyErrors['name']).toContain('Company name required')
|
|
712
|
+
expect(companyForm.errors.value.propertyErrors['user.name']).toBeUndefined()
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('should handle nested error filtering', async () => {
|
|
716
|
+
const form = useForm({
|
|
717
|
+
initialData: {
|
|
718
|
+
user: {
|
|
719
|
+
profile: {
|
|
720
|
+
name: '',
|
|
721
|
+
bio: '',
|
|
722
|
+
},
|
|
723
|
+
settings: {
|
|
724
|
+
theme: '',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
const userForm = form.getSubForm('user')
|
|
731
|
+
const profileForm = userForm.getSubForm('profile')
|
|
732
|
+
const settingsForm = userForm.getSubForm('settings')
|
|
733
|
+
|
|
734
|
+
profileForm.defineValidator({
|
|
735
|
+
schema: z.object({
|
|
736
|
+
name: z.string().min(1, 'Name required'),
|
|
737
|
+
bio: z.string().min(1, 'Bio required'),
|
|
738
|
+
}),
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
settingsForm.defineValidator({
|
|
742
|
+
schema: z.object({
|
|
743
|
+
theme: z.string().min(1, 'Theme required'),
|
|
744
|
+
}),
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
await form.validateForm()
|
|
748
|
+
|
|
749
|
+
// Profile form should only see profile errors
|
|
750
|
+
expect(profileForm.errors.value.propertyErrors['name']).toContain('Name required')
|
|
751
|
+
expect(profileForm.errors.value.propertyErrors['bio']).toContain('Bio required')
|
|
752
|
+
expect(profileForm.errors.value.propertyErrors['settings.theme']).toBeUndefined()
|
|
753
|
+
|
|
754
|
+
// Settings form should only see settings errors
|
|
755
|
+
expect(settingsForm.errors.value.propertyErrors['theme']).toContain('Theme required')
|
|
756
|
+
expect(settingsForm.errors.value.propertyErrors['profile.name']).toBeUndefined()
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it('should handle general errors in subforms', async () => {
|
|
760
|
+
const form = useForm({
|
|
761
|
+
initialData: {
|
|
762
|
+
user: {
|
|
763
|
+
name: 'test',
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
const userForm = form.getSubForm('user')
|
|
769
|
+
userForm.defineValidator({
|
|
770
|
+
validateFn: async () => ({
|
|
771
|
+
isValid: false,
|
|
772
|
+
errors: {
|
|
773
|
+
general: ['General user error'],
|
|
774
|
+
propertyErrors: {},
|
|
775
|
+
},
|
|
776
|
+
}),
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
await form.validateForm()
|
|
780
|
+
|
|
781
|
+
expect(userForm.errors.value.general).toContain('General user error')
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
describe('Reset Functionality', () => {
|
|
786
|
+
it('should reset only subform fields', async () => {
|
|
787
|
+
const form = useForm({
|
|
788
|
+
initialData: {
|
|
789
|
+
user: { name: 'John' },
|
|
790
|
+
company: { name: 'Tech Corp' },
|
|
791
|
+
},
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
const userForm = form.getSubForm('user')
|
|
795
|
+
const companyForm = form.getSubForm('company')
|
|
796
|
+
|
|
797
|
+
const userNameField = userForm.defineField({ path: 'name' })
|
|
798
|
+
const companyNameField = companyForm.defineField({ path: 'name' })
|
|
799
|
+
|
|
800
|
+
// Change values
|
|
801
|
+
userNameField.setValue('Jane')
|
|
802
|
+
companyNameField.setValue('New Corp')
|
|
803
|
+
await nextTick()
|
|
804
|
+
|
|
805
|
+
expect(userForm.formData.value.name).toBe('Jane')
|
|
806
|
+
expect(companyForm.formData.value.name).toBe('New Corp')
|
|
807
|
+
|
|
808
|
+
// Reset only user subform
|
|
809
|
+
userForm.reset()
|
|
810
|
+
await nextTick()
|
|
811
|
+
|
|
812
|
+
expect(userForm.formData.value.name).toBe('John')
|
|
813
|
+
expect(companyForm.formData.value.name).toBe('New Corp')
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
it('should handle nested subform reset', async () => {
|
|
817
|
+
const form = useForm({
|
|
818
|
+
initialData: {
|
|
819
|
+
user: {
|
|
820
|
+
profile: {
|
|
821
|
+
name: 'John',
|
|
822
|
+
bio: 'Developer',
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
const userForm = form.getSubForm('user')
|
|
829
|
+
const profileForm = userForm.getSubForm('profile')
|
|
830
|
+
|
|
831
|
+
const nameField = profileForm.defineField({ path: 'name' })
|
|
832
|
+
const bioField = profileForm.defineField({ path: 'bio' })
|
|
833
|
+
|
|
834
|
+
// Change values
|
|
835
|
+
nameField.setValue('Jane')
|
|
836
|
+
bioField.setValue('Designer')
|
|
837
|
+
await nextTick()
|
|
838
|
+
|
|
839
|
+
expect(profileForm.formData.value.name).toBe('Jane')
|
|
840
|
+
expect(profileForm.formData.value.bio).toBe('Designer')
|
|
841
|
+
|
|
842
|
+
// Reset profile subform
|
|
843
|
+
profileForm.reset()
|
|
844
|
+
await nextTick()
|
|
845
|
+
|
|
846
|
+
expect(profileForm.formData.value.name).toBe('John')
|
|
847
|
+
expect(profileForm.formData.value.bio).toBe('Developer')
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
it('should handle array subform reset', async () => {
|
|
851
|
+
const form = useForm({
|
|
852
|
+
initialData: {
|
|
853
|
+
users: [
|
|
854
|
+
{ name: 'John' },
|
|
855
|
+
{ name: 'Jane' },
|
|
856
|
+
],
|
|
857
|
+
},
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
const firstUserForm = form.getSubForm('users.0')
|
|
861
|
+
const secondUserForm = form.getSubForm('users.1')
|
|
862
|
+
|
|
863
|
+
const firstNameField = firstUserForm.defineField({ path: 'name' })
|
|
864
|
+
const secondNameField = secondUserForm.defineField({ path: 'name' })
|
|
865
|
+
|
|
866
|
+
// Change values
|
|
867
|
+
firstNameField.setValue('Johnny')
|
|
868
|
+
secondNameField.setValue('Janie')
|
|
869
|
+
await nextTick()
|
|
870
|
+
|
|
871
|
+
expect(firstUserForm.formData.value.name).toBe('Johnny')
|
|
872
|
+
expect(secondUserForm.formData.value.name).toBe('Janie')
|
|
873
|
+
|
|
874
|
+
// Reset only first user subform
|
|
875
|
+
firstUserForm.reset()
|
|
876
|
+
await nextTick()
|
|
877
|
+
|
|
878
|
+
expect(firstUserForm.formData.value.name).toBe('John')
|
|
879
|
+
expect(secondUserForm.formData.value.name).toBe('Janie')
|
|
880
|
+
})
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
describe('State Synchronization', () => {
|
|
884
|
+
it('should maintain state consistency between main form and subforms', async () => {
|
|
885
|
+
const form = useForm({
|
|
886
|
+
initialData: {
|
|
887
|
+
user: { name: 'John' },
|
|
888
|
+
},
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
const userForm = form.getSubForm('user')
|
|
892
|
+
const nameField = userForm.defineField({ path: 'name' })
|
|
893
|
+
|
|
894
|
+
// Change through subform
|
|
895
|
+
userForm.formData.value.name = 'Jane'
|
|
896
|
+
await nextTick()
|
|
897
|
+
|
|
898
|
+
expect(form.formData.value.user.name).toBe('Jane')
|
|
899
|
+
expect(nameField.value.value).toBe('Jane')
|
|
900
|
+
|
|
901
|
+
// Change through main form
|
|
902
|
+
form.formData.value.user.name = 'Bob'
|
|
903
|
+
await nextTick()
|
|
904
|
+
|
|
905
|
+
expect(userForm.formData.value.name).toBe('Bob')
|
|
906
|
+
expect(nameField.value.value).toBe('Bob')
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('should handle state changes through main form', async () => {
|
|
910
|
+
const form = useForm({
|
|
911
|
+
initialData: {
|
|
912
|
+
user: {
|
|
913
|
+
profile: {
|
|
914
|
+
name: 'John',
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
const userForm = form.getSubForm('user')
|
|
921
|
+
const profileForm = userForm.getSubForm('profile')
|
|
922
|
+
|
|
923
|
+
// Change through main form
|
|
924
|
+
form.formData.value.user.profile.name = 'Jane'
|
|
925
|
+
await nextTick()
|
|
926
|
+
|
|
927
|
+
expect(userForm.formData.value.profile.name).toBe('Jane')
|
|
928
|
+
expect(profileForm.formData.value.name).toBe('Jane')
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
it('should handle initial data changes', async () => {
|
|
932
|
+
const initialData = {
|
|
933
|
+
user: { name: 'John' },
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const form = useForm({
|
|
937
|
+
initialData: () => initialData,
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
const userForm = form.getSubForm('user')
|
|
941
|
+
|
|
942
|
+
expect(userForm.initialData.value.name).toBe('John')
|
|
943
|
+
|
|
944
|
+
// This test would need reactive initial data to work properly
|
|
945
|
+
// For now, just verify the current behavior
|
|
946
|
+
expect(userForm.initialData.value).toEqual({ name: 'John' })
|
|
947
|
+
})
|
|
948
|
+
})
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
describe('Edge Cases', () => {
|
|
952
|
+
describe('Validation Edge Cases', () => {
|
|
953
|
+
it('should handle validation function that throws error', async () => {
|
|
954
|
+
const form = useForm({
|
|
955
|
+
initialData: {
|
|
956
|
+
user: { name: 'test' },
|
|
957
|
+
},
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
const userForm = form.getSubForm('user')
|
|
961
|
+
userForm.defineValidator({
|
|
962
|
+
validateFn: async () => {
|
|
963
|
+
throw new Error('Validation function error')
|
|
964
|
+
},
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
const result = await form.validateForm()
|
|
968
|
+
|
|
969
|
+
expect(result.isValid).toBe(false)
|
|
970
|
+
expect(result.errors.general).toContain('Validation function error')
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
it('should handle validation function that returns invalid format', async () => {
|
|
974
|
+
const form = useForm({
|
|
975
|
+
initialData: {
|
|
976
|
+
user: { name: 'test' },
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
const userForm = form.getSubForm('user')
|
|
981
|
+
userForm.defineValidator({
|
|
982
|
+
validateFn: async () => {
|
|
983
|
+
// Return invalid format (missing propertyErrors)
|
|
984
|
+
return {
|
|
985
|
+
isValid: false,
|
|
986
|
+
errors: {
|
|
987
|
+
general: ['Invalid format error'],
|
|
988
|
+
propertyErrors: {},
|
|
989
|
+
},
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
const result = await form.validateForm()
|
|
995
|
+
|
|
996
|
+
expect(result.isValid).toBe(false)
|
|
997
|
+
expect(result.errors.general).toContain('Invalid format error')
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
it('should handle validation function with undefined data', async () => {
|
|
1001
|
+
const form = useForm({
|
|
1002
|
+
initialData: {
|
|
1003
|
+
user: undefined,
|
|
1004
|
+
},
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
const userForm = form.getSubForm('user' as never)
|
|
1008
|
+
userForm.defineValidator({
|
|
1009
|
+
validateFn: async (data) => {
|
|
1010
|
+
if (!data) {
|
|
1011
|
+
return {
|
|
1012
|
+
isValid: false,
|
|
1013
|
+
errors: {
|
|
1014
|
+
general: ['Data is undefined'],
|
|
1015
|
+
propertyErrors: {},
|
|
1016
|
+
},
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
isValid: true,
|
|
1021
|
+
errors: {
|
|
1022
|
+
general: [],
|
|
1023
|
+
propertyErrors: {},
|
|
1024
|
+
},
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
const result = await form.validateForm()
|
|
1030
|
+
|
|
1031
|
+
expect(result.isValid).toBe(false)
|
|
1032
|
+
expect(result.errors.general).toContain('Data is undefined')
|
|
1033
|
+
})
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
describe('Path Edge Cases', () => {
|
|
1037
|
+
it('should handle special characters in paths', () => {
|
|
1038
|
+
const form = useForm({
|
|
1039
|
+
initialData: {
|
|
1040
|
+
'user-name': { 'first-name': 'John' },
|
|
1041
|
+
},
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
const userForm = form.getSubForm('user-name')
|
|
1045
|
+
expect(userForm.formData.value).toEqual({ 'first-name': 'John' })
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('should handle numeric string paths', () => {
|
|
1049
|
+
const form = useForm({
|
|
1050
|
+
initialData: {
|
|
1051
|
+
123: { name: 'test' },
|
|
1052
|
+
},
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
const numericForm = form.getSubForm('123')
|
|
1056
|
+
expect(numericForm.formData.value).toEqual({ name: 'test' })
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
describe('Data Structure Edge Cases', () => {
|
|
1061
|
+
it('should handle null values in subform data', () => {
|
|
1062
|
+
const form = useForm({
|
|
1063
|
+
initialData: {
|
|
1064
|
+
user: {
|
|
1065
|
+
name: null,
|
|
1066
|
+
email: 'test@example.com',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
const userForm = form.getSubForm('user')
|
|
1072
|
+
expect(userForm.formData.value.name).toBe(null)
|
|
1073
|
+
expect(userForm.formData.value.email).toBe('test@example.com')
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
it('should handle undefined values in subform data', () => {
|
|
1077
|
+
const form = useForm({
|
|
1078
|
+
initialData: {
|
|
1079
|
+
user: {
|
|
1080
|
+
name: undefined,
|
|
1081
|
+
email: 'test@example.com',
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
const userForm = form.getSubForm('user')
|
|
1087
|
+
expect(userForm.formData.value.name).toBe(undefined)
|
|
1088
|
+
expect(userForm.formData.value.email).toBe('test@example.com')
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
it('should handle empty objects', () => {
|
|
1092
|
+
const form = useForm({
|
|
1093
|
+
initialData: {
|
|
1094
|
+
user: {},
|
|
1095
|
+
},
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
const userForm = form.getSubForm('user' as never)
|
|
1099
|
+
expect(userForm.formData.value).toEqual({})
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
it('should handle empty arrays', () => {
|
|
1103
|
+
const form = useForm({
|
|
1104
|
+
initialData: {
|
|
1105
|
+
users: [],
|
|
1106
|
+
},
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
const usersForm = form.getSubForm('users')
|
|
1110
|
+
expect(usersForm.formData.value).toEqual([])
|
|
1111
|
+
})
|
|
1112
|
+
})
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
describe('Performance', () => {
|
|
1116
|
+
describe('Large Form Performance', () => {
|
|
1117
|
+
it('should handle large numbers of subforms efficiently', () => {
|
|
1118
|
+
const initialData = {
|
|
1119
|
+
users: [] as {
|
|
1120
|
+
id: number
|
|
1121
|
+
name: string
|
|
1122
|
+
email: string
|
|
1123
|
+
}[],
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Create 100 users
|
|
1127
|
+
for (let i = 0; i < 100; i++) {
|
|
1128
|
+
initialData.users.push({
|
|
1129
|
+
id: i,
|
|
1130
|
+
name: `User ${i}`,
|
|
1131
|
+
email: `user${i}@example.com`,
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const form = useForm({ initialData })
|
|
1136
|
+
|
|
1137
|
+
const startTime = performance.now()
|
|
1138
|
+
|
|
1139
|
+
// Create 100 subforms
|
|
1140
|
+
const subforms: Form<{
|
|
1141
|
+
id: number
|
|
1142
|
+
name: string
|
|
1143
|
+
email: string
|
|
1144
|
+
}>[] = []
|
|
1145
|
+
for (let i = 0; i < 100; i++) {
|
|
1146
|
+
subforms.push(form.getSubForm(`users.${i}`))
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const endTime = performance.now()
|
|
1150
|
+
const duration = endTime - startTime
|
|
1151
|
+
|
|
1152
|
+
// Should complete within reasonable time (less than 100ms)
|
|
1153
|
+
expect(duration).toBeLessThan(100)
|
|
1154
|
+
expect(subforms).toHaveLength(100)
|
|
1155
|
+
expect(subforms[0].formData.value.name).toBe('User 0')
|
|
1156
|
+
expect(subforms[99].formData.value.name).toBe('User 99')
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
it('should handle rapid field registration efficiently', () => {
|
|
1160
|
+
const form = useForm({
|
|
1161
|
+
initialData: {
|
|
1162
|
+
user: {
|
|
1163
|
+
name: 'John',
|
|
1164
|
+
email: 'john@example.com',
|
|
1165
|
+
bio: 'Developer',
|
|
1166
|
+
phone: '123-456-7890',
|
|
1167
|
+
address: '123 Main St',
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
const userForm = form.getSubForm('user')
|
|
1173
|
+
|
|
1174
|
+
const startTime = performance.now()
|
|
1175
|
+
|
|
1176
|
+
// Register many fields rapidly
|
|
1177
|
+
const fields = [
|
|
1178
|
+
userForm.defineField({ path: 'name' }),
|
|
1179
|
+
userForm.defineField({ path: 'email' }),
|
|
1180
|
+
userForm.defineField({ path: 'bio' }),
|
|
1181
|
+
userForm.defineField({ path: 'phone' }),
|
|
1182
|
+
userForm.defineField({ path: 'address' }),
|
|
1183
|
+
]
|
|
1184
|
+
|
|
1185
|
+
const endTime = performance.now()
|
|
1186
|
+
const duration = endTime - startTime
|
|
1187
|
+
|
|
1188
|
+
// Should complete within reasonable time
|
|
1189
|
+
expect(duration).toBeLessThan(50)
|
|
1190
|
+
expect(fields).toHaveLength(5)
|
|
1191
|
+
expect(fields[0].value.value).toBe('John')
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
it('should handle complex validation on large forms', async () => {
|
|
1195
|
+
const initialData = {
|
|
1196
|
+
users: [] as {
|
|
1197
|
+
id: number
|
|
1198
|
+
name: string
|
|
1199
|
+
email: string
|
|
1200
|
+
}[],
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Create 50 users for validation test
|
|
1204
|
+
for (let i = 0; i < 50; i++) {
|
|
1205
|
+
initialData.users.push({
|
|
1206
|
+
id: i,
|
|
1207
|
+
name: `User ${i}`,
|
|
1208
|
+
email: `user${i}@example.com`,
|
|
1209
|
+
})
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const form = useForm({ initialData })
|
|
1213
|
+
|
|
1214
|
+
// Add validation to multiple subforms
|
|
1215
|
+
for (let i = 0; i < 50; i++) {
|
|
1216
|
+
const userForm = form.getSubForm(`users.${i}`)
|
|
1217
|
+
userForm.defineValidator({
|
|
1218
|
+
schema: z.object({
|
|
1219
|
+
name: z.string().min(1, 'Name required'),
|
|
1220
|
+
email: z.string().email('Invalid email'),
|
|
1221
|
+
}),
|
|
1222
|
+
})
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const startTime = performance.now()
|
|
1226
|
+
const result = await form.validateForm()
|
|
1227
|
+
const endTime = performance.now()
|
|
1228
|
+
const duration = endTime - startTime
|
|
1229
|
+
|
|
1230
|
+
// Should complete within reasonable time (less than 1000ms)
|
|
1231
|
+
expect(duration).toBeLessThan(1000)
|
|
1232
|
+
expect(result.isValid).toBe(true)
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
describe('Memory Usage', () => {
|
|
1237
|
+
it('should not create excessive memory usage with many subforms', () => {
|
|
1238
|
+
const form = useForm({
|
|
1239
|
+
initialData: {
|
|
1240
|
+
users: Array.from({ length: 1000 }, (_, i) => ({
|
|
1241
|
+
id: i,
|
|
1242
|
+
name: `User ${i}`,
|
|
1243
|
+
})),
|
|
1244
|
+
},
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
// Create many subforms
|
|
1248
|
+
const subforms: Form<{
|
|
1249
|
+
id: number
|
|
1250
|
+
name: string
|
|
1251
|
+
}>[] = []
|
|
1252
|
+
for (let i = 0; i < 100; i++) {
|
|
1253
|
+
subforms.push(form.getSubForm(`users.${i}`))
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Should not crash or cause memory issues
|
|
1257
|
+
expect(subforms).toHaveLength(100)
|
|
1258
|
+
|
|
1259
|
+
// Clear references
|
|
1260
|
+
subforms.length = 0
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
it('should handle subform cleanup properly', () => {
|
|
1264
|
+
const form = useForm({
|
|
1265
|
+
initialData: {
|
|
1266
|
+
users: [
|
|
1267
|
+
{ name: 'John' },
|
|
1268
|
+
{ name: 'Jane' },
|
|
1269
|
+
],
|
|
1270
|
+
},
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
let userForm: Form<{
|
|
1274
|
+
name: string
|
|
1275
|
+
}> | undefined = form.getSubForm('users.0')
|
|
1276
|
+
expect(userForm.formData.value.name).toBe('John')
|
|
1277
|
+
|
|
1278
|
+
// Remove reference
|
|
1279
|
+
userForm = undefined
|
|
1280
|
+
|
|
1281
|
+
// Should not cause issues
|
|
1282
|
+
const newUserForm = form.getSubForm('users.0')
|
|
1283
|
+
expect(newUserForm.formData.value.name).toBe('John')
|
|
1284
|
+
})
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
describe('Reactivity Performance', () => {
|
|
1288
|
+
it('should handle frequent data updates efficiently', async () => {
|
|
1289
|
+
const form = useForm({
|
|
1290
|
+
initialData: {
|
|
1291
|
+
user: { name: 'John' },
|
|
1292
|
+
},
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
const userForm = form.getSubForm('user')
|
|
1296
|
+
const nameField = userForm.defineField({ path: 'name' })
|
|
1297
|
+
|
|
1298
|
+
const startTime = performance.now()
|
|
1299
|
+
|
|
1300
|
+
// Make many rapid updates
|
|
1301
|
+
for (let i = 0; i < 100; i++) {
|
|
1302
|
+
nameField.setValue(`Name ${i}`)
|
|
1303
|
+
await nextTick()
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const endTime = performance.now()
|
|
1307
|
+
const duration = endTime - startTime
|
|
1308
|
+
|
|
1309
|
+
// Should complete within reasonable time (less than 500ms)
|
|
1310
|
+
expect(duration).toBeLessThan(500)
|
|
1311
|
+
expect(nameField.value.value).toBe('Name 99')
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
it('should handle nested reactivity updates efficiently', async () => {
|
|
1315
|
+
const form = useForm({
|
|
1316
|
+
initialData: {
|
|
1317
|
+
user: {
|
|
1318
|
+
profile: {
|
|
1319
|
+
personal: {
|
|
1320
|
+
name: 'John',
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
const userForm = form.getSubForm('user')
|
|
1328
|
+
const profileForm = userForm.getSubForm('profile')
|
|
1329
|
+
const personalForm = profileForm.getSubForm('personal')
|
|
1330
|
+
|
|
1331
|
+
const startTime = performance.now()
|
|
1332
|
+
|
|
1333
|
+
// Make updates at different levels
|
|
1334
|
+
for (let i = 0; i < 50; i++) {
|
|
1335
|
+
personalForm.formData.value.name = `Name ${i}`
|
|
1336
|
+
await nextTick()
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const endTime = performance.now()
|
|
1340
|
+
const duration = endTime - startTime
|
|
1341
|
+
|
|
1342
|
+
// Should complete within reasonable time
|
|
1343
|
+
expect(duration).toBeLessThan(500)
|
|
1344
|
+
expect(personalForm.formData.value.name).toBe('Name 49')
|
|
1345
|
+
})
|
|
1346
|
+
})
|
|
1347
|
+
})
|
|
1348
|
+
})
|