@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.
Files changed (243) hide show
  1. package/README.md +27 -0
  2. package/dist/ktui.js +8780 -6199
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +2744 -1367
  6. package/lib/cjs/components/alert/alert.js +1025 -0
  7. package/lib/cjs/components/alert/alert.js.map +1 -0
  8. package/lib/cjs/components/alert/index.js +20 -0
  9. package/lib/cjs/components/alert/index.js.map +1 -0
  10. package/lib/cjs/components/alert/templates.js +120 -0
  11. package/lib/cjs/components/alert/templates.js.map +1 -0
  12. package/lib/cjs/components/alert/types.js +7 -0
  13. package/lib/cjs/components/alert/types.js.map +1 -0
  14. package/lib/cjs/components/datepicker/config/config.js +42 -0
  15. package/lib/cjs/components/datepicker/config/config.js.map +1 -0
  16. package/lib/cjs/components/datepicker/config/index.js +24 -0
  17. package/lib/cjs/components/datepicker/config/index.js.map +1 -0
  18. package/lib/cjs/components/datepicker/config/interfaces.js +7 -0
  19. package/lib/cjs/components/datepicker/config/interfaces.js.map +1 -0
  20. package/lib/cjs/components/datepicker/config/types.js +7 -0
  21. package/lib/cjs/components/datepicker/config/types.js.map +1 -0
  22. package/lib/cjs/components/datepicker/core/event-manager.js +135 -0
  23. package/lib/cjs/components/datepicker/core/event-manager.js.map +1 -0
  24. package/lib/cjs/components/datepicker/core/focus-manager.js +167 -0
  25. package/lib/cjs/components/datepicker/core/focus-manager.js.map +1 -0
  26. package/lib/cjs/components/datepicker/core/helpers.js +219 -0
  27. package/lib/cjs/components/datepicker/core/helpers.js.map +1 -0
  28. package/lib/cjs/components/datepicker/core/index.js +25 -0
  29. package/lib/cjs/components/datepicker/core/index.js.map +1 -0
  30. package/lib/cjs/components/datepicker/core/unified-state-manager.js +394 -0
  31. package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +1 -0
  32. package/lib/cjs/components/datepicker/datepicker.js +2066 -763
  33. package/lib/cjs/components/datepicker/datepicker.js.map +1 -1
  34. package/lib/cjs/components/datepicker/index.js +19 -8
  35. package/lib/cjs/components/datepicker/index.js.map +1 -1
  36. package/lib/cjs/components/datepicker/ui/index.js +23 -0
  37. package/lib/cjs/components/datepicker/ui/index.js.map +1 -0
  38. package/lib/cjs/components/datepicker/ui/input/dropdown.js +489 -0
  39. package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +1 -0
  40. package/lib/cjs/components/datepicker/ui/input/index.js +23 -0
  41. package/lib/cjs/components/datepicker/ui/input/index.js.map +1 -0
  42. package/lib/cjs/components/datepicker/ui/input/segmented-input.js +640 -0
  43. package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +1 -0
  44. package/lib/cjs/components/datepicker/ui/renderers/calendar.js +446 -0
  45. package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +1 -0
  46. package/lib/cjs/components/datepicker/ui/renderers/footer.js +42 -0
  47. package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +1 -0
  48. package/lib/cjs/components/datepicker/ui/renderers/header.js +32 -0
  49. package/lib/cjs/components/datepicker/ui/renderers/header.js.map +1 -0
  50. package/lib/cjs/components/datepicker/ui/renderers/index.js +25 -0
  51. package/lib/cjs/components/datepicker/ui/renderers/index.js.map +1 -0
  52. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +384 -0
  53. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  54. package/lib/cjs/components/datepicker/ui/templates/index.js +22 -0
  55. package/lib/cjs/components/datepicker/ui/templates/index.js.map +1 -0
  56. package/lib/cjs/components/datepicker/ui/templates/templates.js +253 -0
  57. package/lib/cjs/components/datepicker/ui/templates/templates.js.map +1 -0
  58. package/lib/cjs/components/datepicker/utils/date-formatters.js +88 -0
  59. package/lib/cjs/components/datepicker/utils/date-formatters.js.map +1 -0
  60. package/lib/cjs/components/datepicker/utils/date-utils.js +194 -0
  61. package/lib/cjs/components/datepicker/utils/date-utils.js.map +1 -0
  62. package/lib/cjs/components/datepicker/utils/index.js +24 -0
  63. package/lib/cjs/components/datepicker/utils/index.js.map +1 -0
  64. package/lib/cjs/components/datepicker/utils/time-utils.js +213 -0
  65. package/lib/cjs/components/datepicker/utils/time-utils.js.map +1 -0
  66. package/lib/cjs/index.js +6 -1
  67. package/lib/cjs/index.js.map +1 -1
  68. package/lib/esm/components/alert/alert.js +1022 -0
  69. package/lib/esm/components/alert/alert.js.map +1 -0
  70. package/lib/esm/components/alert/index.js +4 -0
  71. package/lib/esm/components/alert/index.js.map +1 -0
  72. package/lib/esm/components/alert/templates.js +112 -0
  73. package/lib/esm/components/alert/templates.js.map +1 -0
  74. package/lib/esm/components/alert/types.js +6 -0
  75. package/lib/esm/components/alert/types.js.map +1 -0
  76. package/lib/esm/components/datepicker/config/config.js +39 -0
  77. package/lib/esm/components/datepicker/config/config.js.map +1 -0
  78. package/lib/esm/components/datepicker/config/index.js +8 -0
  79. package/lib/esm/components/datepicker/config/index.js.map +1 -0
  80. package/lib/esm/components/datepicker/config/interfaces.js +6 -0
  81. package/lib/esm/components/datepicker/config/interfaces.js.map +1 -0
  82. package/lib/esm/components/datepicker/config/types.js +6 -0
  83. package/lib/esm/components/datepicker/config/types.js.map +1 -0
  84. package/lib/esm/components/datepicker/core/event-manager.js +133 -0
  85. package/lib/esm/components/datepicker/core/event-manager.js.map +1 -0
  86. package/lib/esm/components/datepicker/core/focus-manager.js +164 -0
  87. package/lib/esm/components/datepicker/core/focus-manager.js.map +1 -0
  88. package/lib/esm/components/datepicker/core/helpers.js +211 -0
  89. package/lib/esm/components/datepicker/core/helpers.js.map +1 -0
  90. package/lib/esm/components/datepicker/core/index.js +9 -0
  91. package/lib/esm/components/datepicker/core/index.js.map +1 -0
  92. package/lib/esm/components/datepicker/core/unified-state-manager.js +391 -0
  93. package/lib/esm/components/datepicker/core/unified-state-manager.js.map +1 -0
  94. package/lib/esm/components/datepicker/datepicker.js +2065 -763
  95. package/lib/esm/components/datepicker/datepicker.js.map +1 -1
  96. package/lib/esm/components/datepicker/index.js +6 -8
  97. package/lib/esm/components/datepicker/index.js.map +1 -1
  98. package/lib/esm/components/datepicker/ui/index.js +7 -0
  99. package/lib/esm/components/datepicker/ui/index.js.map +1 -0
  100. package/lib/esm/components/datepicker/ui/input/dropdown.js +486 -0
  101. package/lib/esm/components/datepicker/ui/input/dropdown.js.map +1 -0
  102. package/lib/esm/components/datepicker/ui/input/index.js +7 -0
  103. package/lib/esm/components/datepicker/ui/input/index.js.map +1 -0
  104. package/lib/esm/components/datepicker/ui/input/segmented-input.js +637 -0
  105. package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +1 -0
  106. package/lib/esm/components/datepicker/ui/renderers/calendar.js +443 -0
  107. package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +1 -0
  108. package/lib/esm/components/datepicker/ui/renderers/footer.js +39 -0
  109. package/lib/esm/components/datepicker/ui/renderers/footer.js.map +1 -0
  110. package/lib/esm/components/datepicker/ui/renderers/header.js +29 -0
  111. package/lib/esm/components/datepicker/ui/renderers/header.js.map +1 -0
  112. package/lib/esm/components/datepicker/ui/renderers/index.js +9 -0
  113. package/lib/esm/components/datepicker/ui/renderers/index.js.map +1 -0
  114. package/lib/esm/components/datepicker/ui/renderers/time-picker.js +381 -0
  115. package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  116. package/lib/esm/components/datepicker/ui/templates/index.js +6 -0
  117. package/lib/esm/components/datepicker/ui/templates/index.js.map +1 -0
  118. package/lib/esm/components/datepicker/ui/templates/templates.js +242 -0
  119. package/lib/esm/components/datepicker/ui/templates/templates.js.map +1 -0
  120. package/lib/esm/components/datepicker/utils/date-formatters.js +83 -0
  121. package/lib/esm/components/datepicker/utils/date-formatters.js.map +1 -0
  122. package/lib/esm/components/datepicker/utils/date-utils.js +184 -0
  123. package/lib/esm/components/datepicker/utils/date-utils.js.map +1 -0
  124. package/lib/esm/components/datepicker/utils/index.js +8 -0
  125. package/lib/esm/components/datepicker/utils/index.js.map +1 -0
  126. package/lib/esm/components/datepicker/utils/time-utils.js +201 -0
  127. package/lib/esm/components/datepicker/utils/time-utils.js.map +1 -0
  128. package/lib/esm/index.js +4 -0
  129. package/lib/esm/index.js.map +1 -1
  130. package/package.json +12 -3
  131. package/src/components/alert/alert.css +429 -188
  132. package/src/components/alert/alert.ts +990 -0
  133. package/src/components/alert/index.ts +4 -0
  134. package/src/components/alert/templates.ts +110 -0
  135. package/src/components/alert/tests/accessibility/aria-roles.test.ts +19 -0
  136. package/src/components/alert/tests/accessibility/focus-management.test.ts +19 -0
  137. package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +22 -0
  138. package/src/components/alert/tests/actions/confirm-cancel.test.ts +122 -0
  139. package/src/components/alert/tests/actions/input-field.test.ts +180 -0
  140. package/src/components/alert/tests/alert.basic.test.ts +126 -0
  141. package/src/components/alert/tests/alert.config.test.ts +75 -0
  142. package/src/components/alert/tests/alert.templates.test.ts +17 -0
  143. package/src/components/alert/tests/config/attribute-config.test.ts +94 -0
  144. package/src/components/alert/tests/config/json-config.test.ts +119 -0
  145. package/src/components/alert/tests/config/merging.test.ts +89 -0
  146. package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +96 -0
  147. package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +105 -0
  148. package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +90 -0
  149. package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +91 -0
  150. package/src/components/alert/tests/edge-cases/invalid-config.test.ts +19 -0
  151. package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +19 -0
  152. package/src/components/alert/tests/rendering/custom-content.test.ts +81 -0
  153. package/src/components/alert/tests/rendering/info-alert.test.ts +84 -0
  154. package/src/components/alert/tests/rendering/success-alert.test.ts +100 -0
  155. package/src/components/alert/tests/templates/default-templates.test.ts +16 -0
  156. package/src/components/alert/tests/templates/user-templates.test.ts +16 -0
  157. package/src/components/alert/types.ts +145 -0
  158. package/src/components/datepicker/__tests__/datepicker-events.test.ts +356 -0
  159. package/src/components/datepicker/__tests__/datepicker-init.test.ts +343 -0
  160. package/src/components/datepicker/__tests__/datepicker-integration.test.ts +435 -0
  161. package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +220 -0
  162. package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +380 -0
  163. package/src/components/datepicker/__tests__/selective-state-updates.test.ts +400 -0
  164. package/src/components/datepicker/__tests__/state-manager.test.ts +421 -0
  165. package/src/components/datepicker/__tests__/time-preservation.test.ts +387 -0
  166. package/src/components/datepicker/config/config.ts +40 -0
  167. package/src/components/datepicker/config/index.ts +8 -0
  168. package/src/components/datepicker/config/interfaces.ts +82 -0
  169. package/src/components/datepicker/config/types.ts +188 -0
  170. package/src/components/datepicker/core/event-manager.ts +159 -0
  171. package/src/components/datepicker/core/focus-manager.ts +201 -0
  172. package/src/components/datepicker/core/helpers.ts +231 -0
  173. package/src/components/datepicker/core/index.ts +9 -0
  174. package/src/components/datepicker/core/unified-state-manager.ts +459 -0
  175. package/src/components/datepicker/datepicker.css +429 -1
  176. package/src/components/datepicker/datepicker.ts +2538 -1277
  177. package/src/components/datepicker/index.ts +6 -8
  178. package/src/components/datepicker/ui/index.ts +7 -0
  179. package/src/components/datepicker/ui/input/dropdown.ts +552 -0
  180. package/src/components/datepicker/ui/input/index.ts +7 -0
  181. package/src/components/datepicker/ui/input/segmented-input.ts +638 -0
  182. package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +611 -0
  183. package/src/components/datepicker/ui/renderers/calendar.ts +530 -0
  184. package/src/components/datepicker/ui/renderers/footer.ts +43 -0
  185. package/src/components/datepicker/ui/renderers/header.ts +33 -0
  186. package/src/components/datepicker/ui/renderers/index.ts +9 -0
  187. package/src/components/datepicker/ui/renderers/time-picker.ts +438 -0
  188. package/src/components/datepicker/ui/templates/index.ts +6 -0
  189. package/src/components/datepicker/ui/templates/templates.ts +306 -0
  190. package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +160 -0
  191. package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +86 -0
  192. package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +215 -0
  193. package/src/components/datepicker/utils/date-formatters.ts +85 -0
  194. package/src/components/datepicker/utils/date-utils.ts +172 -0
  195. package/src/components/datepicker/utils/index.ts +8 -0
  196. package/src/components/datepicker/utils/time-utils.ts +221 -0
  197. package/src/index.ts +7 -1
  198. package/lib/cjs/components/datepicker/calendar.js +0 -1061
  199. package/lib/cjs/components/datepicker/calendar.js.map +0 -1
  200. package/lib/cjs/components/datepicker/config.js +0 -332
  201. package/lib/cjs/components/datepicker/config.js.map +0 -1
  202. package/lib/cjs/components/datepicker/dropdown.js +0 -635
  203. package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
  204. package/lib/cjs/components/datepicker/events.js +0 -129
  205. package/lib/cjs/components/datepicker/events.js.map +0 -1
  206. package/lib/cjs/components/datepicker/keyboard.js +0 -536
  207. package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
  208. package/lib/cjs/components/datepicker/locales.js +0 -78
  209. package/lib/cjs/components/datepicker/locales.js.map +0 -1
  210. package/lib/cjs/components/datepicker/templates.js +0 -403
  211. package/lib/cjs/components/datepicker/templates.js.map +0 -1
  212. package/lib/cjs/components/datepicker/types.js +0 -23
  213. package/lib/cjs/components/datepicker/types.js.map +0 -1
  214. package/lib/cjs/components/datepicker/utils.js +0 -524
  215. package/lib/cjs/components/datepicker/utils.js.map +0 -1
  216. package/lib/esm/components/datepicker/calendar.js +0 -1058
  217. package/lib/esm/components/datepicker/calendar.js.map +0 -1
  218. package/lib/esm/components/datepicker/config.js +0 -329
  219. package/lib/esm/components/datepicker/config.js.map +0 -1
  220. package/lib/esm/components/datepicker/dropdown.js +0 -632
  221. package/lib/esm/components/datepicker/dropdown.js.map +0 -1
  222. package/lib/esm/components/datepicker/events.js +0 -126
  223. package/lib/esm/components/datepicker/events.js.map +0 -1
  224. package/lib/esm/components/datepicker/keyboard.js +0 -533
  225. package/lib/esm/components/datepicker/keyboard.js.map +0 -1
  226. package/lib/esm/components/datepicker/locales.js +0 -74
  227. package/lib/esm/components/datepicker/locales.js.map +0 -1
  228. package/lib/esm/components/datepicker/templates.js +0 -390
  229. package/lib/esm/components/datepicker/templates.js.map +0 -1
  230. package/lib/esm/components/datepicker/types.js +0 -20
  231. package/lib/esm/components/datepicker/types.js.map +0 -1
  232. package/lib/esm/components/datepicker/utils.js +0 -508
  233. package/lib/esm/components/datepicker/utils.js.map +0 -1
  234. package/src/components/datepicker/calendar.ts +0 -1397
  235. package/src/components/datepicker/config.ts +0 -368
  236. package/src/components/datepicker/dropdown.ts +0 -757
  237. package/src/components/datepicker/events.ts +0 -149
  238. package/src/components/datepicker/keyboard.ts +0 -646
  239. package/src/components/datepicker/locales.ts +0 -80
  240. package/src/components/datepicker/templates.ts +0 -792
  241. package/src/components/datepicker/types.ts +0 -154
  242. package/src/components/datepicker/utils.ts +0 -631
  243. 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
+ });