@getmicdrop/svelte-components 5.5.1 → 5.5.5

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 (472) hide show
  1. package/dist/calendar/AboutShow/AboutShow.spec.d.ts +2 -0
  2. package/dist/calendar/AboutShow/AboutShow.spec.d.ts.map +1 -0
  3. package/dist/calendar/AboutShow/AboutShow.spec.js +791 -0
  4. package/dist/calendar/AboutShow/AboutShow.svelte +172 -172
  5. package/dist/calendar/Calendar/MiniMonthCalendar.spec.d.ts +2 -0
  6. package/dist/calendar/Calendar/MiniMonthCalendar.spec.d.ts.map +1 -0
  7. package/dist/calendar/Calendar/MiniMonthCalendar.spec.js +1191 -0
  8. package/dist/calendar/Calendar/MiniMonthCalendar.svelte +782 -782
  9. package/dist/calendar/FAQs/FAQs.spec.d.ts +2 -0
  10. package/dist/calendar/FAQs/FAQs.spec.d.ts.map +1 -0
  11. package/dist/calendar/FAQs/FAQs.spec.js +238 -0
  12. package/dist/calendar/FAQs/FAQs.svelte +75 -75
  13. package/dist/calendar/MonthSwitcher/MonthSwitcher.spec.d.ts +2 -0
  14. package/dist/calendar/MonthSwitcher/MonthSwitcher.spec.d.ts.map +1 -0
  15. package/dist/calendar/MonthSwitcher/MonthSwitcher.spec.js +420 -0
  16. package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte +126 -126
  17. package/dist/calendar/OrderSummary/OrderSummary.spec.d.ts +2 -0
  18. package/dist/calendar/OrderSummary/OrderSummary.spec.d.ts.map +1 -0
  19. package/dist/calendar/OrderSummary/OrderSummary.spec.js +808 -0
  20. package/dist/calendar/OrderSummary/OrderSummary.svelte +367 -367
  21. package/dist/calendar/PublicCard/PublicCard.spec.d.ts +2 -0
  22. package/dist/calendar/PublicCard/PublicCard.spec.d.ts.map +1 -0
  23. package/dist/calendar/PublicCard/PublicCard.spec.js +301 -0
  24. package/dist/calendar/PublicCard/PublicCard.svelte +134 -134
  25. package/dist/calendar/ShowCard/ShowCard.spec.d.ts +2 -0
  26. package/dist/calendar/ShowCard/ShowCard.spec.d.ts.map +1 -0
  27. package/dist/calendar/ShowCard/ShowCard.spec.js +714 -0
  28. package/dist/calendar/ShowCard/ShowCard.svelte +157 -157
  29. package/dist/calendar/ShowTimeCard/ShowTimeCard.spec.d.ts +2 -0
  30. package/dist/calendar/ShowTimeCard/ShowTimeCard.spec.d.ts.map +1 -0
  31. package/dist/calendar/ShowTimeCard/ShowTimeCard.spec.js +241 -0
  32. package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte +61 -61
  33. package/dist/components/Layout/Grid.svelte +4 -4
  34. package/dist/components/Layout/Section.spec.d.ts +2 -0
  35. package/dist/components/Layout/Section.spec.d.ts.map +1 -0
  36. package/dist/components/Layout/Section.spec.js +149 -0
  37. package/dist/components/Layout/Section.svelte +80 -80
  38. package/dist/components/Layout/Sidebar.spec.d.ts +2 -0
  39. package/dist/components/Layout/Sidebar.spec.d.ts.map +1 -0
  40. package/dist/components/Layout/Sidebar.spec.js +186 -0
  41. package/dist/components/Layout/Sidebar.svelte +108 -108
  42. package/dist/components/Layout/Stack.spec.js +3 -3
  43. package/dist/components/Layout/Stack.svelte +6 -6
  44. package/dist/constants/formOptions.spec.js +9 -5
  45. package/dist/constants/validation.js +91 -91
  46. package/dist/constants/validation.spec.js +64 -64
  47. package/dist/datetime/__tests__/format.test.js +1 -1
  48. package/dist/datetime/__tests__/parse.test.js +1 -1
  49. package/dist/datetime/__tests__/timezone.test.js +124 -2
  50. package/dist/datetime/parse.js +1 -1
  51. package/dist/forms/createFieldTracker.spec.d.ts +2 -0
  52. package/dist/forms/createFieldTracker.spec.d.ts.map +1 -0
  53. package/dist/forms/createFieldTracker.spec.js +343 -0
  54. package/dist/forms/createFormStore.spec.d.ts +2 -0
  55. package/dist/forms/createFormStore.spec.d.ts.map +1 -0
  56. package/dist/forms/createFormStore.spec.js +689 -0
  57. package/dist/forms/createFormStore.svelte.js +0 -1
  58. package/dist/index.d.ts +5 -112
  59. package/dist/index.js +40 -225
  60. package/dist/patterns/data/DataGrid.spec.d.ts +2 -0
  61. package/dist/patterns/data/DataGrid.spec.d.ts.map +1 -0
  62. package/dist/patterns/data/DataGrid.spec.js +159 -0
  63. package/dist/patterns/data/DataGrid.svelte +45 -45
  64. package/dist/patterns/data/DataList.spec.d.ts +2 -0
  65. package/dist/patterns/data/DataList.spec.d.ts.map +1 -0
  66. package/dist/patterns/data/DataList.spec.js +158 -0
  67. package/dist/patterns/data/DataList.svelte +24 -24
  68. package/dist/patterns/data/DataTable.spec.d.ts +2 -0
  69. package/dist/patterns/data/DataTable.spec.d.ts.map +1 -0
  70. package/dist/patterns/data/DataTable.spec.js +196 -0
  71. package/dist/patterns/data/DataTable.svelte +36 -36
  72. package/dist/patterns/forms/FormActions.spec.js +95 -88
  73. package/dist/patterns/forms/FormActions.stories.svelte +97 -97
  74. package/dist/patterns/forms/FormActions.svelte +46 -46
  75. package/dist/patterns/forms/FormGrid.spec.d.ts +2 -0
  76. package/dist/patterns/forms/FormGrid.spec.d.ts.map +1 -0
  77. package/dist/patterns/forms/FormGrid.spec.js +125 -0
  78. package/dist/patterns/forms/FormGrid.svelte +33 -33
  79. package/dist/patterns/forms/FormSection.spec.d.ts +2 -0
  80. package/dist/patterns/forms/FormSection.spec.d.ts.map +1 -0
  81. package/dist/patterns/forms/FormSection.spec.js +153 -0
  82. package/dist/patterns/forms/FormSection.svelte +32 -32
  83. package/dist/patterns/forms/FormValidationSummary.stories.svelte +83 -83
  84. package/dist/patterns/forms/FormValidationSummary.svelte +33 -33
  85. package/dist/patterns/layout/Sidebar.spec.d.ts +2 -0
  86. package/dist/patterns/layout/Sidebar.spec.d.ts.map +1 -0
  87. package/dist/patterns/layout/Sidebar.spec.js +159 -0
  88. package/dist/patterns/layout/Sidebar.svelte +39 -39
  89. package/dist/patterns/navigation/BottomNav.stories.svelte +117 -117
  90. package/dist/patterns/navigation/BottomNav.svelte +20 -20
  91. package/dist/patterns/navigation/Header.spec.js +33 -24
  92. package/dist/patterns/navigation/Header.stories.svelte +77 -77
  93. package/dist/patterns/navigation/Header.svelte +193 -193
  94. package/dist/patterns/page/PageHeader.spec.d.ts +2 -0
  95. package/dist/patterns/page/PageHeader.spec.d.ts.map +1 -0
  96. package/dist/patterns/page/PageHeader.spec.js +167 -0
  97. package/dist/patterns/page/PageHeader.svelte +18 -18
  98. package/dist/patterns/page/PageLayout.spec.d.ts +2 -0
  99. package/dist/patterns/page/PageLayout.spec.d.ts.map +1 -0
  100. package/dist/patterns/page/PageLayout.spec.js +145 -0
  101. package/dist/patterns/page/PageLayout.svelte +40 -40
  102. package/dist/patterns/page/PageLoader.spec.js +57 -54
  103. package/dist/patterns/page/PageLoader.stories.svelte +137 -137
  104. package/dist/patterns/page/PageLoader.svelte +24 -24
  105. package/dist/patterns/page/SectionHeader.spec.d.ts +2 -0
  106. package/dist/patterns/page/SectionHeader.spec.d.ts.map +1 -0
  107. package/dist/patterns/page/SectionHeader.spec.js +197 -0
  108. package/dist/patterns/page/SectionHeader.svelte +29 -29
  109. package/dist/presets/badges.js +112 -112
  110. package/dist/presets/badges.spec.d.ts +2 -0
  111. package/dist/presets/badges.spec.d.ts.map +1 -0
  112. package/dist/presets/badges.spec.js +172 -0
  113. package/dist/presets/buttons.js +76 -76
  114. package/dist/presets/buttons.spec.d.ts +2 -0
  115. package/dist/presets/buttons.spec.d.ts.map +1 -0
  116. package/dist/presets/buttons.spec.js +135 -0
  117. package/dist/presets/index.js +9 -9
  118. package/dist/primitives/Accordion/Accordion.spec.d.ts +2 -0
  119. package/dist/primitives/Accordion/Accordion.spec.d.ts.map +1 -0
  120. package/dist/primitives/Accordion/Accordion.spec.js +83 -0
  121. package/dist/primitives/Accordion/Accordion.stories.svelte +75 -75
  122. package/dist/primitives/Accordion/Accordion.svelte +42 -42
  123. package/dist/primitives/Accordion/AccordionItem.spec.d.ts +2 -0
  124. package/dist/primitives/Accordion/AccordionItem.spec.d.ts.map +1 -0
  125. package/dist/primitives/Accordion/AccordionItem.spec.js +661 -0
  126. package/dist/primitives/Accordion/AccordionItem.svelte +95 -95
  127. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte +107 -0
  128. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte.d.ts +35 -0
  129. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte.d.ts.map +1 -0
  130. package/dist/primitives/Alert/Alert.spec.js +173 -170
  131. package/dist/primitives/Alert/Alert.stories.svelte +88 -88
  132. package/dist/primitives/Alert/Alert.svelte +27 -27
  133. package/dist/primitives/Avatar/Avatar.spec.d.ts +2 -0
  134. package/dist/primitives/Avatar/Avatar.spec.d.ts.map +1 -0
  135. package/dist/primitives/Avatar/Avatar.spec.js +211 -0
  136. package/dist/primitives/Avatar/Avatar.stories.svelte +94 -94
  137. package/dist/primitives/Avatar/Avatar.svelte +66 -66
  138. package/dist/primitives/Badges/Badge.spec.js +144 -103
  139. package/dist/primitives/Badges/Badge.stories.svelte +86 -86
  140. package/dist/primitives/Badges/Badge.svelte +79 -79
  141. package/dist/primitives/BottomSheet/BottomSheet.spec.js +136 -127
  142. package/dist/primitives/BottomSheet/BottomSheet.stories.svelte +83 -83
  143. package/dist/primitives/BottomSheet/BottomSheet.svelte +100 -100
  144. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte +13 -0
  145. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte.d.ts +7 -0
  146. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte.d.ts.map +1 -0
  147. package/dist/primitives/Breadcrumb/Breadcrumb.spec.js +122 -120
  148. package/dist/primitives/Breadcrumb/Breadcrumb.stories.svelte +23 -23
  149. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +89 -89
  150. package/dist/primitives/Button/Button.spec.js +223 -211
  151. package/dist/primitives/Button/Button.stories.svelte +76 -76
  152. package/dist/primitives/Button/Button.svelte +270 -270
  153. package/dist/primitives/Button/ButtonSaveDemo.spec.js +146 -48
  154. package/dist/primitives/Button/ButtonSaveDemo.svelte +25 -25
  155. package/dist/primitives/Button/ButtonVariantShowcase.spec.d.ts +2 -0
  156. package/dist/primitives/Button/ButtonVariantShowcase.spec.d.ts.map +1 -0
  157. package/dist/primitives/Button/ButtonVariantShowcase.spec.js +202 -0
  158. package/dist/primitives/Button/ButtonVariantShowcase.svelte +129 -129
  159. package/dist/primitives/Card.spec.js +49 -49
  160. package/dist/primitives/Card.stories.svelte +22 -22
  161. package/dist/primitives/Card.svelte +28 -28
  162. package/dist/primitives/Checkbox/Checkbox.spec.d.ts +2 -0
  163. package/dist/primitives/Checkbox/Checkbox.spec.d.ts.map +1 -0
  164. package/dist/primitives/Checkbox/Checkbox.spec.js +252 -0
  165. package/dist/primitives/Checkbox/Checkbox.stories.svelte +84 -84
  166. package/dist/primitives/Checkbox/Checkbox.svelte +88 -88
  167. package/dist/primitives/DarkModeToggle.spec.js +390 -357
  168. package/dist/primitives/DarkModeToggle.stories.svelte +57 -57
  169. package/dist/primitives/DarkModeToggle.svelte +136 -136
  170. package/dist/primitives/Drawer/Drawer.spec.d.ts +2 -0
  171. package/dist/primitives/Drawer/Drawer.spec.d.ts.map +1 -0
  172. package/dist/primitives/Drawer/Drawer.spec.js +212 -0
  173. package/dist/primitives/Drawer/Drawer.stories.svelte +80 -80
  174. package/dist/primitives/Drawer/Drawer.svelte +120 -120
  175. package/dist/primitives/Dropdown/Dropdown.spec.d.ts +2 -0
  176. package/dist/primitives/Dropdown/Dropdown.spec.d.ts.map +1 -0
  177. package/dist/primitives/Dropdown/Dropdown.spec.js +366 -0
  178. package/dist/primitives/Dropdown/Dropdown.stories.svelte +137 -137
  179. package/dist/primitives/Dropdown/Dropdown.svelte +14 -14
  180. package/dist/primitives/Dropdown/DropdownItem.spec.d.ts +2 -0
  181. package/dist/primitives/Dropdown/DropdownItem.spec.d.ts.map +1 -0
  182. package/dist/primitives/Dropdown/DropdownItem.spec.js +182 -0
  183. package/dist/primitives/Dropdown/DropdownItem.svelte +80 -80
  184. package/dist/primitives/Icons/ArrowLeft.svelte +8 -8
  185. package/dist/primitives/Icons/ArrowRight.svelte +8 -8
  186. package/dist/primitives/Icons/Availability.svelte +14 -14
  187. package/dist/primitives/Icons/Back.svelte +14 -14
  188. package/dist/primitives/Icons/CheckCircle.svelte +6 -6
  189. package/dist/primitives/Icons/CheckCircleOutline.svelte +15 -15
  190. package/dist/primitives/Icons/ChevronLeft.svelte +4 -4
  191. package/dist/primitives/Icons/ChevronRight.svelte +4 -4
  192. package/dist/primitives/Icons/Copy.svelte +15 -15
  193. package/dist/primitives/Icons/Cross.svelte +5 -5
  194. package/dist/primitives/Icons/DownArrow.svelte +8 -8
  195. package/dist/primitives/Icons/ErrorCircle.svelte +6 -6
  196. package/dist/primitives/Icons/FacebookIcon.svelte +2 -2
  197. package/dist/primitives/Icons/Home.svelte +15 -15
  198. package/dist/primitives/Icons/Icon.spec.js +169 -169
  199. package/dist/primitives/Icons/Icon.stories.svelte +100 -100
  200. package/dist/primitives/Icons/Icon.svelte +52 -52
  201. package/dist/primitives/Icons/IconGallery.stories.svelte +235 -235
  202. package/dist/primitives/Icons/Info.svelte +7 -7
  203. package/dist/primitives/Icons/InstagramIcon.svelte +4 -4
  204. package/dist/primitives/Icons/LogoInstagram.svelte +2 -2
  205. package/dist/primitives/Icons/Message.svelte +15 -15
  206. package/dist/primitives/Icons/MoonIcon.svelte +5 -5
  207. package/dist/primitives/Icons/More.svelte +21 -21
  208. package/dist/primitives/Icons/MoreHori.spec.js +61 -61
  209. package/dist/primitives/Icons/MoreHori.svelte +22 -22
  210. package/dist/primitives/Icons/Notification.svelte +14 -14
  211. package/dist/primitives/Icons/Payment.svelte +14 -14
  212. package/dist/primitives/Icons/Profile.svelte +21 -21
  213. package/dist/primitives/Icons/Reload.svelte +29 -29
  214. package/dist/primitives/Icons/Shows.svelte +21 -21
  215. package/dist/primitives/Icons/Signout.svelte +21 -21
  216. package/dist/primitives/Icons/SunIcon.svelte +8 -8
  217. package/dist/primitives/Icons/TiktokIcon.svelte +2 -2
  218. package/dist/primitives/Icons/TwitterIcon.svelte +2 -2
  219. package/dist/primitives/Icons/WarningIcon.spec.js +18 -18
  220. package/dist/primitives/Icons/WarningIcon.svelte +5 -5
  221. package/dist/primitives/Icons/iconTestUtils.spec.d.ts +2 -0
  222. package/dist/primitives/Icons/iconTestUtils.spec.d.ts.map +1 -0
  223. package/dist/primitives/Icons/iconTestUtils.spec.js +235 -0
  224. package/dist/primitives/Input/Input.spec.js +573 -573
  225. package/dist/primitives/Input/Input.stories.svelte +139 -139
  226. package/dist/primitives/Input/Input.svelte +384 -397
  227. package/dist/primitives/Input/Input.svelte.d.ts.map +1 -1
  228. package/dist/primitives/Input/Select.spec.js +212 -218
  229. package/dist/primitives/Input/Select.stories.svelte +112 -112
  230. package/dist/primitives/Input/Select.svelte +128 -128
  231. package/dist/primitives/Input/Textarea.spec.d.ts +2 -0
  232. package/dist/primitives/Input/Textarea.spec.d.ts.map +1 -0
  233. package/dist/primitives/Input/Textarea.spec.js +255 -0
  234. package/dist/primitives/Input/Textarea.stories.svelte +137 -137
  235. package/dist/primitives/Input/Textarea.svelte +35 -35
  236. package/dist/primitives/Label/Label.spec.d.ts +2 -0
  237. package/dist/primitives/Label/Label.spec.d.ts.map +1 -0
  238. package/dist/primitives/Label/Label.spec.js +157 -0
  239. package/dist/primitives/Label/Label.svelte +37 -37
  240. package/dist/primitives/Modal/Modal.spec.js +99 -95
  241. package/dist/primitives/Modal/Modal.stories.svelte +86 -86
  242. package/dist/primitives/Modal/Modal.svelte +158 -158
  243. package/dist/primitives/Modal/ModalTestWrapper.svelte +65 -0
  244. package/dist/primitives/Modal/ModalTestWrapper.svelte.d.ts +23 -0
  245. package/dist/primitives/Modal/ModalTestWrapper.svelte.d.ts.map +1 -0
  246. package/dist/primitives/NumberInput/NumberInput.spec.d.ts +2 -0
  247. package/dist/primitives/NumberInput/NumberInput.spec.d.ts.map +1 -0
  248. package/dist/primitives/NumberInput/NumberInput.spec.js +235 -0
  249. package/dist/primitives/NumberInput/NumberInput.svelte +106 -106
  250. package/dist/primitives/Pagination/Pagination.spec.d.ts +2 -0
  251. package/dist/primitives/Pagination/Pagination.spec.d.ts.map +1 -0
  252. package/dist/primitives/Pagination/Pagination.spec.js +266 -0
  253. package/dist/primitives/Pagination/Pagination.stories.svelte +76 -76
  254. package/dist/primitives/Pagination/Pagination.svelte +261 -261
  255. package/dist/primitives/Radio/Radio.spec.d.ts +2 -0
  256. package/dist/primitives/Radio/Radio.spec.d.ts.map +1 -0
  257. package/dist/primitives/Radio/Radio.spec.js +206 -0
  258. package/dist/primitives/Radio/Radio.stories.svelte +80 -80
  259. package/dist/primitives/Radio/Radio.svelte +67 -67
  260. package/dist/primitives/Skeleton/CardPlaceholder.spec.d.ts +2 -0
  261. package/dist/primitives/Skeleton/CardPlaceholder.spec.d.ts.map +1 -0
  262. package/dist/primitives/Skeleton/CardPlaceholder.spec.js +156 -0
  263. package/dist/primitives/Skeleton/CardPlaceholder.svelte +87 -87
  264. package/dist/primitives/Skeleton/ImagePlaceholder.spec.d.ts +2 -0
  265. package/dist/primitives/Skeleton/ImagePlaceholder.spec.d.ts.map +1 -0
  266. package/dist/primitives/Skeleton/ImagePlaceholder.spec.js +120 -0
  267. package/dist/primitives/Skeleton/ImagePlaceholder.svelte +59 -59
  268. package/dist/primitives/Skeleton/ListPlaceholder.spec.d.ts +2 -0
  269. package/dist/primitives/Skeleton/ListPlaceholder.spec.d.ts.map +1 -0
  270. package/dist/primitives/Skeleton/ListPlaceholder.spec.js +220 -0
  271. package/dist/primitives/Skeleton/ListPlaceholder.svelte +76 -76
  272. package/dist/primitives/Skeleton/Skeleton.spec.d.ts +2 -0
  273. package/dist/primitives/Skeleton/Skeleton.spec.d.ts.map +1 -0
  274. package/dist/primitives/Skeleton/Skeleton.spec.js +173 -0
  275. package/dist/primitives/Skeleton/Skeleton.stories.svelte +151 -151
  276. package/dist/primitives/Skeleton/Skeleton.svelte +26 -26
  277. package/dist/primitives/Spinner/Spinner.spec.js +71 -75
  278. package/dist/primitives/Spinner/Spinner.stories.svelte +29 -29
  279. package/dist/primitives/Spinner/Spinner.svelte +20 -20
  280. package/dist/primitives/Tabs/TabItem.spec.d.ts +2 -0
  281. package/dist/primitives/Tabs/TabItem.spec.d.ts.map +1 -0
  282. package/dist/primitives/Tabs/TabItem.spec.js +130 -0
  283. package/dist/primitives/Tabs/TabItem.svelte +49 -49
  284. package/dist/primitives/Tabs/Tabs.spec.d.ts +2 -0
  285. package/dist/primitives/Tabs/Tabs.spec.d.ts.map +1 -0
  286. package/dist/primitives/Tabs/Tabs.spec.js +295 -0
  287. package/dist/primitives/Tabs/Tabs.stories.svelte +112 -112
  288. package/dist/primitives/Tabs/Tabs.svelte +123 -123
  289. package/dist/primitives/Tabs/TabsWithItems.test.svelte +18 -0
  290. package/dist/primitives/Tabs/TabsWithItems.test.svelte.d.ts +16 -0
  291. package/dist/primitives/Tabs/TabsWithItems.test.svelte.d.ts.map +1 -0
  292. package/dist/primitives/Toggle.spec.js +143 -127
  293. package/dist/primitives/Toggle.stories.svelte +92 -92
  294. package/dist/primitives/Toggle.svelte +71 -71
  295. package/dist/primitives/Typography/Typography.spec.d.ts +2 -0
  296. package/dist/primitives/Typography/Typography.spec.d.ts.map +1 -0
  297. package/dist/primitives/Typography/Typography.spec.js +183 -0
  298. package/dist/primitives/Typography/Typography.svelte +53 -53
  299. package/dist/primitives/ValidationError.spec.js +103 -103
  300. package/dist/primitives/ValidationError.stories.svelte +69 -69
  301. package/dist/primitives/ValidationError.svelte +29 -29
  302. package/dist/primitives/index.d.ts +1 -0
  303. package/dist/primitives/index.js +3 -0
  304. package/dist/recipes/CropImage/CropImage.spec.js +208 -216
  305. package/dist/recipes/CropImage/CropImage.stories.svelte +104 -104
  306. package/dist/recipes/CropImage/CropImage.svelte +238 -238
  307. package/dist/recipes/ImageUploader/ImageUploader.spec.d.ts +2 -0
  308. package/dist/recipes/ImageUploader/ImageUploader.spec.d.ts.map +1 -0
  309. package/dist/recipes/ImageUploader/ImageUploader.spec.js +1351 -0
  310. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte +125 -125
  311. package/dist/recipes/ImageUploader/ImageUploader.svelte +804 -804
  312. package/dist/recipes/SuperLogin/SuperLogin.spec.d.ts +2 -0
  313. package/dist/recipes/SuperLogin/SuperLogin.spec.d.ts.map +1 -0
  314. package/dist/recipes/SuperLogin/SuperLogin.spec.js +1436 -0
  315. package/dist/recipes/SuperLogin/SuperLogin.svelte +7 -6
  316. package/dist/recipes/SuperLogin/SuperLogin.svelte.d.ts.map +1 -1
  317. package/dist/recipes/Toaster/Toaster.stories.svelte +62 -62
  318. package/dist/recipes/feedback/EmptyState/EmptyState.spec.d.ts +2 -0
  319. package/dist/recipes/feedback/EmptyState/EmptyState.spec.d.ts.map +1 -0
  320. package/dist/recipes/feedback/EmptyState/EmptyState.spec.js +202 -0
  321. package/dist/recipes/feedback/EmptyState/EmptyState.svelte +1 -1
  322. package/dist/recipes/feedback/ErrorDisplay.spec.js +69 -69
  323. package/dist/recipes/feedback/ErrorDisplay.stories.svelte +101 -101
  324. package/dist/recipes/feedback/ErrorDisplay.svelte +1 -1
  325. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.spec.js +133 -129
  326. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.svelte +157 -157
  327. package/dist/recipes/fields/CheckboxField.spec.d.ts +2 -0
  328. package/dist/recipes/fields/CheckboxField.spec.d.ts.map +1 -0
  329. package/dist/recipes/fields/CheckboxField.spec.js +135 -0
  330. package/dist/recipes/fields/CheckboxField.svelte +85 -85
  331. package/dist/recipes/fields/FormField.spec.d.ts +2 -0
  332. package/dist/recipes/fields/FormField.spec.d.ts.map +1 -0
  333. package/dist/recipes/fields/FormField.spec.js +159 -0
  334. package/dist/recipes/fields/FormField.svelte +58 -58
  335. package/dist/recipes/fields/RadioGroup.spec.d.ts +2 -0
  336. package/dist/recipes/fields/RadioGroup.spec.d.ts.map +1 -0
  337. package/dist/recipes/fields/RadioGroup.spec.js +199 -0
  338. package/dist/recipes/fields/RadioGroup.svelte +95 -95
  339. package/dist/recipes/fields/SelectField.spec.d.ts +2 -0
  340. package/dist/recipes/fields/SelectField.spec.d.ts.map +1 -0
  341. package/dist/recipes/fields/SelectField.spec.js +188 -0
  342. package/dist/recipes/fields/SelectField.svelte +80 -80
  343. package/dist/recipes/fields/TextareaField.spec.d.ts +2 -0
  344. package/dist/recipes/fields/TextareaField.spec.d.ts.map +1 -0
  345. package/dist/recipes/fields/TextareaField.spec.js +205 -0
  346. package/dist/recipes/fields/TextareaField.svelte +97 -97
  347. package/dist/recipes/fields/ToggleField.spec.d.ts +2 -0
  348. package/dist/recipes/fields/ToggleField.spec.d.ts.map +1 -0
  349. package/dist/recipes/fields/ToggleField.spec.js +153 -0
  350. package/dist/recipes/fields/ToggleField.svelte +60 -60
  351. package/dist/recipes/fields/index.js +7 -7
  352. package/dist/recipes/inputs/MultiSelect.spec.js +258 -257
  353. package/dist/recipes/inputs/MultiSelect.stories.svelte +133 -133
  354. package/dist/recipes/inputs/MultiSelect.svelte +256 -249
  355. package/dist/recipes/inputs/MultiSelect.svelte.d.ts +2 -0
  356. package/dist/recipes/inputs/MultiSelect.svelte.d.ts.map +1 -1
  357. package/dist/recipes/inputs/OTPInput.spec.js +251 -238
  358. package/dist/recipes/inputs/OTPInput.stories.svelte +162 -162
  359. package/dist/recipes/inputs/OTPInput.svelte +29 -29
  360. package/dist/recipes/inputs/PasswordInput.spec.d.ts +2 -0
  361. package/dist/recipes/inputs/PasswordInput.spec.d.ts.map +1 -0
  362. package/dist/recipes/inputs/PasswordInput.spec.js +410 -0
  363. package/dist/recipes/inputs/PasswordInput.svelte +22 -22
  364. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.spec.js +245 -165
  365. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte +43 -43
  366. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte +71 -0
  367. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte.d.ts +9 -0
  368. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte.d.ts.map +1 -0
  369. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.spec.js +1139 -193
  370. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.stories.svelte +123 -123
  371. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte +326 -326
  372. package/dist/recipes/inputs/Search.spec.d.ts +2 -0
  373. package/dist/recipes/inputs/Search.spec.d.ts.map +1 -0
  374. package/dist/recipes/inputs/Search.spec.js +177 -0
  375. package/dist/recipes/inputs/Search.svelte +37 -37
  376. package/dist/recipes/inputs/SelectDropdown.spec.d.ts +2 -0
  377. package/dist/recipes/inputs/SelectDropdown.spec.d.ts.map +1 -0
  378. package/dist/recipes/inputs/SelectDropdown.spec.js +512 -0
  379. package/dist/recipes/inputs/SelectDropdown.svelte +57 -57
  380. package/dist/recipes/modals/AlertModal.spec.d.ts +2 -0
  381. package/dist/recipes/modals/AlertModal.spec.d.ts.map +1 -0
  382. package/dist/recipes/modals/AlertModal.spec.js +432 -0
  383. package/dist/recipes/modals/AlertModal.svelte +130 -130
  384. package/dist/recipes/modals/ConfirmationModal.spec.js +206 -191
  385. package/dist/recipes/modals/ConfirmationModal.stories.svelte +119 -119
  386. package/dist/recipes/modals/ConfirmationModal.svelte +152 -152
  387. package/dist/recipes/modals/InputModal.spec.d.ts +2 -0
  388. package/dist/recipes/modals/InputModal.spec.d.ts.map +1 -0
  389. package/dist/recipes/modals/InputModal.spec.js +872 -0
  390. package/dist/recipes/modals/InputModal.svelte +182 -182
  391. package/dist/recipes/modals/ModalStateManager.spec.js +100 -100
  392. package/dist/recipes/modals/ModalStateManager.svelte +77 -77
  393. package/dist/recipes/modals/ModalTestWrapper.spec.d.ts +2 -0
  394. package/dist/recipes/modals/ModalTestWrapper.spec.d.ts.map +1 -0
  395. package/dist/recipes/modals/ModalTestWrapper.spec.js +502 -0
  396. package/dist/recipes/modals/ModalTestWrapper.svelte +65 -65
  397. package/dist/recipes/modals/StatusModal.spec.d.ts +2 -0
  398. package/dist/recipes/modals/StatusModal.spec.d.ts.map +1 -0
  399. package/dist/recipes/modals/StatusModal.spec.js +599 -0
  400. package/dist/recipes/modals/StatusModal.svelte +206 -206
  401. package/dist/services/EventService.js +75 -75
  402. package/dist/services/EventService.spec.js +217 -217
  403. package/dist/services/ShowService.spec.js +345 -342
  404. package/dist/stores/auth.js +36 -36
  405. package/dist/stores/auth.spec.js +139 -139
  406. package/dist/stores/toaster.js +13 -13
  407. package/dist/stories/ButtonAuditDashboard.spec.d.ts +2 -0
  408. package/dist/stories/ButtonAuditDashboard.spec.d.ts.map +1 -0
  409. package/dist/stories/ButtonAuditDashboard.spec.js +913 -0
  410. package/dist/stories/ButtonAuditReview.spec.d.ts +2 -0
  411. package/dist/stories/ButtonAuditReview.spec.d.ts.map +1 -0
  412. package/dist/stories/ButtonAuditReview.spec.js +422 -0
  413. package/dist/stories/ButtonAuditReview.stories.svelte +14 -14
  414. package/dist/stories/ButtonAuditReview.svelte +427 -427
  415. package/dist/stories/ButtonGridView.spec.d.ts +2 -0
  416. package/dist/stories/ButtonGridView.spec.d.ts.map +1 -0
  417. package/dist/stories/ButtonGridView.spec.js +667 -0
  418. package/dist/stories/ButtonShowcase.spec.d.ts +2 -0
  419. package/dist/stories/ButtonShowcase.spec.d.ts.map +1 -0
  420. package/dist/stories/ButtonShowcase.spec.js +499 -0
  421. package/dist/stories/PatternsGallery.spec.d.ts +2 -0
  422. package/dist/stories/PatternsGallery.spec.d.ts.map +1 -0
  423. package/dist/stories/PatternsGallery.spec.js +514 -0
  424. package/dist/stories/PatternsGallery.stories.svelte +19 -19
  425. package/dist/stories/PatternsGallery.svelte +206 -206
  426. package/dist/stories/PrimitivesGallery.spec.d.ts +2 -0
  427. package/dist/stories/PrimitivesGallery.spec.d.ts.map +1 -0
  428. package/dist/stories/PrimitivesGallery.spec.js +813 -0
  429. package/dist/stories/PrimitivesGallery.stories.svelte +19 -19
  430. package/dist/stories/PrimitivesGallery.svelte +725 -725
  431. package/dist/stories/RecipesGallery.spec.d.ts +2 -0
  432. package/dist/stories/RecipesGallery.spec.d.ts.map +1 -0
  433. package/dist/stories/RecipesGallery.spec.js +299 -0
  434. package/dist/stories/RecipesGallery.stories.svelte +19 -19
  435. package/dist/stories/RecipesGallery.svelte +271 -271
  436. package/dist/stories/button-audit-manifest.json +11186 -11186
  437. package/dist/stripe/useStripeTheme.spec.d.ts +2 -0
  438. package/dist/stripe/useStripeTheme.spec.d.ts.map +1 -0
  439. package/dist/stripe/useStripeTheme.spec.js +793 -0
  440. package/dist/tailwind/preset.cjs +82 -82
  441. package/dist/telemetry.d.ts.map +1 -1
  442. package/dist/telemetry.js +6 -5
  443. package/dist/telemetry.spec.js +495 -12
  444. package/dist/tokens/__tests__/colors.test.d.ts +2 -0
  445. package/dist/tokens/__tests__/colors.test.d.ts.map +1 -0
  446. package/dist/tokens/__tests__/colors.test.js +152 -0
  447. package/dist/tokens/__tests__/radius.test.d.ts +2 -0
  448. package/dist/tokens/__tests__/radius.test.d.ts.map +1 -0
  449. package/dist/tokens/__tests__/radius.test.js +118 -0
  450. package/dist/tokens/__tests__/shadows.test.d.ts +2 -0
  451. package/dist/tokens/__tests__/shadows.test.d.ts.map +1 -0
  452. package/dist/tokens/__tests__/shadows.test.js +105 -0
  453. package/dist/tokens/__tests__/spacing.test.js +11 -8
  454. package/dist/tokens/__tests__/typography.test.d.ts +2 -0
  455. package/dist/tokens/__tests__/typography.test.d.ts.map +1 -0
  456. package/dist/tokens/__tests__/typography.test.js +156 -0
  457. package/dist/tokens/__tests__/z-index.test.d.ts +2 -0
  458. package/dist/tokens/__tests__/z-index.test.d.ts.map +1 -0
  459. package/dist/tokens/__tests__/z-index.test.js +121 -0
  460. package/dist/tokens/tokens.css +87 -87
  461. package/dist/utils/apiConfig.spec.js +102 -1
  462. package/dist/utils/formatters.spec.d.ts +2 -0
  463. package/dist/utils/formatters.spec.d.ts.map +1 -0
  464. package/dist/utils/formatters.spec.js +82 -0
  465. package/dist/utils/transitions.d.ts +24 -0
  466. package/dist/utils/transitions.d.ts.map +1 -0
  467. package/dist/utils/transitions.js +62 -0
  468. package/dist/utils/transitions.spec.d.ts +2 -0
  469. package/dist/utils/transitions.spec.d.ts.map +1 -0
  470. package/dist/utils/transitions.spec.js +130 -0
  471. package/dist/utils/utils.js +354 -354
  472. package/package.json +288 -283
