@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,817 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fc from 'fast-check';
|
|
3
|
+
import { mount } from '@vue/test-utils';
|
|
4
|
+
import JsonSchemaForm from './JsonSchemaForm.vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Feature: htlkg-modular-architecture, Property 10: JsonSchemaForm validates input
|
|
8
|
+
* Validates: Requirements 12.5
|
|
9
|
+
*/
|
|
10
|
+
describe('JsonSchemaForm property tests', () => {
|
|
11
|
+
it('should validate required fields for any schema', () => {
|
|
12
|
+
fc.assert(
|
|
13
|
+
fc.property(
|
|
14
|
+
fc.array(
|
|
15
|
+
fc.record({
|
|
16
|
+
name: fc.string({ minLength: 1, maxLength: 20 }),
|
|
17
|
+
type: fc.constantFrom('string', 'number', 'boolean')
|
|
18
|
+
}),
|
|
19
|
+
{ minLength: 1, maxLength: 5 }
|
|
20
|
+
),
|
|
21
|
+
(fields) => {
|
|
22
|
+
// Ensure unique field names
|
|
23
|
+
const uniqueFields = fields.map((field, index) => ({
|
|
24
|
+
...field,
|
|
25
|
+
name: `field_${index}`
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Create schema with all fields required
|
|
29
|
+
const schema = {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: uniqueFields.reduce((acc, field) => {
|
|
32
|
+
acc[field.name] = { type: field.type };
|
|
33
|
+
return acc;
|
|
34
|
+
}, {} as Record<string, any>),
|
|
35
|
+
required: uniqueFields.map(f => f.name)
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
39
|
+
props: {
|
|
40
|
+
schema,
|
|
41
|
+
modelValue: {}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Property: validate() should return errors for all required fields when empty
|
|
46
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
47
|
+
expect(validationErrors.length).toBe(uniqueFields.length);
|
|
48
|
+
|
|
49
|
+
// Property: Each required field should have an error
|
|
50
|
+
uniqueFields.forEach(field => {
|
|
51
|
+
const fieldError = validationErrors.find((e: any) => e.field === field.name);
|
|
52
|
+
expect(fieldError).toBeDefined();
|
|
53
|
+
expect(fieldError.message).toContain('required');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
{ numRuns: 100 }
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should validate type constraints for any schema', () => {
|
|
62
|
+
fc.assert(
|
|
63
|
+
fc.property(
|
|
64
|
+
fc.record({
|
|
65
|
+
stringField: fc.string(),
|
|
66
|
+
numberField: fc.integer(),
|
|
67
|
+
booleanField: fc.boolean()
|
|
68
|
+
}),
|
|
69
|
+
(validData) => {
|
|
70
|
+
const schema = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
stringField: { type: 'string' },
|
|
74
|
+
numberField: { type: 'number' },
|
|
75
|
+
booleanField: { type: 'boolean' }
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
80
|
+
props: {
|
|
81
|
+
schema,
|
|
82
|
+
modelValue: validData
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Property: Valid data should pass validation
|
|
87
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
88
|
+
expect(validationErrors.length).toBe(0);
|
|
89
|
+
}
|
|
90
|
+
),
|
|
91
|
+
{ numRuns: 100 }
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should detect type mismatches for any invalid data', () => {
|
|
96
|
+
fc.assert(
|
|
97
|
+
fc.property(
|
|
98
|
+
fc.string({ minLength: 1 }),
|
|
99
|
+
(stringValue) => {
|
|
100
|
+
const schema = {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
numberField: { type: 'number', title: 'Number Field' }
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Provide string value for number field (type mismatch)
|
|
108
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
109
|
+
props: {
|
|
110
|
+
schema,
|
|
111
|
+
modelValue: { numberField: stringValue }
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Property: Type mismatch should be detected
|
|
116
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
117
|
+
const numberFieldError = validationErrors.find((e: any) => e.field === 'numberField');
|
|
118
|
+
|
|
119
|
+
// Only expect error if the string is not a valid number
|
|
120
|
+
if (isNaN(Number(stringValue))) {
|
|
121
|
+
expect(numberFieldError).toBeDefined();
|
|
122
|
+
expect(numberFieldError.message).toContain('number');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
{ numRuns: 100 }
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should render fields according to schema properties', () => {
|
|
131
|
+
fc.assert(
|
|
132
|
+
fc.property(
|
|
133
|
+
fc.array(
|
|
134
|
+
fc.record({
|
|
135
|
+
name: fc.string({ minLength: 1, maxLength: 20 }),
|
|
136
|
+
type: fc.constantFrom('string', 'number', 'boolean'),
|
|
137
|
+
title: fc.string({ minLength: 1, maxLength: 30 })
|
|
138
|
+
}),
|
|
139
|
+
{ minLength: 1, maxLength: 5 }
|
|
140
|
+
),
|
|
141
|
+
(fields) => {
|
|
142
|
+
// Ensure unique field names
|
|
143
|
+
const uniqueFields = fields.map((field, index) => ({
|
|
144
|
+
...field,
|
|
145
|
+
name: `field_${index}`
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const schema = {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: uniqueFields.reduce((acc, field) => {
|
|
151
|
+
acc[field.name] = {
|
|
152
|
+
type: field.type,
|
|
153
|
+
title: field.title
|
|
154
|
+
};
|
|
155
|
+
return acc;
|
|
156
|
+
}, {} as Record<string, any>)
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
160
|
+
props: {
|
|
161
|
+
schema,
|
|
162
|
+
modelValue: {}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Property: Each field in schema should have a widget type
|
|
167
|
+
const vm = wrapper.vm as any;
|
|
168
|
+
uniqueFields.forEach(field => {
|
|
169
|
+
const widget = vm.getWidget(field.name);
|
|
170
|
+
expect(widget).toBeDefined();
|
|
171
|
+
|
|
172
|
+
// Property: Widget type should match schema type
|
|
173
|
+
if (field.type === 'boolean') {
|
|
174
|
+
expect(widget).toBe('toggle');
|
|
175
|
+
} else if (field.type === 'number') {
|
|
176
|
+
expect(widget).toBe('number');
|
|
177
|
+
} else {
|
|
178
|
+
expect(widget).toBe('text');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
{ numRuns: 100 }
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle partial data validation correctly', () => {
|
|
188
|
+
fc.assert(
|
|
189
|
+
fc.property(
|
|
190
|
+
fc.record({
|
|
191
|
+
requiredFields: fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 1, maxLength: 3 }),
|
|
192
|
+
optionalFields: fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 1, maxLength: 3 }),
|
|
193
|
+
providedFieldIndex: fc.integer({ min: 0, max: 2 })
|
|
194
|
+
}),
|
|
195
|
+
({ requiredFields, optionalFields, providedFieldIndex }) => {
|
|
196
|
+
// Ensure unique field names
|
|
197
|
+
const uniqueRequired = requiredFields.map((_, i) => `req_${i}`);
|
|
198
|
+
const uniqueOptional = optionalFields.map((_, i) => `opt_${i}`);
|
|
199
|
+
|
|
200
|
+
const schema = {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
...uniqueRequired.reduce((acc, name) => {
|
|
204
|
+
acc[name] = { type: 'string' };
|
|
205
|
+
return acc;
|
|
206
|
+
}, {} as Record<string, any>),
|
|
207
|
+
...uniqueOptional.reduce((acc, name) => {
|
|
208
|
+
acc[name] = { type: 'string' };
|
|
209
|
+
return acc;
|
|
210
|
+
}, {} as Record<string, any>)
|
|
211
|
+
},
|
|
212
|
+
required: uniqueRequired
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Provide data for only one required field
|
|
216
|
+
const validIndex = Math.min(providedFieldIndex, uniqueRequired.length - 1);
|
|
217
|
+
const partialData = {
|
|
218
|
+
[uniqueRequired[validIndex]]: 'some value'
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
222
|
+
props: {
|
|
223
|
+
schema,
|
|
224
|
+
modelValue: partialData
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Property: Should have errors for missing required fields only
|
|
229
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
230
|
+
expect(validationErrors.length).toBe(uniqueRequired.length - 1);
|
|
231
|
+
|
|
232
|
+
// Property: Provided field should not have error
|
|
233
|
+
const providedFieldError = validationErrors.find((e: any) => e.field === uniqueRequired[validIndex]);
|
|
234
|
+
expect(providedFieldError).toBeUndefined();
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
{ numRuns: 100 }
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should emit validation-error event with correct error structure', () => {
|
|
242
|
+
fc.assert(
|
|
243
|
+
fc.property(
|
|
244
|
+
fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 1, maxLength: 4 }),
|
|
245
|
+
(fieldNames) => {
|
|
246
|
+
// Ensure unique field names
|
|
247
|
+
const uniqueFields = fieldNames.map((_, i) => `field_${i}`);
|
|
248
|
+
|
|
249
|
+
const schema = {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: uniqueFields.reduce((acc, name) => {
|
|
252
|
+
acc[name] = { type: 'string', title: name };
|
|
253
|
+
return acc;
|
|
254
|
+
}, {} as Record<string, any>),
|
|
255
|
+
required: uniqueFields
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
259
|
+
props: {
|
|
260
|
+
schema,
|
|
261
|
+
modelValue: {}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Trigger form submission to emit validation-error
|
|
266
|
+
wrapper.find('form').trigger('submit');
|
|
267
|
+
|
|
268
|
+
// Property: validation-error event should be emitted
|
|
269
|
+
expect(wrapper.emitted('validation-error')).toBeDefined();
|
|
270
|
+
|
|
271
|
+
// Property: Error structure should have field and message
|
|
272
|
+
const emittedErrors = wrapper.emitted('validation-error')?.[0]?.[0] as any[];
|
|
273
|
+
expect(emittedErrors).toBeDefined();
|
|
274
|
+
expect(emittedErrors.length).toBe(uniqueFields.length);
|
|
275
|
+
|
|
276
|
+
emittedErrors.forEach((error: any) => {
|
|
277
|
+
expect(error).toHaveProperty('field');
|
|
278
|
+
expect(error).toHaveProperty('message');
|
|
279
|
+
expect(typeof error.field).toBe('string');
|
|
280
|
+
expect(typeof error.message).toBe('string');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
),
|
|
284
|
+
{ numRuns: 100 }
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should handle empty schema gracefully', () => {
|
|
289
|
+
fc.assert(
|
|
290
|
+
fc.property(
|
|
291
|
+
fc.constant({}),
|
|
292
|
+
(emptyData) => {
|
|
293
|
+
const schema = {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
299
|
+
props: {
|
|
300
|
+
schema,
|
|
301
|
+
modelValue: emptyData
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Property: Empty schema should validate successfully
|
|
306
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
307
|
+
expect(validationErrors.length).toBe(0);
|
|
308
|
+
|
|
309
|
+
// Property: Form should still render
|
|
310
|
+
expect(wrapper.find('form').exists()).toBe(true);
|
|
311
|
+
}
|
|
312
|
+
),
|
|
313
|
+
{ numRuns: 50 }
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should update modelValue when fields change', () => {
|
|
318
|
+
fc.assert(
|
|
319
|
+
fc.property(
|
|
320
|
+
fc.record({
|
|
321
|
+
fieldName: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
|
322
|
+
fieldValue: fc.string({ minLength: 1, maxLength: 50 })
|
|
323
|
+
}),
|
|
324
|
+
({ fieldName, fieldValue }) => {
|
|
325
|
+
// Create a valid CSS ID by removing spaces and special characters
|
|
326
|
+
const uniqueFieldName = `field_${fieldName.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
327
|
+
|
|
328
|
+
const schema = {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
[uniqueFieldName]: { type: 'string' }
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
336
|
+
props: {
|
|
337
|
+
schema,
|
|
338
|
+
modelValue: {}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Property: Updating field should emit update:modelValue
|
|
343
|
+
const vm = wrapper.vm as any;
|
|
344
|
+
vm.updateField(uniqueFieldName, fieldValue);
|
|
345
|
+
|
|
346
|
+
// Property: modelValue should be updated with new value
|
|
347
|
+
const updateEvents = wrapper.emitted('update:modelValue');
|
|
348
|
+
expect(updateEvents).toBeDefined();
|
|
349
|
+
|
|
350
|
+
if (updateEvents && updateEvents.length > 0) {
|
|
351
|
+
const lastUpdate = updateEvents[updateEvents.length - 1][0] as Record<string, any>;
|
|
352
|
+
expect(lastUpdate[uniqueFieldName]).toBe(fieldValue);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
),
|
|
356
|
+
{ numRuns: 100 }
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should validate minLength constraint for strings', () => {
|
|
361
|
+
fc.assert(
|
|
362
|
+
fc.property(
|
|
363
|
+
fc.record({
|
|
364
|
+
minLength: fc.integer({ min: 1, max: 10 }),
|
|
365
|
+
value: fc.string({ maxLength: 20 })
|
|
366
|
+
}),
|
|
367
|
+
({ minLength, value }) => {
|
|
368
|
+
const schema = {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
testField: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
minLength,
|
|
374
|
+
title: 'Test Field'
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
required: ['testField']
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
381
|
+
props: {
|
|
382
|
+
schema,
|
|
383
|
+
modelValue: { testField: value }
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
388
|
+
|
|
389
|
+
// Property: Should have error if value length < minLength
|
|
390
|
+
if (value.length < minLength) {
|
|
391
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
392
|
+
const error = validationErrors.find((e: any) => e.field === 'testField');
|
|
393
|
+
expect(error).toBeDefined();
|
|
394
|
+
expect(error.message).toContain('at least');
|
|
395
|
+
} else {
|
|
396
|
+
// Should not have minLength error
|
|
397
|
+
const minLengthError = validationErrors.find(
|
|
398
|
+
(e: any) => e.field === 'testField' && e.message.includes('at least')
|
|
399
|
+
);
|
|
400
|
+
expect(minLengthError).toBeUndefined();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
{ numRuns: 100 }
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should validate maxLength constraint for strings', () => {
|
|
409
|
+
fc.assert(
|
|
410
|
+
fc.property(
|
|
411
|
+
fc.record({
|
|
412
|
+
maxLength: fc.integer({ min: 5, max: 20 }),
|
|
413
|
+
value: fc.string({ minLength: 1, maxLength: 30 })
|
|
414
|
+
}),
|
|
415
|
+
({ maxLength, value }) => {
|
|
416
|
+
const schema = {
|
|
417
|
+
type: 'object',
|
|
418
|
+
properties: {
|
|
419
|
+
testField: {
|
|
420
|
+
type: 'string',
|
|
421
|
+
maxLength,
|
|
422
|
+
title: 'Test Field'
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
428
|
+
props: {
|
|
429
|
+
schema,
|
|
430
|
+
modelValue: { testField: value }
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
435
|
+
|
|
436
|
+
// Property: Should have error if value length > maxLength
|
|
437
|
+
if (value.length > maxLength) {
|
|
438
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
439
|
+
const error = validationErrors.find((e: any) => e.field === 'testField');
|
|
440
|
+
expect(error).toBeDefined();
|
|
441
|
+
expect(error.message).toContain('at most');
|
|
442
|
+
} else {
|
|
443
|
+
const maxLengthError = validationErrors.find(
|
|
444
|
+
(e: any) => e.field === 'testField' && e.message.includes('at most')
|
|
445
|
+
);
|
|
446
|
+
expect(maxLengthError).toBeUndefined();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
),
|
|
450
|
+
{ numRuns: 100 }
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should validate minimum constraint for numbers', () => {
|
|
455
|
+
fc.assert(
|
|
456
|
+
fc.property(
|
|
457
|
+
fc.record({
|
|
458
|
+
minimum: fc.integer({ min: 0, max: 100 }),
|
|
459
|
+
value: fc.integer({ min: -50, max: 150 })
|
|
460
|
+
}),
|
|
461
|
+
({ minimum, value }) => {
|
|
462
|
+
const schema = {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
numberField: {
|
|
466
|
+
type: 'number',
|
|
467
|
+
minimum,
|
|
468
|
+
title: 'Number Field'
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
474
|
+
props: {
|
|
475
|
+
schema,
|
|
476
|
+
modelValue: { numberField: value }
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
481
|
+
|
|
482
|
+
// Property: Should have error if value < minimum
|
|
483
|
+
if (value < minimum) {
|
|
484
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
485
|
+
const error = validationErrors.find((e: any) => e.field === 'numberField');
|
|
486
|
+
expect(error).toBeDefined();
|
|
487
|
+
expect(error.message).toContain('at least');
|
|
488
|
+
} else {
|
|
489
|
+
const minError = validationErrors.find(
|
|
490
|
+
(e: any) => e.field === 'numberField' && e.message.includes('at least')
|
|
491
|
+
);
|
|
492
|
+
expect(minError).toBeUndefined();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
),
|
|
496
|
+
{ numRuns: 100 }
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should validate maximum constraint for numbers', () => {
|
|
501
|
+
fc.assert(
|
|
502
|
+
fc.property(
|
|
503
|
+
fc.record({
|
|
504
|
+
maximum: fc.integer({ min: 50, max: 200 }),
|
|
505
|
+
value: fc.integer({ min: 0, max: 250 })
|
|
506
|
+
}),
|
|
507
|
+
({ maximum, value }) => {
|
|
508
|
+
const schema = {
|
|
509
|
+
type: 'object',
|
|
510
|
+
properties: {
|
|
511
|
+
numberField: {
|
|
512
|
+
type: 'number',
|
|
513
|
+
maximum,
|
|
514
|
+
title: 'Number Field'
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
520
|
+
props: {
|
|
521
|
+
schema,
|
|
522
|
+
modelValue: { numberField: value }
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
527
|
+
|
|
528
|
+
// Property: Should have error if value > maximum
|
|
529
|
+
if (value > maximum) {
|
|
530
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
531
|
+
const error = validationErrors.find((e: any) => e.field === 'numberField');
|
|
532
|
+
expect(error).toBeDefined();
|
|
533
|
+
expect(error.message).toContain('at most');
|
|
534
|
+
} else {
|
|
535
|
+
const maxError = validationErrors.find(
|
|
536
|
+
(e: any) => e.field === 'numberField' && e.message.includes('at most')
|
|
537
|
+
);
|
|
538
|
+
expect(maxError).toBeUndefined();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
),
|
|
542
|
+
{ numRuns: 100 }
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should validate enum constraints', () => {
|
|
547
|
+
fc.assert(
|
|
548
|
+
fc.property(
|
|
549
|
+
fc.record({
|
|
550
|
+
allowedValues: fc.array(fc.string({ minLength: 1, maxLength: 10 }), { minLength: 2, maxLength: 5 }),
|
|
551
|
+
testValue: fc.string({ minLength: 1, maxLength: 10 })
|
|
552
|
+
}),
|
|
553
|
+
({ allowedValues, testValue }) => {
|
|
554
|
+
// Ensure unique values
|
|
555
|
+
const uniqueValues = [...new Set(allowedValues)];
|
|
556
|
+
if (uniqueValues.length < 2) return; // Skip if not enough unique values
|
|
557
|
+
|
|
558
|
+
const schema = {
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: {
|
|
561
|
+
enumField: {
|
|
562
|
+
type: 'string',
|
|
563
|
+
enum: uniqueValues,
|
|
564
|
+
title: 'Enum Field'
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
570
|
+
props: {
|
|
571
|
+
schema,
|
|
572
|
+
modelValue: { enumField: testValue }
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
577
|
+
|
|
578
|
+
// Property: Should have error if value not in enum
|
|
579
|
+
if (!uniqueValues.includes(testValue)) {
|
|
580
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
581
|
+
const error = validationErrors.find((e: any) => e.field === 'enumField');
|
|
582
|
+
expect(error).toBeDefined();
|
|
583
|
+
expect(error.message).toContain('one of');
|
|
584
|
+
} else {
|
|
585
|
+
const enumError = validationErrors.find(
|
|
586
|
+
(e: any) => e.field === 'enumField' && e.message.includes('one of')
|
|
587
|
+
);
|
|
588
|
+
expect(enumError).toBeUndefined();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
),
|
|
592
|
+
{ numRuns: 100 }
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should treat empty strings as invalid for required fields with minLength: 1', () => {
|
|
597
|
+
fc.assert(
|
|
598
|
+
fc.property(
|
|
599
|
+
fc.constantFrom(''),
|
|
600
|
+
(emptyValue) => {
|
|
601
|
+
const schema = {
|
|
602
|
+
type: 'object',
|
|
603
|
+
properties: {
|
|
604
|
+
requiredField: {
|
|
605
|
+
type: 'string',
|
|
606
|
+
minLength: 1,
|
|
607
|
+
title: 'Required Field'
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
required: ['requiredField']
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
614
|
+
props: {
|
|
615
|
+
schema,
|
|
616
|
+
modelValue: { requiredField: emptyValue }
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
621
|
+
|
|
622
|
+
// Property: Empty strings should fail validation (either required or minLength)
|
|
623
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
624
|
+
const error = validationErrors.find((e: any) => e.field === 'requiredField');
|
|
625
|
+
expect(error).toBeDefined();
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
{ numRuns: 50 }
|
|
629
|
+
);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should validate email format', () => {
|
|
633
|
+
fc.assert(
|
|
634
|
+
fc.property(
|
|
635
|
+
fc.oneof(
|
|
636
|
+
fc.emailAddress(),
|
|
637
|
+
fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('@'))
|
|
638
|
+
),
|
|
639
|
+
(value) => {
|
|
640
|
+
const schema = {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
emailField: {
|
|
644
|
+
type: 'string',
|
|
645
|
+
format: 'email',
|
|
646
|
+
title: 'Email Field'
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
652
|
+
props: {
|
|
653
|
+
schema,
|
|
654
|
+
modelValue: { emailField: value }
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
659
|
+
|
|
660
|
+
// Property: Invalid emails should fail format validation
|
|
661
|
+
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
662
|
+
if (!isValidEmail && value !== '') {
|
|
663
|
+
const error = validationErrors.find((e: any) => e.field === 'emailField');
|
|
664
|
+
expect(error).toBeDefined();
|
|
665
|
+
expect(error.message).toContain('email');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
{ numRuns: 100 }
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should handle multiple validation errors on same field', () => {
|
|
674
|
+
fc.assert(
|
|
675
|
+
fc.property(
|
|
676
|
+
fc.string({ maxLength: 2 }),
|
|
677
|
+
(shortValue) => {
|
|
678
|
+
const schema = {
|
|
679
|
+
type: 'object',
|
|
680
|
+
properties: {
|
|
681
|
+
complexField: {
|
|
682
|
+
type: 'string',
|
|
683
|
+
minLength: 5,
|
|
684
|
+
maxLength: 10,
|
|
685
|
+
title: 'Complex Field'
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
required: ['complexField']
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
692
|
+
props: {
|
|
693
|
+
schema,
|
|
694
|
+
modelValue: { complexField: shortValue }
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
699
|
+
|
|
700
|
+
// Property: Should report at least one error for constraint violation
|
|
701
|
+
if (shortValue.length < 5) {
|
|
702
|
+
expect(validationErrors.length).toBeGreaterThan(0);
|
|
703
|
+
const error = validationErrors.find((e: any) => e.field === 'complexField');
|
|
704
|
+
expect(error).toBeDefined();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
),
|
|
708
|
+
{ numRuns: 100 }
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('should validate successfully with all valid data', () => {
|
|
713
|
+
fc.assert(
|
|
714
|
+
fc.property(
|
|
715
|
+
fc.record({
|
|
716
|
+
name: fc.string({ minLength: 1, maxLength: 50 }),
|
|
717
|
+
age: fc.integer({ min: 18, max: 120 }),
|
|
718
|
+
email: fc.emailAddress(),
|
|
719
|
+
active: fc.boolean()
|
|
720
|
+
}),
|
|
721
|
+
(validData) => {
|
|
722
|
+
const schema = {
|
|
723
|
+
type: 'object',
|
|
724
|
+
properties: {
|
|
725
|
+
name: { type: 'string', minLength: 1, maxLength: 50 },
|
|
726
|
+
age: { type: 'number', minimum: 18, maximum: 120 },
|
|
727
|
+
email: { type: 'string', format: 'email' },
|
|
728
|
+
active: { type: 'boolean' }
|
|
729
|
+
},
|
|
730
|
+
required: ['name', 'email']
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
734
|
+
props: {
|
|
735
|
+
schema,
|
|
736
|
+
modelValue: validData
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const validationErrors = (wrapper.vm as any).validate();
|
|
741
|
+
|
|
742
|
+
// Property: All valid data should pass validation
|
|
743
|
+
expect(validationErrors.length).toBe(0);
|
|
744
|
+
}
|
|
745
|
+
),
|
|
746
|
+
{ numRuns: 100 }
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should emit submit event only when validation passes', () => {
|
|
751
|
+
fc.assert(
|
|
752
|
+
fc.property(
|
|
753
|
+
fc.record({
|
|
754
|
+
validName: fc.string({ minLength: 1, maxLength: 20 }),
|
|
755
|
+
validEmail: fc.emailAddress()
|
|
756
|
+
}),
|
|
757
|
+
({ validName, validEmail }) => {
|
|
758
|
+
const schema = {
|
|
759
|
+
type: 'object',
|
|
760
|
+
properties: {
|
|
761
|
+
name: { type: 'string', minLength: 1 },
|
|
762
|
+
email: { type: 'string', format: 'email' }
|
|
763
|
+
},
|
|
764
|
+
required: ['name', 'email']
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
768
|
+
props: {
|
|
769
|
+
schema,
|
|
770
|
+
modelValue: { name: validName, email: validEmail }
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Trigger form submission
|
|
775
|
+
wrapper.find('form').trigger('submit');
|
|
776
|
+
|
|
777
|
+
// Property: Submit event should be emitted for valid data
|
|
778
|
+
expect(wrapper.emitted('submit')).toBeDefined();
|
|
779
|
+
expect(wrapper.emitted('validation-error')).toBeUndefined();
|
|
780
|
+
}
|
|
781
|
+
),
|
|
782
|
+
{ numRuns: 100 }
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('should not emit submit event when validation fails', () => {
|
|
787
|
+
fc.assert(
|
|
788
|
+
fc.property(
|
|
789
|
+
fc.constant({}),
|
|
790
|
+
(emptyData) => {
|
|
791
|
+
const schema = {
|
|
792
|
+
type: 'object',
|
|
793
|
+
properties: {
|
|
794
|
+
requiredField: { type: 'string', minLength: 1 }
|
|
795
|
+
},
|
|
796
|
+
required: ['requiredField']
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const wrapper = mount(JsonSchemaForm, {
|
|
800
|
+
props: {
|
|
801
|
+
schema,
|
|
802
|
+
modelValue: emptyData
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Trigger form submission
|
|
807
|
+
wrapper.find('form').trigger('submit');
|
|
808
|
+
|
|
809
|
+
// Property: Submit event should NOT be emitted for invalid data
|
|
810
|
+
expect(wrapper.emitted('submit')).toBeUndefined();
|
|
811
|
+
expect(wrapper.emitted('validation-error')).toBeDefined();
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
{ numRuns: 50 }
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
});
|