@htlkg/components 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/dist/composables/index.js +388 -0
- package/dist/composables/index.js.map +1 -0
- package/package.json +41 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useForm.test.ts +229 -0
- package/src/composables/useForm.ts +130 -0
- package/src/composables/useFormValidation.test.ts +189 -0
- package/src/composables/useFormValidation.ts +83 -0
- package/src/composables/useModal.property.test.ts +164 -0
- package/src/composables/useModal.ts +43 -0
- package/src/composables/useNotifications.test.ts +166 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useTable.property.test.ts +198 -0
- package/src/composables/useTable.ts +134 -0
- package/src/composables/useTabs.property.test.ts +247 -0
- package/src/composables/useTabs.ts +101 -0
- package/src/data/Chart.demo.vue +340 -0
- package/src/data/Chart.md +525 -0
- package/src/data/Chart.vue +133 -0
- package/src/data/DataList.md +80 -0
- package/src/data/DataList.test.ts +69 -0
- package/src/data/DataList.vue +46 -0
- package/src/data/SearchableSelect.md +107 -0
- package/src/data/SearchableSelect.vue +124 -0
- package/src/data/Table.demo.vue +296 -0
- package/src/data/Table.md +588 -0
- package/src/data/Table.property.test.ts +548 -0
- package/src/data/Table.test.ts +562 -0
- package/src/data/Table.unit.test.ts +544 -0
- package/src/data/Table.vue +321 -0
- package/src/data/index.ts +5 -0
- package/src/domain/BrandCard.md +81 -0
- package/src/domain/BrandCard.vue +63 -0
- package/src/domain/BrandSelector.md +84 -0
- package/src/domain/BrandSelector.vue +65 -0
- package/src/domain/ProductBadge.md +60 -0
- package/src/domain/ProductBadge.vue +47 -0
- package/src/domain/UserAvatar.md +84 -0
- package/src/domain/UserAvatar.vue +60 -0
- package/src/domain/domain-components.property.test.ts +449 -0
- package/src/domain/index.ts +4 -0
- package/src/forms/DateRange.demo.vue +273 -0
- package/src/forms/DateRange.md +337 -0
- package/src/forms/DateRange.vue +110 -0
- package/src/forms/JsonSchemaForm.demo.vue +549 -0
- package/src/forms/JsonSchemaForm.md +112 -0
- package/src/forms/JsonSchemaForm.property.test.ts +817 -0
- package/src/forms/JsonSchemaForm.test.ts +601 -0
- package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
- package/src/forms/JsonSchemaForm.vue +615 -0
- package/src/forms/index.ts +3 -0
- package/src/index.ts +17 -0
- package/src/navigation/Breadcrumbs.demo.vue +142 -0
- package/src/navigation/Breadcrumbs.md +102 -0
- package/src/navigation/Breadcrumbs.test.ts +69 -0
- package/src/navigation/Breadcrumbs.vue +58 -0
- package/src/navigation/Stepper.demo.vue +337 -0
- package/src/navigation/Stepper.md +174 -0
- package/src/navigation/Stepper.vue +146 -0
- package/src/navigation/Tabs.demo.vue +293 -0
- package/src/navigation/Tabs.md +163 -0
- package/src/navigation/Tabs.test.ts +176 -0
- package/src/navigation/Tabs.vue +104 -0
- package/src/navigation/index.ts +5 -0
- package/src/overlays/Alert.demo.vue +377 -0
- package/src/overlays/Alert.md +248 -0
- package/src/overlays/Alert.test.ts +166 -0
- package/src/overlays/Alert.vue +70 -0
- package/src/overlays/Drawer.md +140 -0
- package/src/overlays/Drawer.test.ts +92 -0
- package/src/overlays/Drawer.vue +76 -0
- package/src/overlays/Modal.demo.vue +149 -0
- package/src/overlays/Modal.md +385 -0
- package/src/overlays/Modal.test.ts +128 -0
- package/src/overlays/Modal.vue +86 -0
- package/src/overlays/Notification.md +150 -0
- package/src/overlays/Notification.test.ts +96 -0
- package/src/overlays/Notification.vue +58 -0
- package/src/overlays/index.ts +4 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import JsonSchemaForm from './JsonSchemaForm.vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Integration tests for JsonSchemaForm
|
|
8
|
+
* Tests user interactions, real-world scenarios, and component integration
|
|
9
|
+
*/
|
|
10
|
+
describe('JsonSchemaForm integration tests', () => {
|
|
11
|
+
describe('User Registration Form', () => {
|
|
12
|
+
const registrationSchema = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
title: 'User Registration',
|
|
15
|
+
properties: {
|
|
16
|
+
username: { type: 'string', title: 'Username', minLength: 3, maxLength: 20 },
|
|
17
|
+
email: { type: 'string', title: 'Email', format: 'email', minLength: 1 },
|
|
18
|
+
password: { type: 'string', title: 'Password', minLength: 8 },
|
|
19
|
+
age: { type: 'number', title: 'Age', minimum: 18, maximum: 120 },
|
|
20
|
+
terms: { type: 'boolean', title: 'Accept Terms' }
|
|
21
|
+
},
|
|
22
|
+
required: ['username', 'email', 'password', 'terms']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it('should allow user to fill out and submit valid registration form', async () => {
|
|
26
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
27
|
+
props: {
|
|
28
|
+
schema: registrationSchema,
|
|
29
|
+
modelValue: {}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// User fills out form by updating modelValue
|
|
34
|
+
const vm = wrapper.vm as any;
|
|
35
|
+
vm.updateField('username', 'johndoe');
|
|
36
|
+
vm.updateField('email', 'john@example.com');
|
|
37
|
+
vm.updateField('password', 'SecurePass123');
|
|
38
|
+
vm.updateField('age', 25);
|
|
39
|
+
vm.updateField('terms', true);
|
|
40
|
+
|
|
41
|
+
await nextTick();
|
|
42
|
+
|
|
43
|
+
// Update props to reflect the changes
|
|
44
|
+
await wrapper.setProps({
|
|
45
|
+
modelValue: {
|
|
46
|
+
username: 'johndoe',
|
|
47
|
+
email: 'john@example.com',
|
|
48
|
+
password: 'SecurePass123',
|
|
49
|
+
age: 25,
|
|
50
|
+
terms: true
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Submit form
|
|
55
|
+
await wrapper.find('form').trigger('submit');
|
|
56
|
+
await nextTick();
|
|
57
|
+
|
|
58
|
+
// Should emit submit event with form data
|
|
59
|
+
expect(wrapper.emitted('submit')).toBeDefined();
|
|
60
|
+
const submitData = wrapper.emitted('submit')?.[0]?.[0] as any;
|
|
61
|
+
expect(submitData.username).toBe('johndoe');
|
|
62
|
+
expect(submitData.email).toBe('john@example.com');
|
|
63
|
+
expect(submitData.password).toBe('SecurePass123');
|
|
64
|
+
expect(submitData.age).toBe(25);
|
|
65
|
+
expect(submitData.terms).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should show validation errors when submitting incomplete form', async () => {
|
|
69
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
70
|
+
props: {
|
|
71
|
+
schema: registrationSchema,
|
|
72
|
+
modelValue: {}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Try to submit empty form
|
|
77
|
+
await wrapper.find('form').trigger('submit');
|
|
78
|
+
await nextTick();
|
|
79
|
+
|
|
80
|
+
// Should emit validation-error event
|
|
81
|
+
expect(wrapper.emitted('validation-error')).toBeDefined();
|
|
82
|
+
const errors = wrapper.emitted('validation-error')?.[0]?.[0] as any[];
|
|
83
|
+
|
|
84
|
+
// Should have errors for all required fields
|
|
85
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
86
|
+
expect(errors.some(e => e.field === 'username')).toBe(true);
|
|
87
|
+
expect(errors.some(e => e.field === 'email')).toBe(true);
|
|
88
|
+
expect(errors.some(e => e.field === 'password')).toBe(true);
|
|
89
|
+
expect(errors.some(e => e.field === 'terms')).toBe(true);
|
|
90
|
+
|
|
91
|
+
// Should NOT emit submit event
|
|
92
|
+
expect(wrapper.emitted('submit')).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should validate fields as user types', async () => {
|
|
96
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
97
|
+
props: {
|
|
98
|
+
schema: registrationSchema,
|
|
99
|
+
modelValue: {}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const vm = wrapper.vm as any;
|
|
104
|
+
|
|
105
|
+
// User types short username (less than minLength of 3)
|
|
106
|
+
vm.updateField('username', 'ab');
|
|
107
|
+
await wrapper.setProps({ modelValue: { username: 'ab' } });
|
|
108
|
+
await nextTick();
|
|
109
|
+
|
|
110
|
+
// Should show validation error for minLength
|
|
111
|
+
expect(vm.errors.username).toBeDefined();
|
|
112
|
+
expect(vm.errors.username).toContain('at least 3');
|
|
113
|
+
|
|
114
|
+
// User corrects username
|
|
115
|
+
vm.updateField('username', 'validuser');
|
|
116
|
+
await wrapper.setProps({ modelValue: { username: 'validuser' } });
|
|
117
|
+
await nextTick();
|
|
118
|
+
|
|
119
|
+
// Error should be cleared
|
|
120
|
+
expect(vm.errors.username).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should validate email format in real-time', async () => {
|
|
124
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
125
|
+
props: {
|
|
126
|
+
schema: registrationSchema,
|
|
127
|
+
modelValue: {}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const vm = wrapper.vm as any;
|
|
132
|
+
|
|
133
|
+
// Invalid email (not matching email format)
|
|
134
|
+
vm.updateField('email', 'notanemail');
|
|
135
|
+
await wrapper.setProps({ modelValue: { email: 'notanemail' } });
|
|
136
|
+
await nextTick();
|
|
137
|
+
|
|
138
|
+
expect(vm.errors.email).toBeDefined();
|
|
139
|
+
expect(vm.errors.email).toContain('valid email');
|
|
140
|
+
|
|
141
|
+
// Valid email
|
|
142
|
+
vm.updateField('email', 'valid@email.com');
|
|
143
|
+
await wrapper.setProps({ modelValue: { email: 'valid@email.com' } });
|
|
144
|
+
await nextTick();
|
|
145
|
+
|
|
146
|
+
expect(vm.errors.email).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('API Configuration Form', () => {
|
|
151
|
+
const apiSchema = {
|
|
152
|
+
type: 'object',
|
|
153
|
+
title: 'API Configuration',
|
|
154
|
+
properties: {
|
|
155
|
+
apiUrl: { type: 'string', title: 'API URL', format: 'uri', minLength: 1 },
|
|
156
|
+
apiKey: { type: 'string', title: 'API Key', minLength: 1 },
|
|
157
|
+
timeout: { type: 'integer', title: 'Timeout', minimum: 5, maximum: 300 },
|
|
158
|
+
retries: { type: 'integer', title: 'Retries', minimum: 0, maximum: 10 },
|
|
159
|
+
logLevel: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
title: 'Log Level',
|
|
162
|
+
enum: ['debug', 'info', 'warn', 'error']
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: ['apiUrl', 'apiKey']
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const apiUiSchema = {
|
|
169
|
+
timeout: { 'ui:widget': 'slider' },
|
|
170
|
+
retries: { 'ui:widget': 'slider' }
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
it('should validate URL format', async () => {
|
|
174
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
175
|
+
props: {
|
|
176
|
+
schema: apiSchema,
|
|
177
|
+
uiSchema: apiUiSchema,
|
|
178
|
+
modelValue: {}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const vm = wrapper.vm as any;
|
|
183
|
+
|
|
184
|
+
// Invalid URL (not matching uri format)
|
|
185
|
+
vm.updateField('apiUrl', 'not-a-url');
|
|
186
|
+
await wrapper.setProps({ modelValue: { apiUrl: 'not-a-url' } });
|
|
187
|
+
await nextTick();
|
|
188
|
+
|
|
189
|
+
expect(vm.errors.apiUrl).toBeDefined();
|
|
190
|
+
expect(vm.errors.apiUrl).toContain('valid uri');
|
|
191
|
+
|
|
192
|
+
// Valid URL
|
|
193
|
+
vm.updateField('apiUrl', 'https://api.example.com');
|
|
194
|
+
await wrapper.setProps({ modelValue: { apiUrl: 'https://api.example.com' } });
|
|
195
|
+
await nextTick();
|
|
196
|
+
|
|
197
|
+
expect(vm.errors.apiUrl).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should enforce number ranges', async () => {
|
|
201
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
202
|
+
props: {
|
|
203
|
+
schema: apiSchema,
|
|
204
|
+
uiSchema: apiUiSchema,
|
|
205
|
+
modelValue: {
|
|
206
|
+
apiUrl: 'https://api.example.com',
|
|
207
|
+
apiKey: 'test-key'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const vm = wrapper.vm as any;
|
|
213
|
+
|
|
214
|
+
// Timeout too low (minimum is 5)
|
|
215
|
+
vm.updateField('timeout', 2);
|
|
216
|
+
await wrapper.setProps({
|
|
217
|
+
modelValue: {
|
|
218
|
+
apiUrl: 'https://api.example.com',
|
|
219
|
+
apiKey: 'test-key',
|
|
220
|
+
timeout: 2
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
await nextTick();
|
|
224
|
+
|
|
225
|
+
expect(vm.errors.timeout).toBeDefined();
|
|
226
|
+
expect(vm.errors.timeout).toContain('at least 5');
|
|
227
|
+
|
|
228
|
+
// Timeout too high (maximum is 300)
|
|
229
|
+
vm.updateField('timeout', 500);
|
|
230
|
+
await wrapper.setProps({
|
|
231
|
+
modelValue: {
|
|
232
|
+
apiUrl: 'https://api.example.com',
|
|
233
|
+
apiKey: 'test-key',
|
|
234
|
+
timeout: 500
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
await nextTick();
|
|
238
|
+
|
|
239
|
+
expect(vm.errors.timeout).toBeDefined();
|
|
240
|
+
expect(vm.errors.timeout).toContain('at most 300');
|
|
241
|
+
|
|
242
|
+
// Valid timeout
|
|
243
|
+
vm.updateField('timeout', 30);
|
|
244
|
+
await wrapper.setProps({
|
|
245
|
+
modelValue: {
|
|
246
|
+
apiUrl: 'https://api.example.com',
|
|
247
|
+
apiKey: 'test-key',
|
|
248
|
+
timeout: 30
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
await nextTick();
|
|
252
|
+
|
|
253
|
+
expect(vm.errors.timeout).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should validate enum values', async () => {
|
|
257
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
258
|
+
props: {
|
|
259
|
+
schema: apiSchema,
|
|
260
|
+
uiSchema: apiUiSchema,
|
|
261
|
+
modelValue: {
|
|
262
|
+
apiUrl: 'https://api.example.com',
|
|
263
|
+
apiKey: 'test-key',
|
|
264
|
+
logLevel: 'invalid-level'
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Submit to trigger validation
|
|
270
|
+
await wrapper.find('form').trigger('submit');
|
|
271
|
+
await nextTick();
|
|
272
|
+
|
|
273
|
+
const errors = wrapper.emitted('validation-error')?.[0]?.[0] as any[];
|
|
274
|
+
const logLevelError = errors.find(e => e.field === 'logLevel');
|
|
275
|
+
|
|
276
|
+
expect(logLevelError).toBeDefined();
|
|
277
|
+
expect(logLevelError.message).toContain('one of');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not submit with empty required fields', async () => {
|
|
281
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
282
|
+
props: {
|
|
283
|
+
schema: apiSchema,
|
|
284
|
+
uiSchema: apiUiSchema,
|
|
285
|
+
modelValue: {
|
|
286
|
+
apiUrl: '',
|
|
287
|
+
apiKey: ''
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await wrapper.find('form').trigger('submit');
|
|
293
|
+
await nextTick();
|
|
294
|
+
|
|
295
|
+
// Should have validation errors
|
|
296
|
+
expect(wrapper.emitted('validation-error')).toBeDefined();
|
|
297
|
+
const errors = wrapper.emitted('validation-error')?.[0]?.[0] as any[];
|
|
298
|
+
|
|
299
|
+
expect(errors.some(e => e.field === 'apiUrl')).toBe(true);
|
|
300
|
+
expect(errors.some(e => e.field === 'apiKey')).toBe(true);
|
|
301
|
+
|
|
302
|
+
// Should not submit
|
|
303
|
+
expect(wrapper.emitted('submit')).toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('Product Form with Complex Validation', () => {
|
|
308
|
+
const productSchema = {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
name: { type: 'string', title: 'Product Name', minLength: 3, maxLength: 100 },
|
|
312
|
+
sku: { type: 'string', title: 'SKU', minLength: 1 },
|
|
313
|
+
price: { type: 'number', title: 'Price', minimum: 0, maximum: 999999 },
|
|
314
|
+
quantity: { type: 'integer', title: 'Quantity', minimum: 0, maximum: 10000 },
|
|
315
|
+
category: {
|
|
316
|
+
type: 'string',
|
|
317
|
+
title: 'Category',
|
|
318
|
+
enum: ['Electronics', 'Clothing', 'Food', 'Books']
|
|
319
|
+
},
|
|
320
|
+
description: { type: 'string', title: 'Description', minLength: 10, maxLength: 500 },
|
|
321
|
+
active: { type: 'boolean', title: 'Active' }
|
|
322
|
+
},
|
|
323
|
+
required: ['name', 'sku', 'price', 'category']
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
it('should handle complete product creation workflow', async () => {
|
|
327
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
328
|
+
props: {
|
|
329
|
+
schema: productSchema,
|
|
330
|
+
modelValue: {}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const vm = wrapper.vm as any;
|
|
335
|
+
|
|
336
|
+
// Fill required fields
|
|
337
|
+
vm.updateField('name', 'Wireless Headphones');
|
|
338
|
+
vm.updateField('sku', 'WH-001');
|
|
339
|
+
vm.updateField('price', 299.99);
|
|
340
|
+
vm.updateField('category', 'Electronics');
|
|
341
|
+
|
|
342
|
+
// Fill optional fields
|
|
343
|
+
vm.updateField('quantity', 50);
|
|
344
|
+
vm.updateField('description', 'High-quality wireless headphones with noise cancellation');
|
|
345
|
+
vm.updateField('active', true);
|
|
346
|
+
|
|
347
|
+
await nextTick();
|
|
348
|
+
|
|
349
|
+
// Update props to reflect the changes
|
|
350
|
+
await wrapper.setProps({
|
|
351
|
+
modelValue: {
|
|
352
|
+
name: 'Wireless Headphones',
|
|
353
|
+
sku: 'WH-001',
|
|
354
|
+
price: 299.99,
|
|
355
|
+
category: 'Electronics',
|
|
356
|
+
quantity: 50,
|
|
357
|
+
description: 'High-quality wireless headphones with noise cancellation',
|
|
358
|
+
active: true
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Submit
|
|
363
|
+
await wrapper.find('form').trigger('submit');
|
|
364
|
+
await nextTick();
|
|
365
|
+
|
|
366
|
+
// Should submit successfully
|
|
367
|
+
expect(wrapper.emitted('submit')).toBeDefined();
|
|
368
|
+
const submitData = wrapper.emitted('submit')?.[0]?.[0] as any;
|
|
369
|
+
|
|
370
|
+
expect(submitData.name).toBe('Wireless Headphones');
|
|
371
|
+
expect(submitData.sku).toBe('WH-001');
|
|
372
|
+
expect(submitData.price).toBe(299.99);
|
|
373
|
+
expect(submitData.category).toBe('Electronics');
|
|
374
|
+
expect(submitData.quantity).toBe(50);
|
|
375
|
+
expect(submitData.active).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should validate multiple fields simultaneously', async () => {
|
|
379
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
380
|
+
props: {
|
|
381
|
+
schema: productSchema,
|
|
382
|
+
modelValue: {
|
|
383
|
+
name: 'AB', // Too short
|
|
384
|
+
sku: '', // Empty
|
|
385
|
+
price: -10, // Negative
|
|
386
|
+
category: 'InvalidCategory', // Not in enum
|
|
387
|
+
description: 'Short' // Too short
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await wrapper.find('form').trigger('submit');
|
|
393
|
+
await nextTick();
|
|
394
|
+
|
|
395
|
+
const errors = wrapper.emitted('validation-error')?.[0]?.[0] as any[];
|
|
396
|
+
|
|
397
|
+
// Should have multiple errors
|
|
398
|
+
expect(errors.length).toBeGreaterThan(3);
|
|
399
|
+
expect(errors.some(e => e.field === 'name')).toBe(true);
|
|
400
|
+
expect(errors.some(e => e.field === 'sku')).toBe(true);
|
|
401
|
+
expect(errors.some(e => e.field === 'price')).toBe(true);
|
|
402
|
+
expect(errors.some(e => e.field === 'category')).toBe(true);
|
|
403
|
+
expect(errors.some(e => e.field === 'description')).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('Form Reset and State Management', () => {
|
|
408
|
+
const simpleSchema = {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
field1: { type: 'string', title: 'Field 1', minLength: 1 },
|
|
412
|
+
field2: { type: 'number', title: 'Field 2' }
|
|
413
|
+
},
|
|
414
|
+
required: ['field1']
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
it('should reset form data and errors', async () => {
|
|
418
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
419
|
+
props: {
|
|
420
|
+
schema: simpleSchema,
|
|
421
|
+
modelValue: { field1: 'test', field2: 42 }
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const vm = wrapper.vm as any;
|
|
426
|
+
|
|
427
|
+
// Trigger validation error by submitting with invalid data
|
|
428
|
+
await wrapper.setProps({ modelValue: { field1: '', field2: 42 } });
|
|
429
|
+
await wrapper.find('form').trigger('submit');
|
|
430
|
+
await nextTick();
|
|
431
|
+
|
|
432
|
+
expect(vm.errors.field1).toBeDefined();
|
|
433
|
+
|
|
434
|
+
// Reset form
|
|
435
|
+
vm.reset();
|
|
436
|
+
await nextTick();
|
|
437
|
+
|
|
438
|
+
// Should emit empty modelValue
|
|
439
|
+
const updateEvents = wrapper.emitted('update:modelValue');
|
|
440
|
+
expect(updateEvents).toBeDefined();
|
|
441
|
+
const lastUpdate = updateEvents?.[updateEvents.length - 1]?.[0] as any;
|
|
442
|
+
expect(Object.keys(lastUpdate).length).toBe(0);
|
|
443
|
+
|
|
444
|
+
// Errors should be cleared
|
|
445
|
+
expect(Object.keys(vm.errors).length).toBe(0);
|
|
446
|
+
expect(Object.keys(vm.touched).length).toBe(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should track touched fields', async () => {
|
|
450
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
451
|
+
props: {
|
|
452
|
+
schema: simpleSchema,
|
|
453
|
+
modelValue: {}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const vm = wrapper.vm as any;
|
|
458
|
+
|
|
459
|
+
// Initially no fields touched
|
|
460
|
+
expect(vm.touched.field1).toBeUndefined();
|
|
461
|
+
|
|
462
|
+
// User interacts with field
|
|
463
|
+
vm.updateField('field1', 'test');
|
|
464
|
+
await nextTick();
|
|
465
|
+
|
|
466
|
+
// Field should be marked as touched
|
|
467
|
+
expect(vm.touched.field1).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe('v-model Integration', () => {
|
|
472
|
+
const schema = {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
name: { type: 'string', title: 'Name' },
|
|
476
|
+
age: { type: 'number', title: 'Age' }
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
it('should sync with v-model', async () => {
|
|
481
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
482
|
+
props: {
|
|
483
|
+
schema,
|
|
484
|
+
modelValue: { name: 'John', age: 30 }
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const vm = wrapper.vm as any;
|
|
489
|
+
|
|
490
|
+
// Change field value
|
|
491
|
+
vm.updateField('name', 'Jane');
|
|
492
|
+
await nextTick();
|
|
493
|
+
|
|
494
|
+
// Should emit update:modelValue
|
|
495
|
+
const updateEvents = wrapper.emitted('update:modelValue');
|
|
496
|
+
expect(updateEvents).toBeDefined();
|
|
497
|
+
|
|
498
|
+
const lastUpdate = updateEvents?.[updateEvents.length - 1]?.[0] as any;
|
|
499
|
+
expect(lastUpdate.name).toBe('Jane');
|
|
500
|
+
expect(lastUpdate.age).toBe(30);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should react to external modelValue changes', async () => {
|
|
504
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
505
|
+
props: {
|
|
506
|
+
schema,
|
|
507
|
+
modelValue: { name: 'John', age: 30 }
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Update modelValue externally
|
|
512
|
+
await wrapper.setProps({
|
|
513
|
+
modelValue: { name: 'Jane', age: 25 }
|
|
514
|
+
});
|
|
515
|
+
await nextTick();
|
|
516
|
+
|
|
517
|
+
// Form should reflect new values in computed property
|
|
518
|
+
const vm = wrapper.vm as any;
|
|
519
|
+
expect(vm.formData.name).toBe('Jane');
|
|
520
|
+
expect(vm.formData.age).toBe(25);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('Loading State', () => {
|
|
525
|
+
const schema = {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
field: { type: 'string', title: 'Field' }
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
it('should pass loading prop to components', async () => {
|
|
533
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
534
|
+
props: {
|
|
535
|
+
schema,
|
|
536
|
+
modelValue: {},
|
|
537
|
+
loading: true
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Loading prop should be passed to form
|
|
542
|
+
expect(wrapper.props('loading')).toBe(true);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should not be loading by default', async () => {
|
|
546
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
547
|
+
props: {
|
|
548
|
+
schema,
|
|
549
|
+
modelValue: {}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
expect(wrapper.props('loading')).toBe(false);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('Custom UI Schema', () => {
|
|
558
|
+
const schema = {
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: {
|
|
561
|
+
password: { type: 'string', title: 'Password' },
|
|
562
|
+
volume: { type: 'integer', title: 'Volume', minimum: 0, maximum: 100 }
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
it('should render password field when specified in uiSchema', async () => {
|
|
567
|
+
const uiSchema = {
|
|
568
|
+
password: { 'ui:widget': 'password' }
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
572
|
+
props: {
|
|
573
|
+
schema,
|
|
574
|
+
uiSchema,
|
|
575
|
+
modelValue: {}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const vm = wrapper.vm as any;
|
|
580
|
+
expect(vm.getWidget('password')).toBe('password');
|
|
581
|
+
expect(vm.getInputType('password')).toBe('password');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should render slider when specified in uiSchema', async () => {
|
|
585
|
+
const uiSchema = {
|
|
586
|
+
volume: { 'ui:widget': 'slider' }
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
590
|
+
props: {
|
|
591
|
+
schema,
|
|
592
|
+
uiSchema,
|
|
593
|
+
modelValue: {}
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const vm = wrapper.vm as any;
|
|
598
|
+
expect(vm.getWidget('volume')).toBe('slider');
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
});
|