@pnx-mixtape/mxds 0.1.0-rc.4 → 0.1.0-rc.6

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 (236) hide show
  1. package/.claude/skills/create-mxds-component/SKILL.md +77 -0
  2. package/DESIGN.md +595 -0
  3. package/README.md +2 -3
  4. package/dist/build/accordion.entry.js +2 -1
  5. package/dist/build/autocomplete.entry.js +1 -1
  6. package/dist/build/back-to-top.css +1 -0
  7. package/dist/build/back-to-top.entry.js +1 -0
  8. package/dist/build/base.css +1 -1
  9. package/dist/build/button.css +1 -1
  10. package/dist/build/card.css +1 -1
  11. package/dist/build/chunks/Accordion-Bqe8vTNR.js +44 -0
  12. package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
  13. package/dist/build/chunks/AccordionMobile-D517oXYZ.js +26 -0
  14. package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
  15. package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
  16. package/dist/build/chunks/ClosableAlert-CZ114JsL.js +55 -0
  17. package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
  18. package/dist/build/chunks/Dialog-DoYcGqhW.js +48 -0
  19. package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
  20. package/dist/build/chunks/FeedbackForm-BsL7fv8s.js +64 -0
  21. package/dist/build/chunks/Gallery-DqJPujX5.js +165 -0
  22. package/dist/build/chunks/GlobalToggle-7H3F1Wvc.js +82 -0
  23. package/dist/build/chunks/InPageNavigation-D7m3IN89.js +86 -0
  24. package/dist/build/chunks/Navigation-CRModT40.js +142 -0
  25. package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
  26. package/dist/build/chunks/Sticky-C1H8MNqK.js +51 -0
  27. package/dist/build/chunks/Tabs-BJD3kABL.js +160 -0
  28. package/dist/build/chunks/Translate-yAgySwet.js +46 -0
  29. package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
  30. package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
  31. package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
  32. package/dist/build/chunks/utilities-B9mNRs7V.js +255 -0
  33. package/dist/build/content-block.css +1 -1
  34. package/dist/build/cookie-consent.entry.js +1 -1
  35. package/dist/build/dialog.entry.js +1 -1
  36. package/dist/build/drop-menu.entry.js +1 -1
  37. package/dist/build/elements.js +147 -1
  38. package/dist/build/feedback-form.css +1 -0
  39. package/dist/build/feedback-form.entry.js +1 -0
  40. package/dist/build/filters.entry.js +145 -1
  41. package/dist/build/form.css +1 -1
  42. package/dist/build/gallery.entry.js +1 -1
  43. package/dist/build/global-alert.entry.js +1 -1
  44. package/dist/build/header.css +1 -1
  45. package/dist/build/header.entry.js +1 -1
  46. package/dist/build/icon.css +1 -1
  47. package/dist/build/in-page-navigation.entry.js +1 -1
  48. package/dist/build/navigation.css +1 -1
  49. package/dist/build/navigation.entry.js +1 -1
  50. package/dist/build/pagination.css +1 -1
  51. package/dist/build/popover.entry.js +1 -1
  52. package/dist/build/react.js +2291 -1
  53. package/dist/build/side-navigation.css +1 -1
  54. package/dist/build/sidebar.css +1 -1
  55. package/dist/build/sticky.entry.js +1 -1
  56. package/dist/build/tabs.entry.js +1 -1
  57. package/dist/build/translate.css +1 -0
  58. package/dist/build/translate.entry.js +1 -0
  59. package/dist/build/utility-list.entry.js +53 -1
  60. package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
  61. package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
  62. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
  63. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
  64. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
  65. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
  66. package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
  67. package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
  68. package/dist/types/Component/Navigation/Elements/Navigation.d.ts +2 -3
  69. package/dist/types/Component/Navigation/Elements/Navigation.d.ts.map +1 -1
  70. package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts +3 -3
  71. package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts.map +1 -1
  72. package/dist/types/Component/SideNavigation/SideNavigation.d.ts +3 -1
  73. package/dist/types/Component/SideNavigation/SideNavigation.d.ts.map +1 -1
  74. package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
  75. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  76. package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
  77. package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
  78. package/dist/types/Component/Translate/Translate.d.ts +37 -0
  79. package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
  80. package/dist/types/Component/Translate/languages.d.ts +11 -0
  81. package/dist/types/Component/Translate/languages.d.ts.map +1 -0
  82. package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
  83. package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
  84. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
  85. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
  86. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
  87. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
  88. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
  89. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
  90. package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
  91. package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
  92. package/dist/types/Utility/utilities.d.ts +2 -0
  93. package/dist/types/Utility/utilities.d.ts.map +1 -1
  94. package/dist/types/elements.d.ts +3 -0
  95. package/dist/types/elements.d.ts.map +1 -1
  96. package/dist/types/enums.d.ts +3 -1
  97. package/dist/types/enums.d.ts.map +1 -1
  98. package/dist/types/react.d.ts +3 -0
  99. package/dist/types/react.d.ts.map +1 -1
  100. package/dist/types/tokens.d.ts +1 -0
  101. package/dist/types/tokens.d.ts.map +1 -1
  102. package/docs/API.md +1215 -0
  103. package/docs/CSSGuidelines.md +893 -0
  104. package/docs/CodeQuality.md +940 -0
  105. package/docs/ComponentArchitecture.md +714 -0
  106. package/docs/Contributing.md +626 -0
  107. package/docs/MemoryManagement.md +598 -0
  108. package/docs/Migrations.md +6 -0
  109. package/docs/README.md +100 -0
  110. package/docs/Security.md +444 -0
  111. package/docs/Testing.md +739 -0
  112. package/docs/Troubleshooting.md +674 -0
  113. package/package.json +9 -6
  114. package/src/Atom/Button/Button.tsx +1 -1
  115. package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
  116. package/src/Atom/Button/_buttons.css +1 -0
  117. package/src/Atom/Icon/README.md +1 -1
  118. package/src/Atom/Icon/_icon.css +1 -0
  119. package/src/Atom/Icon/images/thumb.svg +1 -0
  120. package/src/Atom/Link/_links.css +1 -0
  121. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
  122. package/src/Atom/Media/media.twig +4 -5
  123. package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
  124. package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
  125. package/src/Component/BackToTop/BackToTop.tsx +120 -0
  126. package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
  127. package/src/Component/BackToTop/README.md +164 -0
  128. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
  129. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
  130. package/src/Component/BackToTop/back-to-top.css +70 -0
  131. package/src/Component/BackToTop/back-to-top.entry.js +1 -0
  132. package/src/Component/BackToTop/back-to-top.twig +26 -0
  133. package/src/Component/Card/Components/CardContent.tsx +1 -1
  134. package/src/Component/Card/Components/CardMedia.tsx +1 -1
  135. package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
  136. package/src/Component/Card/card.css +1 -1
  137. package/src/Component/Carousel/Carousel.stories.ts +66 -36
  138. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
  139. package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
  140. package/src/Component/ContentBlock/content-block.css +1 -1
  141. package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
  142. package/src/Component/DropMenu/DropMenu.tsx +14 -4
  143. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
  144. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
  145. package/src/Component/DropMenu/drop-menu.twig +7 -1
  146. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
  147. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
  148. package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
  149. package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
  150. package/src/Component/Navigation/Elements/Navigation.ts +25 -29
  151. package/src/Component/Navigation/README.md +11 -10
  152. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +8 -8
  153. package/src/Component/Navigation/_navigation-dropdown.css +9 -11
  154. package/src/Component/Navigation/_navigation-mega.css +5 -11
  155. package/src/Component/Navigation/_navigation.css +5 -3
  156. package/src/Component/Navigation/menu.twig +1 -1
  157. package/src/Component/Navigation/navigation.twig +0 -1
  158. package/src/Component/Pagination/Pagination.stories.ts +57 -27
  159. package/src/Component/Pagination/pagination.css +16 -7
  160. package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
  161. package/src/Component/SideNavigation/Components/SideNavigationTree.tsx +68 -24
  162. package/src/Component/SideNavigation/README.md +78 -32
  163. package/src/Component/SideNavigation/SideNavigation.stories.ts +41 -2
  164. package/src/Component/SideNavigation/SideNavigation.stories.tsx +31 -0
  165. package/src/Component/SideNavigation/SideNavigation.tsx +13 -3
  166. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +87 -3
  167. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.tsx.snap +2 -0
  168. package/src/Component/SideNavigation/side-navigation.css +3 -3
  169. package/src/Component/SideNavigation/side-navigation.twig +10 -3
  170. package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
  171. package/src/Component/Tabs/Elements/Tabs.ts +13 -3
  172. package/src/Component/Tabs/Tabs.stories.ts +33 -12
  173. package/src/Component/Tabs/Tabs.stories.tsx +28 -14
  174. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
  175. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
  176. package/src/Component/Translate/Elements/Translate.ts +76 -0
  177. package/src/Component/Translate/README.md +285 -0
  178. package/src/Component/Translate/Translate.stories.ts +87 -0
  179. package/src/Component/Translate/Translate.stories.tsx +77 -0
  180. package/src/Component/Translate/Translate.tsx +213 -0
  181. package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
  182. package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
  183. package/src/Component/Translate/languages.ts +39 -0
  184. package/src/Component/Translate/translate.css +62 -0
  185. package/src/Component/Translate/translate.entry.js +1 -0
  186. package/src/Component/Translate/translate.twig +44 -0
  187. package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
  188. package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
  189. package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
  190. package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
  191. package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
  192. package/src/Form/FeedbackForm/README.md +233 -0
  193. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
  194. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
  195. package/src/Form/FeedbackForm/feedback-form.css +74 -0
  196. package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
  197. package/src/Form/FeedbackForm/feedback-form.twig +80 -0
  198. package/src/Form/form.css +17 -19
  199. package/src/Introduction.mdx +1 -1
  200. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +3 -3
  201. package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
  202. package/src/Layout/Header/Header.stories.ts +53 -48
  203. package/src/Layout/Header/Header.stories.tsx +35 -22
  204. package/src/Layout/Header/Header.tsx +2 -2
  205. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +18 -14
  206. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
  207. package/src/Layout/Header/_toggles.css +7 -7
  208. package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
  209. package/src/Layout/Sidebar/sidebar.css +10 -3
  210. package/src/Utility/Elements/google-translate-widget.ts +293 -0
  211. package/src/Utility/global.d.ts +4 -0
  212. package/src/Utility/utilities.ts +2 -0
  213. package/src/_custom-media.css +2 -0
  214. package/src/elements.ts +3 -0
  215. package/src/enums.ts +2 -0
  216. package/src/react.ts +3 -0
  217. package/src/tokens.js +1 -0
  218. package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
  219. package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
  220. package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
  221. package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
  222. package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
  223. package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
  224. package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
  225. package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
  226. package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
  227. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
  228. package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
  229. package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
  230. package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
  231. package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
  232. package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
  233. package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
  234. package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
  235. package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
  236. package/dist/build/chunks/utilities-_Bq3TxAQ.js +0 -1
