@keenthemes/ktui 1.0.29 → 1.1.0
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/README.md +27 -0
- package/dist/ktui.js +8780 -6199
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +2744 -1367
- package/lib/cjs/components/alert/alert.js +1025 -0
- package/lib/cjs/components/alert/alert.js.map +1 -0
- package/lib/cjs/components/alert/index.js +20 -0
- package/lib/cjs/components/alert/index.js.map +1 -0
- package/lib/cjs/components/alert/templates.js +120 -0
- package/lib/cjs/components/alert/templates.js.map +1 -0
- package/lib/cjs/components/alert/types.js +7 -0
- package/lib/cjs/components/alert/types.js.map +1 -0
- package/lib/cjs/components/datepicker/config/config.js +42 -0
- package/lib/cjs/components/datepicker/config/config.js.map +1 -0
- package/lib/cjs/components/datepicker/config/index.js +24 -0
- package/lib/cjs/components/datepicker/config/index.js.map +1 -0
- package/lib/cjs/components/datepicker/config/interfaces.js +7 -0
- package/lib/cjs/components/datepicker/config/interfaces.js.map +1 -0
- package/lib/cjs/components/datepicker/config/types.js +7 -0
- package/lib/cjs/components/datepicker/config/types.js.map +1 -0
- package/lib/cjs/components/datepicker/core/event-manager.js +135 -0
- package/lib/cjs/components/datepicker/core/event-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/core/focus-manager.js +167 -0
- package/lib/cjs/components/datepicker/core/focus-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/core/helpers.js +219 -0
- package/lib/cjs/components/datepicker/core/helpers.js.map +1 -0
- package/lib/cjs/components/datepicker/core/index.js +25 -0
- package/lib/cjs/components/datepicker/core/index.js.map +1 -0
- package/lib/cjs/components/datepicker/core/unified-state-manager.js +394 -0
- package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/datepicker.js +2066 -763
- package/lib/cjs/components/datepicker/datepicker.js.map +1 -1
- package/lib/cjs/components/datepicker/index.js +19 -8
- package/lib/cjs/components/datepicker/index.js.map +1 -1
- package/lib/cjs/components/datepicker/ui/index.js +23 -0
- package/lib/cjs/components/datepicker/ui/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/dropdown.js +489 -0
- package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/index.js +23 -0
- package/lib/cjs/components/datepicker/ui/input/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/segmented-input.js +640 -0
- package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/calendar.js +446 -0
- package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/footer.js +42 -0
- package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/header.js +32 -0
- package/lib/cjs/components/datepicker/ui/renderers/header.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/index.js +25 -0
- package/lib/cjs/components/datepicker/ui/renderers/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +384 -0
- package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/templates/index.js +22 -0
- package/lib/cjs/components/datepicker/ui/templates/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/templates/templates.js +253 -0
- package/lib/cjs/components/datepicker/ui/templates/templates.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/date-formatters.js +88 -0
- package/lib/cjs/components/datepicker/utils/date-formatters.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/date-utils.js +194 -0
- package/lib/cjs/components/datepicker/utils/date-utils.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/index.js +24 -0
- package/lib/cjs/components/datepicker/utils/index.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/time-utils.js +213 -0
- package/lib/cjs/components/datepicker/utils/time-utils.js.map +1 -0
- package/lib/cjs/index.js +6 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/alert/alert.js +1022 -0
- package/lib/esm/components/alert/alert.js.map +1 -0
- package/lib/esm/components/alert/index.js +4 -0
- package/lib/esm/components/alert/index.js.map +1 -0
- package/lib/esm/components/alert/templates.js +112 -0
- package/lib/esm/components/alert/templates.js.map +1 -0
- package/lib/esm/components/alert/types.js +6 -0
- package/lib/esm/components/alert/types.js.map +1 -0
- package/lib/esm/components/datepicker/config/config.js +39 -0
- package/lib/esm/components/datepicker/config/config.js.map +1 -0
- package/lib/esm/components/datepicker/config/index.js +8 -0
- package/lib/esm/components/datepicker/config/index.js.map +1 -0
- package/lib/esm/components/datepicker/config/interfaces.js +6 -0
- package/lib/esm/components/datepicker/config/interfaces.js.map +1 -0
- package/lib/esm/components/datepicker/config/types.js +6 -0
- package/lib/esm/components/datepicker/config/types.js.map +1 -0
- package/lib/esm/components/datepicker/core/event-manager.js +133 -0
- package/lib/esm/components/datepicker/core/event-manager.js.map +1 -0
- package/lib/esm/components/datepicker/core/focus-manager.js +164 -0
- package/lib/esm/components/datepicker/core/focus-manager.js.map +1 -0
- package/lib/esm/components/datepicker/core/helpers.js +211 -0
- package/lib/esm/components/datepicker/core/helpers.js.map +1 -0
- package/lib/esm/components/datepicker/core/index.js +9 -0
- package/lib/esm/components/datepicker/core/index.js.map +1 -0
- package/lib/esm/components/datepicker/core/unified-state-manager.js +391 -0
- package/lib/esm/components/datepicker/core/unified-state-manager.js.map +1 -0
- package/lib/esm/components/datepicker/datepicker.js +2065 -763
- package/lib/esm/components/datepicker/datepicker.js.map +1 -1
- package/lib/esm/components/datepicker/index.js +6 -8
- package/lib/esm/components/datepicker/index.js.map +1 -1
- package/lib/esm/components/datepicker/ui/index.js +7 -0
- package/lib/esm/components/datepicker/ui/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/dropdown.js +486 -0
- package/lib/esm/components/datepicker/ui/input/dropdown.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/index.js +7 -0
- package/lib/esm/components/datepicker/ui/input/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/segmented-input.js +637 -0
- package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/calendar.js +443 -0
- package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/footer.js +39 -0
- package/lib/esm/components/datepicker/ui/renderers/footer.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/header.js +29 -0
- package/lib/esm/components/datepicker/ui/renderers/header.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/index.js +9 -0
- package/lib/esm/components/datepicker/ui/renderers/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/time-picker.js +381 -0
- package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +1 -0
- package/lib/esm/components/datepicker/ui/templates/index.js +6 -0
- package/lib/esm/components/datepicker/ui/templates/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/templates/templates.js +242 -0
- package/lib/esm/components/datepicker/ui/templates/templates.js.map +1 -0
- package/lib/esm/components/datepicker/utils/date-formatters.js +83 -0
- package/lib/esm/components/datepicker/utils/date-formatters.js.map +1 -0
- package/lib/esm/components/datepicker/utils/date-utils.js +184 -0
- package/lib/esm/components/datepicker/utils/date-utils.js.map +1 -0
- package/lib/esm/components/datepicker/utils/index.js +8 -0
- package/lib/esm/components/datepicker/utils/index.js.map +1 -0
- package/lib/esm/components/datepicker/utils/time-utils.js +201 -0
- package/lib/esm/components/datepicker/utils/time-utils.js.map +1 -0
- package/lib/esm/index.js +4 -0
- package/lib/esm/index.js.map +1 -1
- package/package.json +12 -3
- package/src/components/alert/alert.css +429 -188
- package/src/components/alert/alert.ts +990 -0
- package/src/components/alert/index.ts +4 -0
- package/src/components/alert/templates.ts +110 -0
- package/src/components/alert/tests/accessibility/aria-roles.test.ts +19 -0
- package/src/components/alert/tests/accessibility/focus-management.test.ts +19 -0
- package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +22 -0
- package/src/components/alert/tests/actions/confirm-cancel.test.ts +122 -0
- package/src/components/alert/tests/actions/input-field.test.ts +180 -0
- package/src/components/alert/tests/alert.basic.test.ts +126 -0
- package/src/components/alert/tests/alert.config.test.ts +75 -0
- package/src/components/alert/tests/alert.templates.test.ts +17 -0
- package/src/components/alert/tests/config/attribute-config.test.ts +94 -0
- package/src/components/alert/tests/config/json-config.test.ts +119 -0
- package/src/components/alert/tests/config/merging.test.ts +89 -0
- package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +96 -0
- package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +105 -0
- package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +90 -0
- package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +91 -0
- package/src/components/alert/tests/edge-cases/invalid-config.test.ts +19 -0
- package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +19 -0
- package/src/components/alert/tests/rendering/custom-content.test.ts +81 -0
- package/src/components/alert/tests/rendering/info-alert.test.ts +84 -0
- package/src/components/alert/tests/rendering/success-alert.test.ts +100 -0
- package/src/components/alert/tests/templates/default-templates.test.ts +16 -0
- package/src/components/alert/tests/templates/user-templates.test.ts +16 -0
- package/src/components/alert/types.ts +145 -0
- package/src/components/datepicker/__tests__/datepicker-events.test.ts +356 -0
- package/src/components/datepicker/__tests__/datepicker-init.test.ts +343 -0
- package/src/components/datepicker/__tests__/datepicker-integration.test.ts +435 -0
- package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +220 -0
- package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +380 -0
- package/src/components/datepicker/__tests__/selective-state-updates.test.ts +400 -0
- package/src/components/datepicker/__tests__/state-manager.test.ts +421 -0
- package/src/components/datepicker/__tests__/time-preservation.test.ts +387 -0
- package/src/components/datepicker/config/config.ts +40 -0
- package/src/components/datepicker/config/index.ts +8 -0
- package/src/components/datepicker/config/interfaces.ts +82 -0
- package/src/components/datepicker/config/types.ts +188 -0
- package/src/components/datepicker/core/event-manager.ts +159 -0
- package/src/components/datepicker/core/focus-manager.ts +201 -0
- package/src/components/datepicker/core/helpers.ts +231 -0
- package/src/components/datepicker/core/index.ts +9 -0
- package/src/components/datepicker/core/unified-state-manager.ts +459 -0
- package/src/components/datepicker/datepicker.css +429 -1
- package/src/components/datepicker/datepicker.ts +2538 -1277
- package/src/components/datepicker/index.ts +6 -8
- package/src/components/datepicker/ui/index.ts +7 -0
- package/src/components/datepicker/ui/input/dropdown.ts +552 -0
- package/src/components/datepicker/ui/input/index.ts +7 -0
- package/src/components/datepicker/ui/input/segmented-input.ts +638 -0
- package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +611 -0
- package/src/components/datepicker/ui/renderers/calendar.ts +530 -0
- package/src/components/datepicker/ui/renderers/footer.ts +43 -0
- package/src/components/datepicker/ui/renderers/header.ts +33 -0
- package/src/components/datepicker/ui/renderers/index.ts +9 -0
- package/src/components/datepicker/ui/renderers/time-picker.ts +438 -0
- package/src/components/datepicker/ui/templates/index.ts +6 -0
- package/src/components/datepicker/ui/templates/templates.ts +306 -0
- package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +160 -0
- package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +86 -0
- package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +215 -0
- package/src/components/datepicker/utils/date-formatters.ts +85 -0
- package/src/components/datepicker/utils/date-utils.ts +172 -0
- package/src/components/datepicker/utils/index.ts +8 -0
- package/src/components/datepicker/utils/time-utils.ts +221 -0
- package/src/index.ts +7 -1
- package/lib/cjs/components/datepicker/calendar.js +0 -1061
- package/lib/cjs/components/datepicker/calendar.js.map +0 -1
- package/lib/cjs/components/datepicker/config.js +0 -332
- package/lib/cjs/components/datepicker/config.js.map +0 -1
- package/lib/cjs/components/datepicker/dropdown.js +0 -635
- package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
- package/lib/cjs/components/datepicker/events.js +0 -129
- package/lib/cjs/components/datepicker/events.js.map +0 -1
- package/lib/cjs/components/datepicker/keyboard.js +0 -536
- package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
- package/lib/cjs/components/datepicker/locales.js +0 -78
- package/lib/cjs/components/datepicker/locales.js.map +0 -1
- package/lib/cjs/components/datepicker/templates.js +0 -403
- package/lib/cjs/components/datepicker/templates.js.map +0 -1
- package/lib/cjs/components/datepicker/types.js +0 -23
- package/lib/cjs/components/datepicker/types.js.map +0 -1
- package/lib/cjs/components/datepicker/utils.js +0 -524
- package/lib/cjs/components/datepicker/utils.js.map +0 -1
- package/lib/esm/components/datepicker/calendar.js +0 -1058
- package/lib/esm/components/datepicker/calendar.js.map +0 -1
- package/lib/esm/components/datepicker/config.js +0 -329
- package/lib/esm/components/datepicker/config.js.map +0 -1
- package/lib/esm/components/datepicker/dropdown.js +0 -632
- package/lib/esm/components/datepicker/dropdown.js.map +0 -1
- package/lib/esm/components/datepicker/events.js +0 -126
- package/lib/esm/components/datepicker/events.js.map +0 -1
- package/lib/esm/components/datepicker/keyboard.js +0 -533
- package/lib/esm/components/datepicker/keyboard.js.map +0 -1
- package/lib/esm/components/datepicker/locales.js +0 -74
- package/lib/esm/components/datepicker/locales.js.map +0 -1
- package/lib/esm/components/datepicker/templates.js +0 -390
- package/lib/esm/components/datepicker/templates.js.map +0 -1
- package/lib/esm/components/datepicker/types.js +0 -20
- package/lib/esm/components/datepicker/types.js.map +0 -1
- package/lib/esm/components/datepicker/utils.js +0 -508
- package/lib/esm/components/datepicker/utils.js.map +0 -1
- package/src/components/datepicker/calendar.ts +0 -1397
- package/src/components/datepicker/config.ts +0 -368
- package/src/components/datepicker/dropdown.ts +0 -757
- package/src/components/datepicker/events.ts +0 -149
- package/src/components/datepicker/keyboard.ts +0 -646
- package/src/components/datepicker/locales.ts +0 -80
- package/src/components/datepicker/templates.ts +0 -792
- package/src/components/datepicker/types.ts +0 -154
- package/src/components/datepicker/utils.ts +0 -631
- package/src/components/select/variants.css +0 -4
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state-manager.test.ts - Test suite for KTDatepickerUnifiedStateManager
|
|
3
|
+
* Tests the immediate vs batched update functionality and observer patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { KTDatepickerUnifiedStateManager } from '../core/unified-state-manager';
|
|
8
|
+
import { KTDatepickerState } from '../config/types';
|
|
9
|
+
|
|
10
|
+
describe('KTDatepickerUnifiedStateManager', () => {
|
|
11
|
+
let stateManager: KTDatepickerUnifiedStateManager;
|
|
12
|
+
let mockObserver: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Create a new state manager for each test
|
|
16
|
+
stateManager = new KTDatepickerUnifiedStateManager({
|
|
17
|
+
enableValidation: true,
|
|
18
|
+
enableDebugging: false,
|
|
19
|
+
enableUpdateBatching: true,
|
|
20
|
+
batchDelay: 16
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Create a mock observer
|
|
24
|
+
mockObserver = {
|
|
25
|
+
onStateChange: vi.fn(),
|
|
26
|
+
getUpdatePriority: vi.fn().mockReturnValue(1)
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Immediate Updates', () => {
|
|
31
|
+
it('should apply immediate updates without batching delay', async () => {
|
|
32
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
33
|
+
|
|
34
|
+
const testDate = new Date(2024, 0, 15);
|
|
35
|
+
const success = stateManager.updateState({ selectedDate: testDate }, 'test', true); // immediate = true
|
|
36
|
+
|
|
37
|
+
expect(success).toBe(true);
|
|
38
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
39
|
+
|
|
40
|
+
const [newState, oldState] = mockObserver.onStateChange.mock.calls[0];
|
|
41
|
+
expect(newState.selectedDate).toEqual(testDate);
|
|
42
|
+
|
|
43
|
+
observer(); // unsubscribe
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should apply batched updates with delay when immediate is false', async () => {
|
|
47
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
48
|
+
|
|
49
|
+
const testDate = new Date(2024, 0, 15);
|
|
50
|
+
const success = stateManager.updateState({ selectedDate: testDate }, 'test', false); // immediate = false
|
|
51
|
+
|
|
52
|
+
expect(success).toBe(true);
|
|
53
|
+
|
|
54
|
+
// Should not have been called immediately
|
|
55
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
56
|
+
|
|
57
|
+
// Wait for batch timeout (using a longer timeout for test reliability)
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
59
|
+
|
|
60
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
61
|
+
|
|
62
|
+
observer(); // unsubscribe
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should default to batched updates when immediate is not specified', async () => {
|
|
66
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
67
|
+
|
|
68
|
+
const testDate = new Date(2024, 0, 15);
|
|
69
|
+
const success = stateManager.updateState({ selectedDate: testDate }, 'test'); // no immediate parameter
|
|
70
|
+
|
|
71
|
+
expect(success).toBe(true);
|
|
72
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
73
|
+
|
|
74
|
+
// Wait for batch timeout
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
76
|
+
|
|
77
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
78
|
+
|
|
79
|
+
observer(); // unsubscribe
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Convenience Methods', () => {
|
|
84
|
+
it('should use immediate updates for critical user interaction methods', async () => {
|
|
85
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
86
|
+
|
|
87
|
+
// Test setSelectedDate (should be immediate)
|
|
88
|
+
const testDate = new Date(2024, 0, 15);
|
|
89
|
+
stateManager.setSelectedDate(testDate, 'test');
|
|
90
|
+
|
|
91
|
+
// Should be called immediately, not batched
|
|
92
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
93
|
+
|
|
94
|
+
const [newState] = mockObserver.onStateChange.mock.calls[0];
|
|
95
|
+
expect(newState.selectedDate).toEqual(testDate);
|
|
96
|
+
|
|
97
|
+
observer(); // unsubscribe
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should use immediate updates for setDropdownOpen', async () => {
|
|
101
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
102
|
+
|
|
103
|
+
// Test setDropdownOpen (should be immediate)
|
|
104
|
+
stateManager.setDropdownOpen(true, 'test');
|
|
105
|
+
|
|
106
|
+
// Should be called immediately
|
|
107
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
const [newState] = mockObserver.onStateChange.mock.calls[0];
|
|
110
|
+
expect(newState.dropdownState.isOpen).toBe(true);
|
|
111
|
+
expect(newState.isOpen).toBe(true); // Also updates legacy field
|
|
112
|
+
|
|
113
|
+
observer(); // unsubscribe
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should use batched updates for non-critical methods', async () => {
|
|
117
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
118
|
+
|
|
119
|
+
// Test setCurrentDate (should be batched)
|
|
120
|
+
const testDate = new Date(2024, 0, 15);
|
|
121
|
+
stateManager.setCurrentDate(testDate, 'test');
|
|
122
|
+
|
|
123
|
+
// Should not be called immediately
|
|
124
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
125
|
+
|
|
126
|
+
// Wait for batch timeout
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
128
|
+
|
|
129
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
130
|
+
|
|
131
|
+
observer(); // unsubscribe
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('Observer Pattern', () => {
|
|
136
|
+
it('should notify all subscribed observers', async () => {
|
|
137
|
+
const observer2 = {
|
|
138
|
+
onStateChange: vi.fn(),
|
|
139
|
+
getUpdatePriority: vi.fn().mockReturnValue(1)
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const unsub1 = stateManager.subscribe(mockObserver);
|
|
143
|
+
const unsub2 = stateManager.subscribe(observer2);
|
|
144
|
+
|
|
145
|
+
const testDate = new Date(2024, 0, 15);
|
|
146
|
+
stateManager.updateState({ selectedDate: testDate }, 'test', true); // immediate
|
|
147
|
+
|
|
148
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(observer2.onStateChange).toHaveBeenCalledTimes(1);
|
|
150
|
+
|
|
151
|
+
unsub1();
|
|
152
|
+
unsub2();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should respect observer priority ordering', async () => {
|
|
156
|
+
const highPriorityObserver = {
|
|
157
|
+
onStateChange: vi.fn(),
|
|
158
|
+
getUpdatePriority: vi.fn().mockReturnValue(0) // Higher priority (lower number)
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const lowPriorityObserver = {
|
|
162
|
+
onStateChange: vi.fn(),
|
|
163
|
+
getUpdatePriority: vi.fn().mockReturnValue(2) // Lower priority (higher number)
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const unsub1 = stateManager.subscribe(highPriorityObserver);
|
|
167
|
+
const unsub2 = stateManager.subscribe(lowPriorityObserver);
|
|
168
|
+
|
|
169
|
+
const testDate = new Date(2024, 0, 15);
|
|
170
|
+
stateManager.updateState({ selectedDate: testDate }, 'test', true); // immediate
|
|
171
|
+
|
|
172
|
+
// Both should be called
|
|
173
|
+
expect(highPriorityObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(lowPriorityObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
175
|
+
|
|
176
|
+
unsub1();
|
|
177
|
+
unsub2();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle observer errors gracefully', async () => {
|
|
181
|
+
const errorObserver = {
|
|
182
|
+
onStateChange: vi.fn().mockImplementation(() => {
|
|
183
|
+
throw new Error('Observer error');
|
|
184
|
+
}),
|
|
185
|
+
getUpdatePriority: vi.fn().mockReturnValue(1)
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const normalObserver = {
|
|
189
|
+
onStateChange: vi.fn(),
|
|
190
|
+
getUpdatePriority: vi.fn().mockReturnValue(1)
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const unsub1 = stateManager.subscribe(errorObserver);
|
|
194
|
+
const unsub2 = stateManager.subscribe(normalObserver);
|
|
195
|
+
|
|
196
|
+
const testDate = new Date(2024, 0, 15);
|
|
197
|
+
stateManager.updateState({ selectedDate: testDate }, 'test', true); // immediate
|
|
198
|
+
|
|
199
|
+
// Normal observer should still be called despite error in first observer
|
|
200
|
+
expect(errorObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(normalObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
202
|
+
|
|
203
|
+
unsub1();
|
|
204
|
+
unsub2();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should allow unsubscribing from updates', async () => {
|
|
208
|
+
const unsub = stateManager.subscribe(mockObserver);
|
|
209
|
+
|
|
210
|
+
const testDate = new Date(2024, 0, 15);
|
|
211
|
+
stateManager.updateState({ selectedDate: testDate }, 'test', true); // immediate
|
|
212
|
+
|
|
213
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
214
|
+
|
|
215
|
+
// Unsubscribe
|
|
216
|
+
unsub();
|
|
217
|
+
|
|
218
|
+
// This update should not notify the unsubscribed observer
|
|
219
|
+
stateManager.updateState({ selectedDate: new Date(2024, 1, 20) }, 'test2', true);
|
|
220
|
+
|
|
221
|
+
// Should still be 1 call (not 2)
|
|
222
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('State Validation', () => {
|
|
227
|
+
it('should validate state before applying updates', async () => {
|
|
228
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
229
|
+
|
|
230
|
+
// Try to set an invalid date (NaN)
|
|
231
|
+
const invalidDate = new Date(NaN);
|
|
232
|
+
const success = stateManager.updateState({ selectedDate: invalidDate }, 'test', true);
|
|
233
|
+
|
|
234
|
+
expect(success).toBe(false);
|
|
235
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
236
|
+
|
|
237
|
+
observer(); // unsubscribe
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should validate range constraints', async () => {
|
|
241
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
242
|
+
|
|
243
|
+
// Set start date after end date (invalid range)
|
|
244
|
+
const startDate = new Date(2024, 1, 15);
|
|
245
|
+
const endDate = new Date(2024, 0, 15);
|
|
246
|
+
const success = stateManager.updateState({
|
|
247
|
+
selectedRange: { start: startDate, end: endDate }
|
|
248
|
+
}, 'test', true);
|
|
249
|
+
|
|
250
|
+
expect(success).toBe(false);
|
|
251
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
252
|
+
|
|
253
|
+
observer(); // unsubscribe
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should allow valid state updates', async () => {
|
|
257
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
258
|
+
|
|
259
|
+
const validDate = new Date(2024, 0, 15);
|
|
260
|
+
const success = stateManager.updateState({ selectedDate: validDate }, 'test', true);
|
|
261
|
+
|
|
262
|
+
expect(success).toBe(true);
|
|
263
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
264
|
+
|
|
265
|
+
observer(); // unsubscribe
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('Batching Behavior', () => {
|
|
270
|
+
it('should batch multiple rapid updates', async () => {
|
|
271
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
272
|
+
|
|
273
|
+
// Multiple rapid updates
|
|
274
|
+
stateManager.updateState({ selectedDate: new Date(2024, 0, 15) }, 'test1', false);
|
|
275
|
+
stateManager.updateState({ currentDate: new Date(2024, 1, 20) }, 'test2', false);
|
|
276
|
+
stateManager.updateState({ viewMode: 'months' }, 'test3', false);
|
|
277
|
+
|
|
278
|
+
// Should not have been called yet
|
|
279
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
280
|
+
|
|
281
|
+
// Wait for batch timeout
|
|
282
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
283
|
+
|
|
284
|
+
// Should be called once with merged updates
|
|
285
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
286
|
+
|
|
287
|
+
const [newState] = mockObserver.onStateChange.mock.calls[0];
|
|
288
|
+
expect(newState.selectedDate).toEqual(new Date(2024, 0, 15));
|
|
289
|
+
expect(newState.currentDate).toEqual(new Date(2024, 1, 20));
|
|
290
|
+
expect(newState.viewMode).toBe('months');
|
|
291
|
+
|
|
292
|
+
observer(); // unsubscribe
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should reset batch timeout on new batched updates', async () => {
|
|
296
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
297
|
+
|
|
298
|
+
// First update
|
|
299
|
+
stateManager.updateState({ selectedDate: new Date(2024, 0, 15) }, 'test1', false);
|
|
300
|
+
|
|
301
|
+
// Wait partial time
|
|
302
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
303
|
+
|
|
304
|
+
// Second update (should reset timeout)
|
|
305
|
+
stateManager.updateState({ currentDate: new Date(2024, 1, 20) }, 'test2', false);
|
|
306
|
+
|
|
307
|
+
// Wait another partial time
|
|
308
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
309
|
+
|
|
310
|
+
// Should still not have been called
|
|
311
|
+
expect(mockObserver.onStateChange).not.toHaveBeenCalled();
|
|
312
|
+
|
|
313
|
+
// Wait for full timeout from second update
|
|
314
|
+
await new Promise(resolve => setTimeout(resolve, 12));
|
|
315
|
+
|
|
316
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
317
|
+
|
|
318
|
+
observer(); // unsubscribe
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('Change Tracking', () => {
|
|
323
|
+
it('should provide access to changed properties after state update', () => {
|
|
324
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
325
|
+
|
|
326
|
+
stateManager.updateState({ isOpen: true }, 'test', true);
|
|
327
|
+
|
|
328
|
+
const changedProperties = stateManager.getLastChangedProperties();
|
|
329
|
+
expect(changedProperties).toBeInstanceOf(Set);
|
|
330
|
+
expect(changedProperties.has('isOpen')).toBe(true);
|
|
331
|
+
|
|
332
|
+
observer(); // unsubscribe
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should track multiple changed properties', () => {
|
|
336
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
337
|
+
|
|
338
|
+
stateManager.updateState({
|
|
339
|
+
selectedDate: new Date(2024, 0, 15),
|
|
340
|
+
isOpen: true,
|
|
341
|
+
currentDate: new Date(2024, 1, 1)
|
|
342
|
+
}, 'test', true);
|
|
343
|
+
|
|
344
|
+
const changedProperties = stateManager.getLastChangedProperties();
|
|
345
|
+
expect(changedProperties.has('selectedDate')).toBe(true);
|
|
346
|
+
expect(changedProperties.has('isOpen')).toBe(true);
|
|
347
|
+
expect(changedProperties.has('currentDate')).toBe(true);
|
|
348
|
+
|
|
349
|
+
observer(); // unsubscribe
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should return empty set when no changes occurred', () => {
|
|
353
|
+
// Initially, no changes
|
|
354
|
+
const changedProperties = stateManager.getLastChangedProperties();
|
|
355
|
+
expect(changedProperties.size).toBe(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should update changed properties for each state update', () => {
|
|
359
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
360
|
+
|
|
361
|
+
// First update
|
|
362
|
+
stateManager.updateState({ isOpen: true }, 'test1', true);
|
|
363
|
+
let changedProperties = stateManager.getLastChangedProperties();
|
|
364
|
+
expect(changedProperties.has('isOpen')).toBe(true);
|
|
365
|
+
expect(changedProperties.has('selectedDate')).toBe(false);
|
|
366
|
+
|
|
367
|
+
// Second update
|
|
368
|
+
stateManager.updateState({ selectedDate: new Date(2024, 0, 15) }, 'test2', true);
|
|
369
|
+
changedProperties = stateManager.getLastChangedProperties();
|
|
370
|
+
expect(changedProperties.has('isOpen')).toBe(false); // Previous update
|
|
371
|
+
expect(changedProperties.has('selectedDate')).toBe(true); // Current update
|
|
372
|
+
|
|
373
|
+
observer(); // unsubscribe
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should track nested properties in selectedRange', () => {
|
|
377
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
378
|
+
|
|
379
|
+
// First, set an initial range
|
|
380
|
+
const initialRange = {
|
|
381
|
+
start: new Date(2024, 0, 15),
|
|
382
|
+
end: new Date(2024, 0, 20)
|
|
383
|
+
};
|
|
384
|
+
stateManager.updateState({ selectedRange: initialRange }, 'test', true);
|
|
385
|
+
mockObserver.onStateChange.mockClear();
|
|
386
|
+
|
|
387
|
+
// Then update the range (now both old and new are objects, so nested tracking applies)
|
|
388
|
+
const updatedRange = {
|
|
389
|
+
start: new Date(2024, 0, 16), // Changed
|
|
390
|
+
end: new Date(2024, 0, 20) // Same
|
|
391
|
+
};
|
|
392
|
+
stateManager.updateState({ selectedRange: updatedRange }, 'test2', true);
|
|
393
|
+
|
|
394
|
+
const changedProperties = stateManager.getLastChangedProperties();
|
|
395
|
+
expect(changedProperties.has('selectedRange')).toBe(true);
|
|
396
|
+
// Nested properties should be tracked when both old and new are objects
|
|
397
|
+
expect(changedProperties.has('selectedRange.start')).toBe(true); // Changed
|
|
398
|
+
expect(changedProperties.has('selectedRange.end')).toBe(false); // Not changed
|
|
399
|
+
|
|
400
|
+
observer(); // unsubscribe
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should maintain backward compatibility with existing observer pattern', () => {
|
|
404
|
+
const observer = stateManager.subscribe(mockObserver);
|
|
405
|
+
|
|
406
|
+
const testDate = new Date(2024, 0, 15);
|
|
407
|
+
stateManager.updateState({ selectedDate: testDate }, 'test', true);
|
|
408
|
+
|
|
409
|
+
// Existing behavior should still work
|
|
410
|
+
expect(mockObserver.onStateChange).toHaveBeenCalledTimes(1);
|
|
411
|
+
const [newState, oldState] = mockObserver.onStateChange.mock.calls[0];
|
|
412
|
+
expect(newState.selectedDate).toEqual(testDate);
|
|
413
|
+
|
|
414
|
+
// New functionality should also work
|
|
415
|
+
const changedProperties = stateManager.getLastChangedProperties();
|
|
416
|
+
expect(changedProperties.has('selectedDate')).toBe(true);
|
|
417
|
+
|
|
418
|
+
observer(); // unsubscribe
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
});
|