@keenthemes/ktui 1.0.28 → 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 (288) 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 +22 -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/CONTRIBUTING.md +0 -101
  199. package/examples/datatable/checkbox-events-test.html +0 -400
  200. package/examples/datatable/credentials-test.html +0 -423
  201. package/examples/datatable/remote-checkbox-test.html +0 -365
  202. package/examples/datatable/sorting-test.html +0 -258
  203. package/examples/image-input/file-upload-example.html +0 -189
  204. package/examples/modal/persistent.html +0 -205
  205. package/examples/modal/remote-select-dropdown.html +0 -166
  206. package/examples/modal/select-dropdown-container.html +0 -129
  207. package/examples/select/avatar.html +0 -47
  208. package/examples/select/basic-usage.html +0 -39
  209. package/examples/select/country.html +0 -43
  210. package/examples/select/dark-mode.html +0 -93
  211. package/examples/select/description.html +0 -53
  212. package/examples/select/disable-option.html +0 -37
  213. package/examples/select/disable-select.html +0 -35
  214. package/examples/select/dropdowncontainer.html +0 -111
  215. package/examples/select/dynamic-methods.html +0 -273
  216. package/examples/select/formdata-remote.html +0 -161
  217. package/examples/select/global-config.html +0 -81
  218. package/examples/select/icon-multiple.html +0 -50
  219. package/examples/select/icon.html +0 -48
  220. package/examples/select/max-selection.html +0 -38
  221. package/examples/select/modal-container.html +0 -128
  222. package/examples/select/modal-positioning-test.html +0 -338
  223. package/examples/select/modal.html +0 -80
  224. package/examples/select/multiple.html +0 -40
  225. package/examples/select/native-selected.html +0 -64
  226. package/examples/select/placeholder.html +0 -40
  227. package/examples/select/remote-data-preselected.html +0 -283
  228. package/examples/select/remote-data.html +0 -38
  229. package/examples/select/search.html +0 -57
  230. package/examples/select/sizes.html +0 -94
  231. package/examples/select/tags-enhanced.html +0 -86
  232. package/examples/select/tags-icons.html +0 -57
  233. package/examples/select/template-customization.html +0 -61
  234. package/examples/sticky/README.md +0 -158
  235. package/examples/sticky/debug-sticky.html +0 -144
  236. package/examples/sticky/test-runner.html +0 -175
  237. package/examples/sticky/test-sticky-logic.js +0 -369
  238. package/examples/sticky/test-sticky-positioning.html +0 -386
  239. package/examples/toast/example.html +0 -479
  240. package/lib/cjs/components/datepicker/calendar.js +0 -1061
  241. package/lib/cjs/components/datepicker/calendar.js.map +0 -1
  242. package/lib/cjs/components/datepicker/config.js +0 -332
  243. package/lib/cjs/components/datepicker/config.js.map +0 -1
  244. package/lib/cjs/components/datepicker/dropdown.js +0 -635
  245. package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
  246. package/lib/cjs/components/datepicker/events.js +0 -129
  247. package/lib/cjs/components/datepicker/events.js.map +0 -1
  248. package/lib/cjs/components/datepicker/keyboard.js +0 -536
  249. package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
  250. package/lib/cjs/components/datepicker/locales.js +0 -78
  251. package/lib/cjs/components/datepicker/locales.js.map +0 -1
  252. package/lib/cjs/components/datepicker/templates.js +0 -403
  253. package/lib/cjs/components/datepicker/templates.js.map +0 -1
  254. package/lib/cjs/components/datepicker/types.js +0 -23
  255. package/lib/cjs/components/datepicker/types.js.map +0 -1
  256. package/lib/cjs/components/datepicker/utils.js +0 -524
  257. package/lib/cjs/components/datepicker/utils.js.map +0 -1
  258. package/lib/esm/components/datepicker/calendar.js +0 -1058
  259. package/lib/esm/components/datepicker/calendar.js.map +0 -1
  260. package/lib/esm/components/datepicker/config.js +0 -329
  261. package/lib/esm/components/datepicker/config.js.map +0 -1
  262. package/lib/esm/components/datepicker/dropdown.js +0 -632
  263. package/lib/esm/components/datepicker/dropdown.js.map +0 -1
  264. package/lib/esm/components/datepicker/events.js +0 -126
  265. package/lib/esm/components/datepicker/events.js.map +0 -1
  266. package/lib/esm/components/datepicker/keyboard.js +0 -533
  267. package/lib/esm/components/datepicker/keyboard.js.map +0 -1
  268. package/lib/esm/components/datepicker/locales.js +0 -74
  269. package/lib/esm/components/datepicker/locales.js.map +0 -1
  270. package/lib/esm/components/datepicker/templates.js +0 -390
  271. package/lib/esm/components/datepicker/templates.js.map +0 -1
  272. package/lib/esm/components/datepicker/types.js +0 -20
  273. package/lib/esm/components/datepicker/types.js.map +0 -1
  274. package/lib/esm/components/datepicker/utils.js +0 -508
  275. package/lib/esm/components/datepicker/utils.js.map +0 -1
  276. package/prettier.config.js +0 -9
  277. package/src/components/datepicker/calendar.ts +0 -1397
  278. package/src/components/datepicker/config.ts +0 -368
  279. package/src/components/datepicker/dropdown.ts +0 -757
  280. package/src/components/datepicker/events.ts +0 -149
  281. package/src/components/datepicker/keyboard.ts +0 -646
  282. package/src/components/datepicker/locales.ts +0 -80
  283. package/src/components/datepicker/templates.ts +0 -792
  284. package/src/components/datepicker/types.ts +0 -154
  285. package/src/components/datepicker/utils.ts +0 -631
  286. package/src/components/select/variants.css +0 -4
  287. package/tsconfig.json +0 -17
  288. package/webpack.config.js +0 -118
@@ -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
+ });