@fpkit/acss 0.5.12 → 0.5.13

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 (264) hide show
  1. package/README.md +89 -0
  2. package/libs/{chunk-DV56L5YX.cjs → chunk-2LTJ7HHX.cjs} +4 -4
  3. package/libs/{chunk-EQ67LF46.js → chunk-2Y7W75TT.js} +3 -3
  4. package/libs/{chunk-KKLTUJFB.cjs → chunk-3MKLDCKQ.cjs} +5 -5
  5. package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
  6. package/libs/{chunk-X3EVB7VS.cjs → chunk-5S4ORA4C.cjs} +3 -3
  7. package/libs/{chunk-O6QZBB6G.js → chunk-772NRB75.js} +5 -5
  8. package/libs/chunk-772NRB75.js.map +1 -0
  9. package/libs/{chunk-6BVXFW7U.cjs → chunk-AHDJGCG5.cjs} +3 -3
  10. package/libs/{chunk-E3XP6BEX.cjs → chunk-B7F5FS6D.cjs} +3 -3
  11. package/libs/chunk-D4YLRWAO.cjs +18 -0
  12. package/libs/chunk-D4YLRWAO.cjs.map +1 -0
  13. package/libs/chunk-ETFLFC2S.js +10 -0
  14. package/libs/chunk-ETFLFC2S.js.map +1 -0
  15. package/libs/chunk-GZ4QFPRY.js +9 -0
  16. package/libs/chunk-GZ4QFPRY.js.map +1 -0
  17. package/libs/{chunk-LHVJKDMA.cjs → chunk-J32EZPYD.cjs} +3 -3
  18. package/libs/chunk-JJ43O4Y5.js +8 -0
  19. package/libs/chunk-JJ43O4Y5.js.map +1 -0
  20. package/libs/chunk-KUKIVRC2.js +7 -0
  21. package/libs/chunk-KUKIVRC2.js.map +1 -0
  22. package/libs/chunk-L75OQKEI.cjs +13 -0
  23. package/libs/chunk-L75OQKEI.cjs.map +1 -0
  24. package/libs/{chunk-LL7HTLMS.cjs → chunk-M5RRNTVX.cjs} +3 -3
  25. package/libs/{chunk-LIQJ7ZZR.js → chunk-NGTJDDFO.js} +2 -2
  26. package/libs/chunk-OK5QEIMD.cjs +17 -0
  27. package/libs/chunk-OK5QEIMD.cjs.map +1 -0
  28. package/libs/chunk-P2DC76ZZ.cjs +18 -0
  29. package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
  30. package/libs/chunk-PQ2K3BM6.cjs +17 -0
  31. package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
  32. package/libs/{chunk-QCMV4VQZ.js → chunk-QLZWHAMK.js} +2 -2
  33. package/libs/{chunk-BIP2NY53.js → chunk-RIVUMPOG.js} +2 -2
  34. package/libs/{chunk-ICCKQ2GC.cjs → chunk-ROZI23GS.cjs} +4 -4
  35. package/libs/{chunk-NHYXGV3L.js → chunk-SMYRLO3E.js} +2 -2
  36. package/libs/{chunk-5ZM4XL44.js → chunk-TYRCEX2L.js} +2 -2
  37. package/libs/chunk-VUH3FXGJ.js +11 -0
  38. package/libs/chunk-VUH3FXGJ.js.map +1 -0
  39. package/libs/{chunk-PPOOBUOS.js → chunk-XBA562WW.js} +2 -2
  40. package/libs/{chunk-QVV34QEH.cjs → chunk-XTQKWY7W.cjs} +3 -3
  41. package/libs/{chunk-YWOYVRFT.js → chunk-ZANSFMTD.js} +3 -3
  42. package/libs/components/alert/alert.css +1 -1
  43. package/libs/components/alert/alert.css.map +1 -1
  44. package/libs/components/alert/alert.min.css +2 -2
  45. package/libs/components/badge/badge.css +1 -1
  46. package/libs/components/badge/badge.css.map +1 -1
  47. package/libs/components/badge/badge.min.css +2 -2
  48. package/libs/components/breadcrumbs/breadcrumb.cjs +9 -5
  49. package/libs/components/breadcrumbs/breadcrumb.d.cts +271 -32
  50. package/libs/components/breadcrumbs/breadcrumb.d.ts +271 -32
  51. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  52. package/libs/components/button.cjs +4 -4
  53. package/libs/components/button.d.cts +2 -2
  54. package/libs/components/button.d.ts +2 -2
  55. package/libs/components/button.js +2 -2
  56. package/libs/components/buttons/button.css +1 -1
  57. package/libs/components/buttons/button.css.map +1 -1
  58. package/libs/components/buttons/button.min.css +2 -2
  59. package/libs/components/card.cjs +7 -7
  60. package/libs/components/card.d.cts +277 -33
  61. package/libs/components/card.d.ts +277 -33
  62. package/libs/components/card.js +2 -2
  63. package/libs/components/cards/card.css +1 -1
  64. package/libs/components/cards/card.css.map +1 -1
  65. package/libs/components/cards/card.min.css +2 -2
  66. package/libs/components/details/details.css +1 -1
  67. package/libs/components/details/details.css.map +1 -1
  68. package/libs/components/details/details.min.css +2 -2
  69. package/libs/components/dialog/dialog.cjs +7 -7
  70. package/libs/components/dialog/dialog.css +1 -1
  71. package/libs/components/dialog/dialog.css.map +1 -1
  72. package/libs/components/dialog/dialog.d.cts +88 -34
  73. package/libs/components/dialog/dialog.d.ts +88 -34
  74. package/libs/components/dialog/dialog.js +5 -5
  75. package/libs/components/dialog/dialog.min.css +2 -2
  76. package/libs/components/form/fields.cjs +4 -4
  77. package/libs/components/form/fields.d.cts +2 -2
  78. package/libs/components/form/fields.d.ts +2 -2
  79. package/libs/components/form/fields.js +2 -2
  80. package/libs/components/form/textarea.cjs +4 -4
  81. package/libs/components/form/textarea.d.cts +2 -2
  82. package/libs/components/form/textarea.d.ts +2 -2
  83. package/libs/components/form/textarea.js +2 -2
  84. package/libs/components/heading/heading.cjs +3 -3
  85. package/libs/components/heading/heading.d.cts +3 -14
  86. package/libs/components/heading/heading.d.ts +3 -14
  87. package/libs/components/heading/heading.js +2 -2
  88. package/libs/components/icons/icon.cjs +4 -4
  89. package/libs/components/icons/icon.d.cts +148 -4
  90. package/libs/components/icons/icon.d.ts +148 -4
  91. package/libs/components/icons/icon.js +2 -2
  92. package/libs/components/images/img.css +1 -1
  93. package/libs/components/images/img.css.map +1 -1
  94. package/libs/components/images/img.min.css +2 -2
  95. package/libs/components/link/link.cjs +4 -4
  96. package/libs/components/link/link.d.cts +2 -2
  97. package/libs/components/link/link.d.ts +2 -2
  98. package/libs/components/link/link.js +2 -2
  99. package/libs/components/list/list.cjs +5 -5
  100. package/libs/components/list/list.d.cts +3 -3
  101. package/libs/components/list/list.d.ts +3 -3
  102. package/libs/components/list/list.js +2 -2
  103. package/libs/components/modal.cjs +4 -4
  104. package/libs/components/modal.js +3 -3
  105. package/libs/components/nav/nav.cjs +7 -7
  106. package/libs/components/nav/nav.d.cts +2 -2
  107. package/libs/components/nav/nav.d.ts +2 -2
  108. package/libs/components/nav/nav.js +3 -3
  109. package/libs/components/text/text.cjs +5 -5
  110. package/libs/components/text/text.d.cts +2 -2
  111. package/libs/components/text/text.d.ts +2 -2
  112. package/libs/components/text/text.js +2 -2
  113. package/libs/heading-3648c538.d.ts +250 -0
  114. package/libs/hooks.cjs +7 -0
  115. package/libs/hooks.d.cts +5 -0
  116. package/libs/hooks.d.ts +5 -0
  117. package/libs/hooks.js +3 -0
  118. package/libs/icons.cjs +3 -3
  119. package/libs/icons.d.cts +1 -1
  120. package/libs/icons.d.ts +1 -1
  121. package/libs/icons.js +2 -2
  122. package/libs/index.cjs +112 -91
  123. package/libs/index.cjs.map +1 -1
  124. package/libs/index.css +1 -1
  125. package/libs/index.css.map +1 -1
  126. package/libs/index.d.cts +515 -31
  127. package/libs/index.d.ts +515 -31
  128. package/libs/index.js +31 -19
  129. package/libs/index.js.map +1 -1
  130. package/libs/ui-645f95b5.d.ts +285 -0
  131. package/package.json +2 -83
  132. package/src/components/README-UI.mdx +416 -0
  133. package/src/components/alert/ACCESSIBILITY.md +319 -0
  134. package/src/components/alert/README.mdx +475 -19
  135. package/src/components/alert/alert.scss +113 -6
  136. package/src/components/alert/alert.stories.tsx +372 -0
  137. package/src/components/alert/alert.test.tsx +762 -0
  138. package/src/components/alert/alert.tsx +331 -66
  139. package/src/components/alert/views/alert-actions.tsx +13 -0
  140. package/src/components/alert/views/alert-content.tsx +17 -0
  141. package/src/components/alert/views/alert-icon.tsx +53 -0
  142. package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
  143. package/src/components/alert/views/alert-title.tsx +23 -0
  144. package/src/components/alert/views/alert-view.tsx +158 -0
  145. package/src/components/alert/views/index.ts +12 -0
  146. package/src/components/badge/badge.mdx +186 -49
  147. package/src/components/badge/badge.scss +20 -2
  148. package/src/components/badge/badge.stories.tsx +160 -14
  149. package/src/components/badge/badge.test.tsx +179 -0
  150. package/src/components/badge/badge.tsx +97 -4
  151. package/src/components/breadcrumbs/README.mdx +364 -45
  152. package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
  153. package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
  154. package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
  155. package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
  156. package/src/components/buttons/button.scss +34 -31
  157. package/src/components/buttons/button.stories.tsx +35 -0
  158. package/src/components/cards/README.mdx +657 -0
  159. package/src/components/cards/card.scss +22 -0
  160. package/src/components/cards/card.stories.tsx +167 -5
  161. package/src/components/cards/card.test.tsx +360 -20
  162. package/src/components/cards/card.tsx +200 -79
  163. package/src/components/cards/card.types.ts +135 -0
  164. package/src/components/cards/card.utils.ts +79 -0
  165. package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
  166. package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
  167. package/src/components/details/README.mdx +437 -69
  168. package/src/components/details/details.scss +16 -7
  169. package/src/components/details/details.test.tsx +385 -0
  170. package/src/components/details/details.tsx +101 -69
  171. package/src/components/details/details.types.ts +76 -0
  172. package/src/components/dialog/README.mdx +513 -110
  173. package/src/components/dialog/dialog-modal.tsx +79 -56
  174. package/src/components/dialog/dialog.scss +53 -3
  175. package/src/components/dialog/dialog.stories.tsx +10 -7
  176. package/src/components/dialog/dialog.test.tsx +450 -0
  177. package/src/components/dialog/dialog.tsx +69 -59
  178. package/src/components/dialog/dialog.types.ts +133 -0
  179. package/src/components/dialog/views/dialog-footer.tsx +54 -11
  180. package/src/components/dialog/views/dialog-header.tsx +20 -15
  181. package/src/components/heading/heading.stories.tsx +44 -4
  182. package/src/components/heading/heading.tsx +89 -23
  183. package/src/components/icons/README.mdx +332 -0
  184. package/src/components/icons/icon.stories.tsx +74 -1
  185. package/src/components/icons/icon.tsx +89 -1
  186. package/src/components/icons/types.ts +47 -0
  187. package/src/components/images/README.mdx +340 -24
  188. package/src/components/images/img.scss +19 -3
  189. package/src/components/images/img.stories.tsx +424 -15
  190. package/src/components/images/img.test.tsx +354 -25
  191. package/src/components/images/img.tsx +186 -63
  192. package/src/components/images/img.types.ts +211 -0
  193. package/src/components/title/MIGRATION.md +199 -0
  194. package/src/components/title/README.md +326 -0
  195. package/src/components/title/README.mdx +452 -0
  196. package/src/components/title/title.stories.tsx +393 -0
  197. package/src/components/title/title.test.tsx +251 -0
  198. package/src/components/title/title.tsx +219 -0
  199. package/src/components/ui.stories.tsx +894 -0
  200. package/src/components/ui.test.tsx +559 -0
  201. package/src/components/ui.tsx +266 -15
  202. package/src/components/word-count/README.md +240 -0
  203. package/src/hooks.ts +1 -0
  204. package/src/index.ts +10 -2
  205. package/src/sass/_properties.scss +1 -0
  206. package/src/styles/alert/alert.css +94 -4
  207. package/src/styles/alert/alert.css.map +1 -1
  208. package/src/styles/badge/badge.css +20 -2
  209. package/src/styles/badge/badge.css.map +1 -1
  210. package/src/styles/buttons/button.css +31 -31
  211. package/src/styles/buttons/button.css.map +1 -1
  212. package/src/styles/cards/card.css +16 -0
  213. package/src/styles/cards/card.css.map +1 -1
  214. package/src/styles/details/details.css +19 -8
  215. package/src/styles/details/details.css.map +1 -1
  216. package/src/styles/dialog/dialog.css +43 -2
  217. package/src/styles/dialog/dialog.css.map +1 -1
  218. package/src/styles/images/img.css +15 -3
  219. package/src/styles/images/img.css.map +1 -1
  220. package/src/styles/index.css +240 -51
  221. package/src/styles/index.css.map +1 -1
  222. package/src/test/setup.d.ts +9 -0
  223. package/src/test/setup.ts +53 -1
  224. package/libs/chunk-6TE5QEVE.cjs +0 -13
  225. package/libs/chunk-6TE5QEVE.cjs.map +0 -1
  226. package/libs/chunk-7K76RW2A.cjs +0 -18
  227. package/libs/chunk-7K76RW2A.cjs.map +0 -1
  228. package/libs/chunk-BSPKFLO4.js +0 -8
  229. package/libs/chunk-BSPKFLO4.js.map +0 -1
  230. package/libs/chunk-BV5CLH44.cjs +0 -18
  231. package/libs/chunk-BV5CLH44.cjs.map +0 -1
  232. package/libs/chunk-DKGJHKGW.js +0 -9
  233. package/libs/chunk-DKGJHKGW.js.map +0 -1
  234. package/libs/chunk-ECLD37WN.cjs +0 -16
  235. package/libs/chunk-ECLD37WN.cjs.map +0 -1
  236. package/libs/chunk-HYBZBN4G.js +0 -8
  237. package/libs/chunk-HYBZBN4G.js.map +0 -1
  238. package/libs/chunk-KKLTUJFB.cjs.map +0 -1
  239. package/libs/chunk-M5QL5TAE.cjs +0 -14
  240. package/libs/chunk-M5QL5TAE.cjs.map +0 -1
  241. package/libs/chunk-NE6YXTMC.js +0 -7
  242. package/libs/chunk-NE6YXTMC.js.map +0 -1
  243. package/libs/chunk-O6QZBB6G.js.map +0 -1
  244. package/libs/chunk-SXVZSWX6.js +0 -11
  245. package/libs/chunk-SXVZSWX6.js.map +0 -1
  246. package/libs/ui-9a6f9f8d.d.ts +0 -24
  247. package/src/components/cards/README.md +0 -80
  248. package/src/components/dialog/hooks/useClickOutside.ts +0 -33
  249. /package/libs/{chunk-DV56L5YX.cjs.map → chunk-2LTJ7HHX.cjs.map} +0 -0
  250. /package/libs/{chunk-EQ67LF46.js.map → chunk-2Y7W75TT.js.map} +0 -0
  251. /package/libs/{chunk-X3EVB7VS.cjs.map → chunk-5S4ORA4C.cjs.map} +0 -0
  252. /package/libs/{chunk-6BVXFW7U.cjs.map → chunk-AHDJGCG5.cjs.map} +0 -0
  253. /package/libs/{chunk-E3XP6BEX.cjs.map → chunk-B7F5FS6D.cjs.map} +0 -0
  254. /package/libs/{chunk-LHVJKDMA.cjs.map → chunk-J32EZPYD.cjs.map} +0 -0
  255. /package/libs/{chunk-LL7HTLMS.cjs.map → chunk-M5RRNTVX.cjs.map} +0 -0
  256. /package/libs/{chunk-LIQJ7ZZR.js.map → chunk-NGTJDDFO.js.map} +0 -0
  257. /package/libs/{chunk-QCMV4VQZ.js.map → chunk-QLZWHAMK.js.map} +0 -0
  258. /package/libs/{chunk-BIP2NY53.js.map → chunk-RIVUMPOG.js.map} +0 -0
  259. /package/libs/{chunk-ICCKQ2GC.cjs.map → chunk-ROZI23GS.cjs.map} +0 -0
  260. /package/libs/{chunk-NHYXGV3L.js.map → chunk-SMYRLO3E.js.map} +0 -0
  261. /package/libs/{chunk-5ZM4XL44.js.map → chunk-TYRCEX2L.js.map} +0 -0
  262. /package/libs/{chunk-PPOOBUOS.js.map → chunk-XBA562WW.js.map} +0 -0
  263. /package/libs/{chunk-QVV34QEH.cjs.map → chunk-XTQKWY7W.cjs.map} +0 -0
  264. /package/libs/{chunk-YWOYVRFT.js.map → chunk-ZANSFMTD.js.map} +0 -0
