@furystack/shades-common-components 10.0.35 → 11.0.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 (295) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/esm/components/animations.spec.d.ts +2 -0
  3. package/esm/components/animations.spec.d.ts.map +1 -0
  4. package/esm/components/animations.spec.js +201 -0
  5. package/esm/components/animations.spec.js.map +1 -0
  6. package/esm/components/app-bar-link.js +21 -20
  7. package/esm/components/app-bar-link.js.map +1 -1
  8. package/esm/components/app-bar-link.spec.d.ts +2 -0
  9. package/esm/components/app-bar-link.spec.d.ts.map +1 -0
  10. package/esm/components/app-bar-link.spec.js +252 -0
  11. package/esm/components/app-bar-link.spec.js.map +1 -0
  12. package/esm/components/app-bar.js +21 -21
  13. package/esm/components/app-bar.js.map +1 -1
  14. package/esm/components/app-bar.spec.d.ts +2 -0
  15. package/esm/components/app-bar.spec.d.ts.map +1 -0
  16. package/esm/components/app-bar.spec.js +117 -0
  17. package/esm/components/app-bar.spec.js.map +1 -0
  18. package/esm/components/avatar.d.ts.map +1 -1
  19. package/esm/components/avatar.js +15 -19
  20. package/esm/components/avatar.js.map +1 -1
  21. package/esm/components/avatar.spec.d.ts +2 -0
  22. package/esm/components/avatar.spec.d.ts.map +1 -0
  23. package/esm/components/avatar.spec.js +114 -0
  24. package/esm/components/avatar.spec.js.map +1 -0
  25. package/esm/components/button.d.ts.map +1 -1
  26. package/esm/components/button.js +145 -156
  27. package/esm/components/button.js.map +1 -1
  28. package/esm/components/button.spec.d.ts +2 -0
  29. package/esm/components/button.spec.d.ts.map +1 -0
  30. package/esm/components/button.spec.js +155 -0
  31. package/esm/components/button.spec.js.map +1 -0
  32. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  33. package/esm/components/command-palette/command-palette-input.js +18 -16
  34. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  35. package/esm/components/command-palette/command-palette-input.spec.d.ts +2 -0
  36. package/esm/components/command-palette/command-palette-input.spec.d.ts.map +1 -0
  37. package/esm/components/command-palette/command-palette-input.spec.js +233 -0
  38. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -0
  39. package/esm/components/command-palette/command-palette-manager.spec.d.ts +2 -0
  40. package/esm/components/command-palette/command-palette-manager.spec.d.ts.map +1 -0
  41. package/esm/components/command-palette/command-palette-manager.spec.js +362 -0
  42. package/esm/components/command-palette/command-palette-manager.spec.js.map +1 -0
  43. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  44. package/esm/components/command-palette/command-palette-suggestion-list.js +42 -46
  45. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  46. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts +2 -0
  47. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts.map +1 -0
  48. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +376 -0
  49. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -0
  50. package/esm/components/command-palette/index.d.ts.map +1 -1
  51. package/esm/components/command-palette/index.js +100 -110
  52. package/esm/components/command-palette/index.js.map +1 -1
  53. package/esm/components/command-palette/index.spec.d.ts +2 -0
  54. package/esm/components/command-palette/index.spec.d.ts.map +1 -0
  55. package/esm/components/command-palette/index.spec.js +509 -0
  56. package/esm/components/command-palette/index.spec.js.map +1 -0
  57. package/esm/components/data-grid/body.js +1 -1
  58. package/esm/components/data-grid/body.js.map +1 -1
  59. package/esm/components/data-grid/body.spec.d.ts +2 -0
  60. package/esm/components/data-grid/body.spec.d.ts.map +1 -0
  61. package/esm/components/data-grid/body.spec.js +228 -0
  62. package/esm/components/data-grid/body.spec.js.map +1 -0
  63. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  64. package/esm/components/data-grid/data-grid-row.js +49 -73
  65. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  66. package/esm/components/data-grid/data-grid-row.spec.d.ts +2 -0
  67. package/esm/components/data-grid/data-grid-row.spec.d.ts.map +1 -0
  68. package/esm/components/data-grid/data-grid-row.spec.js +296 -0
  69. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -0
  70. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  71. package/esm/components/data-grid/data-grid.js +35 -28
  72. package/esm/components/data-grid/data-grid.js.map +1 -1
  73. package/esm/components/data-grid/data-grid.spec.d.ts +2 -0
  74. package/esm/components/data-grid/data-grid.spec.d.ts.map +1 -0
  75. package/esm/components/data-grid/data-grid.spec.js +544 -0
  76. package/esm/components/data-grid/data-grid.spec.js.map +1 -0
  77. package/esm/components/data-grid/footer.js +21 -15
  78. package/esm/components/data-grid/footer.js.map +1 -1
  79. package/esm/components/data-grid/footer.spec.d.ts +2 -0
  80. package/esm/components/data-grid/footer.spec.d.ts.map +1 -0
  81. package/esm/components/data-grid/footer.spec.js +264 -0
  82. package/esm/components/data-grid/footer.spec.js.map +1 -0
  83. package/esm/components/data-grid/header.d.ts.map +1 -1
  84. package/esm/components/data-grid/header.js +55 -33
  85. package/esm/components/data-grid/header.js.map +1 -1
  86. package/esm/components/data-grid/header.spec.d.ts +2 -0
  87. package/esm/components/data-grid/header.spec.d.ts.map +1 -0
  88. package/esm/components/data-grid/header.spec.js +421 -0
  89. package/esm/components/data-grid/header.spec.js.map +1 -0
  90. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  91. package/esm/components/data-grid/selection-cell.js +13 -6
  92. package/esm/components/data-grid/selection-cell.js.map +1 -1
  93. package/esm/components/data-grid/selection-cell.spec.d.ts +2 -0
  94. package/esm/components/data-grid/selection-cell.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/selection-cell.spec.js +118 -0
  96. package/esm/components/data-grid/selection-cell.spec.js.map +1 -0
  97. package/esm/components/fab.d.ts.map +1 -1
  98. package/esm/components/fab.js +10 -1
  99. package/esm/components/fab.js.map +1 -1
  100. package/esm/components/fab.spec.d.ts +2 -0
  101. package/esm/components/fab.spec.d.ts.map +1 -0
  102. package/esm/components/fab.spec.js +95 -0
  103. package/esm/components/fab.spec.js.map +1 -0
  104. package/esm/components/form.spec.d.ts +2 -0
  105. package/esm/components/form.spec.d.ts.map +1 -0
  106. package/esm/components/form.spec.js +314 -0
  107. package/esm/components/form.spec.js.map +1 -0
  108. package/esm/components/grid.d.ts.map +1 -1
  109. package/esm/components/grid.js +40 -37
  110. package/esm/components/grid.js.map +1 -1
  111. package/esm/components/grid.spec.d.ts +2 -0
  112. package/esm/components/grid.spec.d.ts.map +1 -0
  113. package/esm/components/grid.spec.js +316 -0
  114. package/esm/components/grid.spec.js.map +1 -0
  115. package/esm/components/inputs/autocomplete.spec.d.ts +2 -0
  116. package/esm/components/inputs/autocomplete.spec.d.ts.map +1 -0
  117. package/esm/components/inputs/autocomplete.spec.js +194 -0
  118. package/esm/components/inputs/autocomplete.spec.js.map +1 -0
  119. package/esm/components/inputs/input.d.ts.map +1 -1
  120. package/esm/components/inputs/input.js +141 -109
  121. package/esm/components/inputs/input.js.map +1 -1
  122. package/esm/components/inputs/input.spec.d.ts +2 -0
  123. package/esm/components/inputs/input.spec.d.ts.map +1 -0
  124. package/esm/components/inputs/input.spec.js +577 -0
  125. package/esm/components/inputs/input.spec.js.map +1 -0
  126. package/esm/components/inputs/text-area.d.ts.map +1 -1
  127. package/esm/components/inputs/text-area.js +54 -58
  128. package/esm/components/inputs/text-area.js.map +1 -1
  129. package/esm/components/inputs/text-area.spec.d.ts +2 -0
  130. package/esm/components/inputs/text-area.spec.d.ts.map +1 -0
  131. package/esm/components/inputs/text-area.spec.js +214 -0
  132. package/esm/components/inputs/text-area.spec.js.map +1 -0
  133. package/esm/components/loader.js +1 -1
  134. package/esm/components/loader.js.map +1 -1
  135. package/esm/components/loader.spec.d.ts +2 -0
  136. package/esm/components/loader.spec.d.ts.map +1 -0
  137. package/esm/components/loader.spec.js +251 -0
  138. package/esm/components/loader.spec.js.map +1 -0
  139. package/esm/components/modal.d.ts.map +1 -1
  140. package/esm/components/modal.js +11 -9
  141. package/esm/components/modal.js.map +1 -1
  142. package/esm/components/modal.spec.d.ts +2 -0
  143. package/esm/components/modal.spec.d.ts.map +1 -0
  144. package/esm/components/modal.spec.js +227 -0
  145. package/esm/components/modal.spec.js.map +1 -0
  146. package/esm/components/noty-list.d.ts.map +1 -1
  147. package/esm/components/noty-list.js +39 -40
  148. package/esm/components/noty-list.js.map +1 -1
  149. package/esm/components/noty-list.spec.d.ts +2 -0
  150. package/esm/components/noty-list.spec.d.ts.map +1 -0
  151. package/esm/components/noty-list.spec.js +486 -0
  152. package/esm/components/noty-list.spec.js.map +1 -0
  153. package/esm/components/paper.d.ts.map +1 -1
  154. package/esm/components/paper.js +15 -12
  155. package/esm/components/paper.js.map +1 -1
  156. package/esm/components/paper.spec.d.ts +2 -0
  157. package/esm/components/paper.spec.d.ts.map +1 -0
  158. package/esm/components/paper.spec.js +63 -0
  159. package/esm/components/paper.spec.js.map +1 -0
  160. package/esm/components/skeleton.js +1 -1
  161. package/esm/components/skeleton.js.map +1 -1
  162. package/esm/components/skeleton.spec.d.ts +2 -0
  163. package/esm/components/skeleton.spec.d.ts.map +1 -0
  164. package/esm/components/skeleton.spec.js +159 -0
  165. package/esm/components/skeleton.spec.js.map +1 -0
  166. package/esm/components/styles.spec.d.ts +2 -0
  167. package/esm/components/styles.spec.d.ts.map +1 -0
  168. package/esm/components/styles.spec.js +56 -0
  169. package/esm/components/styles.spec.js.map +1 -0
  170. package/esm/components/suggest/index.d.ts.map +1 -1
  171. package/esm/components/suggest/index.js +74 -83
  172. package/esm/components/suggest/index.js.map +1 -1
  173. package/esm/components/suggest/index.spec.d.ts +2 -0
  174. package/esm/components/suggest/index.spec.d.ts.map +1 -0
  175. package/esm/components/suggest/index.spec.js +515 -0
  176. package/esm/components/suggest/index.spec.js.map +1 -0
  177. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  178. package/esm/components/suggest/suggest-input.js +16 -17
  179. package/esm/components/suggest/suggest-input.js.map +1 -1
  180. package/esm/components/suggest/suggest-input.spec.d.ts +2 -0
  181. package/esm/components/suggest/suggest-input.spec.d.ts.map +1 -0
  182. package/esm/components/suggest/suggest-input.spec.js +138 -0
  183. package/esm/components/suggest/suggest-input.spec.js.map +1 -0
  184. package/esm/components/suggest/suggest-manager.spec.d.ts +2 -0
  185. package/esm/components/suggest/suggest-manager.spec.d.ts.map +1 -0
  186. package/esm/components/suggest/suggest-manager.spec.js +308 -0
  187. package/esm/components/suggest/suggest-manager.spec.js.map +1 -0
  188. package/esm/components/suggest/suggestion-list.d.ts.map +1 -1
  189. package/esm/components/suggest/suggestion-list.js +43 -48
  190. package/esm/components/suggest/suggestion-list.js.map +1 -1
  191. package/esm/components/suggest/suggestion-list.spec.d.ts +2 -0
  192. package/esm/components/suggest/suggestion-list.spec.d.ts.map +1 -0
  193. package/esm/components/suggest/suggestion-list.spec.js +252 -0
  194. package/esm/components/suggest/suggestion-list.spec.js.map +1 -0
  195. package/esm/components/tabs.d.ts.map +1 -1
  196. package/esm/components/tabs.js +32 -18
  197. package/esm/components/tabs.js.map +1 -1
  198. package/esm/components/tabs.spec.d.ts +2 -0
  199. package/esm/components/tabs.spec.d.ts.map +1 -0
  200. package/esm/components/tabs.spec.js +187 -0
  201. package/esm/components/tabs.spec.js.map +1 -0
  202. package/esm/components/wizard/index.d.ts.map +1 -1
  203. package/esm/components/wizard/index.js +10 -7
  204. package/esm/components/wizard/index.js.map +1 -1
  205. package/esm/components/wizard/index.spec.d.ts +2 -0
  206. package/esm/components/wizard/index.spec.d.ts.map +1 -0
  207. package/esm/components/wizard/index.spec.js +171 -0
  208. package/esm/components/wizard/index.spec.js.map +1 -0
  209. package/esm/services/collection-service.spec.js +391 -2
  210. package/esm/services/collection-service.spec.js.map +1 -1
  211. package/esm/services/css-variable-theme.d.ts.map +1 -1
  212. package/esm/services/css-variable-theme.js +21 -1
  213. package/esm/services/css-variable-theme.js.map +1 -1
  214. package/esm/services/css-variable-theme.spec.d.ts +2 -0
  215. package/esm/services/css-variable-theme.spec.d.ts.map +1 -0
  216. package/esm/services/css-variable-theme.spec.js +169 -0
  217. package/esm/services/css-variable-theme.spec.js.map +1 -0
  218. package/esm/services/default-palette.d.ts +4 -0
  219. package/esm/services/default-palette.d.ts.map +1 -1
  220. package/esm/services/default-palette.js +22 -0
  221. package/esm/services/default-palette.js.map +1 -1
  222. package/esm/services/theme-provider-service.d.ts +59 -1
  223. package/esm/services/theme-provider-service.d.ts.map +1 -1
  224. package/esm/services/theme-provider-service.js.map +1 -1
  225. package/esm/services/theme-provider-service.spec.d.ts +2 -0
  226. package/esm/services/theme-provider-service.spec.d.ts.map +1 -0
  227. package/esm/services/theme-provider-service.spec.js +166 -0
  228. package/esm/services/theme-provider-service.spec.js.map +1 -0
  229. package/package.json +2 -2
  230. package/src/components/animations.spec.ts +299 -0
  231. package/src/components/app-bar-link.spec.tsx +341 -0
  232. package/src/components/app-bar-link.tsx +21 -21
  233. package/src/components/app-bar.spec.tsx +142 -0
  234. package/src/components/app-bar.tsx +22 -22
  235. package/src/components/avatar.spec.tsx +146 -0
  236. package/src/components/avatar.tsx +17 -20
  237. package/src/components/button.spec.tsx +193 -0
  238. package/src/components/button.tsx +162 -197
  239. package/src/components/command-palette/command-palette-input.spec.tsx +320 -0
  240. package/src/components/command-palette/command-palette-input.tsx +19 -22
  241. package/src/components/command-palette/command-palette-manager.spec.ts +470 -0
  242. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +499 -0
  243. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -46
  244. package/src/components/command-palette/index.spec.tsx +684 -0
  245. package/src/components/command-palette/index.tsx +107 -136
  246. package/src/components/data-grid/body.spec.tsx +340 -0
  247. package/src/components/data-grid/body.tsx +1 -1
  248. package/src/components/data-grid/data-grid-row.spec.tsx +382 -0
  249. package/src/components/data-grid/data-grid-row.tsx +50 -82
  250. package/src/components/data-grid/data-grid.spec.tsx +939 -0
  251. package/src/components/data-grid/data-grid.tsx +38 -35
  252. package/src/components/data-grid/footer.spec.tsx +344 -0
  253. package/src/components/data-grid/footer.tsx +19 -19
  254. package/src/components/data-grid/header.spec.tsx +563 -0
  255. package/src/components/data-grid/header.tsx +53 -44
  256. package/src/components/data-grid/selection-cell.spec.tsx +150 -0
  257. package/src/components/data-grid/selection-cell.tsx +12 -6
  258. package/src/components/fab.spec.tsx +108 -0
  259. package/src/components/fab.tsx +10 -1
  260. package/src/components/form.spec.tsx +481 -0
  261. package/src/components/grid.spec.tsx +334 -0
  262. package/src/components/grid.tsx +57 -63
  263. package/src/components/inputs/autocomplete.spec.tsx +258 -0
  264. package/src/components/inputs/input.spec.tsx +808 -0
  265. package/src/components/inputs/input.tsx +153 -139
  266. package/src/components/inputs/text-area.spec.tsx +285 -0
  267. package/src/components/inputs/text-area.tsx +53 -79
  268. package/src/components/loader.spec.tsx +346 -0
  269. package/src/components/loader.tsx +1 -1
  270. package/src/components/modal.spec.tsx +304 -0
  271. package/src/components/modal.tsx +11 -9
  272. package/src/components/noty-list.spec.tsx +631 -0
  273. package/src/components/noty-list.tsx +39 -50
  274. package/src/components/paper.spec.tsx +72 -0
  275. package/src/components/paper.tsx +15 -13
  276. package/src/components/skeleton.spec.tsx +219 -0
  277. package/src/components/skeleton.tsx +1 -1
  278. package/src/components/styles.spec.ts +70 -0
  279. package/src/components/suggest/index.spec.tsx +861 -0
  280. package/src/components/suggest/index.tsx +74 -101
  281. package/src/components/suggest/suggest-input.spec.tsx +181 -0
  282. package/src/components/suggest/suggest-input.tsx +16 -24
  283. package/src/components/suggest/suggest-manager.spec.ts +409 -0
  284. package/src/components/suggest/suggestion-list.spec.tsx +334 -0
  285. package/src/components/suggest/suggestion-list.tsx +43 -48
  286. package/src/components/tabs.spec.tsx +236 -0
  287. package/src/components/tabs.tsx +33 -21
  288. package/src/components/wizard/index.spec.tsx +224 -0
  289. package/src/components/wizard/index.tsx +10 -9
  290. package/src/services/collection-service.spec.ts +492 -3
  291. package/src/services/css-variable-theme.spec.ts +204 -0
  292. package/src/services/css-variable-theme.ts +21 -1
  293. package/src/services/default-palette.ts +22 -0
  294. package/src/services/theme-provider-service.spec.ts +195 -0
  295. package/src/services/theme-provider-service.ts +60 -2
