@getmicdrop/svelte-components 5.5.4 → 5.6.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 (512) 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/AppShell.svelte +104 -0
  34. package/dist/components/Layout/AppShell.svelte.d.ts +26 -0
  35. package/dist/components/Layout/AppShell.svelte.d.ts.map +1 -0
  36. package/dist/components/Layout/ContentSection.svelte +80 -0
  37. package/dist/components/Layout/ContentSection.svelte.d.ts +23 -0
  38. package/dist/components/Layout/ContentSection.svelte.d.ts.map +1 -0
  39. package/dist/components/Layout/Grid.svelte +4 -4
  40. package/dist/components/Layout/Heading.svelte +81 -0
  41. package/dist/components/Layout/Heading.svelte.d.ts +24 -0
  42. package/dist/components/Layout/Heading.svelte.d.ts.map +1 -0
  43. package/dist/components/Layout/PageContainer.svelte +69 -0
  44. package/dist/components/Layout/PageContainer.svelte.d.ts +23 -0
  45. package/dist/components/Layout/PageContainer.svelte.d.ts.map +1 -0
  46. package/dist/components/Layout/Responsive.svelte +75 -0
  47. package/dist/components/Layout/Responsive.svelte.d.ts +19 -0
  48. package/dist/components/Layout/Responsive.svelte.d.ts.map +1 -0
  49. package/dist/components/Layout/Section.spec.d.ts +2 -0
  50. package/dist/components/Layout/Section.spec.d.ts.map +1 -0
  51. package/dist/components/Layout/Section.spec.js +149 -0
  52. package/dist/components/Layout/Section.svelte +80 -80
  53. package/dist/components/Layout/ShowOnDesktop.svelte +37 -0
  54. package/dist/components/Layout/ShowOnDesktop.svelte.d.ts +16 -0
  55. package/dist/components/Layout/ShowOnDesktop.svelte.d.ts.map +1 -0
  56. package/dist/components/Layout/ShowOnMobile.svelte +37 -0
  57. package/dist/components/Layout/ShowOnMobile.svelte.d.ts +16 -0
  58. package/dist/components/Layout/ShowOnMobile.svelte.d.ts.map +1 -0
  59. package/dist/components/Layout/Sidebar.spec.d.ts +2 -0
  60. package/dist/components/Layout/Sidebar.spec.d.ts.map +1 -0
  61. package/dist/components/Layout/Sidebar.spec.js +186 -0
  62. package/dist/components/Layout/Sidebar.svelte +108 -108
  63. package/dist/components/Layout/Stack.spec.js +2 -2
  64. package/dist/components/Layout/Stack.svelte +6 -6
  65. package/dist/components/Layout/Text.svelte +87 -0
  66. package/dist/components/Layout/Text.svelte.d.ts +28 -0
  67. package/dist/components/Layout/Text.svelte.d.ts.map +1 -0
  68. package/dist/components/Layout/TwoColumn.svelte +108 -0
  69. package/dist/components/Layout/TwoColumn.svelte.d.ts +28 -0
  70. package/dist/components/Layout/TwoColumn.svelte.d.ts.map +1 -0
  71. package/dist/components/Layout/__tests__/Heading.test.d.ts +2 -0
  72. package/dist/components/Layout/__tests__/Heading.test.d.ts.map +1 -0
  73. package/dist/components/Layout/__tests__/Heading.test.js +123 -0
  74. package/dist/components/Layout/__tests__/ShowOnDesktop.test.d.ts +2 -0
  75. package/dist/components/Layout/__tests__/ShowOnDesktop.test.d.ts.map +1 -0
  76. package/dist/components/Layout/__tests__/ShowOnDesktop.test.js +84 -0
  77. package/dist/components/Layout/__tests__/ShowOnMobile.test.d.ts +2 -0
  78. package/dist/components/Layout/__tests__/ShowOnMobile.test.d.ts.map +1 -0
  79. package/dist/components/Layout/__tests__/ShowOnMobile.test.js +80 -0
  80. package/dist/components/Layout/__tests__/Text.test.d.ts +2 -0
  81. package/dist/components/Layout/__tests__/Text.test.d.ts.map +1 -0
  82. package/dist/components/Layout/__tests__/Text.test.js +146 -0
  83. package/dist/components/Layout/__tests__/TwoColumn.test.d.ts +2 -0
  84. package/dist/components/Layout/__tests__/TwoColumn.test.d.ts.map +1 -0
  85. package/dist/components/Layout/__tests__/TwoColumn.test.js +129 -0
  86. package/dist/constants/formOptions.spec.js +9 -5
  87. package/dist/constants/validation.js +91 -91
  88. package/dist/constants/validation.spec.js +64 -64
  89. package/dist/datetime/__tests__/timezone.test.js +123 -1
  90. package/dist/forms/createFieldTracker.spec.d.ts +2 -0
  91. package/dist/forms/createFieldTracker.spec.d.ts.map +1 -0
  92. package/dist/forms/createFieldTracker.spec.js +343 -0
  93. package/dist/forms/createFormStore.spec.d.ts +2 -0
  94. package/dist/forms/createFormStore.spec.d.ts.map +1 -0
  95. package/dist/forms/createFormStore.spec.js +689 -0
  96. package/dist/index.d.ts +4 -112
  97. package/dist/index.js +40 -226
  98. package/dist/patterns/data/DataGrid.spec.d.ts +2 -0
  99. package/dist/patterns/data/DataGrid.spec.d.ts.map +1 -0
  100. package/dist/patterns/data/DataGrid.spec.js +159 -0
  101. package/dist/patterns/data/DataGrid.svelte +45 -45
  102. package/dist/patterns/data/DataList.spec.d.ts +2 -0
  103. package/dist/patterns/data/DataList.spec.d.ts.map +1 -0
  104. package/dist/patterns/data/DataList.spec.js +158 -0
  105. package/dist/patterns/data/DataList.svelte +24 -24
  106. package/dist/patterns/data/DataTable.spec.d.ts +2 -0
  107. package/dist/patterns/data/DataTable.spec.d.ts.map +1 -0
  108. package/dist/patterns/data/DataTable.spec.js +196 -0
  109. package/dist/patterns/data/DataTable.svelte +36 -36
  110. package/dist/patterns/forms/FormActions.spec.js +95 -88
  111. package/dist/patterns/forms/FormActions.stories.svelte +97 -97
  112. package/dist/patterns/forms/FormActions.svelte +46 -46
  113. package/dist/patterns/forms/FormGrid.spec.d.ts +2 -0
  114. package/dist/patterns/forms/FormGrid.spec.d.ts.map +1 -0
  115. package/dist/patterns/forms/FormGrid.spec.js +125 -0
  116. package/dist/patterns/forms/FormGrid.svelte +33 -33
  117. package/dist/patterns/forms/FormSection.spec.d.ts +2 -0
  118. package/dist/patterns/forms/FormSection.spec.d.ts.map +1 -0
  119. package/dist/patterns/forms/FormSection.spec.js +153 -0
  120. package/dist/patterns/forms/FormSection.svelte +32 -32
  121. package/dist/patterns/forms/FormValidationSummary.stories.svelte +83 -83
  122. package/dist/patterns/forms/FormValidationSummary.svelte +74 -74
  123. package/dist/patterns/layout/Sidebar.spec.d.ts +2 -0
  124. package/dist/patterns/layout/Sidebar.spec.d.ts.map +1 -0
  125. package/dist/patterns/layout/Sidebar.spec.js +159 -0
  126. package/dist/patterns/layout/Sidebar.svelte +39 -39
  127. package/dist/patterns/layout/index.d.ts +9 -0
  128. package/dist/patterns/layout/index.js +22 -0
  129. package/dist/patterns/navigation/BottomNav.stories.svelte +117 -117
  130. package/dist/patterns/navigation/BottomNav.svelte +64 -64
  131. package/dist/patterns/navigation/Header.spec.js +33 -24
  132. package/dist/patterns/navigation/Header.stories.svelte +77 -77
  133. package/dist/patterns/navigation/Header.svelte +193 -193
  134. package/dist/patterns/page/PageHeader.spec.d.ts +2 -0
  135. package/dist/patterns/page/PageHeader.spec.d.ts.map +1 -0
  136. package/dist/patterns/page/PageHeader.spec.js +167 -0
  137. package/dist/patterns/page/PageHeader.svelte +18 -18
  138. package/dist/patterns/page/PageLayout.spec.d.ts +2 -0
  139. package/dist/patterns/page/PageLayout.spec.d.ts.map +1 -0
  140. package/dist/patterns/page/PageLayout.spec.js +145 -0
  141. package/dist/patterns/page/PageLayout.svelte +40 -40
  142. package/dist/patterns/page/PageLoader.spec.js +57 -54
  143. package/dist/patterns/page/PageLoader.stories.svelte +137 -137
  144. package/dist/patterns/page/PageLoader.svelte +24 -24
  145. package/dist/patterns/page/SectionHeader.spec.d.ts +2 -0
  146. package/dist/patterns/page/SectionHeader.spec.d.ts.map +1 -0
  147. package/dist/patterns/page/SectionHeader.spec.js +197 -0
  148. package/dist/patterns/page/SectionHeader.svelte +29 -29
  149. package/dist/presets/badges.js +112 -112
  150. package/dist/presets/badges.spec.d.ts +2 -0
  151. package/dist/presets/badges.spec.d.ts.map +1 -0
  152. package/dist/presets/badges.spec.js +172 -0
  153. package/dist/presets/buttons.js +76 -76
  154. package/dist/presets/buttons.spec.d.ts +2 -0
  155. package/dist/presets/buttons.spec.d.ts.map +1 -0
  156. package/dist/presets/buttons.spec.js +135 -0
  157. package/dist/presets/index.js +9 -9
  158. package/dist/primitives/Accordion/Accordion.spec.d.ts +2 -0
  159. package/dist/primitives/Accordion/Accordion.spec.d.ts.map +1 -0
  160. package/dist/primitives/Accordion/Accordion.spec.js +83 -0
  161. package/dist/primitives/Accordion/Accordion.stories.svelte +75 -75
  162. package/dist/primitives/Accordion/Accordion.svelte +42 -42
  163. package/dist/primitives/Accordion/AccordionItem.spec.d.ts +2 -0
  164. package/dist/primitives/Accordion/AccordionItem.spec.d.ts.map +1 -0
  165. package/dist/primitives/Accordion/AccordionItem.spec.js +661 -0
  166. package/dist/primitives/Accordion/AccordionItem.svelte +95 -95
  167. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte +107 -0
  168. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte.d.ts +35 -0
  169. package/dist/primitives/Accordion/AccordionItemWrapper.test.svelte.d.ts.map +1 -0
  170. package/dist/primitives/Alert/Alert.spec.js +173 -170
  171. package/dist/primitives/Alert/Alert.stories.svelte +88 -88
  172. package/dist/primitives/Alert/Alert.svelte +27 -27
  173. package/dist/primitives/Avatar/Avatar.spec.d.ts +2 -0
  174. package/dist/primitives/Avatar/Avatar.spec.d.ts.map +1 -0
  175. package/dist/primitives/Avatar/Avatar.spec.js +211 -0
  176. package/dist/primitives/Avatar/Avatar.stories.svelte +94 -94
  177. package/dist/primitives/Avatar/Avatar.svelte +66 -66
  178. package/dist/primitives/Badges/Badge.spec.js +144 -103
  179. package/dist/primitives/Badges/Badge.stories.svelte +86 -86
  180. package/dist/primitives/Badges/Badge.svelte +79 -79
  181. package/dist/primitives/BottomSheet/BottomSheet.spec.js +136 -127
  182. package/dist/primitives/BottomSheet/BottomSheet.stories.svelte +83 -83
  183. package/dist/primitives/BottomSheet/BottomSheet.svelte +100 -100
  184. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte +13 -0
  185. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte.d.ts +7 -0
  186. package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte.d.ts.map +1 -0
  187. package/dist/primitives/Breadcrumb/Breadcrumb.spec.js +122 -120
  188. package/dist/primitives/Breadcrumb/Breadcrumb.stories.svelte +23 -23
  189. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +89 -89
  190. package/dist/primitives/Button/Button.spec.js +223 -211
  191. package/dist/primitives/Button/Button.stories.svelte +76 -76
  192. package/dist/primitives/Button/Button.svelte +270 -270
  193. package/dist/primitives/Button/ButtonSaveDemo.spec.js +146 -48
  194. package/dist/primitives/Button/ButtonSaveDemo.svelte +25 -25
  195. package/dist/primitives/Button/ButtonVariantShowcase.spec.d.ts +2 -0
  196. package/dist/primitives/Button/ButtonVariantShowcase.spec.d.ts.map +1 -0
  197. package/dist/primitives/Button/ButtonVariantShowcase.spec.js +202 -0
  198. package/dist/primitives/Button/ButtonVariantShowcase.svelte +129 -129
  199. package/dist/primitives/Card.spec.js +49 -49
  200. package/dist/primitives/Card.stories.svelte +22 -22
  201. package/dist/primitives/Card.svelte +28 -28
  202. package/dist/primitives/Checkbox/Checkbox.spec.d.ts +2 -0
  203. package/dist/primitives/Checkbox/Checkbox.spec.d.ts.map +1 -0
  204. package/dist/primitives/Checkbox/Checkbox.spec.js +252 -0
  205. package/dist/primitives/Checkbox/Checkbox.stories.svelte +84 -84
  206. package/dist/primitives/Checkbox/Checkbox.svelte +88 -88
  207. package/dist/primitives/DarkModeToggle.spec.js +390 -357
  208. package/dist/primitives/DarkModeToggle.stories.svelte +57 -57
  209. package/dist/primitives/DarkModeToggle.svelte +136 -136
  210. package/dist/primitives/Drawer/Drawer.spec.d.ts +2 -0
  211. package/dist/primitives/Drawer/Drawer.spec.d.ts.map +1 -0
  212. package/dist/primitives/Drawer/Drawer.spec.js +212 -0
  213. package/dist/primitives/Drawer/Drawer.stories.svelte +80 -80
  214. package/dist/primitives/Drawer/Drawer.svelte +120 -120
  215. package/dist/primitives/Dropdown/Dropdown.spec.d.ts +2 -0
  216. package/dist/primitives/Dropdown/Dropdown.spec.d.ts.map +1 -0
  217. package/dist/primitives/Dropdown/Dropdown.spec.js +366 -0
  218. package/dist/primitives/Dropdown/Dropdown.stories.svelte +137 -137
  219. package/dist/primitives/Dropdown/Dropdown.svelte +14 -14
  220. package/dist/primitives/Dropdown/DropdownItem.spec.d.ts +2 -0
  221. package/dist/primitives/Dropdown/DropdownItem.spec.d.ts.map +1 -0
  222. package/dist/primitives/Dropdown/DropdownItem.spec.js +182 -0
  223. package/dist/primitives/Dropdown/DropdownItem.svelte +80 -80
  224. package/dist/primitives/Icons/ArrowLeft.svelte +8 -8
  225. package/dist/primitives/Icons/ArrowRight.svelte +8 -8
  226. package/dist/primitives/Icons/Availability.svelte +14 -14
  227. package/dist/primitives/Icons/Back.svelte +14 -14
  228. package/dist/primitives/Icons/CheckCircle.svelte +6 -6
  229. package/dist/primitives/Icons/CheckCircleOutline.svelte +15 -15
  230. package/dist/primitives/Icons/ChevronLeft.svelte +4 -4
  231. package/dist/primitives/Icons/ChevronRight.svelte +4 -4
  232. package/dist/primitives/Icons/Copy.svelte +15 -15
  233. package/dist/primitives/Icons/Cross.svelte +5 -5
  234. package/dist/primitives/Icons/DownArrow.svelte +8 -8
  235. package/dist/primitives/Icons/ErrorCircle.svelte +6 -6
  236. package/dist/primitives/Icons/FacebookIcon.svelte +2 -2
  237. package/dist/primitives/Icons/Home.svelte +15 -15
  238. package/dist/primitives/Icons/Icon.spec.js +169 -169
  239. package/dist/primitives/Icons/Icon.stories.svelte +100 -100
  240. package/dist/primitives/Icons/Icon.svelte +52 -52
  241. package/dist/primitives/Icons/IconGallery.stories.svelte +235 -235
  242. package/dist/primitives/Icons/Info.svelte +7 -7
  243. package/dist/primitives/Icons/InstagramIcon.svelte +4 -4
  244. package/dist/primitives/Icons/LogoInstagram.svelte +2 -2
  245. package/dist/primitives/Icons/Message.svelte +15 -15
  246. package/dist/primitives/Icons/MoonIcon.svelte +5 -5
  247. package/dist/primitives/Icons/More.svelte +21 -21
  248. package/dist/primitives/Icons/MoreHori.spec.js +61 -61
  249. package/dist/primitives/Icons/MoreHori.svelte +22 -22
  250. package/dist/primitives/Icons/Notification.svelte +14 -14
  251. package/dist/primitives/Icons/Payment.svelte +14 -14
  252. package/dist/primitives/Icons/Profile.svelte +21 -21
  253. package/dist/primitives/Icons/Reload.svelte +29 -29
  254. package/dist/primitives/Icons/Shows.svelte +21 -21
  255. package/dist/primitives/Icons/Signout.svelte +21 -21
  256. package/dist/primitives/Icons/SunIcon.svelte +8 -8
  257. package/dist/primitives/Icons/TiktokIcon.svelte +2 -2
  258. package/dist/primitives/Icons/TwitterIcon.svelte +2 -2
  259. package/dist/primitives/Icons/WarningIcon.spec.js +18 -18
  260. package/dist/primitives/Icons/WarningIcon.svelte +5 -5
  261. package/dist/primitives/Icons/iconTestUtils.spec.d.ts +2 -0
  262. package/dist/primitives/Icons/iconTestUtils.spec.d.ts.map +1 -0
  263. package/dist/primitives/Icons/iconTestUtils.spec.js +235 -0
  264. package/dist/primitives/Input/Input.spec.js +573 -573
  265. package/dist/primitives/Input/Input.stories.svelte +139 -139
  266. package/dist/primitives/Input/Input.svelte +418 -431
  267. package/dist/primitives/Input/Input.svelte.d.ts.map +1 -1
  268. package/dist/primitives/Input/Select.spec.js +212 -218
  269. package/dist/primitives/Input/Select.stories.svelte +112 -112
  270. package/dist/primitives/Input/Select.svelte +128 -128
  271. package/dist/primitives/Input/Textarea.spec.d.ts +2 -0
  272. package/dist/primitives/Input/Textarea.spec.d.ts.map +1 -0
  273. package/dist/primitives/Input/Textarea.spec.js +255 -0
  274. package/dist/primitives/Input/Textarea.stories.svelte +137 -137
  275. package/dist/primitives/Input/Textarea.svelte +35 -35
  276. package/dist/primitives/Label/Label.spec.d.ts +2 -0
  277. package/dist/primitives/Label/Label.spec.d.ts.map +1 -0
  278. package/dist/primitives/Label/Label.spec.js +157 -0
  279. package/dist/primitives/Label/Label.svelte +37 -37
  280. package/dist/primitives/Modal/Modal.spec.js +99 -95
  281. package/dist/primitives/Modal/Modal.stories.svelte +86 -86
  282. package/dist/primitives/Modal/Modal.svelte +158 -158
  283. package/dist/primitives/Modal/ModalTestWrapper.svelte +65 -0
  284. package/dist/primitives/Modal/ModalTestWrapper.svelte.d.ts +23 -0
  285. package/dist/primitives/Modal/ModalTestWrapper.svelte.d.ts.map +1 -0
  286. package/dist/primitives/NumberInput/NumberInput.spec.d.ts +2 -0
  287. package/dist/primitives/NumberInput/NumberInput.spec.d.ts.map +1 -0
  288. package/dist/primitives/NumberInput/NumberInput.spec.js +235 -0
  289. package/dist/primitives/NumberInput/NumberInput.svelte +106 -106
  290. package/dist/primitives/Pagination/Pagination.spec.d.ts +2 -0
  291. package/dist/primitives/Pagination/Pagination.spec.d.ts.map +1 -0
  292. package/dist/primitives/Pagination/Pagination.spec.js +266 -0
  293. package/dist/primitives/Pagination/Pagination.stories.svelte +76 -76
  294. package/dist/primitives/Pagination/Pagination.svelte +261 -261
  295. package/dist/primitives/Radio/Radio.spec.d.ts +2 -0
  296. package/dist/primitives/Radio/Radio.spec.d.ts.map +1 -0
  297. package/dist/primitives/Radio/Radio.spec.js +206 -0
  298. package/dist/primitives/Radio/Radio.stories.svelte +80 -80
  299. package/dist/primitives/Radio/Radio.svelte +67 -67
  300. package/dist/primitives/Skeleton/CardPlaceholder.spec.d.ts +2 -0
  301. package/dist/primitives/Skeleton/CardPlaceholder.spec.d.ts.map +1 -0
  302. package/dist/primitives/Skeleton/CardPlaceholder.spec.js +156 -0
  303. package/dist/primitives/Skeleton/CardPlaceholder.svelte +87 -87
  304. package/dist/primitives/Skeleton/ImagePlaceholder.spec.d.ts +2 -0
  305. package/dist/primitives/Skeleton/ImagePlaceholder.spec.d.ts.map +1 -0
  306. package/dist/primitives/Skeleton/ImagePlaceholder.spec.js +120 -0
  307. package/dist/primitives/Skeleton/ImagePlaceholder.svelte +59 -59
  308. package/dist/primitives/Skeleton/ListPlaceholder.spec.d.ts +2 -0
  309. package/dist/primitives/Skeleton/ListPlaceholder.spec.d.ts.map +1 -0
  310. package/dist/primitives/Skeleton/ListPlaceholder.spec.js +220 -0
  311. package/dist/primitives/Skeleton/ListPlaceholder.svelte +76 -76
  312. package/dist/primitives/Skeleton/Skeleton.spec.d.ts +2 -0
  313. package/dist/primitives/Skeleton/Skeleton.spec.d.ts.map +1 -0
  314. package/dist/primitives/Skeleton/Skeleton.spec.js +173 -0
  315. package/dist/primitives/Skeleton/Skeleton.stories.svelte +151 -151
  316. package/dist/primitives/Skeleton/Skeleton.svelte +26 -26
  317. package/dist/primitives/Spinner/Spinner.spec.js +71 -75
  318. package/dist/primitives/Spinner/Spinner.stories.svelte +29 -29
  319. package/dist/primitives/Spinner/Spinner.svelte +20 -20
  320. package/dist/primitives/Tabs/TabItem.spec.d.ts +2 -0
  321. package/dist/primitives/Tabs/TabItem.spec.d.ts.map +1 -0
  322. package/dist/primitives/Tabs/TabItem.spec.js +130 -0
  323. package/dist/primitives/Tabs/TabItem.svelte +49 -49
  324. package/dist/primitives/Tabs/Tabs.spec.d.ts +2 -0
  325. package/dist/primitives/Tabs/Tabs.spec.d.ts.map +1 -0
  326. package/dist/primitives/Tabs/Tabs.spec.js +295 -0
  327. package/dist/primitives/Tabs/Tabs.stories.svelte +112 -112
  328. package/dist/primitives/Tabs/Tabs.svelte +123 -123
  329. package/dist/primitives/Tabs/TabsWithItems.test.svelte +18 -0
  330. package/dist/primitives/Tabs/TabsWithItems.test.svelte.d.ts +16 -0
  331. package/dist/primitives/Tabs/TabsWithItems.test.svelte.d.ts.map +1 -0
  332. package/dist/primitives/Toggle.spec.js +143 -127
  333. package/dist/primitives/Toggle.stories.svelte +92 -92
  334. package/dist/primitives/Toggle.svelte +71 -71
  335. package/dist/primitives/Typography/Typography.spec.d.ts +2 -0
  336. package/dist/primitives/Typography/Typography.spec.d.ts.map +1 -0
  337. package/dist/primitives/Typography/Typography.spec.js +183 -0
  338. package/dist/primitives/Typography/Typography.svelte +53 -53
  339. package/dist/primitives/ValidationError.spec.js +103 -103
  340. package/dist/primitives/ValidationError.stories.svelte +69 -69
  341. package/dist/primitives/ValidationError.svelte +29 -29
  342. package/dist/primitives/index.d.ts +1 -0
  343. package/dist/primitives/index.js +84 -81
  344. package/dist/recipes/CropImage/CropImage.spec.js +208 -216
  345. package/dist/recipes/CropImage/CropImage.stories.svelte +104 -104
  346. package/dist/recipes/CropImage/CropImage.svelte +238 -238
  347. package/dist/recipes/ImageUploader/ImageUploader.spec.d.ts +2 -0
  348. package/dist/recipes/ImageUploader/ImageUploader.spec.d.ts.map +1 -0
  349. package/dist/recipes/ImageUploader/ImageUploader.spec.js +1351 -0
  350. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte +125 -125
  351. package/dist/recipes/ImageUploader/ImageUploader.svelte +804 -804
  352. package/dist/recipes/SuperLogin/SuperLogin.spec.d.ts +2 -0
  353. package/dist/recipes/SuperLogin/SuperLogin.spec.d.ts.map +1 -0
  354. package/dist/recipes/SuperLogin/SuperLogin.spec.js +1436 -0
  355. package/dist/recipes/Toaster/Toaster.stories.svelte +62 -62
  356. package/dist/recipes/feedback/EmptyState/EmptyState.spec.d.ts +2 -0
  357. package/dist/recipes/feedback/EmptyState/EmptyState.spec.d.ts.map +1 -0
  358. package/dist/recipes/feedback/EmptyState/EmptyState.spec.js +202 -0
  359. package/dist/recipes/feedback/EmptyState/EmptyState.svelte +1 -1
  360. package/dist/recipes/feedback/ErrorDisplay.spec.js +69 -69
  361. package/dist/recipes/feedback/ErrorDisplay.stories.svelte +101 -101
  362. package/dist/recipes/feedback/ErrorDisplay.svelte +1 -1
  363. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.spec.js +133 -129
  364. package/dist/recipes/feedback/StatusIndicator/StatusIndicator.svelte +157 -157
  365. package/dist/recipes/fields/CheckboxField.spec.d.ts +2 -0
  366. package/dist/recipes/fields/CheckboxField.spec.d.ts.map +1 -0
  367. package/dist/recipes/fields/CheckboxField.spec.js +135 -0
  368. package/dist/recipes/fields/CheckboxField.svelte +85 -85
  369. package/dist/recipes/fields/FormField.spec.d.ts +2 -0
  370. package/dist/recipes/fields/FormField.spec.d.ts.map +1 -0
  371. package/dist/recipes/fields/FormField.spec.js +159 -0
  372. package/dist/recipes/fields/FormField.svelte +58 -58
  373. package/dist/recipes/fields/RadioGroup.spec.d.ts +2 -0
  374. package/dist/recipes/fields/RadioGroup.spec.d.ts.map +1 -0
  375. package/dist/recipes/fields/RadioGroup.spec.js +199 -0
  376. package/dist/recipes/fields/RadioGroup.svelte +95 -95
  377. package/dist/recipes/fields/SelectField.spec.d.ts +2 -0
  378. package/dist/recipes/fields/SelectField.spec.d.ts.map +1 -0
  379. package/dist/recipes/fields/SelectField.spec.js +188 -0
  380. package/dist/recipes/fields/SelectField.svelte +80 -80
  381. package/dist/recipes/fields/TextareaField.spec.d.ts +2 -0
  382. package/dist/recipes/fields/TextareaField.spec.d.ts.map +1 -0
  383. package/dist/recipes/fields/TextareaField.spec.js +205 -0
  384. package/dist/recipes/fields/TextareaField.svelte +97 -97
  385. package/dist/recipes/fields/ToggleField.spec.d.ts +2 -0
  386. package/dist/recipes/fields/ToggleField.spec.d.ts.map +1 -0
  387. package/dist/recipes/fields/ToggleField.spec.js +153 -0
  388. package/dist/recipes/fields/ToggleField.svelte +60 -60
  389. package/dist/recipes/fields/index.js +7 -7
  390. package/dist/recipes/inputs/MultiSelect.spec.js +258 -257
  391. package/dist/recipes/inputs/MultiSelect.stories.svelte +133 -133
  392. package/dist/recipes/inputs/MultiSelect.svelte +256 -249
  393. package/dist/recipes/inputs/MultiSelect.svelte.d.ts +2 -0
  394. package/dist/recipes/inputs/MultiSelect.svelte.d.ts.map +1 -1
  395. package/dist/recipes/inputs/OTPInput.spec.js +251 -238
  396. package/dist/recipes/inputs/OTPInput.stories.svelte +162 -162
  397. package/dist/recipes/inputs/OTPInput.svelte +29 -29
  398. package/dist/recipes/inputs/PasswordInput.spec.d.ts +2 -0
  399. package/dist/recipes/inputs/PasswordInput.spec.d.ts.map +1 -0
  400. package/dist/recipes/inputs/PasswordInput.spec.js +410 -0
  401. package/dist/recipes/inputs/PasswordInput.svelte +22 -22
  402. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.spec.js +253 -173
  403. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte +117 -117
  404. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte +71 -0
  405. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte.d.ts +9 -0
  406. package/dist/recipes/inputs/PasswordStrengthIndicator/TestWrapper.svelte.d.ts.map +1 -0
  407. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.spec.js +1246 -300
  408. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.stories.svelte +123 -123
  409. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte +326 -326
  410. package/dist/recipes/inputs/Search.spec.d.ts +2 -0
  411. package/dist/recipes/inputs/Search.spec.d.ts.map +1 -0
  412. package/dist/recipes/inputs/Search.spec.js +177 -0
  413. package/dist/recipes/inputs/Search.svelte +37 -37
  414. package/dist/recipes/inputs/SelectDropdown.spec.d.ts +2 -0
  415. package/dist/recipes/inputs/SelectDropdown.spec.d.ts.map +1 -0
  416. package/dist/recipes/inputs/SelectDropdown.spec.js +512 -0
  417. package/dist/recipes/inputs/SelectDropdown.svelte +57 -57
  418. package/dist/recipes/modals/AlertModal.spec.d.ts +2 -0
  419. package/dist/recipes/modals/AlertModal.spec.d.ts.map +1 -0
  420. package/dist/recipes/modals/AlertModal.spec.js +432 -0
  421. package/dist/recipes/modals/AlertModal.svelte +130 -130
  422. package/dist/recipes/modals/ConfirmationModal.spec.js +206 -191
  423. package/dist/recipes/modals/ConfirmationModal.stories.svelte +119 -119
  424. package/dist/recipes/modals/ConfirmationModal.svelte +152 -152
  425. package/dist/recipes/modals/InputModal.spec.d.ts +2 -0
  426. package/dist/recipes/modals/InputModal.spec.d.ts.map +1 -0
  427. package/dist/recipes/modals/InputModal.spec.js +872 -0
  428. package/dist/recipes/modals/InputModal.svelte +182 -182
  429. package/dist/recipes/modals/ModalStateManager.spec.js +100 -100
  430. package/dist/recipes/modals/ModalStateManager.svelte +77 -77
  431. package/dist/recipes/modals/ModalTestWrapper.spec.d.ts +2 -0
  432. package/dist/recipes/modals/ModalTestWrapper.spec.d.ts.map +1 -0
  433. package/dist/recipes/modals/ModalTestWrapper.spec.js +502 -0
  434. package/dist/recipes/modals/ModalTestWrapper.svelte +65 -65
  435. package/dist/recipes/modals/StatusModal.spec.d.ts +2 -0
  436. package/dist/recipes/modals/StatusModal.spec.d.ts.map +1 -0
  437. package/dist/recipes/modals/StatusModal.spec.js +599 -0
  438. package/dist/recipes/modals/StatusModal.svelte +206 -206
  439. package/dist/services/EventService.js +75 -75
  440. package/dist/services/EventService.spec.js +217 -217
  441. package/dist/services/ShowService.spec.js +345 -342
  442. package/dist/stores/auth.js +36 -36
  443. package/dist/stores/auth.spec.js +139 -139
  444. package/dist/stores/toaster.js +13 -13
  445. package/dist/stories/ButtonAuditDashboard.spec.d.ts +2 -0
  446. package/dist/stories/ButtonAuditDashboard.spec.d.ts.map +1 -0
  447. package/dist/stories/ButtonAuditDashboard.spec.js +913 -0
  448. package/dist/stories/ButtonAuditReview.spec.d.ts +2 -0
  449. package/dist/stories/ButtonAuditReview.spec.d.ts.map +1 -0
  450. package/dist/stories/ButtonAuditReview.spec.js +422 -0
  451. package/dist/stories/ButtonAuditReview.stories.svelte +14 -14
  452. package/dist/stories/ButtonAuditReview.svelte +427 -427
  453. package/dist/stories/ButtonGridView.spec.d.ts +2 -0
  454. package/dist/stories/ButtonGridView.spec.d.ts.map +1 -0
  455. package/dist/stories/ButtonGridView.spec.js +667 -0
  456. package/dist/stories/ButtonShowcase.spec.d.ts +2 -0
  457. package/dist/stories/ButtonShowcase.spec.d.ts.map +1 -0
  458. package/dist/stories/ButtonShowcase.spec.js +499 -0
  459. package/dist/stories/PatternsGallery.spec.d.ts +2 -0
  460. package/dist/stories/PatternsGallery.spec.d.ts.map +1 -0
  461. package/dist/stories/PatternsGallery.spec.js +514 -0
  462. package/dist/stories/PatternsGallery.stories.svelte +19 -19
  463. package/dist/stories/PatternsGallery.svelte +206 -206
  464. package/dist/stories/PrimitivesGallery.spec.d.ts +2 -0
  465. package/dist/stories/PrimitivesGallery.spec.d.ts.map +1 -0
  466. package/dist/stories/PrimitivesGallery.spec.js +813 -0
  467. package/dist/stories/PrimitivesGallery.stories.svelte +19 -19
  468. package/dist/stories/PrimitivesGallery.svelte +725 -725
  469. package/dist/stories/RecipesGallery.spec.d.ts +2 -0
  470. package/dist/stories/RecipesGallery.spec.d.ts.map +1 -0
  471. package/dist/stories/RecipesGallery.spec.js +299 -0
  472. package/dist/stories/RecipesGallery.stories.svelte +19 -19
  473. package/dist/stories/RecipesGallery.svelte +271 -271
  474. package/dist/stories/button-audit-manifest.json +11186 -11186
  475. package/dist/stripe/useStripeTheme.spec.d.ts +2 -0
  476. package/dist/stripe/useStripeTheme.spec.d.ts.map +1 -0
  477. package/dist/stripe/useStripeTheme.spec.js +793 -0
  478. package/dist/tailwind/preset.cjs +82 -82
  479. package/dist/telemetry.d.ts.map +1 -1
  480. package/dist/telemetry.js +405 -404
  481. package/dist/telemetry.spec.js +1144 -661
  482. package/dist/tokens/__tests__/colors.test.d.ts +2 -0
  483. package/dist/tokens/__tests__/colors.test.d.ts.map +1 -0
  484. package/dist/tokens/__tests__/colors.test.js +152 -0
  485. package/dist/tokens/__tests__/radius.test.d.ts +2 -0
  486. package/dist/tokens/__tests__/radius.test.d.ts.map +1 -0
  487. package/dist/tokens/__tests__/radius.test.js +118 -0
  488. package/dist/tokens/__tests__/shadows.test.d.ts +2 -0
  489. package/dist/tokens/__tests__/shadows.test.d.ts.map +1 -0
  490. package/dist/tokens/__tests__/shadows.test.js +105 -0
  491. package/dist/tokens/__tests__/spacing.test.js +11 -8
  492. package/dist/tokens/__tests__/typography-base.test.d.ts +2 -0
  493. package/dist/tokens/__tests__/typography-base.test.d.ts.map +1 -0
  494. package/dist/tokens/__tests__/typography-base.test.js +138 -0
  495. package/dist/tokens/__tests__/typography.test.d.ts +2 -0
  496. package/dist/tokens/__tests__/typography.test.d.ts.map +1 -0
  497. package/dist/tokens/__tests__/typography.test.js +156 -0
  498. package/dist/tokens/__tests__/z-index.test.d.ts +2 -0
  499. package/dist/tokens/__tests__/z-index.test.d.ts.map +1 -0
  500. package/dist/tokens/__tests__/z-index.test.js +121 -0
  501. package/dist/tokens/tokens.css +87 -87
  502. package/dist/tokens/typography-base.css +163 -0
  503. package/dist/utils/apiConfig.spec.js +219 -118
  504. package/dist/utils/formatters.spec.d.ts +2 -0
  505. package/dist/utils/formatters.spec.d.ts.map +1 -0
  506. package/dist/utils/formatters.spec.js +82 -0
  507. package/dist/utils/transitions.js +62 -62
  508. package/dist/utils/transitions.spec.d.ts +2 -0
  509. package/dist/utils/transitions.spec.d.ts.map +1 -0
  510. package/dist/utils/transitions.spec.js +130 -0
  511. package/dist/utils/utils.js +354 -354
  512. package/package.json +292 -286
@@ -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
+ });