@@ -0,0 +1,762 @@
1
+ import React from 'react';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { render, screen, waitFor, act } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import Alert from './alert';
6
+
7
+ describe('Alert', () => {
8
+ describe('Rendering', () => {
9
+ it('should render alert with title and message', () => {
10
+ render(
11
+ <Alert open={true} title="Test Title" severity="info">
12
+ Test message
13
+ </Alert>
14
+ );
15
+
16
+ expect(screen.getByRole('alert')).toBeInTheDocument();
17
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
18
+ expect(screen.getByText('Test message')).toBeInTheDocument();
19
+ });
20
+
21
+ it('should not render when open is false', () => {
22
+ render(
23
+ <Alert open={false} severity="info">
24
+ Test message
25
+ </Alert>
26
+ );
27
+
28
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
29
+ });
30
+
31
+ it('should render all severity levels correctly', () => {
32
+ const severities = ['default', 'info', 'success', 'warning', 'error'] as const;
33
+
34
+ severities.forEach((severity) => {
35
+ const { unmount } = render(
36
+ <Alert open={true} severity={severity}>
37
+ {severity} message
38
+ </Alert>
39
+ );
40
+
41
+ const alert = screen.getByRole('alert');
42
+ expect(alert).toHaveAttribute('data-alert', severity);
43
+ unmount();
44
+ });
45
+ });
46
+
47
+ it('should hide icon when hideIcon is true', () => {
48
+ const { container } = render(
49
+ <Alert open={true} severity="info" hideIcon={true}>
50
+ Test message
51
+ </Alert>
52
+ );
53
+
54
+ expect(container.querySelector('.alert-icon')).not.toBeInTheDocument();
55
+ });
56
+
57
+ it('should show icon by default', () => {
58
+ const { container } = render(
59
+ <Alert open={true} severity="info">
60
+ Test message
61
+ </Alert>
62
+ );
63
+
64
+ expect(container.querySelector('.alert-icon')).toBeInTheDocument();
65
+ });
66
+
67
+ it('should render with custom icon size', () => {
68
+ const { container } = render(
69
+ <Alert open={true} severity="info" iconSize={32}>
70
+ Test message
71
+ </Alert>
72
+ );
73
+
74
+ const icon = container.querySelector('.alert-icon svg');
75
+ expect(icon).toBeInTheDocument();
76
+ // Note: Actual size verification would require checking the SVG element's attributes
77
+ // which depends on the Icon component implementation
78
+ });
79
+
80
+ it('should use default icon size of 24px when iconSize not specified', () => {
81
+ const { container } = render(
82
+ <Alert open={true} severity="info">
83
+ Test message
84
+ </Alert>
85
+ );
86
+
87
+ const iconContainer = container.querySelector('.alert-icon');
88
+ expect(iconContainer).toBeInTheDocument();
89
+ });
90
+
91
+ it('should render actions when provided', () => {
92
+ render(
93
+ <Alert
94
+ open={true}
95
+ severity="info"
96
+ actions={
97
+ <>
98
+ <button>Undo</button>
99
+ <button>Dismiss</button>
100
+ </>
101
+ }
102
+ >
103
+ Test message
104
+ </Alert>
105
+ );
106
+
107
+ expect(screen.getByText('Undo')).toBeInTheDocument();
108
+ expect(screen.getByText('Dismiss')).toBeInTheDocument();
109
+ });
110
+
111
+ it('should render dismiss button when dismissible is true', () => {
112
+ render(
113
+ <Alert open={true} severity="info" dismissible={true}>
114
+ Test message
115
+ </Alert>
116
+ );
117
+
118
+ expect(screen.getByRole('button', { name: /close alert/i })).toBeInTheDocument();
119
+ });
120
+
121
+ it('should apply correct variant attribute', () => {
122
+ const variants = ['outlined', 'filled', 'soft'] as const;
123
+
124
+ variants.forEach((variant) => {
125
+ const { unmount } = render(
126
+ <Alert open={true} severity="info" variant={variant}>
127
+ Test message
128
+ </Alert>
129
+ );
130
+
131
+ const alert = screen.getByRole('alert');
132
+ expect(alert).toHaveAttribute('data-variant', variant);
133
+ unmount();
134
+ });
135
+ });
136
+ });
137
+
138
+ describe('Interactions', () => {
139
+ it('should call onDismiss when dismiss button is clicked', async () => {
140
+ const user = userEvent.setup();
141
+ const onDismiss = vi.fn();
142
+
143
+ render(
144
+ <Alert open={true} severity="info" dismissible={true} onDismiss={onDismiss}>
145
+ Test message
146
+ </Alert>
147
+ );
148
+
149
+ const dismissButton = screen.getByRole('button', { name: /close alert/i });
150
+ await user.click(dismissButton);
151
+
152
+ // Wait for animation timeout (300ms)
153
+ await waitFor(() => {
154
+ expect(onDismiss).toHaveBeenCalledTimes(1);
155
+ }, { timeout: 500 });
156
+ });
157
+
158
+ it('should dismiss alert when ESC key is pressed', async () => {
159
+ const user = userEvent.setup();
160
+ const onDismiss = vi.fn();
161
+
162
+ render(
163
+ <Alert open={true} severity="info" dismissible={true} onDismiss={onDismiss}>
164
+ Test message
165
+ </Alert>
166
+ );
167
+
168
+ await user.keyboard('{Escape}');
169
+
170
+ // Wait for animation timeout
171
+ await waitFor(() => {
172
+ expect(onDismiss).toHaveBeenCalledTimes(1);
173
+ }, { timeout: 500 });
174
+ });
175
+
176
+ it('should not dismiss with ESC key when not dismissible', async () => {
177
+ const user = userEvent.setup();
178
+ const onDismiss = vi.fn();
179
+
180
+ render(
181
+ <Alert open={true} severity="info" dismissible={false} onDismiss={onDismiss}>
182
+ Test message
183
+ </Alert>
184
+ );
185
+
186
+ await user.keyboard('{Escape}');
187
+
188
+ // Wait a bit to ensure it doesn't dismiss
189
+ await new Promise(resolve => setTimeout(resolve, 100));
190
+ expect(onDismiss).not.toHaveBeenCalled();
191
+ });
192
+
193
+ it('should set data-visible to false when dismissing', async () => {
194
+ const user = userEvent.setup();
195
+
196
+ render(
197
+ <Alert open={true} severity="info" dismissible={true}>
198
+ Test message
199
+ </Alert>
200
+ );
201
+
202
+ const alert = screen.getByRole('alert');
203
+ expect(alert).toHaveAttribute('data-visible', 'true');
204
+
205
+ const dismissButton = screen.getByRole('button', { name: /close alert/i });
206
+ await user.click(dismissButton);
207
+
208
+ // Check that data-visible changes immediately
209
+ expect(alert).toHaveAttribute('data-visible', 'false');
210
+ });
211
+ });
212
+
213
+ describe('Auto-dismiss', () => {
214
+ beforeEach(() => {
215
+ vi.useFakeTimers();
216
+ });
217
+
218
+ afterEach(() => {
219
+ vi.useRealTimers();
220
+ vi.restoreAllMocks();
221
+ });
222
+
223
+ it('should auto-dismiss after specified duration', async () => {
224
+ const onDismiss = vi.fn();
225
+
226
+ render(
227
+ <Alert
228
+ open={true}
229
+ severity="success"
230
+ dismissible={true}
231
+ autoHideDuration={3000}
232
+ onDismiss={onDismiss}
233
+ >
234
+ Auto-dismiss message
235
+ </Alert>
236
+ );
237
+
238
+ // Fast-forward time by 3000ms (auto-hide duration)
239
+ act(() => {
240
+ vi.advanceTimersByTime(3000);
241
+ });
242
+
243
+ // Fast-forward by 300ms for animation
244
+ await act(async () => {
245
+ vi.advanceTimersByTime(300);
246
+ });
247
+
248
+ expect(onDismiss).toHaveBeenCalledTimes(1);
249
+ });
250
+
251
+ it('should not auto-dismiss when autoHideDuration is 0', async () => {
252
+ const onDismiss = vi.fn();
253
+
254
+ render(
255
+ <Alert
256
+ open={true}
257
+ severity="success"
258
+ autoHideDuration={0}
259
+ onDismiss={onDismiss}
260
+ >
261
+ No auto-dismiss
262
+ </Alert>
263
+ );
264
+
265
+ vi.advanceTimersByTime(5000);
266
+
267
+ expect(onDismiss).not.toHaveBeenCalled();
268
+ });
269
+
270
+ it('should not auto-dismiss when autoHideDuration is undefined', async () => {
271
+ const onDismiss = vi.fn();
272
+
273
+ render(
274
+ <Alert
275
+ open={true}
276
+ severity="success"
277
+ onDismiss={onDismiss}
278
+ >
279
+ No auto-dismiss
280
+ </Alert>
281
+ );
282
+
283
+ vi.advanceTimersByTime(5000);
284
+
285
+ expect(onDismiss).not.toHaveBeenCalled();
286
+ });
287
+ });
288
+
289
+ describe('Accessibility', () => {
290
+ it('should have correct aria-live for error severity', () => {
291
+ render(
292
+ <Alert open={true} severity="error">
293
+ Error message
294
+ </Alert>
295
+ );
296
+
297
+ const alert = screen.getByRole('alert');
298
+ expect(alert).toHaveAttribute('aria-live', 'assertive');
299
+ });
300
+
301
+ it('should have correct aria-live for non-error severities', () => {
302
+ const severities = ['default', 'info', 'success', 'warning'] as const;
303
+
304
+ severities.forEach((severity) => {
305
+ const { unmount } = render(
306
+ <Alert open={true} severity={severity}>
307
+ Test message
308
+ </Alert>
309
+ );
310
+
311
+ const alert = screen.getByRole('alert');
312
+ expect(alert).toHaveAttribute('aria-live', 'polite');
313
+ unmount();
314
+ });
315
+ });
316
+
317
+ it('should have aria-atomic attribute', () => {
318
+ render(
319
+ <Alert open={true} severity="info">
320
+ Test message
321
+ </Alert>
322
+ );
323
+
324
+ const alert = screen.getByRole('alert');
325
+ expect(alert).toHaveAttribute('aria-atomic', 'true');
326
+ });
327
+
328
+ it('should have role="alert"', () => {
329
+ render(
330
+ <Alert open={true} severity="info">
331
+ Test message
332
+ </Alert>
333
+ );
334
+
335
+ expect(screen.getByRole('alert')).toBeInTheDocument();
336
+ });
337
+
338
+ it('should focus alert when autoFocus is true', () => {
339
+ render(
340
+ <Alert open={true} severity="error" autoFocus={true}>
341
+ Critical alert
342
+ </Alert>
343
+ );
344
+
345
+ const alert = screen.getByRole('alert');
346
+ expect(alert).toHaveFocus();
347
+ });
348
+
349
+ it('should not focus alert when autoFocus is false', () => {
350
+ render(
351
+ <Alert open={true} severity="info" autoFocus={false}>
352
+ Normal alert
353
+ </Alert>
354
+ );
355
+
356
+ const alert = screen.getByRole('alert');
357
+ expect(alert).not.toHaveFocus();
358
+ });
359
+
360
+ it('should have tabIndex=-1 when autoFocus is true', () => {
361
+ render(
362
+ <Alert open={true} severity="error" autoFocus={true}>
363
+ Critical alert
364
+ </Alert>
365
+ );
366
+
367
+ const alert = screen.getByRole('alert');
368
+ expect(alert).toHaveAttribute('tabIndex', '-1');
369
+ });
370
+ });
371
+
372
+ describe('Animation State', () => {
373
+ it('should start with data-visible=true when open=true', () => {
374
+ render(
375
+ <Alert open={true} severity="info">
376
+ Test message
377
+ </Alert>
378
+ );
379
+
380
+ const alert = screen.getByRole('alert');
381
+ expect(alert).toHaveAttribute('data-visible', 'true');
382
+ });
383
+
384
+ it('should transition visibility states when dismissing', async () => {
385
+ const user = userEvent.setup();
386
+
387
+ render(
388
+ <Alert open={true} severity="info" dismissible={true}>
389
+ Test message
390
+ </Alert>
391
+ );
392
+
393
+ const alert = screen.getByRole('alert');
394
+ expect(alert).toHaveAttribute('data-visible', 'true');
395
+
396
+ const dismissButton = screen.getByRole('button', { name: /close alert/i });
397
+ await user.click(dismissButton);
398
+
399
+ // After click, visibility should be false but component still rendered
400
+ expect(alert).toHaveAttribute('data-visible', 'false');
401
+ expect(screen.getByRole('alert')).toBeInTheDocument();
402
+ });
403
+ });
404
+
405
+ describe('Phase 4: WCAG 2.1 Accessibility', () => {
406
+ describe('Severity Text for Screen Readers', () => {
407
+ it('should include visually hidden severity text for info', () => {
408
+ const { container } = render(
409
+ <Alert open={true} severity="info">
410
+ Test message
411
+ </Alert>
412
+ );
413
+
414
+ const srOnlyText = container.querySelector('.sr-only');
415
+ expect(srOnlyText).toBeInTheDocument();
416
+ expect(srOnlyText).toHaveTextContent('Information:');
417
+ });
418
+
419
+ it('should include visually hidden severity text for success', () => {
420
+ const { container } = render(
421
+ <Alert open={true} severity="success">
422
+ Test message
423
+ </Alert>
424
+ );
425
+
426
+ const srOnlyText = container.querySelector('.sr-only');
427
+ expect(srOnlyText).toHaveTextContent('Success:');
428
+ });
429
+
430
+ it('should include visually hidden severity text for warning', () => {
431
+ const { container } = render(
432
+ <Alert open={true} severity="warning">
433
+ Test message
434
+ </Alert>
435
+ );
436
+
437
+ const srOnlyText = container.querySelector('.sr-only');
438
+ expect(srOnlyText).toHaveTextContent('Warning:');
439
+ });
440
+
441
+ it('should include visually hidden severity text for error', () => {
442
+ const { container } = render(
443
+ <Alert open={true} severity="error">
444
+ Test message
445
+ </Alert>
446
+ );
447
+
448
+ const srOnlyText = container.querySelector('.sr-only');
449
+ expect(srOnlyText).toHaveTextContent('Error:');
450
+ });
451
+
452
+ it('should not include severity text for default severity', () => {
453
+ const { container } = render(
454
+ <Alert open={true} severity="default">
455
+ Test message
456
+ </Alert>
457
+ );
458
+
459
+ const srOnlyText = container.querySelector('.sr-only');
460
+ expect(srOnlyText).not.toBeInTheDocument();
461
+ });
462
+ });
463
+
464
+ describe('Configurable Heading Level', () => {
465
+ it('should render h2 when titleLevel is 2', () => {
466
+ const { container } = render(
467
+ <Alert open={true} severity="info" title="Test Title" titleLevel={2}>
468
+ Test message
469
+ </Alert>
470
+ );
471
+
472
+ const heading = container.querySelector('h2.alert-title');
473
+ expect(heading).toBeInTheDocument();
474
+ expect(heading).toHaveTextContent('Test Title');
475
+ });
476
+
477
+ it('should render h3 when titleLevel is 3', () => {
478
+ const { container } = render(
479
+ <Alert open={true} severity="info" title="Test Title" titleLevel={3}>
480
+ Test message
481
+ </Alert>
482
+ );
483
+
484
+ const heading = container.querySelector('h3.alert-title');
485
+ expect(heading).toBeInTheDocument();
486
+ expect(heading).toHaveTextContent('Test Title');
487
+ });
488
+
489
+ it('should render h4 when titleLevel is 4', () => {
490
+ const { container } = render(
491
+ <Alert open={true} severity="info" title="Test Title" titleLevel={4}>
492
+ Test message
493
+ </Alert>
494
+ );
495
+
496
+ const heading = container.querySelector('h4.alert-title');
497
+ expect(heading).toBeInTheDocument();
498
+ });
499
+
500
+ it('should render h5 when titleLevel is 5', () => {
501
+ const { container } = render(
502
+ <Alert open={true} severity="info" title="Test Title" titleLevel={5}>
503
+ Test message
504
+ </Alert>
505
+ );
506
+
507
+ const heading = container.querySelector('h5.alert-title');
508
+ expect(heading).toBeInTheDocument();
509
+ });
510
+
511
+ it('should render h6 when titleLevel is 6', () => {
512
+ const { container } = render(
513
+ <Alert open={true} severity="info" title="Test Title" titleLevel={6}>
514
+ Test message
515
+ </Alert>
516
+ );
517
+
518
+ const heading = container.querySelector('h6.alert-title');
519
+ expect(heading).toBeInTheDocument();
520
+ });
521
+
522
+ it('should render default heading element when titleLevel is undefined', () => {
523
+ const { container } = render(
524
+ <Alert open={true} severity="info" title="Test Title">
525
+ Test message
526
+ </Alert>
527
+ );
528
+
529
+ // The UI component wraps the title, check that title is rendered
530
+ const titleElement = container.querySelector('.alert-title');
531
+ expect(titleElement).toBeInTheDocument();
532
+ expect(titleElement).toHaveTextContent('Test Title');
533
+ // The element should be rendered (exact tag depends on UI component implementation)
534
+ });
535
+
536
+ it('should not render title element when title prop is not provided', () => {
537
+ const { container } = render(
538
+ <Alert open={true} severity="info">
539
+ Test message
540
+ </Alert>
541
+ );
542
+
543
+ expect(container.querySelector('.alert-title')).not.toBeInTheDocument();
544
+ });
545
+
546
+ it('should apply alert-title class to all heading levels', () => {
547
+ const levels = [2, 3, 4, 5, 6] as const;
548
+
549
+ levels.forEach((level) => {
550
+ const { container, unmount } = render(
551
+ <Alert open={true} severity="info" title="Test" titleLevel={level}>
552
+ Message
553
+ </Alert>
554
+ );
555
+
556
+ const heading = container.querySelector(`h${level}`);
557
+ expect(heading).toHaveClass('alert-title');
558
+ unmount();
559
+ });
560
+ });
561
+ });
562
+
563
+ describe('Pause on Hover/Focus', () => {
564
+ it('should have mouse enter and leave handlers when pauseOnHover is true', () => {
565
+ render(
566
+ <Alert
567
+ open={true}
568
+ severity="info"
569
+ autoHideDuration={3000}
570
+ pauseOnHover={true}
571
+ >
572
+ Test message
573
+ </Alert>
574
+ );
575
+
576
+ const alert = screen.getByRole('alert');
577
+
578
+ // Verify the handlers are attached by checking we can dispatch events without errors
579
+ expect(() => {
580
+ alert.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
581
+ alert.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
582
+ }).not.toThrow();
583
+ });
584
+
585
+ it('should have focus and blur handlers when pauseOnHover is true', () => {
586
+ render(
587
+ <Alert
588
+ open={true}
589
+ severity="info"
590
+ autoHideDuration={3000}
591
+ autoFocus={true}
592
+ pauseOnHover={true}
593
+ >
594
+ Test message
595
+ </Alert>
596
+ );
597
+
598
+ const alert = screen.getByRole('alert');
599
+
600
+ // Verify the handlers are attached
601
+ expect(() => {
602
+ alert.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
603
+ alert.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
604
+ }).not.toThrow();
605
+ });
606
+
607
+ it('should accept pauseOnHover prop with default true', () => {
608
+ const { rerender } = render(
609
+ <Alert open={true} severity="info" autoHideDuration={3000}>
610
+ Test message
611
+ </Alert>
612
+ );
613
+
614
+ // Just verify component renders without issues
615
+ expect(screen.getByRole('alert')).toBeInTheDocument();
616
+
617
+ rerender(
618
+ <Alert open={true} severity="info" autoHideDuration={3000} pauseOnHover={false}>
619
+ Test message
620
+ </Alert>
621
+ );
622
+
623
+ expect(screen.getByRole('alert')).toBeInTheDocument();
624
+ });
625
+ });
626
+
627
+ describe('Touch Target Size', () => {
628
+ it('should apply alert-dismiss class to dismiss button', () => {
629
+ const { container } = render(
630
+ <Alert open={true} severity="info" dismissible={true}>
631
+ Test message
632
+ </Alert>
633
+ );
634
+
635
+ const dismissButton = container.querySelector('.alert-dismiss');
636
+ expect(dismissButton).toBeInTheDocument();
637
+ });
638
+ });
639
+
640
+ describe('Focus Indicators', () => {
641
+ it('should be focusable when autoFocus is true', () => {
642
+ render(
643
+ <Alert open={true} severity="info" autoFocus={true}>
644
+ Test message
645
+ </Alert>
646
+ );
647
+
648
+ const alert = screen.getByRole('alert');
649
+ expect(alert).toHaveAttribute('tabIndex', '-1');
650
+ });
651
+
652
+ it('should not have tabIndex when autoFocus is false', () => {
653
+ render(
654
+ <Alert open={true} severity="info" autoFocus={false}>
655
+ Test message
656
+ </Alert>
657
+ );
658
+
659
+ const alert = screen.getByRole('alert');
660
+ expect(alert).not.toHaveAttribute('tabIndex');
661
+ });
662
+ });
663
+
664
+ describe('Content Type', () => {
665
+ it('should wrap children in paragraph tag when contentType is "text" (default)', () => {
666
+ const { container } = render(
667
+ <Alert open={true} severity="info">
668
+ Simple text content
669
+ </Alert>
670
+ );
671
+
672
+ const paragraph = container.querySelector('.alert-message p');
673
+ expect(paragraph).toBeInTheDocument();
674
+ expect(paragraph).toHaveTextContent('Simple text content');
675
+ });
676
+
677
+ it('should wrap children in paragraph tag when contentType is explicitly set to "text"', () => {
678
+ const { container } = render(
679
+ <Alert open={true} severity="warning" contentType="text">
680
+ Explicit text content
681
+ </Alert>
682
+ );
683
+
684
+ const paragraph = container.querySelector('.alert-message p');
685
+ expect(paragraph).toBeInTheDocument();
686
+ expect(paragraph).toHaveTextContent('Explicit text content');
687
+ });
688
+
689
+ it('should render children directly without paragraph wrapper when contentType is "node"', () => {
690
+ const { container } = render(
691
+ <Alert open={true} severity="error" contentType="node">
692
+ <div className="custom-content">Custom layout</div>
693
+ </Alert>
694
+ );
695
+
696
+ // Should not have a paragraph wrapper
697
+ const paragraph = container.querySelector('.alert-message > p');
698
+ expect(paragraph).not.toBeInTheDocument();
699
+
700
+ // Should have direct custom content
701
+ const customContent = container.querySelector('.alert-message .custom-content');
702
+ expect(customContent).toBeInTheDocument();
703
+ expect(customContent).toHaveTextContent('Custom layout');
704
+ });
705
+
706
+ it('should render complex content with lists when contentType is "node"', () => {
707
+ const { container } = render(
708
+ <Alert open={true} severity="warning" contentType="node">
709
+ <ul>
710
+ <li>First item</li>
711
+ <li>Second item</li>
712
+ <li>Third item</li>
713
+ </ul>
714
+ </Alert>
715
+ );
716
+
717
+ const list = container.querySelector('.alert-message ul');
718
+ expect(list).toBeInTheDocument();
719
+
720
+ const listItems = container.querySelectorAll('.alert-message li');
721
+ expect(listItems).toHaveLength(3);
722
+ expect(listItems[0]).toHaveTextContent('First item');
723
+ expect(listItems[1]).toHaveTextContent('Second item');
724
+ expect(listItems[2]).toHaveTextContent('Third item');
725
+ });
726
+
727
+ it('should render multiple child elements when contentType is "node"', () => {
728
+ const { container } = render(
729
+ <Alert open={true} severity="success" contentType="node">
730
+ <p>First paragraph</p>
731
+ <p>Second paragraph</p>
732
+ <div>Additional content</div>
733
+ </Alert>
734
+ );
735
+
736
+ const messageDiv = container.querySelector('.alert-message');
737
+ const paragraphs = messageDiv?.querySelectorAll('p') || [];
738
+ const divs = messageDiv?.querySelectorAll('div') || [];
739
+
740
+ expect(paragraphs.length).toBeGreaterThanOrEqual(2);
741
+ expect(divs.length).toBeGreaterThanOrEqual(1);
742
+ });
743
+
744
+ it('should maintain accessibility with contentType="node"', () => {
745
+ render(
746
+ <Alert open={true} severity="info" contentType="node">
747
+ <div>
748
+ <p>Complex content structure</p>
749
+ <ul>
750
+ <li>Item 1</li>
751
+ </ul>
752
+ </div>
753
+ </Alert>
754
+ );
755
+
756
+ const alert = screen.getByRole('alert');
757
+ expect(alert).toHaveAttribute('aria-live', 'polite');
758
+ expect(alert).toHaveAttribute('aria-atomic', 'true');
759
+ });
760
+ });
761
+ });
762
+ });