@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.
Files changed (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. 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
+ });