@@ -0,0 +1,739 @@
1
+ # Testing Guide
2
+
3
+ Testing strategies and best practices for the Mixtape Design System.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Testing Philosophy](#testing-philosophy)
8
+ - [Storybook Stories](#storybook-stories)
9
+ - [Snapshot Testing](#snapshot-testing)
10
+ - [Accessibility Testing](#accessibility-testing)
11
+ - [Running Tests](#running-tests)
12
+ - [Writing Tests](#writing-tests)
13
+
14
+ ---
15
+
16
+ ## Testing Philosophy
17
+
18
+ The design system uses **visual regression testing** through Storybook instead of traditional unit tests.
19
+
20
+ ### Why Visual Testing?
21
+
22
+ - ✅ **Tests actual UI** - Not just logic
23
+ - ✅ **Catches visual regressions** - Breaking changes visible immediately
24
+ - ✅ **Living documentation** - Stories serve as docs and tests
25
+ - ✅ **Faster to write** - No complex test setup
26
+ - ✅ **Accessible testing** - Built-in a11y checks
27
+
28
+ ### Test Stack
29
+
30
+ - **Storybook** - Interactive component development
31
+ - **Vitest** - Test runner (browser mode with Playwright)
32
+ - **@storybook/addon-vitest** - Snapshot testing from stories
33
+ - **@storybook/addon-a11y** - Accessibility testing (axe-core rule violations)
34
+ - **@guidepup/virtual-screen-reader** - Automated screen reader announcement testing
35
+
36
+ ---
37
+
38
+ ## Storybook Stories
39
+
40
+ Every component must have Storybook stories.
41
+
42
+ ### Basic Story Structure
43
+
44
+ ```typescript
45
+ // Component.stories.ts (Twig/HTML)
46
+ import type { Meta, StoryObj } from "@storybook/html"
47
+
48
+ const meta: Meta = {
49
+ title: "Component/ComponentName",
50
+ tags: ["autodocs"],
51
+ render: args => {
52
+ return `
53
+ <mx-component class="${args.className}">
54
+ ${args.content}
55
+ </mx-component>
56
+ `
57
+ },
58
+ argTypes: {
59
+ content: { control: "text" },
60
+ className: { control: "text" },
61
+ },
62
+ }
63
+
64
+ export default meta
65
+ type Story = StoryObj
66
+
67
+ export const Default: Story = {
68
+ args: {
69
+ content: "Default content",
70
+ className: "",
71
+ },
72
+ }
73
+
74
+ export const WithModifier: Story = {
75
+ args: {
76
+ content: "Modified content",
77
+ className: "mx-component--reversed",
78
+ },
79
+ }
80
+ ```
81
+
82
+ ### React Story Structure
83
+
84
+ ```typescript
85
+ // Component.stories.tsx (React)
86
+ import type { Meta, StoryObj } from "@storybook/react"
87
+ import Component from "./Component"
88
+
89
+ const meta: Meta<typeof Component> = {
90
+ title: "Component/ComponentName",
91
+ component: Component,
92
+ tags: ["autodocs"],
93
+ }
94
+
95
+ export default meta
96
+ type Story = StoryObj<typeof Component>
97
+
98
+ export const Default: Story = {
99
+ args: {
100
+ children: "Default content",
101
+ },
102
+ }
103
+
104
+ export const Reversed: Story = {
105
+ args: {
106
+ children: "Reversed content",
107
+ reversed: true,
108
+ },
109
+ }
110
+ ```
111
+
112
+ ### Story Best Practices
113
+
114
+ 1. **Name stories descriptively**
115
+
116
+ ```typescript
117
+ export const Default: Story = {}
118
+ export const WithIcon: Story = {}
119
+ export const DisabledState: Story = {}
120
+ export const LongContent: Story = {}
121
+ export const MobileView: Story = {}
122
+ ```
123
+
124
+ 2. **Use argTypes for controls**
125
+
126
+ ```typescript
127
+ argTypes: {
128
+ variant: {
129
+ control: "select",
130
+ options: ["primary", "secondary", "tertiary"],
131
+ },
132
+ size: {
133
+ control: "radio",
134
+ options: ["small", "medium", "large"],
135
+ },
136
+ disabled: {
137
+ control: "boolean",
138
+ },
139
+ content: {
140
+ control: "text",
141
+ },
142
+ }
143
+ ```
144
+
145
+ 3. **Add play functions for interactions**
146
+
147
+ ```typescript
148
+ import { userEvent, within } from "@storybook/test"
149
+
150
+ export const InteractiveExample: Story = {
151
+ play: async ({ canvasElement }) => {
152
+ const canvas = within(canvasElement)
153
+ const button = canvas.getByRole("button")
154
+ await userEvent.click(button)
155
+ },
156
+ }
157
+ ```
158
+
159
+ 4. **Tag stories appropriately**
160
+ ```typescript
161
+ export const NoSnapshot: Story = {
162
+ tags: ["no-snapshot"], // Skip snapshot testing
163
+ args: { ... },
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Snapshot Testing
170
+
171
+ Snapshots are automatically generated from Storybook stories.
172
+
173
+ ### How It Works
174
+
175
+ 1. Write Storybook story
176
+ 2. Run tests: `pnpm test`
177
+ 3. Snapshot created in `__snapshots__/` directory
178
+ 4. Future tests compare against snapshot
179
+ 5. Visual changes detected automatically
180
+
181
+ ### Snapshot Files
182
+
183
+ ```
184
+ Component/
185
+ ├── Component.stories.ts
186
+ ├── Component.stories.tsx
187
+ └── __snapshots__/
188
+ ├── Component.stories.ts.snap
189
+ └── Component.stories.tsx.snap
190
+ ```
191
+
192
+ ### Updating Snapshots
193
+
194
+ ```bash
195
+ # Run tests (fails if snapshots don't match)
196
+ pnpm test
197
+
198
+ # Update snapshots after intentional changes
199
+ pnpm test -- -u
200
+
201
+ # Run tests for specific project
202
+ pnpm test-html # HTML/Twig components
203
+ pnpm test-react # React components
204
+ ```
205
+
206
+ ### When to Update Snapshots
207
+
208
+ ✅ **Update when:**
209
+
210
+ - Intentional visual changes
211
+ - Component refactored (same appearance)
212
+ - Design tokens updated
213
+ - CSS improvements
214
+
215
+ ❌ **Don't update for:**
216
+
217
+ - Failing tests you haven't reviewed
218
+ - Unknown changes
219
+ - Regressions you haven't investigated
220
+
221
+ ### Skip Snapshot for Specific Stories
222
+
223
+ ```typescript
224
+ // For stories that shouldn't be snapshot tested
225
+ export const AnimatedExample: Story = {
226
+ tags: ["no-snapshot"],
227
+ args: {
228
+ animated: true,
229
+ },
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Accessibility Testing
236
+
237
+ ### Storybook A11y Addon
238
+
239
+ The a11y addon runs automatically in Storybook:
240
+
241
+ 1. Open any story in Storybook
242
+ 2. Check "Accessibility" tab at bottom
243
+ 3. Review violations, passes, and incomplete checks
244
+ 4. Fix violations before shipping
245
+
246
+ ### Common A11y Issues
247
+
248
+ ```typescript
249
+ // ❌ BAD - Missing ARIA label
250
+ <button onclick="toggle()">
251
+ <span class="mx-icon--menu"></span>
252
+ </button>
253
+
254
+ // ✅ GOOD - Has ARIA label
255
+ <button onclick="toggle()" aria-label="Toggle menu">
256
+ <span class="mx-icon--menu"></span>
257
+ </button>
258
+
259
+ // ❌ BAD - Low contrast
260
+ color: #999;
261
+ background: #fff; // Fails WCAG AA
262
+
263
+ // ✅ GOOD - Sufficient contrast
264
+ color: #666;
265
+ background: #fff; // Passes WCAG AA
266
+
267
+ // ❌ BAD - Missing alt text
268
+ <img src="photo.jpg">
269
+
270
+ // ✅ GOOD - Has alt text
271
+ <img src="photo.jpg" alt="Description">
272
+ ```
273
+
274
+ ### Automated Screen Reader Testing
275
+
276
+ The a11y addon (above) catches static rule violations - missing labels, contrast, etc. - but can't tell you what a screen reader actually *announces* when navigating a component. [`@guidepup/virtual-screen-reader`](https://guidepup.dev) simulates that: it walks the accessible tree and produces the sequence of roles/states/text a screen reader would speak.
277
+
278
+ This is opt-in, not required for every component - it earns its keep on components with complex ARIA behavior (disclosure, roving tabindex, live regions, focus management), not on simple atoms that get nothing meaningful out of a walkthrough.
279
+
280
+ Use the shared helper in `tools/test-utils/screen-reader.ts` inside a dedicated `ScreenReaderAnnouncements` story, kept separate from any keyboard-interaction `play:` function so the two stay independently readable:
281
+
282
+ ```typescript
283
+ import { expect } from "storybook/test"
284
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
285
+
286
+ export const ScreenReaderAnnouncements: Story = {
287
+ play: async ({ canvas, userEvent }) => {
288
+ // Open/expand whatever the component needs to reach its interesting state.
289
+ const log = await runScreenReaderWalkthrough()
290
+ await expect(log).toEqual([
291
+ "document",
292
+ // ...the rest of the expected spoken-phrase sequence.
293
+ "end of document",
294
+ ])
295
+ // Then restore original state (close/deselect), same as any other play function.
296
+ },
297
+ }
298
+ ```
299
+
300
+ **Important: don't use `vitest`'s `expect`/`toMatchSnapshot` here, even though the DOM `innerHTML` snapshot in `vitest.setup.ts` does.** Story files are loaded by two different runtimes: the Vitest test runner (`pnpm test-html`/`test-react`) *and* Storybook's own dev server/build/Chromatic, which execute `play:` functions directly in the browser with no Vitest runtime present. `vitest`'s `expect` reaches for a matchers-object global that only exists inside an active Vitest run - importing and calling it from a story file works under `pnpm test-*` but throws (`Cannot access property "customEqualityTesters"`, `globalThis[JEST_MATCHERS_OBJECT]` undefined) the moment Storybook tries to render the same story, breaking it there. `vitest.setup.ts` is safe because it's only ever loaded by Vitest's `setupFiles`, never by Storybook. Use `storybook/test`'s `expect` instead (safe in both contexts, and what every other assertion in this codebase already uses) with a hard-coded `.toEqual([...])` array rather than a snapshot - `storybook/test`'s `expect` doesn't support `.toMatchSnapshot()` at all.
301
+
302
+ The helper scopes the walkthrough to `document.body` (Storybook renders one story per page, so that's exactly the story under test) and steps through until "end of document" is announced.
303
+
304
+ See `src/Component/Tabs/Tabs.stories.ts`, `src/Component/Dialog/Dialog.stories.ts`, and `src/Component/DropMenu/DropMenu.stories.ts` for worked examples.
305
+
306
+ In a React (`.stories.tsx`) file, add the story **last**. React's `useId()` counters (`id="_r_N_"`) are global per test-file run, so inserting a new story earlier in the file shifts the counters - and therefore the DOM snapshots - of every story that renders after it.
307
+
308
+ **Known limitation: a link with an icon next to its text announces the text twice.** A link whose only child is a bare text node (`<a>Read more</a>`) is announced once, atomically. The moment that link also has a sibling element - most commonly a decorative icon rendered alongside the title (e.g. `Pagination`'s Previous/Next, `InPageNavigation`'s items with `iconStart`) - the walker treats the link as a container and re-announces the text on the way in, even though the icon itself contributes nothing. This is a raw-DOM-structure check, not an accessibility-tree one: `aria-hidden="true"` on the icon does not fix it (verified). The only real fix would be rendering such icons as CSS pseudo-elements instead of real DOM nodes, which this codebase deliberately doesn't do. Buttons are unaffected (they're always announced atomically regardless of internal structure - only links behave this way). Treat this as a known tool limitation when reviewing a `ScreenReaderAnnouncements` snapshot for an icon+text link - don't spend time trying to "fix" the duplicate.
309
+
310
+ **If the component has `chromatic.modes` (mobile/desktop), check whether the walkthrough needs to branch too.** Chromatic renders every story - including `ScreenReaderAnnouncements` - at each configured mode's viewport width. If anything is hidden/shown or added/removed from the DOM between those widths (a CSS container query swapping content, a `matchMedia`-driven breakpoint, a JS-driven observer that hides off-screen items), the accessibility tree differs between modes and a single hardcoded `.toEqual([...])` will fail under whichever mode it wasn't written against. Branch the same way the [matchMedia pattern above](#testing-mobile--matchmedia-driven-behavior-in-play-functions) does - on the component's own real rendered state - and assert the correct array for each shape:
311
+
312
+ ```typescript
313
+ export const ScreenReaderAnnouncements: Story = {
314
+ play: async ({ canvasElement }) => {
315
+ const isMobile = matchMedia("(max-width: 767px)").matches // real matchMedia breakpoint
316
+ // ...or, for a CSS *container* query (no matchMedia equivalent exists),
317
+ // check the actually-rendered state directly, e.g.:
318
+ // const label = canvasElement.querySelector(".mx-pagination__label")
319
+ // const isNarrow = label !== null && getComputedStyle(label).display !== "none"
320
+
321
+ const log = await runScreenReaderWalkthrough()
322
+
323
+ if (isMobile) {
324
+ await expect(log).toEqual([/* ...mobile shape... */])
325
+ } else {
326
+ await expect(log).toEqual([/* ...desktop shape... */])
327
+ }
328
+ },
329
+ }
330
+ ```
331
+
332
+ As with the matchMedia pattern, a local run only meaningfully exercises one branch (whichever width it happens to render at); Chromatic's other mode exercises the other in CI. See `src/Component/Pagination/Pagination.stories.ts` (container-query-driven), `src/Component/Carousel/Carousel.stories.ts` (IntersectionObserver-driven), and `src/Component/Tabs/Tabs.stories.ts`/`src/Layout/Header/Header.stories.ts` (`matchMedia`-driven, via a custom element's `Elements/*.ts`) for worked examples. Components without `chromatic.modes` on their `meta` don't need this; for ones that do, check **both** the component's CSS (`@media`/`@container` rules toggling `display`/`visibility`) **and** its `Elements/*.ts` custom element(s) for a `matchMedia`/`mq` breakpoint - CSS-only checking isn't enough: `Tabs` collapses its tab list into a `DropMenu` purely via JS (`Elements/Tabs.ts`'s `handleBreakpoint`), with nothing in `tabs.css` suggesting a difference. Confirm with the real rendered DOM, not just a reading of the source - temporarily set `parameters: { viewport: { defaultViewport: "xs" } }` on the story (this is a local-only override `@storybook/addon-vitest` reads; it doesn't affect Chromatic) to force the actual narrow-viewport render and see what the walkthrough really produces, then remove the override once the branches are written. `Accordion`/`AccordionItem` were checked this way and don't need branching - their default render has no breakpoint logic (the separate `mx-accordionmobile` variant is a distinct, explicitly-chosen template, not something the default item switches to).
333
+
334
+ ### Manual A11y Testing
335
+
336
+ 1. **Keyboard navigation**
337
+ - Tab through all interactive elements
338
+ - Enter/Space activates buttons
339
+ - Escape closes modals/menus
340
+ - Arrow keys navigate menus
341
+
342
+ 2. **Screen reader testing**
343
+ - Automated coverage exists for components with a `ScreenReaderAnnouncements` story (see [Automated Screen Reader Testing](#automated-screen-reader-testing) above)
344
+ - For everything else, or for release-critical flows, spot-check manually:
345
+ - MacOS: VoiceOver (Cmd+F5)
346
+ - Windows: NVDA or JAWS
347
+ - Mobile: TalkBack (Android) / VoiceOver (iOS)
348
+
349
+ 3. **Color contrast**
350
+ - Use browser DevTools color picker
351
+ - Check contrast ratio ≥ 4.5:1 for text
352
+ - Check contrast ratio ≥ 3:1 for UI components
353
+
354
+ ---
355
+
356
+ ## Running Tests
357
+
358
+ ### All Tests
359
+
360
+ ```bash
361
+ # Run all tests
362
+ pnpm test
363
+
364
+ # Run with coverage
365
+ pnpm test -- --coverage
366
+
367
+ # Run in watch mode (development)
368
+ pnpm test -- --watch
369
+ ```
370
+
371
+ ### Specific Projects
372
+
373
+ ```bash
374
+ # HTML/Twig components only
375
+ pnpm test-html
376
+
377
+ # React components only
378
+ pnpm test-react
379
+
380
+ # Run specific story
381
+ pnpm test -- Component.stories.ts
382
+ ```
383
+
384
+ ### CI/CD Testing
385
+
386
+ ```bash
387
+ # Lint and test (used in CI)
388
+ pnpm lint-ci
389
+ pnpm test
390
+
391
+ # Build and test
392
+ pnpm build-ci
393
+ ```
394
+
395
+ ### Development Workflow
396
+
397
+ ```bash
398
+ # 1. Start Storybook
399
+ pnpm dev
400
+
401
+ # 2. Develop component with live reload
402
+ # Visit http://localhost:6006 (HTML)
403
+ # Visit http://localhost:6008 (React)
404
+
405
+ # 3. Run tests when ready
406
+ pnpm test
407
+
408
+ # 4. Update snapshots if needed
409
+ pnpm test -- -u
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Writing Tests
415
+
416
+ ### Test Checklist
417
+
418
+ Before submitting a component:
419
+
420
+ - [ ] Storybook story created
421
+ - [ ] Default story exists
422
+ - [ ] Variant stories created
423
+ - [ ] Edge cases covered (long content, empty state)
424
+ - [ ] Responsive variants shown
425
+ - [ ] Interactive states tested (hover, focus, active)
426
+ - [ ] Snapshots generated and reviewed
427
+ - [ ] A11y violations = 0
428
+ - [ ] Keyboard navigation works
429
+ - [ ] Screen reader tested (automated `ScreenReaderAnnouncements` story for complex ARIA components, manual spot-check otherwise)
430
+ - [ ] Color contrast checked
431
+
432
+ ### Testing Mobile / `matchMedia`-Driven Behavior in Play Functions
433
+
434
+ Some components (`Filters`, `Header`) switch behavior based on `window.matchMedia` rather than (only) a `viewport` story parameter. Don't reach for `vitest/browser`'s `page.viewport()` (or the deprecated `@vitest/browser/context`) to force this from a play function. That module **only exists inside Vitest's browser-mode test runner** - importing it anywhere in a `.stories.tsx` file throws `"vitest/browser can be imported only inside the Browser Mode. Instead, it was imported outside of Vitest."` the moment Storybook loads the story directly (`pnpm dev-react`, `pnpm build-react`). Storybook imports story files outside of Vitest entirely, so this breaks the live story for everyone viewing it, not just the test run.
435
+
436
+ Instead, write **one** play function per story that branches on the **real** `matchMedia(...).matches` and asserts something meaningful on both sides, rather than a mobile-only story or a mock. Pair it with a `chromatic.modes` entry (desktop + mobile) on the component's `meta` - Chromatic renders the story at a real viewport width for each mode in CI, so across the mobile and desktop CI runs both branches of the play function get exercised against a real, correctly-sized viewport. No in-test viewport mocking or forcing is needed, and there's no separate "mobile" story to maintain:
437
+
438
+ ```typescript
439
+ // Component.stories.tsx
440
+ import { modes } from "../../../.storybook/modes"
441
+
442
+ const meta: Meta<typeof Component> = {
443
+ // ...
444
+ parameters: {
445
+ chromatic: {
446
+ modes: {
447
+ mobile: modes["xs"],
448
+ desktop: modes["xl"],
449
+ },
450
+ },
451
+ },
452
+ }
453
+
454
+ export const CollapsibleRight: StoryObj<typeof meta> = {
455
+ play: async ({ canvasElement, canvas, userEvent, step }) => {
456
+ const details = canvasElement.querySelector("details")
457
+ const isMobile = matchMedia("(max-width: 767px)").matches
458
+
459
+ if (!isMobile) {
460
+ await step("Is open on desktop", async () => {
461
+ await waitFor(() => expect(details).toHaveAttribute("open"))
462
+ })
463
+ } else {
464
+ await step("Starts closed on mobile", async () => {
465
+ await waitFor(() => expect(details).not.toHaveAttribute("open"))
466
+ })
467
+ // ...mobile-only interaction assertions...
468
+ }
469
+ },
470
+ }
471
+ ```
472
+
473
+ Whichever mode a given local run (`pnpm dev-react`, `vitest --project=storybook-react`) happens to render at, only one branch runs meaningfully for that run - the other is a no-op. That's expected: Chromatic's CI job covers the branch a given local run skips, by rendering the same story again under the other mode.
474
+
475
+ ### Story Coverage
476
+
477
+ Ensure stories cover:
478
+
479
+ 1. **Default state**
480
+
481
+ ```typescript
482
+ export const Default: Story = {
483
+ args: {},
484
+ }
485
+ ```
486
+
487
+ 2. **All modifiers**
488
+
489
+ ```typescript
490
+ export const Primary: Story = {
491
+ args: { variant: "primary" },
492
+ }
493
+
494
+ export const Secondary: Story = {
495
+ args: { variant: "secondary" },
496
+ }
497
+ ```
498
+
499
+ 3. **Different sizes**
500
+
501
+ ```typescript
502
+ export const Small: Story = {
503
+ args: { size: "small" },
504
+ }
505
+
506
+ export const Large: Story = {
507
+ args: { size: "large" },
508
+ }
509
+ ```
510
+
511
+ 4. **Interactive states**
512
+
513
+ ```typescript
514
+ export const Disabled: Story = {
515
+ args: { disabled: true },
516
+ }
517
+
518
+ export const Loading: Story = {
519
+ args: { loading: true },
520
+ }
521
+ ```
522
+
523
+ 5. **Edge cases**
524
+
525
+ ```typescript
526
+ export const EmptyState: Story = {
527
+ args: { children: "" },
528
+ }
529
+
530
+ export const LongContent: Story = {
531
+ args: {
532
+ children: "Very long content that wraps multiple lines...",
533
+ },
534
+ }
535
+ ```
536
+
537
+ ---
538
+
539
+ ## Vitest Configuration
540
+
541
+ The test setup uses browser mode with Playwright:
542
+
543
+ ```javascript
544
+ // vitest.config.js
545
+ export default defineConfig({
546
+ test: {
547
+ browser: {
548
+ enabled: true,
549
+ provider: playwright({}),
550
+ headless: true,
551
+ instances: [{ browser: "chromium" }],
552
+ },
553
+ projects: [
554
+ {
555
+ name: "storybook",
556
+ plugins: [
557
+ storybookTest({
558
+ configDir: ".storybook",
559
+ storybookScript: "pnpm dev-html --no-open",
560
+ }),
561
+ ],
562
+ setupFiles: ["./.storybook/vitest.setup.ts"],
563
+ },
564
+ {
565
+ name: "storybook-react",
566
+ plugins: [
567
+ storybookTest({
568
+ configDir: ".storybook-react",
569
+ storybookScript: "pnpm dev-react --no-open",
570
+ }),
571
+ ],
572
+ setupFiles: ["./.storybook-react/vitest.setup.ts"],
573
+ },
574
+ ],
575
+ },
576
+ })
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Troubleshooting Tests
582
+
583
+ ### Snapshots Failing
584
+
585
+ ```bash
586
+ # 1. Review changes
587
+ pnpm test
588
+
589
+ # 2. Check what changed
590
+ git diff src/Component/__snapshots__/
591
+
592
+ # 3. If intentional, update snapshots
593
+ pnpm test -- -u
594
+
595
+ # 4. Commit updated snapshots
596
+ git add src/Component/__snapshots__/
597
+ git commit -m "test: update snapshots for Component"
598
+ ```
599
+
600
+ ### Storybook Not Starting
601
+
602
+ ```bash
603
+ # Clear cache
604
+ rm -rf node_modules/.cache
605
+
606
+ # Reinstall dependencies
607
+ pnpm install
608
+
609
+ # Try again
610
+ pnpm dev
611
+ ```
612
+
613
+ ### A11y Violations
614
+
615
+ 1. Open story in Storybook
616
+ 2. Click "Accessibility" tab
617
+ 3. Review violations
618
+ 4. Click violation for remediation guidance
619
+ 5. Fix issue in component
620
+ 6. Verify violation cleared
621
+
622
+ ### Tests Timing Out
623
+
624
+ ```bash
625
+ # Increase timeout
626
+ pnpm test -- --testTimeout=30000
627
+
628
+ # Run single test
629
+ pnpm test -- Component.stories.ts
630
+
631
+ # Check Storybook is accessible
632
+ curl http://localhost:6006
633
+ ```
634
+
635
+ ---
636
+
637
+ ## Best Practices
638
+
639
+ ### Do's ✅
640
+
641
+ 1. **Write stories for every component**
642
+ 2. **Cover all variants and states**
643
+ 3. **Review snapshots before updating**
644
+ 4. **Fix a11y violations immediately**
645
+ 5. **Test keyboard navigation manually**
646
+ 6. **Use semantic HTML**
647
+ 7. **Add ARIA labels where needed**
648
+ 8. **Test with actual screen readers**
649
+ 9. **Check color contrast**
650
+ 10. **Document complex interactions**
651
+
652
+ ### Don'ts ❌
653
+
654
+ 1. **Don't skip snapshot review**
655
+ 2. **Don't ignore a11y violations**
656
+ 3. **Don't update snapshots without reviewing**
657
+ 4. **Don't test implementation details**
658
+ 5. **Don't create stories that aren't tested**
659
+ 6. **Don't forget mobile viewports**
660
+ 7. **Don't skip keyboard testing**
661
+ 8. **Don't use poor color contrast**
662
+ 9. **Don't forget alt text**
663
+ 10. **Don't ship with failing tests**
664
+ 11. **Don't mock or force a viewport from inside a play function** - branch the same play function on the real `matchMedia(...).matches` and pair it with `chromatic.modes` instead; see [Testing Mobile / matchMedia-Driven Behavior in Play Functions](#testing-mobile--matchmedia-driven-behavior-in-play-functions)
665
+
666
+ ---
667
+
668
+ ## Examples from Codebase
669
+
670
+ ### Accordion Stories
671
+
672
+ ```typescript
673
+ // src/Component/Accordion/Accordion.stories.ts
674
+ export const Default: Story = {
675
+ args: {
676
+ content: `
677
+ <details>
678
+ <summary>Toggle content</summary>
679
+ <p>Hidden content</p>
680
+ </details>
681
+ `,
682
+ },
683
+ }
684
+
685
+ export const Open: Story = {
686
+ args: {
687
+ content: `
688
+ <details open>
689
+ <summary>Already open</summary>
690
+ <p>Visible content</p>
691
+ </details>
692
+ `,
693
+ },
694
+ }
695
+ ```
696
+
697
+ ### Button Stories
698
+
699
+ ```typescript
700
+ // src/Atom/Button/Button.stories.tsx
701
+ export const Primary: Story = {
702
+ args: {
703
+ children: "Primary Button",
704
+ modifiers: [ButtonModifiers.PRIMARY],
705
+ },
706
+ }
707
+
708
+ export const Disabled: Story = {
709
+ args: {
710
+ children: "Disabled Button",
711
+ disabled: true,
712
+ },
713
+ }
714
+
715
+ export const WithIcon: Story = {
716
+ args: {
717
+ children: "Icon Button",
718
+ icon: Icons.ARROW_RIGHT,
719
+ },
720
+ }
721
+ ```
722
+
723
+ ---
724
+
725
+ ## Resources
726
+
727
+ - [Storybook Documentation](https://storybook.js.org/docs)
728
+ - [Vitest Documentation](https://vitest.dev/)
729
+ - [WCAG Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
730
+ - [axe DevTools](https://www.deque.com/axe/devtools/)
731
+ - [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
732
+
733
+ ---
734
+
735
+ ## Related Documentation
736
+
737
+ - [Code Quality Guidelines](./CodeQuality.md)
738
+ - [Component Architecture](./ComponentArchitecture.md)
739
+ - [Contributing Guide](./Contributing.md)