@@ -0,0 +1,1351 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
3
+ import userEvent from '@testing-library/user-event';
4
+ import ImageUploader from './ImageUploader.svelte';
5
+
6
+ // Mock Sortable.js
7
+ const mockSortableDestroy = vi.fn();
8
+ const mockSortableInstance = {
9
+ destroy: mockSortableDestroy,
10
+ };
11
+
12
+ vi.mock('sortablejs', () => ({
13
+ default: vi.fn(() => mockSortableInstance),
14
+ }));
15
+
16
+ // Helper to create a mock file
17
+ function createMockFile(name = 'test.jpg', type = 'image/jpeg', size = 1024) {
18
+ const blob = new Blob(['test'], { type });
19
+ const file = new File([blob], name, { type });
20
+ Object.defineProperty(file, 'size', { value: size });
21
+ return file;
22
+ }
23
+
24
+ // Helper to create a mock DragEvent with files
25
+ function createDragEvent(type, files = []) {
26
+ const dataTransfer = {
27
+ files,
28
+ types: files.length > 0 ? ['Files'] : [],
29
+ };
30
+ const event = new Event(type, { bubbles: true, cancelable: true });
31
+ Object.defineProperty(event, 'dataTransfer', { value: dataTransfer });
32
+ return event;
33
+ }
34
+
35
+ describe('ImageUploader', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ // Mock URL.createObjectURL
39
+ global.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
40
+ global.URL.revokeObjectURL = vi.fn();
41
+ });
42
+
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ describe('Single Image Mode', () => {
48
+ it('renders empty dropzone when no image provided', () => {
49
+ const { container } = render(ImageUploader, {
50
+ props: { maxImages: 1 },
51
+ });
52
+ expect(container.querySelector('.filepond-wrapper-single')).toBeInTheDocument();
53
+ });
54
+
55
+ it('renders existing image when provided', () => {
56
+ const { container } = render(ImageUploader, {
57
+ props: {
58
+ images: 'https://example.com/image.jpg',
59
+ maxImages: 1,
60
+ },
61
+ });
62
+ const img = container.querySelector('img[alt="Uploaded"]');
63
+ expect(img).toBeInTheDocument();
64
+ expect(img).toHaveAttribute('src', 'https://example.com/image.jpg');
65
+ });
66
+
67
+ it('shows replace and delete buttons on hover when image exists', () => {
68
+ render(ImageUploader, {
69
+ props: {
70
+ images: 'https://example.com/image.jpg',
71
+ maxImages: 1,
72
+ },
73
+ });
74
+ expect(screen.getByText('Replace')).toBeInTheDocument();
75
+ expect(screen.getByText('Delete')).toBeInTheDocument();
76
+ });
77
+
78
+ it('does not show action buttons when disabled', () => {
79
+ render(ImageUploader, {
80
+ props: {
81
+ images: 'https://example.com/image.jpg',
82
+ maxImages: 1,
83
+ disabled: true,
84
+ },
85
+ });
86
+ expect(screen.queryByText('Replace')).not.toBeInTheDocument();
87
+ expect(screen.queryByText('Delete')).not.toBeInTheDocument();
88
+ });
89
+
90
+ it('calls onRemove when delete button clicked', async () => {
91
+ const onRemove = vi.fn();
92
+ const user = userEvent.setup();
93
+
94
+ render(ImageUploader, {
95
+ props: {
96
+ images: 'https://example.com/image.jpg',
97
+ maxImages: 1,
98
+ onRemove,
99
+ },
100
+ });
101
+
102
+ const deleteButton = screen.getByText('Delete');
103
+ await user.click(deleteButton);
104
+ expect(onRemove).toHaveBeenCalledWith(0);
105
+ });
106
+
107
+ it('displays helper text when provided', () => {
108
+ const helperText = 'Upload a profile picture';
109
+ render(ImageUploader, {
110
+ props: {
111
+ maxImages: 1,
112
+ helperText,
113
+ },
114
+ });
115
+ expect(screen.getByText(helperText)).toBeInTheDocument();
116
+ });
117
+
118
+ it('displays error message when provided', () => {
119
+ const error = 'File too large';
120
+ render(ImageUploader, {
121
+ props: {
122
+ maxImages: 1,
123
+ error,
124
+ },
125
+ });
126
+ expect(screen.getByText(error)).toBeInTheDocument();
127
+ });
128
+
129
+ it('applies custom className', () => {
130
+ const { container } = render(ImageUploader, {
131
+ props: {
132
+ maxImages: 1,
133
+ class: 'custom-uploader',
134
+ },
135
+ });
136
+ expect(container.querySelector('.custom-uploader')).toBeInTheDocument();
137
+ });
138
+ });
139
+
140
+ describe('Multi Image Mode', () => {
141
+ it('renders grid layout for multi-image mode', () => {
142
+ const { container } = render(ImageUploader, {
143
+ props: { maxImages: 6 },
144
+ });
145
+ // Should have container with flex/grid layout
146
+ expect(container.querySelector('.image-uploader-multi')).toBeInTheDocument();
147
+ });
148
+
149
+ it('renders existing images in grid', () => {
150
+ const { container } = render(ImageUploader, {
151
+ props: {
152
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
153
+ maxImages: 6,
154
+ },
155
+ });
156
+ const images = container.querySelectorAll('img[alt="Uploaded"]');
157
+ expect(images).toHaveLength(2);
158
+ expect(images[0]).toHaveAttribute('src', 'https://example.com/1.jpg');
159
+ expect(images[1]).toHaveAttribute('src', 'https://example.com/2.jpg');
160
+ });
161
+
162
+ it('shows "Main" badge on first image when enabled', () => {
163
+ render(ImageUploader, {
164
+ props: {
165
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
166
+ maxImages: 6,
167
+ showMainBadge: true,
168
+ },
169
+ });
170
+ expect(screen.getByText('Main')).toBeInTheDocument();
171
+ });
172
+
173
+ it('does not show "Main" badge when disabled', () => {
174
+ render(ImageUploader, {
175
+ props: {
176
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
177
+ maxImages: 6,
178
+ showMainBadge: false,
179
+ },
180
+ });
181
+ expect(screen.queryByText('Main')).not.toBeInTheDocument();
182
+ });
183
+
184
+ it('shows empty slot when can add more images', () => {
185
+ const { container } = render(ImageUploader, {
186
+ props: {
187
+ images: ['https://example.com/1.jpg'],
188
+ maxImages: 6,
189
+ },
190
+ });
191
+ const emptySlot = container.querySelector('.empty-slot');
192
+ expect(emptySlot).toBeInTheDocument();
193
+ });
194
+
195
+ it('does not show empty slot when max images reached', () => {
196
+ const { container } = render(ImageUploader, {
197
+ props: {
198
+ images: Array(6).fill('https://example.com/image.jpg'),
199
+ maxImages: 6,
200
+ },
201
+ });
202
+ const emptySlots = container.querySelectorAll('.empty-slot');
203
+ expect(emptySlots).toHaveLength(0);
204
+ });
205
+
206
+ it('calls onSetMain when non-main image clicked', async () => {
207
+ const onSetMain = vi.fn();
208
+ const user = userEvent.setup();
209
+
210
+ const { container } = render(ImageUploader, {
211
+ props: {
212
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
213
+ maxImages: 6,
214
+ onSetMain,
215
+ },
216
+ });
217
+
218
+ const images = container.querySelectorAll('img[alt="Uploaded"]');
219
+ await user.click(images[1]);
220
+ expect(onSetMain).toHaveBeenCalledWith({ index: 1 });
221
+ });
222
+
223
+ it('does not call onSetMain when main image clicked', async () => {
224
+ const onSetMain = vi.fn();
225
+ const user = userEvent.setup();
226
+
227
+ const { container } = render(ImageUploader, {
228
+ props: {
229
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
230
+ maxImages: 6,
231
+ onSetMain,
232
+ },
233
+ });
234
+
235
+ const images = container.querySelectorAll('img[alt="Uploaded"]');
236
+ await user.click(images[0]);
237
+ expect(onSetMain).not.toHaveBeenCalled();
238
+ });
239
+
240
+ it('shows remove button on each image', () => {
241
+ render(ImageUploader, {
242
+ props: {
243
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
244
+ maxImages: 6,
245
+ },
246
+ });
247
+ const removeButtons = screen.getAllByLabelText('Remove photo');
248
+ expect(removeButtons).toHaveLength(2);
249
+ });
250
+
251
+ it('calls onRemove with correct index', async () => {
252
+ const onRemove = vi.fn();
253
+ const user = userEvent.setup();
254
+
255
+ render(ImageUploader, {
256
+ props: {
257
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
258
+ maxImages: 6,
259
+ onRemove,
260
+ },
261
+ });
262
+
263
+ const removeButtons = screen.getAllByLabelText('Remove photo');
264
+ await user.click(removeButtons[1]);
265
+ expect(onRemove).toHaveBeenCalledWith(1);
266
+ });
267
+
268
+ it('opens upload modal when empty slot clicked', async () => {
269
+ const user = userEvent.setup();
270
+
271
+ render(ImageUploader, {
272
+ props: {
273
+ images: ['https://example.com/1.jpg'],
274
+ maxImages: 6,
275
+ },
276
+ });
277
+
278
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
279
+ await user.click(addButton);
280
+
281
+ await waitFor(() => {
282
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
283
+ });
284
+ });
285
+
286
+ it('closes modal when close button clicked', async () => {
287
+ const user = userEvent.setup();
288
+
289
+ render(ImageUploader, {
290
+ props: {
291
+ images: ['https://example.com/1.jpg'],
292
+ maxImages: 6,
293
+ },
294
+ });
295
+
296
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
297
+ await user.click(addButton);
298
+
299
+ await waitFor(() => {
300
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
301
+ });
302
+
303
+ const closeButton = screen.getByLabelText('Close');
304
+ await user.click(closeButton);
305
+
306
+ await waitFor(() => {
307
+ expect(screen.queryByText('Upload photo')).not.toBeInTheDocument();
308
+ });
309
+ });
310
+
311
+ it('closes modal when backdrop clicked', async () => {
312
+ const user = userEvent.setup();
313
+
314
+ render(ImageUploader, {
315
+ props: {
316
+ images: ['https://example.com/1.jpg'],
317
+ maxImages: 6,
318
+ },
319
+ });
320
+
321
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
322
+ await user.click(addButton);
323
+
324
+ await waitFor(() => {
325
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
326
+ });
327
+
328
+ const backdrop = screen.getByText('Upload photo').closest('.fixed');
329
+ await user.click(backdrop);
330
+
331
+ await waitFor(() => {
332
+ expect(screen.queryByText('Upload photo')).not.toBeInTheDocument();
333
+ });
334
+ });
335
+
336
+ describe('Size configurations', () => {
337
+ it('applies small size classes', () => {
338
+ const { container } = render(ImageUploader, {
339
+ props: {
340
+ images: ['https://example.com/1.jpg'],
341
+ maxImages: 6,
342
+ size: 'sm',
343
+ },
344
+ });
345
+ const slot = container.querySelector('.w-16');
346
+ expect(slot).toBeInTheDocument();
347
+ });
348
+
349
+ it('applies medium size classes', () => {
350
+ const { container } = render(ImageUploader, {
351
+ props: {
352
+ images: ['https://example.com/1.jpg'],
353
+ maxImages: 6,
354
+ size: 'md',
355
+ },
356
+ });
357
+ const slot = container.querySelector('.w-24');
358
+ expect(slot).toBeInTheDocument();
359
+ });
360
+
361
+ it('applies large size classes', () => {
362
+ const { container } = render(ImageUploader, {
363
+ props: {
364
+ images: ['https://example.com/1.jpg'],
365
+ maxImages: 6,
366
+ size: 'lg',
367
+ },
368
+ });
369
+ const gridContainer = container.querySelector('.grid-cols-3');
370
+ expect(gridContainer).toBeInTheDocument();
371
+ });
372
+ });
373
+
374
+ describe('Shape configurations', () => {
375
+ it('applies square aspect ratio', () => {
376
+ const { container } = render(ImageUploader, {
377
+ props: {
378
+ images: ['https://example.com/1.jpg'],
379
+ maxImages: 6,
380
+ shape: 'square',
381
+ size: 'lg',
382
+ },
383
+ });
384
+ const slot = container.querySelector('.aspect-square');
385
+ expect(slot).toBeInTheDocument();
386
+ });
387
+
388
+ it('applies wide aspect ratio', () => {
389
+ const { container } = render(ImageUploader, {
390
+ props: {
391
+ images: [],
392
+ maxImages: 1,
393
+ shape: 'wide',
394
+ },
395
+ });
396
+ const wrapper = container.querySelector('.aspect-video');
397
+ expect(wrapper).toBeInTheDocument();
398
+ });
399
+ });
400
+ });
401
+
402
+ describe('Drag and Drop', () => {
403
+ it('handles dragover event on empty dropzone', () => {
404
+ const { container } = render(ImageUploader, {
405
+ props: { maxImages: 1 },
406
+ });
407
+
408
+ const dropzone = container.querySelector('.filepond-wrapper-single');
409
+ const event = createDragEvent('dragover', [createMockFile()]);
410
+
411
+ fireEvent(dropzone, event);
412
+ // Should prevent default
413
+ expect(event.defaultPrevented).toBe(true);
414
+ });
415
+
416
+ it('handles drop event on single mode dropzone', async () => {
417
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
418
+ const { container } = render(ImageUploader, {
419
+ props: {
420
+ maxImages: 1,
421
+ onUpload,
422
+ enableCrop: false,
423
+ },
424
+ });
425
+
426
+ const dropzone = container.querySelector('.filepond-wrapper-single');
427
+ const file = createMockFile();
428
+ const event = createDragEvent('drop', [file]);
429
+
430
+ fireEvent(dropzone, event);
431
+
432
+ await waitFor(() => {
433
+ expect(onUpload).toHaveBeenCalledWith(file);
434
+ });
435
+ });
436
+
437
+ it('handles drop event on multi mode empty slot', async () => {
438
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
439
+ const { container } = render(ImageUploader, {
440
+ props: {
441
+ images: [],
442
+ maxImages: 6,
443
+ onUpload,
444
+ enableCrop: false,
445
+ },
446
+ });
447
+
448
+ const emptySlot = container.querySelector('.border-dashed');
449
+ const file = createMockFile();
450
+ const event = createDragEvent('drop', [file]);
451
+
452
+ fireEvent(emptySlot, event);
453
+
454
+ await waitFor(() => {
455
+ expect(onUpload).toHaveBeenCalledWith(file);
456
+ });
457
+ });
458
+
459
+ it('does not handle drop when disabled', () => {
460
+ const onUpload = vi.fn();
461
+ const { container } = render(ImageUploader, {
462
+ props: {
463
+ maxImages: 1,
464
+ onUpload,
465
+ disabled: true,
466
+ },
467
+ });
468
+
469
+ const dropzone = container.querySelector('.filepond-wrapper-single');
470
+ const file = createMockFile();
471
+ const event = createDragEvent('drop', [file]);
472
+
473
+ fireEvent(dropzone, event);
474
+
475
+ expect(onUpload).not.toHaveBeenCalled();
476
+ });
477
+
478
+ it('does not handle drop when max images reached', () => {
479
+ const onUpload = vi.fn();
480
+ const { container } = render(ImageUploader, {
481
+ props: {
482
+ images: Array(6).fill('https://example.com/image.jpg'),
483
+ maxImages: 6,
484
+ onUpload,
485
+ },
486
+ });
487
+
488
+ const gridContainer = container.querySelector('.image-uploader-multi > div');
489
+ const file = createMockFile();
490
+ const event = createDragEvent('drop', [file]);
491
+
492
+ fireEvent(gridContainer, event);
493
+
494
+ expect(onUpload).not.toHaveBeenCalled();
495
+ });
496
+
497
+ it('validates file type on drop', () => {
498
+ const onUpload = vi.fn();
499
+ const { container } = render(ImageUploader, {
500
+ props: {
501
+ maxImages: 1,
502
+ onUpload,
503
+ acceptedTypes: ['image/jpeg', 'image/png'],
504
+ enableCrop: false,
505
+ },
506
+ });
507
+
508
+ const dropzone = container.querySelector('.filepond-wrapper-single');
509
+ const file = createMockFile('test.txt', 'text/plain');
510
+ const event = createDragEvent('drop', [file]);
511
+
512
+ fireEvent(dropzone, event);
513
+
514
+ expect(onUpload).not.toHaveBeenCalled();
515
+ });
516
+
517
+ it('shows drag over state', () => {
518
+ const { container } = render(ImageUploader, {
519
+ props: { maxImages: 1 },
520
+ });
521
+
522
+ const dropzone = container.querySelector('.filepond-wrapper-single');
523
+ const dragOverEvent = createDragEvent('dragover', [createMockFile()]);
524
+
525
+ fireEvent(dropzone, dragOverEvent);
526
+
527
+ expect(container.querySelector('.dropzone-drag-over')).toBeInTheDocument();
528
+ });
529
+
530
+ it('clears drag over state on dragleave', () => {
531
+ const { container } = render(ImageUploader, {
532
+ props: { maxImages: 1 },
533
+ });
534
+
535
+ const dropzone = container.querySelector('.filepond-wrapper-single');
536
+
537
+ // Trigger dragover first
538
+ const dragOverEvent = createDragEvent('dragover', [createMockFile()]);
539
+ fireEvent(dropzone, dragOverEvent);
540
+
541
+ // Then dragleave
542
+ const dragLeaveEvent = createDragEvent('dragleave');
543
+ fireEvent(dropzone, dragLeaveEvent);
544
+
545
+ expect(container.querySelector('.dropzone-drag-over')).not.toBeInTheDocument();
546
+ });
547
+ });
548
+
549
+ describe('Upload functionality', () => {
550
+ it('calls onUpload when file is uploaded', async () => {
551
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
552
+ const { container } = render(ImageUploader, {
553
+ props: {
554
+ maxImages: 1,
555
+ onUpload,
556
+ enableCrop: false,
557
+ },
558
+ });
559
+
560
+ const dropzone = container.querySelector('.filepond-wrapper-single');
561
+ const file = createMockFile();
562
+ const event = createDragEvent('drop', [file]);
563
+
564
+ fireEvent(dropzone, event);
565
+
566
+ await waitFor(() => {
567
+ expect(onUpload).toHaveBeenCalledWith(file);
568
+ });
569
+ });
570
+
571
+ it('updates images when onUpload returns URL (self-managed mode)', async () => {
572
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
573
+ const onchange = vi.fn();
574
+
575
+ const { container } = render(ImageUploader, {
576
+ props: {
577
+ images: [],
578
+ maxImages: 1,
579
+ onUpload,
580
+ onchange,
581
+ enableCrop: false,
582
+ },
583
+ });
584
+
585
+ const dropzone = container.querySelector('.filepond-wrapper-single');
586
+ const file = createMockFile();
587
+ const event = createDragEvent('drop', [file]);
588
+
589
+ fireEvent(dropzone, event);
590
+
591
+ await waitFor(() => {
592
+ expect(onUpload).toHaveBeenCalledWith(file);
593
+ expect(onchange).toHaveBeenCalledWith(['https://example.com/uploaded.jpg']);
594
+ });
595
+ });
596
+
597
+ it('calls onchange when image is uploaded', async () => {
598
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
599
+ const onchange = vi.fn();
600
+ const { container } = render(ImageUploader, {
601
+ props: {
602
+ images: [],
603
+ maxImages: 1,
604
+ onUpload,
605
+ onchange,
606
+ enableCrop: false,
607
+ },
608
+ });
609
+
610
+ const dropzone = container.querySelector('.filepond-wrapper-single');
611
+ const file = createMockFile();
612
+ const event = createDragEvent('drop', [file]);
613
+
614
+ fireEvent(dropzone, event);
615
+
616
+ await waitFor(() => {
617
+ expect(onchange).toHaveBeenCalledWith(['https://example.com/uploaded.jpg']);
618
+ });
619
+ });
620
+
621
+ it('handles upload error gracefully', async () => {
622
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
623
+ const onUpload = vi.fn().mockRejectedValue(new Error('Upload failed'));
624
+ const { container } = render(ImageUploader, {
625
+ props: {
626
+ maxImages: 1,
627
+ onUpload,
628
+ enableCrop: false,
629
+ },
630
+ });
631
+
632
+ const dropzone = container.querySelector('.filepond-wrapper-single');
633
+ const file = createMockFile();
634
+ const event = createDragEvent('drop', [file]);
635
+
636
+ fireEvent(dropzone, event);
637
+
638
+ await waitFor(() => {
639
+ expect(consoleError).toHaveBeenCalledWith('Upload failed:', expect.any(Error));
640
+ });
641
+
642
+ consoleError.mockRestore();
643
+ });
644
+
645
+ it('warns when onUpload is not provided', async () => {
646
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
647
+ const { container } = render(ImageUploader, {
648
+ props: {
649
+ maxImages: 1,
650
+ enableCrop: false,
651
+ },
652
+ });
653
+
654
+ const dropzone = container.querySelector('.filepond-wrapper-single');
655
+ const file = createMockFile();
656
+ const event = createDragEvent('drop', [file]);
657
+
658
+ fireEvent(dropzone, event);
659
+
660
+ await waitFor(() => {
661
+ expect(consoleWarn).toHaveBeenCalledWith('ImageUploader: onUpload callback not provided');
662
+ });
663
+
664
+ consoleWarn.mockRestore();
665
+ });
666
+
667
+ it('handles controlled mode (onUpload returns void)', async () => {
668
+ const onUpload = vi.fn().mockResolvedValue(undefined);
669
+ const onchange = vi.fn();
670
+ const { container } = render(ImageUploader, {
671
+ props: {
672
+ images: [],
673
+ maxImages: 1,
674
+ onUpload,
675
+ onchange,
676
+ enableCrop: false,
677
+ },
678
+ });
679
+
680
+ const dropzone = container.querySelector('.filepond-wrapper-single');
681
+ const file = createMockFile();
682
+ const event = createDragEvent('drop', [file]);
683
+
684
+ fireEvent(dropzone, event);
685
+
686
+ await waitFor(() => {
687
+ expect(onUpload).toHaveBeenCalledWith(file);
688
+ });
689
+
690
+ // In controlled mode, onchange should not be called
691
+ expect(onchange).not.toHaveBeenCalled();
692
+ });
693
+ });
694
+
695
+ describe('Cropping functionality', () => {
696
+ it('shows crop modal when enableCrop is true', async () => {
697
+ const onUpload = vi.fn();
698
+ const { container } = render(ImageUploader, {
699
+ props: {
700
+ maxImages: 1,
701
+ onUpload,
702
+ enableCrop: true,
703
+ },
704
+ });
705
+
706
+ const dropzone = container.querySelector('.filepond-wrapper-single');
707
+ const file = createMockFile();
708
+ const event = createDragEvent('drop', [file]);
709
+
710
+ fireEvent(dropzone, event);
711
+
712
+ await waitFor(() => {
713
+ expect(global.URL.createObjectURL).toHaveBeenCalledWith(file);
714
+ });
715
+ });
716
+
717
+ it('skips crop modal when enableCrop is false', async () => {
718
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
719
+ const { container } = render(ImageUploader, {
720
+ props: {
721
+ maxImages: 1,
722
+ onUpload,
723
+ enableCrop: false,
724
+ },
725
+ });
726
+
727
+ const dropzone = container.querySelector('.filepond-wrapper-single');
728
+ const file = createMockFile();
729
+ const event = createDragEvent('drop', [file]);
730
+
731
+ fireEvent(dropzone, event);
732
+
733
+ await waitFor(() => {
734
+ expect(onUpload).toHaveBeenCalledWith(file);
735
+ });
736
+
737
+ expect(global.URL.createObjectURL).not.toHaveBeenCalled();
738
+ });
739
+
740
+ it('revokes object URL when crop is cancelled', async () => {
741
+ const { container } = render(ImageUploader, {
742
+ props: {
743
+ maxImages: 1,
744
+ onUpload: vi.fn(),
745
+ enableCrop: true,
746
+ },
747
+ });
748
+
749
+ const dropzone = container.querySelector('.filepond-wrapper-single');
750
+ const file = createMockFile();
751
+ const event = createDragEvent('drop', [file]);
752
+
753
+ fireEvent(dropzone, event);
754
+
755
+ await waitFor(() => {
756
+ expect(global.URL.createObjectURL).toHaveBeenCalled();
757
+ });
758
+
759
+ // Simulate cancel would trigger revokeObjectURL
760
+ // This is handled in the component's handleCropCancel function
761
+ });
762
+ });
763
+
764
+ describe('Reordering functionality', () => {
765
+ it('renders grid with reorderable items when enableReorder is true', () => {
766
+ const { container } = render(ImageUploader, {
767
+ props: {
768
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
769
+ maxImages: 6,
770
+ enableReorder: true,
771
+ },
772
+ });
773
+
774
+ // Should have grid container that Sortable would attach to
775
+ const gridContainer = container.querySelector('.image-uploader-multi > div');
776
+ expect(gridContainer).toBeInTheDocument();
777
+
778
+ // Should have cursor-grab class on filled slots
779
+ const slots = container.querySelectorAll('[class*="cursor-grab"]');
780
+ expect(slots.length).toBeGreaterThan(0);
781
+ });
782
+
783
+ it('renders grid without cursor-grab when enableReorder is false', () => {
784
+ const { container } = render(ImageUploader, {
785
+ props: {
786
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
787
+ maxImages: 6,
788
+ enableReorder: false,
789
+ },
790
+ });
791
+
792
+ // Should not have cursor-grab class
793
+ const slots = container.querySelectorAll('[class*="cursor-grab"]');
794
+ expect(slots).toHaveLength(0);
795
+ });
796
+
797
+ it('cleans up on unmount', () => {
798
+ const { unmount } = render(ImageUploader, {
799
+ props: {
800
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
801
+ maxImages: 6,
802
+ enableReorder: true,
803
+ },
804
+ });
805
+
806
+ // Should not throw when unmounting
807
+ expect(() => unmount()).not.toThrow();
808
+ });
809
+ });
810
+
811
+ describe('Props validation', () => {
812
+ it('normalizes single image string to array', () => {
813
+ const { container } = render(ImageUploader, {
814
+ props: {
815
+ images: 'https://example.com/image.jpg',
816
+ maxImages: 6,
817
+ },
818
+ });
819
+
820
+ const img = container.querySelector('img[alt="Uploaded"]');
821
+ expect(img).toHaveAttribute('src', 'https://example.com/image.jpg');
822
+ });
823
+
824
+ it('filters out falsy values from images array', () => {
825
+ const { container } = render(ImageUploader, {
826
+ props: {
827
+ images: ['https://example.com/1.jpg', null, '', 'https://example.com/2.jpg', undefined],
828
+ maxImages: 6,
829
+ },
830
+ });
831
+
832
+ const images = container.querySelectorAll('img[alt="Uploaded"]');
833
+ expect(images).toHaveLength(2);
834
+ });
835
+
836
+ it('respects maxImages limit', () => {
837
+ const { container } = render(ImageUploader, {
838
+ props: {
839
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
840
+ maxImages: 2,
841
+ },
842
+ });
843
+
844
+ // Should not show empty slot when at max
845
+ const emptySlots = container.querySelectorAll('.border-dashed');
846
+ expect(emptySlots).toHaveLength(0);
847
+ });
848
+
849
+ it('uses default maxImages of 6', () => {
850
+ const { container } = render(ImageUploader, {
851
+ props: {
852
+ images: Array(5).fill('https://example.com/image.jpg'),
853
+ },
854
+ });
855
+
856
+ // Should show empty slot since only 5 out of default 6
857
+ const emptySlot = container.querySelector('.border-dashed');
858
+ expect(emptySlot).toBeInTheDocument();
859
+ });
860
+
861
+ it('uses custom emptyLabel', () => {
862
+ const emptyLabel = 'Drop your awesome photo here';
863
+ const { container } = render(ImageUploader, {
864
+ props: {
865
+ maxImages: 6,
866
+ emptyLabel,
867
+ },
868
+ });
869
+
870
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
871
+ expect(addButton).toBeInTheDocument();
872
+ });
873
+
874
+ it('uses custom acceptedTypes', () => {
875
+ const { container } = render(ImageUploader, {
876
+ props: {
877
+ maxImages: 1,
878
+ acceptedTypes: ['image/png'],
879
+ },
880
+ });
881
+
882
+ // Component should be rendered with custom types
883
+ expect(container.querySelector('.filepond-wrapper-single')).toBeInTheDocument();
884
+ });
885
+
886
+ it('uses custom maxFileSize', () => {
887
+ const { container } = render(ImageUploader, {
888
+ props: {
889
+ maxImages: 1,
890
+ maxFileSize: '5MB',
891
+ },
892
+ });
893
+
894
+ expect(container.querySelector('.filepond-wrapper-single')).toBeInTheDocument();
895
+ });
896
+ });
897
+
898
+ describe('Accessibility', () => {
899
+ it('empty slot has proper aria-label', () => {
900
+ render(ImageUploader, {
901
+ props: {
902
+ images: [],
903
+ maxImages: 6,
904
+ },
905
+ });
906
+
907
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
908
+ expect(addButton).toBeInTheDocument();
909
+ expect(addButton).toHaveAttribute('role', 'button');
910
+ expect(addButton).toHaveAttribute('tabindex', '0');
911
+ });
912
+
913
+ it('remove buttons have proper aria-label', () => {
914
+ render(ImageUploader, {
915
+ props: {
916
+ images: ['https://example.com/1.jpg'],
917
+ maxImages: 6,
918
+ },
919
+ });
920
+
921
+ const removeButton = screen.getByLabelText('Remove photo');
922
+ expect(removeButton).toBeInTheDocument();
923
+ });
924
+
925
+ it('close button in modal has proper aria-label', async () => {
926
+ const user = userEvent.setup();
927
+
928
+ render(ImageUploader, {
929
+ props: {
930
+ images: ['https://example.com/1.jpg'],
931
+ maxImages: 6,
932
+ },
933
+ });
934
+
935
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
936
+ await user.click(addButton);
937
+
938
+ await waitFor(() => {
939
+ const closeButton = screen.getByLabelText('Close');
940
+ expect(closeButton).toBeInTheDocument();
941
+ });
942
+ });
943
+
944
+ it('dropzone is keyboard accessible', async () => {
945
+ const user = userEvent.setup();
946
+
947
+ render(ImageUploader, {
948
+ props: {
949
+ images: [],
950
+ maxImages: 6,
951
+ },
952
+ });
953
+
954
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
955
+
956
+ // Should respond to Enter key
957
+ addButton.focus();
958
+ await user.keyboard('{Enter}');
959
+
960
+ await waitFor(() => {
961
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
962
+ });
963
+ });
964
+
965
+ it('dropzone responds to Space key', async () => {
966
+ const user = userEvent.setup();
967
+
968
+ render(ImageUploader, {
969
+ props: {
970
+ images: [],
971
+ maxImages: 6,
972
+ },
973
+ });
974
+
975
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
976
+
977
+ addButton.focus();
978
+ await user.keyboard('{ }');
979
+
980
+ await waitFor(() => {
981
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
982
+ });
983
+ });
984
+ });
985
+
986
+ describe('Modal functionality', () => {
987
+ it('modal has native dropzone for drag and drop', async () => {
988
+ const user = userEvent.setup();
989
+
990
+ render(ImageUploader, {
991
+ props: {
992
+ images: [],
993
+ maxImages: 6,
994
+ },
995
+ });
996
+
997
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
998
+ await user.click(addButton);
999
+
1000
+ await waitFor(() => {
1001
+ const dropzoneText = screen.getByText('Drag & drop or click to upload');
1002
+ expect(dropzoneText).toBeInTheDocument();
1003
+ });
1004
+ });
1005
+
1006
+ it('modal shows accepted file types', async () => {
1007
+ const user = userEvent.setup();
1008
+
1009
+ render(ImageUploader, {
1010
+ props: {
1011
+ images: [],
1012
+ maxImages: 6,
1013
+ },
1014
+ });
1015
+
1016
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1017
+ await user.click(addButton);
1018
+
1019
+ await waitFor(() => {
1020
+ expect(screen.getByText('JPG, PNG, WebP')).toBeInTheDocument();
1021
+ });
1022
+ });
1023
+
1024
+ it('modal handles file drop', async () => {
1025
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
1026
+ const user = userEvent.setup();
1027
+
1028
+ render(ImageUploader, {
1029
+ props: {
1030
+ images: [],
1031
+ maxImages: 6,
1032
+ onUpload,
1033
+ enableCrop: false,
1034
+ },
1035
+ });
1036
+
1037
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1038
+ await user.click(addButton);
1039
+
1040
+ await waitFor(() => {
1041
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
1042
+ });
1043
+
1044
+ const dropzone = screen.getByText('Drag & drop or click to upload').closest('[role="button"]');
1045
+ const file = createMockFile();
1046
+ const event = createDragEvent('drop', [file]);
1047
+
1048
+ fireEvent(dropzone, event);
1049
+
1050
+ await waitFor(() => {
1051
+ expect(onUpload).toHaveBeenCalledWith(file);
1052
+ });
1053
+ });
1054
+
1055
+ it('modal handles file drop with crop enabled', async () => {
1056
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
1057
+ const user = userEvent.setup();
1058
+
1059
+ render(ImageUploader, {
1060
+ props: {
1061
+ images: [],
1062
+ maxImages: 6,
1063
+ onUpload,
1064
+ enableCrop: true,
1065
+ },
1066
+ });
1067
+
1068
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1069
+ await user.click(addButton);
1070
+
1071
+ await waitFor(() => {
1072
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
1073
+ });
1074
+
1075
+ const dropzone = screen.getByText('Drag & drop or click to upload').closest('[role="button"]');
1076
+ const file = createMockFile();
1077
+ const event = createDragEvent('drop', [file]);
1078
+
1079
+ fireEvent(dropzone, event);
1080
+
1081
+ await waitFor(() => {
1082
+ expect(global.URL.createObjectURL).toHaveBeenCalledWith(file);
1083
+ });
1084
+ });
1085
+
1086
+ it('modal does not propagate click to backdrop', async () => {
1087
+ const user = userEvent.setup();
1088
+
1089
+ render(ImageUploader, {
1090
+ props: {
1091
+ images: [],
1092
+ maxImages: 6,
1093
+ },
1094
+ });
1095
+
1096
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1097
+ await user.click(addButton);
1098
+
1099
+ await waitFor(() => {
1100
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
1101
+ });
1102
+
1103
+ // Click inside modal content
1104
+ const modalContent = screen.getByText('Upload photo').closest('.bg-white, .dark\\:bg-gray-800');
1105
+ await user.click(modalContent);
1106
+
1107
+ // Modal should still be open
1108
+ expect(screen.getByText('Upload photo')).toBeInTheDocument();
1109
+ });
1110
+ });
1111
+
1112
+ describe('Edge cases', () => {
1113
+ it('handles undefined images prop', () => {
1114
+ const { container } = render(ImageUploader, {
1115
+ props: {
1116
+ images: undefined,
1117
+ maxImages: 6,
1118
+ },
1119
+ });
1120
+
1121
+ expect(container.querySelector('.image-uploader-multi')).toBeInTheDocument();
1122
+ });
1123
+
1124
+ it('handles null images prop', () => {
1125
+ const { container } = render(ImageUploader, {
1126
+ props: {
1127
+ images: null,
1128
+ maxImages: 6,
1129
+ },
1130
+ });
1131
+
1132
+ expect(container.querySelector('.image-uploader-multi')).toBeInTheDocument();
1133
+ });
1134
+
1135
+ it('handles empty string image', () => {
1136
+ const { container } = render(ImageUploader, {
1137
+ props: {
1138
+ images: '',
1139
+ maxImages: 6,
1140
+ },
1141
+ });
1142
+
1143
+ // Empty string should be filtered out
1144
+ const images = container.querySelectorAll('img[alt="Uploaded"]');
1145
+ expect(images).toHaveLength(0);
1146
+ });
1147
+
1148
+ it('prevents file drop when at max capacity', () => {
1149
+ const onUpload = vi.fn();
1150
+ const { container } = render(ImageUploader, {
1151
+ props: {
1152
+ images: Array(6).fill('https://example.com/image.jpg'),
1153
+ maxImages: 6,
1154
+ onUpload,
1155
+ },
1156
+ });
1157
+
1158
+ const gridContainer = container.querySelector('.image-uploader-multi');
1159
+ const file = createMockFile();
1160
+ const event = createDragEvent('drop', [file]);
1161
+
1162
+ fireEvent(gridContainer, event);
1163
+
1164
+ expect(onUpload).not.toHaveBeenCalled();
1165
+ });
1166
+
1167
+ it('handles remove in controlled mode with onReorderIndices', async () => {
1168
+ const onRemove = vi.fn();
1169
+ const onReorderIndices = vi.fn();
1170
+ const user = userEvent.setup();
1171
+
1172
+ render(ImageUploader, {
1173
+ props: {
1174
+ images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
1175
+ maxImages: 6,
1176
+ onRemove,
1177
+ onReorderIndices,
1178
+ },
1179
+ });
1180
+
1181
+ const removeButtons = screen.getAllByLabelText('Remove photo');
1182
+ await user.click(removeButtons[0]);
1183
+
1184
+ // Should call onRemove but not update state internally
1185
+ expect(onRemove).toHaveBeenCalledWith(0);
1186
+ });
1187
+
1188
+ it('handles single mode file input change', async () => {
1189
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
1190
+ const { container } = render(ImageUploader, {
1191
+ props: {
1192
+ images: 'https://example.com/existing.jpg',
1193
+ maxImages: 1,
1194
+ onUpload,
1195
+ enableCrop: false,
1196
+ },
1197
+ });
1198
+
1199
+ // Get the hidden file input via container
1200
+ const fileInput = container.querySelector('input[type="file"]');
1201
+ expect(fileInput).toBeInTheDocument();
1202
+
1203
+ // Create a mock file and FileList
1204
+ const file = createMockFile();
1205
+ const fileList = {
1206
+ 0: file,
1207
+ length: 1,
1208
+ item: (index) => (index === 0 ? file : null),
1209
+ };
1210
+
1211
+ // Mock the files property
1212
+ Object.defineProperty(fileInput, 'files', {
1213
+ value: fileList,
1214
+ writable: false,
1215
+ });
1216
+
1217
+ // Trigger change event
1218
+ fireEvent.change(fileInput);
1219
+
1220
+ await waitFor(() => {
1221
+ expect(onUpload).toHaveBeenCalledWith(file);
1222
+ });
1223
+ });
1224
+
1225
+ it('handles single mode file input change with crop enabled', async () => {
1226
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
1227
+ const { container } = render(ImageUploader, {
1228
+ props: {
1229
+ images: 'https://example.com/existing.jpg',
1230
+ maxImages: 1,
1231
+ onUpload,
1232
+ enableCrop: true,
1233
+ },
1234
+ });
1235
+
1236
+ // Get the hidden file input via container
1237
+ const fileInput = container.querySelector('input[type="file"]');
1238
+ expect(fileInput).toBeInTheDocument();
1239
+
1240
+ // Create a mock file and FileList
1241
+ const file = createMockFile();
1242
+ const fileList = {
1243
+ 0: file,
1244
+ length: 1,
1245
+ item: (index) => (index === 0 ? file : null),
1246
+ };
1247
+
1248
+ // Mock the files property
1249
+ Object.defineProperty(fileInput, 'files', {
1250
+ value: fileList,
1251
+ writable: false,
1252
+ });
1253
+
1254
+ // Trigger change event
1255
+ fireEvent.change(fileInput);
1256
+
1257
+ await waitFor(() => {
1258
+ expect(global.URL.createObjectURL).toHaveBeenCalledWith(file);
1259
+ });
1260
+ });
1261
+ });
1262
+
1263
+ describe('Integration scenarios', () => {
1264
+ it('handles complete upload flow in self-managed mode', async () => {
1265
+ const onUpload = vi.fn().mockResolvedValue('https://example.com/uploaded.jpg');
1266
+ const onchange = vi.fn();
1267
+
1268
+ const { container, rerender } = render(ImageUploader, {
1269
+ props: {
1270
+ images: [],
1271
+ maxImages: 6,
1272
+ onUpload,
1273
+ onchange,
1274
+ enableCrop: false,
1275
+ },
1276
+ });
1277
+
1278
+ // Upload first image
1279
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1280
+ const file = createMockFile();
1281
+ const event = createDragEvent('drop', [file]);
1282
+
1283
+ const dropzone = addButton;
1284
+ fireEvent(dropzone, event);
1285
+
1286
+ await waitFor(() => {
1287
+ expect(onUpload).toHaveBeenCalledWith(file);
1288
+ expect(onchange).toHaveBeenCalledWith(['https://example.com/uploaded.jpg']);
1289
+ });
1290
+ });
1291
+
1292
+ it('handles multiple uploads sequentially', async () => {
1293
+ let callCount = 0;
1294
+ const onUpload = vi.fn().mockImplementation(() => {
1295
+ callCount++;
1296
+ return Promise.resolve(`https://example.com/image${callCount}.jpg`);
1297
+ });
1298
+ const onchange = vi.fn();
1299
+
1300
+ render(ImageUploader, {
1301
+ props: {
1302
+ images: [],
1303
+ maxImages: 6,
1304
+ onUpload,
1305
+ onchange,
1306
+ enableCrop: false,
1307
+ },
1308
+ });
1309
+
1310
+ // Upload first image
1311
+ const addButton = screen.getByLabelText('Add photo - drag and drop or click to upload');
1312
+ const file1 = createMockFile('image1.jpg');
1313
+ const event1 = createDragEvent('drop', [file1]);
1314
+ fireEvent(addButton, event1);
1315
+
1316
+ await waitFor(() => {
1317
+ expect(onUpload).toHaveBeenCalledTimes(1);
1318
+ });
1319
+
1320
+ // Upload second image would require re-rendering with updated images
1321
+ // This tests that the component can handle sequential uploads
1322
+ });
1323
+
1324
+ it('shows correct number of slots based on current images', () => {
1325
+ const { container, rerender } = render(ImageUploader, {
1326
+ props: {
1327
+ images: ['https://example.com/1.jpg'],
1328
+ maxImages: 6,
1329
+ },
1330
+ });
1331
+
1332
+ // Should show 1 filled + 1 empty = 2 total slots
1333
+ let slots = container.querySelectorAll('[data-slot-id]');
1334
+ expect(slots).toHaveLength(2);
1335
+
1336
+ // Add more images
1337
+ rerender({
1338
+ images: [
1339
+ 'https://example.com/1.jpg',
1340
+ 'https://example.com/2.jpg',
1341
+ 'https://example.com/3.jpg',
1342
+ ],
1343
+ maxImages: 6,
1344
+ });
1345
+
1346
+ // Should show 3 filled + 1 empty = 4 total slots
1347
+ slots = container.querySelectorAll('[data-slot-id]');
1348
+ expect(slots).toHaveLength(4);
1349
+ });
1350
+ });
1351
+ });