@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,164 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fc from 'fast-check';
3
+ import { useModal } from './useModal';
4
+
5
+ /**
6
+ * Feature: htlkg-modular-architecture, Property 9: Components support v-model
7
+ * Validates: Requirements 12.2
8
+ */
9
+ describe('useModal property tests', () => {
10
+ it('should manage open/close state reactively', () => {
11
+ fc.assert(
12
+ fc.property(
13
+ fc.boolean(),
14
+ (initialOpen) => {
15
+ const { isOpen, open, close, toggle } = useModal({ initialOpen });
16
+
17
+ // Property: Initial state should match initialOpen
18
+ expect(isOpen.value).toBe(initialOpen);
19
+
20
+ // Property: open() should set isOpen to true
21
+ open();
22
+ expect(isOpen.value).toBe(true);
23
+
24
+ // Property: close() should set isOpen to false
25
+ close();
26
+ expect(isOpen.value).toBe(false);
27
+
28
+ // Property: toggle() should flip the state
29
+ const beforeToggle = isOpen.value;
30
+ toggle();
31
+ expect(isOpen.value).toBe(!beforeToggle);
32
+
33
+ // Property: toggle() again should return to original state
34
+ toggle();
35
+ expect(isOpen.value).toBe(beforeToggle);
36
+ }
37
+ ),
38
+ { numRuns: 100 }
39
+ );
40
+ });
41
+
42
+ it('should call callbacks when opening and closing', () => {
43
+ fc.assert(
44
+ fc.property(
45
+ fc.boolean(),
46
+ (initialOpen) => {
47
+ let openCallCount = 0;
48
+ let closeCallCount = 0;
49
+
50
+ const { open, close, toggle } = useModal({
51
+ initialOpen,
52
+ onOpen: () => { openCallCount++; },
53
+ onClose: () => { closeCallCount++; }
54
+ });
55
+
56
+ const initialOpenCalls = openCallCount;
57
+ const initialCloseCalls = closeCallCount;
58
+
59
+ // Property: open() should call onOpen callback
60
+ open();
61
+ expect(openCallCount).toBe(initialOpenCalls + 1);
62
+
63
+ // Property: close() should call onClose callback
64
+ close();
65
+ expect(closeCallCount).toBe(initialCloseCalls + 1);
66
+
67
+ // Property: toggle() should call appropriate callback
68
+ const beforeToggleOpen = openCallCount;
69
+ const beforeToggleClose = closeCallCount;
70
+ toggle(); // Should open
71
+ expect(openCallCount).toBe(beforeToggleOpen + 1);
72
+
73
+ toggle(); // Should close
74
+ expect(closeCallCount).toBe(beforeToggleClose + 1);
75
+ }
76
+ ),
77
+ { numRuns: 100 }
78
+ );
79
+ });
80
+
81
+ it('should maintain state consistency across multiple operations', () => {
82
+ fc.assert(
83
+ fc.property(
84
+ fc.array(fc.constantFrom('open', 'close', 'toggle'), { minLength: 1, maxLength: 20 }),
85
+ (operations) => {
86
+ const { isOpen, open, close, toggle } = useModal({ initialOpen: false });
87
+
88
+ let expectedState = false;
89
+
90
+ for (const operation of operations) {
91
+ switch (operation) {
92
+ case 'open':
93
+ open();
94
+ expectedState = true;
95
+ break;
96
+ case 'close':
97
+ close();
98
+ expectedState = false;
99
+ break;
100
+ case 'toggle':
101
+ toggle();
102
+ expectedState = !expectedState;
103
+ break;
104
+ }
105
+
106
+ // Property: State should always match expected state after operation
107
+ expect(isOpen.value).toBe(expectedState);
108
+ }
109
+ }
110
+ ),
111
+ { numRuns: 100 }
112
+ );
113
+ });
114
+
115
+ it('should handle rapid state changes correctly', () => {
116
+ fc.assert(
117
+ fc.property(
118
+ fc.integer({ min: 1, max: 10 }),
119
+ (toggleCount) => {
120
+ const { isOpen, toggle } = useModal({ initialOpen: false });
121
+
122
+ const initialState = isOpen.value;
123
+
124
+ // Property: Toggling an even number of times should return to initial state
125
+ for (let i = 0; i < toggleCount * 2; i++) {
126
+ toggle();
127
+ }
128
+ expect(isOpen.value).toBe(initialState);
129
+
130
+ // Property: Toggling an odd number of times should flip the state
131
+ toggle();
132
+ expect(isOpen.value).toBe(!initialState);
133
+ }
134
+ ),
135
+ { numRuns: 100 }
136
+ );
137
+ });
138
+
139
+ it('should support v-model pattern with reactive updates', () => {
140
+ fc.assert(
141
+ fc.property(
142
+ fc.boolean(),
143
+ fc.boolean(),
144
+ (initialState, targetState) => {
145
+ const { isOpen, open, close } = useModal({ initialOpen: initialState });
146
+
147
+ // Property: Directly setting isOpen.value should update state (v-model pattern)
148
+ isOpen.value = targetState;
149
+ expect(isOpen.value).toBe(targetState);
150
+
151
+ // Property: Methods should still work after direct state change
152
+ if (targetState) {
153
+ close();
154
+ expect(isOpen.value).toBe(false);
155
+ } else {
156
+ open();
157
+ expect(isOpen.value).toBe(true);
158
+ }
159
+ }
160
+ ),
161
+ { numRuns: 100 }
162
+ );
163
+ });
164
+ });
@@ -0,0 +1,43 @@
1
+ import { ref, type Ref } from 'vue';
2
+
3
+ export interface UseModalOptions {
4
+ initialOpen?: boolean;
5
+ onOpen?: () => void;
6
+ onClose?: () => void;
7
+ }
8
+
9
+ export interface UseModalReturn {
10
+ isOpen: Ref<boolean>;
11
+ open: () => void;
12
+ close: () => void;
13
+ toggle: () => void;
14
+ }
15
+
16
+ export function useModal(options: UseModalOptions = {}): UseModalReturn {
17
+ const isOpen = ref(options.initialOpen ?? false);
18
+
19
+ function open() {
20
+ isOpen.value = true;
21
+ options.onOpen?.();
22
+ }
23
+
24
+ function close() {
25
+ isOpen.value = false;
26
+ options.onClose?.();
27
+ }
28
+
29
+ function toggle() {
30
+ if (isOpen.value) {
31
+ close();
32
+ } else {
33
+ open();
34
+ }
35
+ }
36
+
37
+ return {
38
+ isOpen,
39
+ open,
40
+ close,
41
+ toggle
42
+ };
43
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { useNotifications } from './useNotifications';
3
+
4
+ describe('useNotifications composable', () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ });
8
+
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ it('initializes with empty notifications', () => {
14
+ const { notifications, clearAll } = useNotifications();
15
+ clearAll(); // Clear any existing notifications
16
+ expect(notifications.value).toEqual([]);
17
+ });
18
+
19
+ it('adds notification', () => {
20
+ const { notifications, addNotification, clearAll } = useNotifications();
21
+ clearAll();
22
+
23
+ const id = addNotification({
24
+ message: 'Test notification',
25
+ type: 'info'
26
+ });
27
+
28
+ expect(notifications.value).toHaveLength(1);
29
+ expect(notifications.value[0].id).toBe(id);
30
+ expect(notifications.value[0].message).toBe('Test notification');
31
+ expect(notifications.value[0].type).toBe('info');
32
+ });
33
+
34
+ it('generates unique IDs for notifications', () => {
35
+ const { addNotification, clearAll } = useNotifications();
36
+ clearAll();
37
+
38
+ const id1 = addNotification({ message: 'Test 1', type: 'info' });
39
+ const id2 = addNotification({ message: 'Test 2', type: 'info' });
40
+
41
+ expect(id1).not.toBe(id2);
42
+ });
43
+
44
+ it('removes notification by ID', () => {
45
+ const { notifications, addNotification, removeNotification, clearAll } = useNotifications();
46
+ clearAll();
47
+
48
+ const id = addNotification({ message: 'Test', type: 'info' });
49
+ expect(notifications.value).toHaveLength(1);
50
+
51
+ removeNotification(id);
52
+ expect(notifications.value).toHaveLength(0);
53
+ });
54
+
55
+ it('clears all notifications', () => {
56
+ const { notifications, addNotification, clearAll } = useNotifications();
57
+ clearAll();
58
+
59
+ addNotification({ message: 'Test 1', type: 'info' });
60
+ addNotification({ message: 'Test 2', type: 'success' });
61
+ expect(notifications.value).toHaveLength(2);
62
+
63
+ clearAll();
64
+ expect(notifications.value).toEqual([]);
65
+ });
66
+
67
+ it('auto-removes notification after duration', () => {
68
+ const { notifications, addNotification, clearAll } = useNotifications();
69
+ clearAll();
70
+
71
+ addNotification({
72
+ message: 'Test',
73
+ type: 'info',
74
+ duration: 3000
75
+ });
76
+
77
+ expect(notifications.value).toHaveLength(1);
78
+
79
+ vi.advanceTimersByTime(3000);
80
+ expect(notifications.value).toHaveLength(0);
81
+ });
82
+
83
+ it('adds info notification with default duration', () => {
84
+ const { notifications, info, clearAll } = useNotifications();
85
+ clearAll();
86
+
87
+ info('Info message');
88
+ expect(notifications.value).toHaveLength(1);
89
+ expect(notifications.value[0].type).toBe('info');
90
+ expect(notifications.value[0].message).toBe('Info message');
91
+
92
+ vi.advanceTimersByTime(3000);
93
+ expect(notifications.value).toHaveLength(0);
94
+ });
95
+
96
+ it('adds success notification with default duration', () => {
97
+ const { notifications, success, clearAll } = useNotifications();
98
+ clearAll();
99
+
100
+ success('Success message');
101
+ expect(notifications.value).toHaveLength(1);
102
+ expect(notifications.value[0].type).toBe('success');
103
+ expect(notifications.value[0].message).toBe('Success message');
104
+
105
+ vi.advanceTimersByTime(3000);
106
+ expect(notifications.value).toHaveLength(0);
107
+ });
108
+
109
+ it('adds warning notification with default duration', () => {
110
+ const { notifications, warning, clearAll } = useNotifications();
111
+ clearAll();
112
+
113
+ warning('Warning message');
114
+ expect(notifications.value).toHaveLength(1);
115
+ expect(notifications.value[0].type).toBe('warning');
116
+ expect(notifications.value[0].message).toBe('Warning message');
117
+
118
+ vi.advanceTimersByTime(3000);
119
+ expect(notifications.value).toHaveLength(0);
120
+ });
121
+
122
+ it('adds error notification with longer default duration', () => {
123
+ const { notifications, error, clearAll } = useNotifications();
124
+ clearAll();
125
+
126
+ error('Error message');
127
+ expect(notifications.value).toHaveLength(1);
128
+ expect(notifications.value[0].type).toBe('error');
129
+ expect(notifications.value[0].message).toBe('Error message');
130
+
131
+ vi.advanceTimersByTime(5000);
132
+ expect(notifications.value).toHaveLength(0);
133
+ });
134
+
135
+ it('allows custom duration for helper methods', () => {
136
+ const { notifications, info, clearAll } = useNotifications();
137
+ clearAll();
138
+
139
+ info('Custom duration', 1000);
140
+ expect(notifications.value).toHaveLength(1);
141
+
142
+ vi.advanceTimersByTime(1000);
143
+ expect(notifications.value).toHaveLength(0);
144
+ });
145
+
146
+ it('handles removing non-existent notification gracefully', () => {
147
+ const { notifications, removeNotification, clearAll } = useNotifications();
148
+ clearAll();
149
+
150
+ expect(() => removeNotification('non-existent-id')).not.toThrow();
151
+ expect(notifications.value).toEqual([]);
152
+ });
153
+
154
+ it('maintains notification order', () => {
155
+ const { notifications, addNotification, clearAll } = useNotifications();
156
+ clearAll();
157
+
158
+ addNotification({ message: 'First', type: 'info' });
159
+ addNotification({ message: 'Second', type: 'success' });
160
+ addNotification({ message: 'Third', type: 'warning' });
161
+
162
+ expect(notifications.value[0].message).toBe('First');
163
+ expect(notifications.value[1].message).toBe('Second');
164
+ expect(notifications.value[2].message).toBe('Third');
165
+ });
166
+ });
@@ -0,0 +1,81 @@
1
+ import { ref, type Ref } from 'vue';
2
+
3
+ export interface Notification {
4
+ id: string;
5
+ message: string;
6
+ type: 'info' | 'success' | 'warning' | 'error';
7
+ duration?: number;
8
+ }
9
+
10
+ export interface UseNotificationsReturn {
11
+ notifications: Ref<Notification[]>;
12
+ addNotification: (notification: Omit<Notification, 'id'>) => string;
13
+ removeNotification: (id: string) => void;
14
+ clearAll: () => void;
15
+ info: (message: string, duration?: number) => string;
16
+ success: (message: string, duration?: number) => string;
17
+ warning: (message: string, duration?: number) => string;
18
+ error: (message: string, duration?: number) => string;
19
+ }
20
+
21
+ // Global notifications state
22
+ const notifications = ref<Notification[]>([]);
23
+
24
+ export function useNotifications(): UseNotificationsReturn {
25
+ function addNotification(notification: Omit<Notification, 'id'>): string {
26
+ const id = `notification-${Date.now()}-${Math.random()}`;
27
+ const newNotification: Notification = {
28
+ ...notification,
29
+ id
30
+ };
31
+
32
+ notifications.value.push(newNotification);
33
+
34
+ // Auto-remove after duration
35
+ if (notification.duration) {
36
+ setTimeout(() => {
37
+ removeNotification(id);
38
+ }, notification.duration);
39
+ }
40
+
41
+ return id;
42
+ }
43
+
44
+ function removeNotification(id: string) {
45
+ const index = notifications.value.findIndex(n => n.id === id);
46
+ if (index !== -1) {
47
+ notifications.value.splice(index, 1);
48
+ }
49
+ }
50
+
51
+ function clearAll() {
52
+ notifications.value = [];
53
+ }
54
+
55
+ function info(message: string, duration = 3000): string {
56
+ return addNotification({ message, type: 'info', duration });
57
+ }
58
+
59
+ function success(message: string, duration = 3000): string {
60
+ return addNotification({ message, type: 'success', duration });
61
+ }
62
+
63
+ function warning(message: string, duration = 3000): string {
64
+ return addNotification({ message, type: 'warning', duration });
65
+ }
66
+
67
+ function error(message: string, duration = 5000): string {
68
+ return addNotification({ message, type: 'error', duration });
69
+ }
70
+
71
+ return {
72
+ notifications,
73
+ addNotification,
74
+ removeNotification,
75
+ clearAll,
76
+ info,
77
+ success,
78
+ warning,
79
+ error
80
+ };
81
+ }
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fc from 'fast-check';
3
+ import { useTable } from './useTable';
4
+
5
+ /**
6
+ * Feature: htlkg-modular-architecture, Property 8: Stateful components manage state
7
+ * Validates: Requirements 12.1
8
+ */
9
+ describe('useTable property tests', () => {
10
+ it('should manage pagination state reactively', () => {
11
+ fc.assert(
12
+ fc.property(
13
+ fc.array(fc.record({
14
+ id: fc.string(),
15
+ name: fc.string(),
16
+ value: fc.integer()
17
+ }), { minLength: 0, maxLength: 100 }),
18
+ fc.integer({ min: 1, max: 50 }),
19
+ fc.integer({ min: 1, max: 10 }),
20
+ (items, pageSize, targetPage) => {
21
+ const { currentPage, paginatedItems, totalPages, setPage } = useTable({
22
+ items,
23
+ pageSize
24
+ });
25
+
26
+ // Property: Setting page should update currentPage
27
+ const validPage = Math.min(targetPage, totalPages.value);
28
+ if (validPage >= 1) {
29
+ setPage(validPage);
30
+ expect(currentPage.value).toBe(validPage);
31
+ }
32
+
33
+ // Property: Paginated items should not exceed page size
34
+ expect(paginatedItems.value.length).toBeLessThanOrEqual(pageSize);
35
+
36
+ // Property: Total pages should be correct
37
+ const expectedPages = Math.ceil(items.length / pageSize);
38
+ expect(totalPages.value).toBe(expectedPages);
39
+ }
40
+ ),
41
+ { numRuns: 100 }
42
+ );
43
+ });
44
+
45
+ it('should manage sorting state reactively', () => {
46
+ fc.assert(
47
+ fc.property(
48
+ fc.array(fc.record({
49
+ id: fc.string(),
50
+ name: fc.string(),
51
+ value: fc.integer()
52
+ }), { minLength: 2, maxLength: 50 }),
53
+ fc.constantFrom('id', 'name', 'value'),
54
+ fc.constantFrom('asc' as const, 'desc' as const),
55
+ (items, sortField, sortDirection) => {
56
+ const { sortKey, sortOrder, sortedItems, setSorting } = useTable({
57
+ items
58
+ });
59
+
60
+ // Property: Setting sorting should update sortKey and sortOrder
61
+ setSorting(sortField, sortDirection);
62
+ expect(sortKey.value).toBe(sortField);
63
+ expect(sortOrder.value).toBe(sortDirection);
64
+
65
+ // Property: Sorted items should maintain same length
66
+ expect(sortedItems.value.length).toBe(items.length);
67
+
68
+ // Property: Sorted items should be in correct order
69
+ if (sortedItems.value.length > 1) {
70
+ for (let i = 0; i < sortedItems.value.length - 1; i++) {
71
+ const current = sortedItems.value[i][sortField];
72
+ const next = sortedItems.value[i + 1][sortField];
73
+
74
+ if (current != null && next != null) {
75
+ if (sortDirection === 'asc') {
76
+ expect(current <= next).toBe(true);
77
+ } else {
78
+ expect(current >= next).toBe(true);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ),
85
+ { numRuns: 100 }
86
+ );
87
+ });
88
+
89
+ it('should manage selection state reactively', () => {
90
+ fc.assert(
91
+ fc.property(
92
+ fc.array(fc.record({
93
+ id: fc.string(),
94
+ name: fc.string()
95
+ }), { minLength: 1, maxLength: 50 }),
96
+ (items) => {
97
+ const { selectedItems, selectItem, deselectItem, selectAll, clearSelection, isSelected } = useTable({
98
+ items
99
+ });
100
+
101
+ // Property: Initially no items selected
102
+ expect(selectedItems.value.length).toBe(0);
103
+
104
+ // Property: Selecting an item should add it to selection
105
+ const itemToSelect = items[0];
106
+ selectItem(itemToSelect);
107
+ expect(isSelected(itemToSelect)).toBe(true);
108
+ expect(selectedItems.value.length).toBe(1);
109
+
110
+ // Property: Deselecting an item should remove it from selection
111
+ deselectItem(itemToSelect);
112
+ expect(isSelected(itemToSelect)).toBe(false);
113
+ expect(selectedItems.value.length).toBe(0);
114
+
115
+ // Property: Select all should select all items
116
+ selectAll();
117
+ expect(selectedItems.value.length).toBe(items.length);
118
+ items.forEach(item => {
119
+ expect(isSelected(item)).toBe(true);
120
+ });
121
+
122
+ // Property: Clear selection should remove all selections
123
+ clearSelection();
124
+ expect(selectedItems.value.length).toBe(0);
125
+ items.forEach(item => {
126
+ expect(isSelected(item)).toBe(false);
127
+ });
128
+ }
129
+ ),
130
+ { numRuns: 100 }
131
+ );
132
+ });
133
+
134
+ it('should handle filtering with pagination correctly', () => {
135
+ fc.assert(
136
+ fc.property(
137
+ fc.array(fc.record({
138
+ id: fc.string(),
139
+ name: fc.string(),
140
+ active: fc.boolean()
141
+ }), { minLength: 0, maxLength: 100 }),
142
+ fc.integer({ min: 5, max: 20 }),
143
+ (items, pageSize) => {
144
+ const { paginatedItems, totalPages, currentPage } = useTable({
145
+ items,
146
+ pageSize
147
+ });
148
+
149
+ // Property: First page should start from index 0
150
+ if (items.length > 0) {
151
+ const firstPageItem = paginatedItems.value[0];
152
+ expect(items.slice(0, pageSize)).toContainEqual(firstPageItem);
153
+ }
154
+
155
+ // Property: Total pages calculation should be consistent
156
+ const expectedPages = Math.ceil(items.length / pageSize);
157
+ expect(totalPages.value).toBe(expectedPages);
158
+
159
+ // Property: Current page should always be valid
160
+ expect(currentPage.value).toBeGreaterThanOrEqual(1);
161
+ expect(currentPage.value).toBeLessThanOrEqual(Math.max(1, totalPages.value));
162
+ }
163
+ ),
164
+ { numRuns: 100 }
165
+ );
166
+ });
167
+
168
+ it('should maintain state consistency when changing page size', () => {
169
+ fc.assert(
170
+ fc.property(
171
+ fc.array(fc.record({
172
+ id: fc.string(),
173
+ value: fc.integer()
174
+ }), { minLength: 10, maxLength: 100 }),
175
+ fc.integer({ min: 5, max: 20 }),
176
+ fc.integer({ min: 5, max: 30 }),
177
+ (items, initialPageSize, newPageSize) => {
178
+ const { currentPage, pageSize, totalPages, setPageSize } = useTable({
179
+ items,
180
+ pageSize: initialPageSize
181
+ });
182
+
183
+ const initialTotalPages = totalPages.value;
184
+
185
+ // Property: Changing page size should reset to page 1
186
+ setPageSize(newPageSize);
187
+ expect(currentPage.value).toBe(1);
188
+ expect(pageSize.value).toBe(newPageSize);
189
+
190
+ // Property: Total pages should update correctly
191
+ const expectedPages = Math.ceil(items.length / newPageSize);
192
+ expect(totalPages.value).toBe(expectedPages);
193
+ }
194
+ ),
195
+ { numRuns: 100 }
196
+ );
197
+ });
198
+ });