@@ -0,0 +1,631 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { defaultDarkTheme } from '../services/default-dark-theme.js'
6
+ import type { NotyModel } from '../services/noty-service.js'
7
+ import { NotyService } from '../services/noty-service.js'
8
+ import { ThemeProviderService } from '../services/theme-provider-service.js'
9
+ import { getDefaultNotyTimeouts, NotyComponent, NotyList } from './noty-list.js'
10
+
11
+ describe('getDefaultNotyTimeouts', () => {
12
+ it('should return 0 for error type', () => {
13
+ expect(getDefaultNotyTimeouts('error')).toBe(0)
14
+ })
15
+
16
+ it('should return 0 for warning type', () => {
17
+ expect(getDefaultNotyTimeouts('warning')).toBe(0)
18
+ })
19
+
20
+ it('should return 5000 for success type', () => {
21
+ expect(getDefaultNotyTimeouts('success')).toBe(5000)
22
+ })
23
+
24
+ it('should return 20000 for info type', () => {
25
+ expect(getDefaultNotyTimeouts('info')).toBe(20000)
26
+ })
27
+
28
+ it('should return 0 for unknown type', () => {
29
+ expect(getDefaultNotyTimeouts('unknown' as NotyModel['type'])).toBe(0)
30
+ })
31
+ })
32
+
33
+ describe('NotyComponent', () => {
34
+ let originalAnimate: typeof Element.prototype.animate
35
+ let animateCalls: Array<{ keyframes: unknown; options: unknown }>
36
+
37
+ const setupTheme = (injector: Injector) => {
38
+ const themeProvider = injector.getInstance(ThemeProviderService)
39
+ themeProvider.setAssignedTheme(defaultDarkTheme)
40
+ }
41
+
42
+ beforeEach(() => {
43
+ document.body.innerHTML = '<div id="root"></div>'
44
+ animateCalls = []
45
+ originalAnimate = Element.prototype.animate
46
+
47
+ Element.prototype.animate = vi.fn(
48
+ (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
49
+ animateCalls.push({ keyframes, options })
50
+ const mockAnimation = {
51
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
52
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
53
+ cancel: vi.fn(),
54
+ play: vi.fn(),
55
+ pause: vi.fn(),
56
+ finish: vi.fn(),
57
+ addEventListener: vi.fn(),
58
+ removeEventListener: vi.fn(),
59
+ }
60
+ return mockAnimation as unknown as Animation
61
+ },
62
+ ) as typeof Element.prototype.animate
63
+ })
64
+
65
+ afterEach(() => {
66
+ document.body.innerHTML = ''
67
+ Element.prototype.animate = originalAnimate
68
+ vi.restoreAllMocks()
69
+ })
70
+
71
+ it('should render the shade-noty custom element', async () => {
72
+ await usingAsync(new Injector(), async (injector) => {
73
+ setupTheme(injector)
74
+ const rootElement = document.getElementById('root') as HTMLDivElement
75
+ const model: NotyModel = { type: 'info', title: 'Test', body: 'Test body' }
76
+
77
+ initializeShadeRoot({
78
+ injector,
79
+ rootElement,
80
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
81
+ })
82
+
83
+ await sleepAsync(50)
84
+
85
+ const noty = document.querySelector('shade-noty')
86
+ expect(noty).not.toBeNull()
87
+ expect(noty?.tagName.toLowerCase()).toBe('shade-noty')
88
+ })
89
+ })
90
+
91
+ it('should render title and body content', async () => {
92
+ await usingAsync(new Injector(), async (injector) => {
93
+ setupTheme(injector)
94
+ const rootElement = document.getElementById('root') as HTMLDivElement
95
+ const model: NotyModel = { type: 'success', title: 'Success Title', body: 'Success message body' }
96
+
97
+ initializeShadeRoot({
98
+ injector,
99
+ rootElement,
100
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
101
+ })
102
+
103
+ await sleepAsync(50)
104
+
105
+ expect(document.body.innerHTML).toContain('Success Title')
106
+ expect(document.body.innerHTML).toContain('Success message body')
107
+ })
108
+ })
109
+
110
+ it('should apply noty class with type on the element', async () => {
111
+ await usingAsync(new Injector(), async (injector) => {
112
+ setupTheme(injector)
113
+ const rootElement = document.getElementById('root') as HTMLDivElement
114
+ const model: NotyModel = { type: 'error', title: 'Error', body: 'Error occurred' }
115
+
116
+ initializeShadeRoot({
117
+ injector,
118
+ rootElement,
119
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
120
+ })
121
+
122
+ await sleepAsync(50)
123
+
124
+ const noty = document.querySelector('shade-noty') as HTMLElement
125
+ expect(noty).not.toBeNull()
126
+ expect(noty.classList.contains('noty')).toBe(true)
127
+ expect(noty.classList.contains('error')).toBe(true)
128
+ })
129
+ })
130
+
131
+ it('should render dismiss button', async () => {
132
+ await usingAsync(new Injector(), async (injector) => {
133
+ setupTheme(injector)
134
+ const rootElement = document.getElementById('root') as HTMLDivElement
135
+ const model: NotyModel = { type: 'warning', title: 'Warning', body: 'Warning message' }
136
+
137
+ initializeShadeRoot({
138
+ injector,
139
+ rootElement,
140
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
141
+ })
142
+
143
+ await sleepAsync(50)
144
+
145
+ const dismissButton = document.querySelector('button[is="shade-button"]')
146
+ expect(dismissButton).not.toBeNull()
147
+ expect(dismissButton?.textContent).toContain('✖')
148
+ })
149
+ })
150
+
151
+ it('should call onDismiss when dismiss button is clicked', async () => {
152
+ const onDismiss = vi.fn()
153
+
154
+ Element.prototype.animate = vi.fn(
155
+ (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
156
+ animateCalls.push({ keyframes, options })
157
+ const mockAnimation = {
158
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
159
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
160
+ cancel: vi.fn(),
161
+ play: vi.fn(),
162
+ pause: vi.fn(),
163
+ finish: vi.fn(),
164
+ addEventListener: vi.fn(),
165
+ removeEventListener: vi.fn(),
166
+ }
167
+ setTimeout(() => {
168
+ mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
169
+ }, 10)
170
+ return mockAnimation as unknown as Animation
171
+ },
172
+ ) as typeof Element.prototype.animate
173
+
174
+ await usingAsync(new Injector(), async (injector) => {
175
+ setupTheme(injector)
176
+ const rootElement = document.getElementById('root') as HTMLDivElement
177
+ const model: NotyModel = { type: 'info', title: 'Info', body: 'Info message' }
178
+
179
+ initializeShadeRoot({
180
+ injector,
181
+ rootElement,
182
+ jsxElement: <NotyComponent model={model} onDismiss={onDismiss} />,
183
+ })
184
+
185
+ await sleepAsync(50)
186
+
187
+ const dismissButton = document.querySelector('button[is="shade-button"]') as HTMLButtonElement
188
+ expect(dismissButton).not.toBeNull()
189
+ dismissButton.click()
190
+
191
+ await sleepAsync(50)
192
+
193
+ expect(onDismiss).toHaveBeenCalled()
194
+ })
195
+ })
196
+
197
+ it('should start fade-in animation on mount', async () => {
198
+ vi.useFakeTimers()
199
+
200
+ const injector = new Injector()
201
+ setupTheme(injector)
202
+ const rootElement = document.getElementById('root') as HTMLDivElement
203
+ const model: NotyModel = { type: 'success', title: 'Success', body: 'Success message' }
204
+
205
+ initializeShadeRoot({
206
+ injector,
207
+ rootElement,
208
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
209
+ })
210
+
211
+ // Wait for render
212
+ await vi.advanceTimersByTimeAsync(50)
213
+
214
+ // The constructed hook schedules the animation via setTimeout
215
+ await vi.advanceTimersByTimeAsync(10)
216
+
217
+ const fadeInCall = animateCalls.find(
218
+ (call) =>
219
+ Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'opacity' in kf && 'height' in kf),
220
+ )
221
+
222
+ expect(fadeInCall).toBeDefined()
223
+ expect((fadeInCall?.options as KeyframeAnimationOptions)?.duration).toBe(700)
224
+ expect((fadeInCall?.options as KeyframeAnimationOptions)?.fill).toBe('forwards')
225
+
226
+ vi.useRealTimers()
227
+ })
228
+
229
+ it('should auto-dismiss after timeout for success type', async () => {
230
+ vi.useFakeTimers()
231
+ const onDismiss = vi.fn()
232
+
233
+ Element.prototype.animate = vi.fn(
234
+ (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
235
+ animateCalls.push({ keyframes, options })
236
+ const mockAnimation = {
237
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
238
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
239
+ cancel: vi.fn(),
240
+ play: vi.fn(),
241
+ pause: vi.fn(),
242
+ finish: vi.fn(),
243
+ addEventListener: vi.fn(),
244
+ removeEventListener: vi.fn(),
245
+ }
246
+ setTimeout(() => {
247
+ mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
248
+ }, 10)
249
+ return mockAnimation as unknown as Animation
250
+ },
251
+ ) as typeof Element.prototype.animate
252
+
253
+ const injector = new Injector()
254
+ setupTheme(injector)
255
+ const rootElement = document.getElementById('root') as HTMLDivElement
256
+ const model: NotyModel = { type: 'success', title: 'Success', body: 'Success message' }
257
+
258
+ initializeShadeRoot({
259
+ injector,
260
+ rootElement,
261
+ jsxElement: <NotyComponent model={model} onDismiss={onDismiss} />,
262
+ })
263
+
264
+ await vi.advanceTimersByTimeAsync(50)
265
+ expect(onDismiss).not.toHaveBeenCalled()
266
+
267
+ // Success timeout is 5000ms
268
+ await vi.advanceTimersByTimeAsync(5000)
269
+ await vi.advanceTimersByTimeAsync(50)
270
+
271
+ expect(onDismiss).toHaveBeenCalled()
272
+
273
+ vi.useRealTimers()
274
+ })
275
+
276
+ it('should use custom timeout when provided', async () => {
277
+ vi.useFakeTimers()
278
+ const onDismiss = vi.fn()
279
+
280
+ Element.prototype.animate = vi.fn(
281
+ (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
282
+ animateCalls.push({ keyframes, options })
283
+ const mockAnimation = {
284
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
285
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
286
+ cancel: vi.fn(),
287
+ play: vi.fn(),
288
+ pause: vi.fn(),
289
+ finish: vi.fn(),
290
+ addEventListener: vi.fn(),
291
+ removeEventListener: vi.fn(),
292
+ }
293
+ setTimeout(() => {
294
+ mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
295
+ }, 10)
296
+ return mockAnimation as unknown as Animation
297
+ },
298
+ ) as typeof Element.prototype.animate
299
+
300
+ const injector = new Injector()
301
+ setupTheme(injector)
302
+ const rootElement = document.getElementById('root') as HTMLDivElement
303
+ // Info default timeout is 20000, but we set custom 1000
304
+ const model: NotyModel = { type: 'info', title: 'Info', body: 'Info message', timeout: 1000 }
305
+
306
+ initializeShadeRoot({
307
+ injector,
308
+ rootElement,
309
+ jsxElement: <NotyComponent model={model} onDismiss={onDismiss} />,
310
+ })
311
+
312
+ await vi.advanceTimersByTimeAsync(50)
313
+ expect(onDismiss).not.toHaveBeenCalled()
314
+
315
+ await vi.advanceTimersByTimeAsync(1000)
316
+ await vi.advanceTimersByTimeAsync(50)
317
+
318
+ expect(onDismiss).toHaveBeenCalled()
319
+
320
+ vi.useRealTimers()
321
+ })
322
+
323
+ it('should not auto-dismiss for error type (timeout 0)', async () => {
324
+ vi.useFakeTimers()
325
+ const onDismiss = vi.fn()
326
+
327
+ const injector = new Injector()
328
+ setupTheme(injector)
329
+ const rootElement = document.getElementById('root') as HTMLDivElement
330
+ const model: NotyModel = { type: 'error', title: 'Error', body: 'Error message' }
331
+
332
+ initializeShadeRoot({
333
+ injector,
334
+ rootElement,
335
+ jsxElement: <NotyComponent model={model} onDismiss={onDismiss} />,
336
+ })
337
+
338
+ await vi.advanceTimersByTimeAsync(50)
339
+ await vi.advanceTimersByTimeAsync(30000)
340
+
341
+ expect(onDismiss).not.toHaveBeenCalled()
342
+
343
+ vi.useRealTimers()
344
+ })
345
+
346
+ it('should render all noty types with appropriate styling', async () => {
347
+ const types: Array<NotyModel['type']> = ['error', 'warning', 'info', 'success']
348
+
349
+ for (const type of types) {
350
+ document.body.innerHTML = '<div id="root"></div>'
351
+
352
+ await usingAsync(new Injector(), async (injector) => {
353
+ setupTheme(injector)
354
+ const rootElement = document.getElementById('root') as HTMLDivElement
355
+ const model: NotyModel = { type, title: `${type} Title`, body: `${type} body` }
356
+
357
+ initializeShadeRoot({
358
+ injector,
359
+ rootElement,
360
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
361
+ })
362
+
363
+ await sleepAsync(50)
364
+
365
+ const noty = document.querySelector('shade-noty') as HTMLElement
366
+ expect(noty).not.toBeNull()
367
+ expect(noty.classList.contains('noty')).toBe(true)
368
+ expect(noty.classList.contains(type)).toBe(true)
369
+ })
370
+ }
371
+ })
372
+
373
+ it('should apply background color from theme', async () => {
374
+ await usingAsync(new Injector(), async (injector) => {
375
+ setupTheme(injector)
376
+ const rootElement = document.getElementById('root') as HTMLDivElement
377
+ const model: NotyModel = { type: 'success', title: 'Success', body: 'Message' }
378
+
379
+ initializeShadeRoot({
380
+ injector,
381
+ rootElement,
382
+ jsxElement: <NotyComponent model={model} onDismiss={() => {}} />,
383
+ })
384
+
385
+ await sleepAsync(50)
386
+
387
+ const noty = document.querySelector('shade-noty') as HTMLElement
388
+ expect(noty).not.toBeNull()
389
+ // The component sets backgroundColor via style
390
+ expect(noty.style.backgroundColor).toBeTruthy()
391
+ })
392
+ })
393
+ })
394
+
395
+ describe('NotyList', () => {
396
+ let originalAnimate: typeof Element.prototype.animate
397
+ let animateCalls: Array<{ keyframes: unknown; options: unknown }>
398
+
399
+ const setupTheme = (injector: Injector) => {
400
+ const themeProvider = injector.getInstance(ThemeProviderService)
401
+ themeProvider.setAssignedTheme(defaultDarkTheme)
402
+ }
403
+
404
+ beforeEach(() => {
405
+ document.body.innerHTML = '<div id="root"></div>'
406
+ animateCalls = []
407
+ originalAnimate = Element.prototype.animate
408
+
409
+ Element.prototype.animate = vi.fn(
410
+ (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
411
+ animateCalls.push({ keyframes, options })
412
+ const mockAnimation = {
413
+ onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
414
+ oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
415
+ cancel: vi.fn(),
416
+ play: vi.fn(),
417
+ pause: vi.fn(),
418
+ finish: vi.fn(),
419
+ addEventListener: vi.fn(),
420
+ removeEventListener: vi.fn(),
421
+ }
422
+ return mockAnimation as unknown as Animation
423
+ },
424
+ ) as typeof Element.prototype.animate
425
+ })
426
+
427
+ afterEach(() => {
428
+ document.body.innerHTML = ''
429
+ Element.prototype.animate = originalAnimate
430
+ vi.restoreAllMocks()
431
+ })
432
+
433
+ it('should render the shade-noty-list custom element', async () => {
434
+ await usingAsync(new Injector(), async (injector) => {
435
+ const rootElement = document.getElementById('root') as HTMLDivElement
436
+
437
+ initializeShadeRoot({
438
+ injector,
439
+ rootElement,
440
+ jsxElement: <NotyList />,
441
+ })
442
+
443
+ await sleepAsync(50)
444
+
445
+ const notyList = document.querySelector('shade-noty-list')
446
+ expect(notyList).not.toBeNull()
447
+ expect(notyList?.tagName.toLowerCase()).toBe('shade-noty-list')
448
+ })
449
+ })
450
+
451
+ it('should have fixed positioning styles', async () => {
452
+ await usingAsync(new Injector(), async (injector) => {
453
+ const rootElement = document.getElementById('root') as HTMLDivElement
454
+
455
+ initializeShadeRoot({
456
+ injector,
457
+ rootElement,
458
+ jsxElement: <NotyList />,
459
+ })
460
+
461
+ await sleepAsync(50)
462
+
463
+ const notyList = document.querySelector('shade-noty-list') as HTMLElement
464
+ expect(notyList).not.toBeNull()
465
+
466
+ const computedStyle = window.getComputedStyle(notyList)
467
+ expect(computedStyle.position).toBe('fixed')
468
+ expect(computedStyle.display).toBe('flex')
469
+ expect(computedStyle.flexDirection).toBe('column')
470
+ })
471
+ })
472
+
473
+ it('should render existing notys from NotyService', async () => {
474
+ await usingAsync(new Injector(), async (injector) => {
475
+ setupTheme(injector)
476
+ const notyService = injector.getInstance(NotyService)
477
+ const noty1: NotyModel = { type: 'info', title: 'Info 1', body: 'Info body 1' }
478
+ const noty2: NotyModel = { type: 'success', title: 'Success 1', body: 'Success body 1' }
479
+
480
+ notyService.emit('onNotyAdded', noty1)
481
+ notyService.emit('onNotyAdded', noty2)
482
+
483
+ const rootElement = document.getElementById('root') as HTMLDivElement
484
+
485
+ initializeShadeRoot({
486
+ injector,
487
+ rootElement,
488
+ jsxElement: <NotyList />,
489
+ })
490
+
491
+ await sleepAsync(50)
492
+
493
+ const notys = document.querySelectorAll('shade-noty')
494
+ expect(notys.length).toBe(2)
495
+ })
496
+ })
497
+
498
+ it('should add new noty when NotyService emits onNotyAdded', async () => {
499
+ await usingAsync(new Injector(), async (injector) => {
500
+ setupTheme(injector)
501
+ const notyService = injector.getInstance(NotyService)
502
+ const rootElement = document.getElementById('root') as HTMLDivElement
503
+
504
+ initializeShadeRoot({
505
+ injector,
506
+ rootElement,
507
+ jsxElement: <NotyList />,
508
+ })
509
+
510
+ await sleepAsync(50)
511
+
512
+ let notys = document.querySelectorAll('shade-noty')
513
+ expect(notys.length).toBe(0)
514
+
515
+ const newNoty: NotyModel = { type: 'warning', title: 'Warning', body: 'Warning message' }
516
+ notyService.emit('onNotyAdded', newNoty)
517
+
518
+ await sleepAsync(50)
519
+
520
+ notys = document.querySelectorAll('shade-noty')
521
+ expect(notys.length).toBe(1)
522
+ })
523
+ })
524
+
525
+ it('should remove noty when NotyService emits onNotyRemoved', async () => {
526
+ await usingAsync(new Injector(), async (injector) => {
527
+ setupTheme(injector)
528
+ const notyService = injector.getInstance(NotyService)
529
+ const noty: NotyModel = { type: 'error', title: 'Error', body: 'Error message' }
530
+
531
+ notyService.emit('onNotyAdded', noty)
532
+
533
+ const rootElement = document.getElementById('root') as HTMLDivElement
534
+
535
+ initializeShadeRoot({
536
+ injector,
537
+ rootElement,
538
+ jsxElement: <NotyList />,
539
+ })
540
+
541
+ await sleepAsync(50)
542
+
543
+ let notys = document.querySelectorAll('shade-noty')
544
+ expect(notys.length).toBe(1)
545
+
546
+ notyService.emit('onNotyRemoved', noty)
547
+
548
+ await sleepAsync(50)
549
+
550
+ notys = document.querySelectorAll('shade-noty')
551
+ expect(notys.length).toBe(0)
552
+ })
553
+ })
554
+
555
+ it('should handle multiple notys being added and removed', async () => {
556
+ await usingAsync(new Injector(), async (injector) => {
557
+ setupTheme(injector)
558
+ const notyService = injector.getInstance(NotyService)
559
+ const rootElement = document.getElementById('root') as HTMLDivElement
560
+
561
+ initializeShadeRoot({
562
+ injector,
563
+ rootElement,
564
+ jsxElement: <NotyList />,
565
+ })
566
+
567
+ await sleepAsync(50)
568
+
569
+ const noty1: NotyModel = { type: 'info', title: 'Info 1', body: 'Body 1' }
570
+ const noty2: NotyModel = { type: 'success', title: 'Success 1', body: 'Body 2' }
571
+ const noty3: NotyModel = { type: 'warning', title: 'Warning 1', body: 'Body 3' }
572
+
573
+ notyService.emit('onNotyAdded', noty1)
574
+ notyService.emit('onNotyAdded', noty2)
575
+ notyService.emit('onNotyAdded', noty3)
576
+
577
+ await sleepAsync(50)
578
+
579
+ let notys = document.querySelectorAll('shade-noty')
580
+ expect(notys.length).toBe(3)
581
+
582
+ notyService.emit('onNotyRemoved', noty2)
583
+
584
+ await sleepAsync(50)
585
+
586
+ notys = document.querySelectorAll('shade-noty')
587
+ expect(notys.length).toBe(2)
588
+
589
+ notyService.emit('onNotyRemoved', noty1)
590
+ notyService.emit('onNotyRemoved', noty3)
591
+
592
+ await sleepAsync(50)
593
+
594
+ notys = document.querySelectorAll('shade-noty')
595
+ expect(notys.length).toBe(0)
596
+ })
597
+ })
598
+
599
+ it('should only remove the specific noty that was requested', async () => {
600
+ await usingAsync(new Injector(), async (injector) => {
601
+ setupTheme(injector)
602
+ const notyService = injector.getInstance(NotyService)
603
+ const rootElement = document.getElementById('root') as HTMLDivElement
604
+
605
+ const noty1: NotyModel = { type: 'info', title: 'Keep this', body: 'Body 1' }
606
+ const noty2: NotyModel = { type: 'error', title: 'Remove this', body: 'Body 2' }
607
+
608
+ notyService.emit('onNotyAdded', noty1)
609
+ notyService.emit('onNotyAdded', noty2)
610
+
611
+ initializeShadeRoot({
612
+ injector,
613
+ rootElement,
614
+ jsxElement: <NotyList />,
615
+ })
616
+
617
+ await sleepAsync(50)
618
+
619
+ notyService.emit('onNotyRemoved', noty2)
620
+
621
+ await sleepAsync(50)
622
+
623
+ const notys = document.querySelectorAll('shade-noty')
624
+ expect(notys.length).toBe(1)
625
+
626
+ // Check that the remaining noty is the correct one
627
+ expect(document.body.innerHTML).toContain('Keep this')
628
+ expect(document.body.innerHTML).not.toContain('Remove this')
629
+ })
630
+ })
631
+ })