@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,548 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fc from 'fast-check';
3
+ import { mount } from '@vue/test-utils';
4
+ import Table from './Table.vue';
5
+
6
+ /**
7
+ * Property tests for Table component
8
+ * Tests with random data generation to find edge cases
9
+ */
10
+ describe('Table property tests', () => {
11
+ const globalStubs = {
12
+ uiTable: true,
13
+ uiPagination: true,
14
+ uiDropdown: true,
15
+ uiSmartFilterMultipleV2: true,
16
+ uiNoResults: true
17
+ };
18
+
19
+ it('should handle any number of columns', () => {
20
+ fc.assert(
21
+ fc.property(
22
+ fc.array(
23
+ fc.record({
24
+ name: fc.string({ minLength: 1, maxLength: 20 }),
25
+ value: fc.string({ minLength: 1, maxLength: 20 })
26
+ }),
27
+ { minLength: 1, maxLength: 10 }
28
+ ),
29
+ (columns) => {
30
+ const items = [{ id: 1 }];
31
+
32
+ const wrapper = mount(Table, {
33
+ props: { items, columns },
34
+ global: { stubs: globalStubs }
35
+ });
36
+
37
+ const vm = wrapper.vm as any;
38
+ expect(vm.tableHeader).toHaveLength(columns.length);
39
+ expect(vm.tableItems[0].row).toHaveLength(columns.length);
40
+ }
41
+ ),
42
+ { numRuns: 100 }
43
+ );
44
+ });
45
+
46
+ it('should handle any number of items', () => {
47
+ fc.assert(
48
+ fc.property(
49
+ fc.array(
50
+ fc.record({
51
+ id: fc.integer({ min: 1, max: 10000 }),
52
+ name: fc.string({ minLength: 1, maxLength: 50 }),
53
+ value: fc.integer()
54
+ }),
55
+ { minLength: 0, maxLength: 100 }
56
+ ),
57
+ (items) => {
58
+ const columns = [
59
+ { name: 'ID', value: 'id' },
60
+ { name: 'Name', value: 'name' },
61
+ { name: 'Value', value: 'value' }
62
+ ];
63
+
64
+ const wrapper = mount(Table, {
65
+ props: { items, columns },
66
+ global: { stubs: globalStubs }
67
+ });
68
+
69
+ const vm = wrapper.vm as any;
70
+ expect(vm.tableItems).toHaveLength(items.length);
71
+
72
+ if (items.length === 0) {
73
+ expect(vm.showInitialNoResults).toBe(true);
74
+ } else {
75
+ expect(vm.showTable).toBe(true);
76
+ }
77
+ }
78
+ ),
79
+ { numRuns: 100 }
80
+ );
81
+ });
82
+
83
+ it('should correctly map item properties to table rows', () => {
84
+ fc.assert(
85
+ fc.property(
86
+ fc.array(
87
+ fc.record({
88
+ id: fc.integer(),
89
+ field1: fc.string(),
90
+ field2: fc.integer(),
91
+ field3: fc.boolean()
92
+ }),
93
+ { minLength: 1, maxLength: 10 }
94
+ ),
95
+ (items) => {
96
+ const columns = [
97
+ { name: 'Field 1', value: 'field1' },
98
+ { name: 'Field 2', value: 'field2' },
99
+ { name: 'Field 3', value: 'field3' }
100
+ ];
101
+
102
+ const wrapper = mount(Table, {
103
+ props: { items, columns },
104
+ global: { stubs: globalStubs }
105
+ });
106
+
107
+ const vm = wrapper.vm as any;
108
+
109
+ items.forEach((item, index) => {
110
+ const tableItem = vm.tableItems[index];
111
+ expect(tableItem.id).toBe(item.id);
112
+ expect(tableItem.row[0]).toBe(String(item.field1));
113
+ expect(tableItem.row[1]).toBe(String(item.field2));
114
+ expect(tableItem.row[2]).toBe(String(item.field3));
115
+ });
116
+ }
117
+ ),
118
+ { numRuns: 100 }
119
+ );
120
+ });
121
+
122
+ it('should handle page changes for any valid page number', () => {
123
+ fc.assert(
124
+ fc.property(
125
+ fc.integer({ min: 1, max: 100 }),
126
+ fc.integer({ min: 1, max: 100 }),
127
+ (currentPage, newPage) => {
128
+ const items = Array.from({ length: 50 }, (_, i) => ({ id: i + 1 }));
129
+ const columns = [{ name: 'ID', value: 'id' }];
130
+
131
+ const wrapper = mount(Table, {
132
+ props: { items, columns, currentPage, totalPages: 100 },
133
+ global: { stubs: globalStubs }
134
+ });
135
+
136
+ const vm = wrapper.vm as any;
137
+ vm.handleChangePage(newPage);
138
+
139
+ expect(wrapper.emitted('update:currentPage')).toBeDefined();
140
+ expect(wrapper.emitted('update:currentPage')?.[0]).toEqual([newPage]);
141
+ expect(wrapper.emitted('page-changed')?.[0]).toEqual([newPage]);
142
+ }
143
+ ),
144
+ { numRuns: 100 }
145
+ );
146
+ });
147
+
148
+ it('should handle page size changes for any valid size', () => {
149
+ fc.assert(
150
+ fc.property(
151
+ fc.constantFrom(10, 25, 50, 100),
152
+ (pageSize) => {
153
+ const items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1 }));
154
+ const columns = [{ name: 'ID', value: 'id' }];
155
+
156
+ const wrapper = mount(Table, {
157
+ props: { items, columns, itemsPerPage: 10 },
158
+ global: { stubs: globalStubs }
159
+ });
160
+
161
+ const vm = wrapper.vm as any;
162
+ vm.handleChangePageSize({ value: String(pageSize) });
163
+
164
+ expect(wrapper.emitted('update:itemsPerPage')?.[0]).toEqual([pageSize]);
165
+ expect(wrapper.emitted('update:currentPage')?.[0]).toEqual([1]);
166
+ }
167
+ ),
168
+ { numRuns: 50 }
169
+ );
170
+ });
171
+
172
+ it('should handle sorting by any column', () => {
173
+ fc.assert(
174
+ fc.property(
175
+ fc.array(
176
+ fc.record({
177
+ name: fc.string({ minLength: 1, maxLength: 20 }),
178
+ value: fc.string({ minLength: 1, maxLength: 20 })
179
+ }),
180
+ { minLength: 1, maxLength: 5 }
181
+ ),
182
+ fc.constantFrom('asc', 'desc'),
183
+ (columns, direction) => {
184
+ const items = [{ id: 1 }];
185
+
186
+ const wrapper = mount(Table, {
187
+ props: { items, columns },
188
+ global: { stubs: globalStubs }
189
+ });
190
+
191
+ const vm = wrapper.vm as any;
192
+ const columnToSort = columns[0].value;
193
+
194
+ vm.handleOrderBy({ value: columnToSort, orderDirection: direction });
195
+
196
+ expect(wrapper.emitted('update:orderedBy')?.[0]).toEqual([columnToSort]);
197
+ expect(wrapper.emitted('update:orderDirection')?.[0]).toEqual([direction]);
198
+ }
199
+ ),
200
+ { numRuns: 100 }
201
+ );
202
+ });
203
+
204
+ it('should handle column visibility for any column index', () => {
205
+ fc.assert(
206
+ fc.property(
207
+ fc.integer({ min: 3, max: 10 }),
208
+ fc.integer({ min: 0, max: 9 }),
209
+ (numColumns, columnIndex) => {
210
+ const columns = Array.from({ length: numColumns }, (_, i) => ({
211
+ name: `Column ${i}`,
212
+ value: `col${i}`
213
+ }));
214
+ const items = [{ id: 1 }];
215
+
216
+ if (columnIndex >= numColumns) return;
217
+
218
+ const wrapper = mount(Table, {
219
+ props: { items, columns },
220
+ global: { stubs: globalStubs }
221
+ });
222
+
223
+ const vm = wrapper.vm as any;
224
+
225
+ // Hide column
226
+ vm.handleColumnsVisibilityChanged({ index: columnIndex, hidden: true });
227
+ expect(vm.hiddenColumns).toContain(columnIndex);
228
+
229
+ // Show column
230
+ vm.handleColumnsVisibilityChanged({ index: columnIndex, hidden: false });
231
+ expect(vm.hiddenColumns).not.toContain(columnIndex);
232
+ }
233
+ ),
234
+ { numRuns: 100 }
235
+ );
236
+ });
237
+
238
+ it('should handle selection of any subset of items', () => {
239
+ fc.assert(
240
+ fc.property(
241
+ fc.array(
242
+ fc.record({
243
+ id: fc.integer({ min: 1, max: 1000 }),
244
+ name: fc.string()
245
+ }),
246
+ { minLength: 1, maxLength: 20 }
247
+ ),
248
+ fc.array(fc.integer({ min: 0, max: 19 }), { maxLength: 10 }),
249
+ (items, selectedIndices) => {
250
+ const columns = [
251
+ { name: 'ID', value: 'id' },
252
+ { name: 'Name', value: 'name' }
253
+ ];
254
+
255
+ const wrapper = mount(Table, {
256
+ props: { items, columns },
257
+ global: { stubs: globalStubs }
258
+ });
259
+
260
+ const vm = wrapper.vm as any;
261
+
262
+ // Select items by indices
263
+ const selectedIds = selectedIndices
264
+ .filter(i => i < items.length)
265
+ .map(i => items[i].id);
266
+
267
+ // Get unique IDs (Set removes duplicates)
268
+ const uniqueSelectedIds = [...new Set(selectedIds)];
269
+
270
+ vm.handleTableAction({ action: 'test', items: selectedIds });
271
+
272
+ expect(vm.selectedItemIds.size).toBe(uniqueSelectedIds.length);
273
+ uniqueSelectedIds.forEach(id => {
274
+ expect(vm.selectedItemIds.has(id)).toBe(true);
275
+ });
276
+ }
277
+ ),
278
+ { numRuns: 100 }
279
+ );
280
+ });
281
+
282
+ it('should emit correct events for any action', () => {
283
+ fc.assert(
284
+ fc.property(
285
+ fc.string({ minLength: 1, maxLength: 20 }),
286
+ fc.array(fc.integer({ min: 1, max: 100 }), { maxLength: 10 }),
287
+ (action, itemIds) => {
288
+ const items = itemIds.map(id => ({ id, name: `Item ${id}` }));
289
+ const columns = [{ name: 'ID', value: 'id' }];
290
+
291
+ const wrapper = mount(Table, {
292
+ props: { items, columns },
293
+ global: { stubs: globalStubs }
294
+ });
295
+
296
+ const vm = wrapper.vm as any;
297
+ vm.handleTableAction({ action, items: itemIds });
298
+
299
+ expect(wrapper.emitted('table-action')).toBeDefined();
300
+ expect(wrapper.emitted('table-action')?.[0]).toEqual([
301
+ { action, items: itemIds }
302
+ ]);
303
+ }
304
+ ),
305
+ { numRuns: 100 }
306
+ );
307
+ });
308
+
309
+ it('should handle items with missing properties gracefully', () => {
310
+ fc.assert(
311
+ fc.property(
312
+ fc.array(
313
+ fc.record({
314
+ id: fc.option(fc.integer(), { nil: undefined }),
315
+ name: fc.option(fc.string(), { nil: undefined }),
316
+ email: fc.option(fc.string(), { nil: undefined })
317
+ }),
318
+ { minLength: 1, maxLength: 10 }
319
+ ),
320
+ (items) => {
321
+ const columns = [
322
+ { name: 'ID', value: 'id' },
323
+ { name: 'Name', value: 'name' },
324
+ { name: 'Email', value: 'email' }
325
+ ];
326
+
327
+ const wrapper = mount(Table, {
328
+ props: { items, columns },
329
+ global: { stubs: globalStubs }
330
+ });
331
+
332
+ const vm = wrapper.vm as any;
333
+
334
+ // Should not throw and should handle undefined values
335
+ expect(vm.tableItems).toHaveLength(items.length);
336
+ vm.tableItems.forEach((tableItem: any) => {
337
+ expect(tableItem.row).toHaveLength(3);
338
+ tableItem.row.forEach((cell: any) => {
339
+ expect(typeof cell).toBe('string');
340
+ });
341
+ });
342
+ }
343
+ ),
344
+ { numRuns: 100 }
345
+ );
346
+ });
347
+
348
+ it('should maintain selection state across operations', () => {
349
+ fc.assert(
350
+ fc.property(
351
+ fc.array(
352
+ fc.record({
353
+ id: fc.integer({ min: 1, max: 100 }),
354
+ name: fc.string()
355
+ }),
356
+ { minLength: 5, maxLength: 20 }
357
+ ),
358
+ (items) => {
359
+ const columns = [
360
+ { name: 'ID', value: 'id' },
361
+ { name: 'Name', value: 'name' }
362
+ ];
363
+
364
+ const wrapper = mount(Table, {
365
+ props: { items, columns },
366
+ global: { stubs: globalStubs }
367
+ });
368
+
369
+ const vm = wrapper.vm as any;
370
+
371
+ // Select all items
372
+ vm.handleSelectAllItems();
373
+ const initialSize = vm.selectedItemIds.size;
374
+
375
+ // Size should match unique IDs (Set removes duplicates)
376
+ const uniqueIds = new Set(items.map(item => item.id));
377
+ expect(initialSize).toBe(uniqueIds.size);
378
+
379
+ // Deselect all
380
+ vm.handleDeselectAllItems();
381
+ expect(vm.selectedItemIds.size).toBe(0);
382
+
383
+ // Select all again
384
+ vm.handleSelectAllItems();
385
+ expect(vm.selectedItemIds.size).toBe(initialSize);
386
+ }
387
+ ),
388
+ { numRuns: 100 }
389
+ );
390
+ });
391
+
392
+ it('should handle loading state correctly', () => {
393
+ fc.assert(
394
+ fc.property(
395
+ fc.boolean(),
396
+ fc.array(fc.record({ id: fc.integer() }), { maxLength: 10 }),
397
+ (loading, items) => {
398
+ const columns = [{ name: 'ID', value: 'id' }];
399
+
400
+ const wrapper = mount(Table, {
401
+ props: { items, columns, loading },
402
+ global: { stubs: globalStubs }
403
+ });
404
+
405
+ const vm = wrapper.vm as any;
406
+
407
+ if (loading) {
408
+ expect(vm.showTable).toBe(true);
409
+ expect(vm.showInitialNoResults).toBe(false);
410
+ } else if (items.length === 0) {
411
+ expect(vm.showInitialNoResults).toBe(true);
412
+ } else {
413
+ expect(vm.showTable).toBe(true);
414
+ }
415
+ }
416
+ ),
417
+ { numRuns: 100 }
418
+ );
419
+ });
420
+
421
+ it('should reset selection when sorting or paginating', () => {
422
+ fc.assert(
423
+ fc.property(
424
+ fc.array(
425
+ fc.record({ id: fc.integer({ min: 1, max: 100 }) }),
426
+ { minLength: 5, maxLength: 20 }
427
+ ),
428
+ fc.constantFrom('name', 'id', 'email'),
429
+ fc.integer({ min: 1, max: 10 }),
430
+ (items, sortField, newPage) => {
431
+ const columns = [{ name: 'ID', value: 'id' }];
432
+
433
+ const wrapper = mount(Table, {
434
+ props: { items, columns },
435
+ global: { stubs: globalStubs }
436
+ });
437
+
438
+ const vm = wrapper.vm as any;
439
+
440
+ // Select items
441
+ vm.handleSelectAllItems();
442
+ const uniqueIds = new Set(items.map(item => item.id));
443
+ expect(vm.selectedItemIds.size).toBe(uniqueIds.size);
444
+
445
+ // Sort - should reset
446
+ vm.handleOrderBy({ value: sortField, orderDirection: 'asc' });
447
+ expect(vm.resetSelected).toBe(true);
448
+
449
+ // Reset flag
450
+ vm.resetSelected = false;
451
+
452
+ // Change page - should reset
453
+ vm.handleChangePage(newPage);
454
+ expect(vm.resetSelected).toBe(true);
455
+ }
456
+ ),
457
+ { numRuns: 100 }
458
+ );
459
+ });
460
+
461
+ it('should handle filters with any structure', () => {
462
+ fc.assert(
463
+ fc.property(
464
+ fc.dictionary(
465
+ fc.string({ minLength: 1, maxLength: 20 }),
466
+ fc.oneof(
467
+ fc.string(),
468
+ fc.integer(),
469
+ fc.boolean()
470
+ )
471
+ ),
472
+ (filters) => {
473
+ const items = [{ id: 1 }];
474
+ const columns = [{ name: 'ID', value: 'id' }];
475
+
476
+ const wrapper = mount(Table, {
477
+ props: { items, columns },
478
+ global: { stubs: globalStubs }
479
+ });
480
+
481
+ const vm = wrapper.vm as any;
482
+ vm.handleSmartFiltersSent(filters);
483
+
484
+ expect(wrapper.emitted('smart-filters-sent')?.[0]).toEqual([filters]);
485
+ expect(wrapper.emitted('multiple-filters-applied')?.[0]).toEqual([filters]);
486
+ expect(vm.hasUserSearched).toBe(true);
487
+ }
488
+ ),
489
+ { numRuns: 100 }
490
+ );
491
+ });
492
+
493
+ it('should handle pagination calculations correctly', () => {
494
+ fc.assert(
495
+ fc.property(
496
+ fc.integer({ min: 0, max: 1000 }),
497
+ fc.constantFrom(10, 25, 50, 100),
498
+ (totalItems, itemsPerPage) => {
499
+ const items = Array.from({ length: Math.min(totalItems, itemsPerPage) }, (_, i) => ({ id: i }));
500
+ const columns = [{ name: 'ID', value: 'id' }];
501
+ const totalPages = Math.ceil(totalItems / itemsPerPage) || 1;
502
+
503
+ const wrapper = mount(Table, {
504
+ props: {
505
+ items,
506
+ columns,
507
+ totalItems,
508
+ itemsPerPage,
509
+ totalPages
510
+ },
511
+ global: { stubs: globalStubs }
512
+ });
513
+
514
+ const vm = wrapper.vm as any;
515
+ expect(vm.totalItems).toBe(totalItems);
516
+ expect(vm.itemsPerPage).toBe(itemsPerPage);
517
+ expect(vm.totalPages).toBe(totalPages);
518
+ }
519
+ ),
520
+ { numRuns: 100 }
521
+ );
522
+ });
523
+
524
+ it('should expose all required methods', () => {
525
+ fc.assert(
526
+ fc.property(
527
+ fc.constant(null),
528
+ () => {
529
+ const items = [{ id: 1 }];
530
+ const columns = [{ name: 'ID', value: 'id' }];
531
+
532
+ const wrapper = mount(Table, {
533
+ props: { items, columns },
534
+ global: { stubs: globalStubs }
535
+ });
536
+
537
+ const vm = wrapper.vm as any;
538
+
539
+ // Check all exposed methods exist
540
+ expect(typeof vm.clearSelection).toBe('function');
541
+ expect(typeof vm.getHiddenColumns).toBe('function');
542
+ expect(typeof vm.getSelectedItems).toBe('function');
543
+ }
544
+ ),
545
+ { numRuns: 50 }
546
+ );
547
+ });
548
+ });