@astryxdesign/core 0.6.2 → 0.6.3-canary.0788c49

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 (558) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +13 -28
  10. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  11. package/dist/Chat/ChatComposer.js +10 -5
  12. package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
  13. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposerDrawer.js +48 -23
  15. package/dist/Chat/ChatComposerInput.d.ts +11 -3
  16. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  17. package/dist/Chat/ChatComposerInput.js +63 -16
  18. package/dist/Chat/ChatDictationButton.d.ts +3 -2
  19. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  20. package/dist/Chat/ChatDictationButton.js +6 -4
  21. package/dist/Chat/ChatLayout.d.ts +19 -0
  22. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  23. package/dist/Chat/ChatLayout.js +19 -0
  24. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  25. package/dist/Chat/ChatLayoutScrollButton.js +44 -34
  26. package/dist/Chat/ChatMessageBubble.d.ts +2 -0
  27. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  28. package/dist/Chat/ChatMessageBubble.js +5 -3
  29. package/dist/Chat/ChatMessageList.d.ts +2 -1
  30. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  31. package/dist/Chat/ChatMessageList.js +4 -3
  32. package/dist/Chat/ChatMessageMetadata.d.ts +5 -4
  33. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  34. package/dist/Chat/ChatMessageMetadata.js +12 -9
  35. package/dist/Chat/ChatSendButton.d.ts +2 -1
  36. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  37. package/dist/Chat/ChatSendButton.js +6 -4
  38. package/dist/Chat/index.d.ts +1 -1
  39. package/dist/Chat/index.d.ts.map +1 -1
  40. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  41. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  42. package/dist/Citation/Citation.d.ts +1 -1
  43. package/dist/Citation/Citation.d.ts.map +1 -1
  44. package/dist/Citation/Citation.js +3 -2
  45. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  46. package/dist/CodeBlock/highlightRanges.js +14 -3
  47. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  48. package/dist/Collapsible/Collapsible.js +6 -1
  49. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  50. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  51. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  52. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  53. package/dist/DateInput/DateInput.d.ts.map +1 -1
  54. package/dist/DateInput/DateInput.js +4 -4
  55. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  56. package/dist/DateInput/NativeDateField.js +16 -16
  57. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  58. package/dist/DateInput/TouchDateField.js +2 -2
  59. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  60. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  61. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  62. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  63. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  64. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  65. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  66. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  67. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  68. package/dist/Dialog/DialogHeader.d.ts +8 -1
  69. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  70. package/dist/Dialog/DialogHeader.js +13 -4
  71. package/dist/Field/InputClearButton.d.ts +4 -2
  72. package/dist/Field/InputClearButton.d.ts.map +1 -1
  73. package/dist/Field/InputClearButton.js +4 -2
  74. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  75. package/dist/Field/PanelSearchInput.js +1 -1
  76. package/dist/FileInput/FileInput.d.ts.map +1 -1
  77. package/dist/FileInput/FileInput.js +7 -7
  78. package/dist/Icon/Icon.d.ts +6 -31
  79. package/dist/Icon/Icon.d.ts.map +1 -1
  80. package/dist/Icon/Icon.js +8 -71
  81. package/dist/Icon/IconDefaultSizeContext.d.ts +4 -0
  82. package/dist/Icon/IconDefaultSizeContext.d.ts.map +1 -0
  83. package/dist/Icon/IconDefaultSizeContext.js +18 -0
  84. package/dist/Icon/IconSize.stylex.d.ts +65 -0
  85. package/dist/Icon/IconSize.stylex.d.ts.map +1 -0
  86. package/dist/Icon/IconSize.stylex.js +75 -0
  87. package/dist/Item/Item.d.ts +1 -1
  88. package/dist/Item/Item.d.ts.map +1 -1
  89. package/dist/Item/Item.js +4 -3
  90. package/dist/Kbd/Kbd.d.ts +5 -3
  91. package/dist/Kbd/Kbd.d.ts.map +1 -1
  92. package/dist/Kbd/Kbd.js +36 -42
  93. package/dist/Link/Link.d.ts.map +1 -1
  94. package/dist/Link/Link.js +6 -2
  95. package/dist/Link/useLinkComponent.d.ts +5 -15
  96. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  97. package/dist/Link/useLinkComponent.js +34 -51
  98. package/dist/List/List.d.ts +19 -1
  99. package/dist/List/List.d.ts.map +1 -1
  100. package/dist/List/List.js +9 -2
  101. package/dist/List/ListContext.d.ts +1 -0
  102. package/dist/List/ListContext.d.ts.map +1 -1
  103. package/dist/List/ListContext.js +2 -1
  104. package/dist/List/ListItem.d.ts +1 -1
  105. package/dist/List/ListItem.d.ts.map +1 -1
  106. package/dist/List/ListItem.js +9 -2
  107. package/dist/Markdown/Markdown.d.ts +8 -2
  108. package/dist/Markdown/Markdown.d.ts.map +1 -1
  109. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  110. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  111. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  112. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  113. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  114. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  115. package/dist/Markdown/Markdown.js +262 -109
  116. package/dist/Markdown/ast.d.ts +162 -0
  117. package/dist/Markdown/ast.d.ts.map +1 -0
  118. package/dist/Markdown/ast.js +74 -0
  119. package/dist/Markdown/index.d.ts +5 -3
  120. package/dist/Markdown/index.d.ts.map +1 -1
  121. package/dist/Markdown/index.js +3 -2
  122. package/dist/Markdown/parser/index.d.ts +10 -0
  123. package/dist/Markdown/parser/index.d.ts.map +1 -0
  124. package/dist/Markdown/parser/index.js +10 -0
  125. package/dist/Markdown/parser.d.ts +126 -58
  126. package/dist/Markdown/parser.d.ts.map +1 -1
  127. package/dist/Markdown/parser.js +726 -177
  128. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  129. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  130. package/dist/Markdown/plugins/frontmatter.js +132 -0
  131. package/dist/Markdown/plugins/index.d.ts +17 -0
  132. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  133. package/dist/Markdown/plugins/index.js +14 -0
  134. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  135. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  136. package/dist/Markdown/plugins/protocol.js +957 -0
  137. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  138. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  139. package/dist/Markdown/plugins/semanticFence.js +213 -0
  140. package/dist/Markdown/plugins/softBreaks.d.ts +3 -0
  141. package/dist/Markdown/plugins/softBreaks.d.ts.map +1 -0
  142. package/dist/Markdown/plugins/softBreaks.js +172 -0
  143. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  144. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  145. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  146. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  147. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  148. package/dist/Markdown/plugins/textTransform.js +374 -0
  149. package/dist/Markdown/remark.d.ts +196 -0
  150. package/dist/Markdown/remark.d.ts.map +1 -0
  151. package/dist/Markdown/remark.js +1062 -0
  152. package/dist/Markdown/url.d.ts +6 -0
  153. package/dist/Markdown/url.d.ts.map +1 -0
  154. package/dist/Markdown/url.js +31 -0
  155. package/dist/Markdown/utils.d.ts +4 -2
  156. package/dist/Markdown/utils.d.ts.map +1 -1
  157. package/dist/Markdown/utils.js +2 -1
  158. package/dist/MetadataList/MetadataList.js +3 -3
  159. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  160. package/dist/NumberInput/NumberInput.js +4 -4
  161. package/dist/Outline/index.d.ts +1 -0
  162. package/dist/Outline/index.d.ts.map +1 -1
  163. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  164. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  165. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  166. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  167. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  168. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  169. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  170. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  171. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  172. package/dist/PowerSearch/PowerSearchValueEditor.js +5 -0
  173. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  174. package/dist/ProgressBar/ProgressBar.js +36 -31
  175. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  176. package/dist/RadioList/RadioListItem.js +13 -1
  177. package/dist/ScrollableArea/ScrollableArea.d.ts +1 -0
  178. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -1
  179. package/dist/ScrollableArea/ScrollableArea.js +1 -0
  180. package/dist/SegmentedControl/SegmentedControl.d.ts +1 -1
  181. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  182. package/dist/SegmentedControl/SegmentedControl.js +1 -0
  183. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  184. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  185. package/dist/Selector/Selector.d.ts +9 -7
  186. package/dist/Selector/Selector.d.ts.map +1 -1
  187. package/dist/Selector/Selector.js +16 -7
  188. package/dist/SideNav/SideNav.d.ts +2 -1
  189. package/dist/SideNav/SideNav.d.ts.map +1 -1
  190. package/dist/SideNav/SideNav.js +9 -3
  191. package/dist/Slider/Slider.d.ts.map +1 -1
  192. package/dist/Slider/Slider.js +28 -12
  193. package/dist/Spinner/Spinner.d.ts.map +1 -1
  194. package/dist/Spinner/Spinner.js +23 -7
  195. package/dist/Switch/Switch.d.ts.map +1 -1
  196. package/dist/Switch/Switch.js +11 -0
  197. package/dist/TabList/Tab.d.ts +1 -1
  198. package/dist/TabList/Tab.d.ts.map +1 -1
  199. package/dist/TabList/Tab.js +20 -7
  200. package/dist/Table/Table.d.ts +1 -0
  201. package/dist/Table/Table.d.ts.map +1 -1
  202. package/dist/Table/Table.js +29 -13
  203. package/dist/TextArea/TextArea.d.ts.map +1 -1
  204. package/dist/TextArea/TextArea.js +1 -1
  205. package/dist/TextInput/TextInput.d.ts.map +1 -1
  206. package/dist/TextInput/TextInput.js +2 -2
  207. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  208. package/dist/TimeInput/TimeInput.js +4 -4
  209. package/dist/Timer/Timer.d.ts +49 -0
  210. package/dist/Timer/Timer.d.ts.map +1 -0
  211. package/dist/Timer/Timer.js +158 -0
  212. package/dist/Timer/index.d.ts +9 -0
  213. package/dist/Timer/index.d.ts.map +1 -0
  214. package/dist/Timer/index.js +10 -0
  215. package/dist/ToggleButton/ToggleButton.d.ts +9 -6
  216. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  217. package/dist/ToggleButton/ToggleButton.js +15 -27
  218. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  219. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  220. package/dist/TopNav/TopNavMenu.js +3 -2
  221. package/dist/TreeList/TreeList.d.ts.map +1 -1
  222. package/dist/TreeList/TreeList.js +3 -0
  223. package/dist/TreeList/TreeListItem.d.ts +6 -1
  224. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  225. package/dist/TreeList/TreeListItem.js +12 -2
  226. package/dist/TreeList/TreeListTypes.d.ts +8 -1
  227. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  228. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  229. package/dist/Typeahead/BaseTypeahead.js +16 -7
  230. package/dist/astryx.css +27 -11
  231. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  232. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  233. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  234. package/dist/hooks/useClickableContainer.d.ts +10 -1
  235. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  236. package/dist/hooks/useClickableContainer.js +15 -4
  237. package/dist/hooks/useScrollableArea.d.ts +6 -2
  238. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  239. package/dist/hooks/useScrollableArea.js +17 -5
  240. package/dist/index.d.ts +1 -0
  241. package/dist/index.d.ts.map +1 -1
  242. package/dist/index.js +3 -0
  243. package/dist/theme/declarationBoundary.d.ts +17 -0
  244. package/dist/theme/declarationBoundary.d.ts.map +1 -0
  245. package/dist/theme/declarationBoundary.js +343 -0
  246. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  247. package/dist/theme/derivedVarRegistry.js +4 -0
  248. package/dist/theme/generateThemeRules.d.ts +13 -7
  249. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  250. package/dist/theme/generateThemeRules.js +140 -34
  251. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  252. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  253. package/dist/utils/interactionOverlay.stylex.js +9 -0
  254. package/dist/utils/safeUrl.d.ts +11 -0
  255. package/dist/utils/safeUrl.d.ts.map +1 -0
  256. package/dist/utils/safeUrl.js +78 -0
  257. package/locales/af-ZA.json +530 -1
  258. package/locales/ar-SA.json +490 -1
  259. package/locales/ca-ES.json +514 -1
  260. package/locales/cs-CZ.json +498 -1
  261. package/locales/da-DK.json +522 -1
  262. package/locales/de-DE.json +522 -1
  263. package/locales/el-GR.json +498 -1
  264. package/locales/es-ES.json +510 -1
  265. package/locales/fi-FI.json +498 -1
  266. package/locales/fr-FR.json +534 -1
  267. package/locales/he-IL.json +498 -1
  268. package/locales/hu-HU.json +498 -1
  269. package/locales/it-IT.json +510 -1
  270. package/locales/ja-JP.json +490 -1
  271. package/locales/ko-KR.json +498 -1
  272. package/locales/nl-NL.json +534 -1
  273. package/locales/no-NO.json +510 -1
  274. package/locales/pl-PL.json +506 -1
  275. package/locales/pt-BR.json +506 -1
  276. package/locales/pt-PT.json +506 -1
  277. package/locales/ro-RO.json +506 -1
  278. package/locales/ru-RU.json +498 -1
  279. package/locales/sr-SP.json +498 -1
  280. package/locales/sv-SE.json +502 -1
  281. package/locales/tr-TR.json +498 -1
  282. package/locales/uk-UA.json +498 -1
  283. package/locales/vi-VN.json +502 -1
  284. package/locales/zh-CN.json +490 -1
  285. package/locales/zh-TW.json +490 -1
  286. package/package.json +24 -4
  287. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  288. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  289. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  290. package/src/BottomSheet/BottomSheet.tsx +3 -1
  291. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  292. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  293. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  294. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  295. package/src/Button/Button.doc.mjs +5 -5
  296. package/src/Button/Button.tsx +15 -13
  297. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  298. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  299. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  300. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  301. package/src/Chat/ChatComposer.doc.mjs +14 -0
  302. package/src/Chat/ChatComposer.spec.md +230 -0
  303. package/src/Chat/ChatComposer.test.tsx +57 -0
  304. package/src/Chat/ChatComposer.tsx +21 -9
  305. package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
  306. package/src/Chat/ChatComposerDrawer.spec.md +267 -0
  307. package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
  308. package/src/Chat/ChatComposerDrawer.tsx +38 -9
  309. package/src/Chat/ChatComposerInput.doc.mjs +114 -26
  310. package/src/Chat/ChatComposerInput.spec.md +185 -0
  311. package/src/Chat/ChatComposerInput.test.tsx +113 -3
  312. package/src/Chat/ChatComposerInput.tsx +78 -13
  313. package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
  314. package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
  315. package/src/Chat/ChatDictationButton.doc.mjs +3 -3
  316. package/src/Chat/ChatDictationButton.spec.md +160 -0
  317. package/src/Chat/ChatDictationButton.test.tsx +92 -27
  318. package/src/Chat/ChatDictationButton.tsx +8 -6
  319. package/src/Chat/ChatLayout.doc.mjs +12 -4
  320. package/src/Chat/ChatLayout.spec.md +271 -0
  321. package/src/Chat/ChatLayout.test.tsx +28 -0
  322. package/src/Chat/ChatLayout.tsx +19 -0
  323. package/src/Chat/ChatLayoutScrollButton.spec.md +338 -0
  324. package/src/Chat/ChatLayoutScrollButton.test.tsx +31 -0
  325. package/src/Chat/ChatLayoutScrollButton.tsx +31 -14
  326. package/src/Chat/ChatMessageBubble.doc.mjs +28 -15
  327. package/src/Chat/ChatMessageBubble.spec.md +263 -0
  328. package/src/Chat/ChatMessageBubble.test.tsx +93 -0
  329. package/src/Chat/ChatMessageBubble.tsx +8 -3
  330. package/src/Chat/ChatMessageList.spec.md +222 -0
  331. package/src/Chat/ChatMessageList.test.tsx +64 -2
  332. package/src/Chat/ChatMessageList.tsx +4 -3
  333. package/src/Chat/ChatMessageMetadata.doc.mjs +57 -3
  334. package/src/Chat/ChatMessageMetadata.spec.md +220 -0
  335. package/src/Chat/ChatMessageMetadata.test.tsx +34 -0
  336. package/src/Chat/ChatMessageMetadata.tsx +13 -13
  337. package/src/Chat/ChatSendButton.doc.mjs +32 -0
  338. package/src/Chat/ChatSendButton.spec.md +227 -0
  339. package/src/Chat/ChatSendButton.test.tsx +128 -8
  340. package/src/Chat/ChatSendButton.tsx +6 -4
  341. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +514 -0
  342. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +740 -0
  343. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +457 -0
  344. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +91 -0
  345. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +91 -0
  346. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +456 -0
  347. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +413 -0
  348. package/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts +1522 -0
  349. package/src/Chat/index.ts +1 -0
  350. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  351. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  352. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  353. package/src/Citation/Citation.doc.mjs +2 -2
  354. package/src/Citation/Citation.test.tsx +17 -0
  355. package/src/Citation/Citation.tsx +4 -2
  356. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  357. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  358. package/src/CodeBlock/highlightRanges.test.ts +143 -1
  359. package/src/CodeBlock/highlightRanges.ts +15 -5
  360. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  361. package/src/Collapsible/Collapsible.test.tsx +21 -0
  362. package/src/Collapsible/Collapsible.tsx +5 -0
  363. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  364. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  365. package/src/DateInput/DateInput.tsx +6 -1
  366. package/src/DateInput/NativeDateField.tsx +24 -8
  367. package/src/DateInput/TouchDateField.tsx +6 -2
  368. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  369. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  370. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  371. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  372. package/src/Dialog/Dialog.doc.mjs +2 -0
  373. package/src/Dialog/DialogHeader.doc.mjs +22 -0
  374. package/src/Dialog/DialogHeader.test.tsx +167 -3
  375. package/src/Dialog/DialogHeader.tsx +37 -6
  376. package/src/Dialog/modules/DialogHeader.spec.md +101 -42
  377. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -2
  378. package/src/Field/InputClearButton.test.tsx +17 -3
  379. package/src/Field/InputClearButton.tsx +5 -2
  380. package/src/Field/PanelSearchInput.tsx +5 -2
  381. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  382. package/src/FileInput/FileInput.tsx +11 -2
  383. package/src/Icon/Icon.doc.mjs +7 -5
  384. package/src/Icon/Icon.spec.md +81 -40
  385. package/src/Icon/Icon.test.tsx +6 -3
  386. package/src/Icon/Icon.tsx +20 -68
  387. package/src/Icon/IconDefaultSizeContext.ts +23 -0
  388. package/src/Icon/IconSize.stylex.ts +70 -0
  389. package/src/IconButton/IconButton.doc.mjs +3 -3
  390. package/src/IconButton/IconButton.test.tsx +40 -0
  391. package/src/Item/Item.doc.mjs +6 -2
  392. package/src/Item/Item.tsx +9 -3
  393. package/src/Kbd/Kbd.doc.mjs +3 -3
  394. package/src/Kbd/Kbd.test.tsx +57 -1
  395. package/src/Kbd/Kbd.tsx +57 -37
  396. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  397. package/src/Link/Link.doc.mjs +17 -4
  398. package/src/Link/Link.test.tsx +24 -0
  399. package/src/Link/Link.tsx +5 -0
  400. package/src/Link/LinkProvider.doc.mjs +2 -2
  401. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
  402. package/src/Link/useLinkComponent.test.tsx +333 -0
  403. package/src/Link/useLinkComponent.ts +29 -57
  404. package/src/List/List.doc.mjs +6 -0
  405. package/src/List/List.spec.md +200 -0
  406. package/src/List/List.test.tsx +117 -1
  407. package/src/List/List.tsx +22 -2
  408. package/src/List/ListContext.tsx +3 -1
  409. package/src/List/ListItem.doc.mjs +2 -2
  410. package/src/List/ListItem.source-build.test.mjs +41 -0
  411. package/src/List/ListItem.tsx +26 -2
  412. package/src/Markdown/Markdown.doc.mjs +300 -2
  413. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  414. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  415. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  416. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  417. package/src/Markdown/Markdown.public.test.ts +130 -3
  418. package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
  419. package/src/Markdown/Markdown.spec.md +210 -45
  420. package/src/Markdown/Markdown.test.tsx +87 -3
  421. package/src/Markdown/Markdown.tsx +414 -135
  422. package/src/Markdown/ast.test.ts +307 -0
  423. package/src/Markdown/ast.ts +337 -0
  424. package/src/Markdown/incremental.test.ts +43 -0
  425. package/src/Markdown/index.ts +37 -1
  426. package/src/Markdown/modules/remark.spec.md +269 -0
  427. package/src/Markdown/modules/softBreaks.spec.md +137 -0
  428. package/src/Markdown/parser/index.ts +64 -0
  429. package/src/Markdown/parser.public.test.ts +103 -0
  430. package/src/Markdown/parser.test.ts +38 -6
  431. package/src/Markdown/parser.ts +1221 -242
  432. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  433. package/src/Markdown/plugins/frontmatter.ts +198 -0
  434. package/src/Markdown/plugins/index.ts +54 -0
  435. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  436. package/src/Markdown/plugins/protocol.ts +1618 -0
  437. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  438. package/src/Markdown/plugins/semanticFence.ts +424 -0
  439. package/src/Markdown/plugins/softBreaks.test.tsx +172 -0
  440. package/src/Markdown/plugins/softBreaks.ts +169 -0
  441. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  442. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  443. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  444. package/src/Markdown/plugins/textTransform.ts +554 -0
  445. package/src/Markdown/remark.test.tsx +1877 -0
  446. package/src/Markdown/remark.ts +1574 -0
  447. package/src/Markdown/url.ts +36 -0
  448. package/src/Markdown/utils.ts +13 -1
  449. package/src/MetadataList/MetadataList.test.tsx +4 -2
  450. package/src/MetadataList/MetadataList.tsx +3 -3
  451. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  452. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  453. package/src/NumberInput/NumberInput.tsx +6 -1
  454. package/src/Outline/Outline.doc.mjs +7 -5
  455. package/src/Outline/Outline.spec.md +1 -1
  456. package/src/Outline/index.ts +1 -0
  457. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  458. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  459. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  460. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  461. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  462. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  463. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +51 -0
  464. package/src/PowerSearch/PowerSearchValueEditor.tsx +4 -0
  465. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  466. package/src/ProgressBar/ProgressBar.tsx +59 -72
  467. package/src/RadioList/RadioList.doc.mjs +11 -0
  468. package/src/RadioList/RadioList.test.tsx +32 -0
  469. package/src/RadioList/RadioListItem.tsx +25 -1
  470. package/src/ScrollableArea/ScrollableArea.tsx +1 -0
  471. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  472. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  473. package/src/SegmentedControl/SegmentedControl.test.tsx +61 -0
  474. package/src/SegmentedControl/SegmentedControl.tsx +2 -1
  475. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  476. package/src/Selector/Selector.doc.mjs +3 -3
  477. package/src/Selector/Selector.spec.md +51 -49
  478. package/src/Selector/Selector.test.tsx +61 -55
  479. package/src/Selector/Selector.tsx +34 -15
  480. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  481. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  482. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  483. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  484. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  485. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +45 -0
  486. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  487. package/src/SideNav/SideNav.doc.mjs +1 -1
  488. package/src/SideNav/SideNav.test.tsx +10 -0
  489. package/src/SideNav/SideNav.tsx +14 -2
  490. package/src/Slider/Slider.doc.mjs +17 -3
  491. package/src/Slider/Slider.test.tsx +221 -0
  492. package/src/Slider/Slider.tsx +40 -9
  493. package/src/Spinner/Spinner.test.tsx +4 -0
  494. package/src/Spinner/Spinner.tsx +31 -18
  495. package/src/Switch/Switch.doc.mjs +11 -0
  496. package/src/Switch/Switch.test.tsx +16 -0
  497. package/src/Switch/Switch.tsx +28 -0
  498. package/src/TabList/Tab.tsx +35 -7
  499. package/src/TabList/TabList.doc.mjs +11 -0
  500. package/src/TabList/TabList.spec.md +204 -0
  501. package/src/TabList/TabList.test.tsx +66 -0
  502. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  503. package/src/Table/Table.doc.mjs +1 -1
  504. package/src/Table/Table.spec.md +54 -40
  505. package/src/Table/Table.test.tsx +83 -10
  506. package/src/Table/Table.tsx +30 -22
  507. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  508. package/src/TextArea/TextArea.tsx +6 -1
  509. package/src/TextInput/TextInput.test.tsx +20 -1
  510. package/src/TextInput/TextInput.tsx +6 -1
  511. package/src/TimeInput/TimeInput.tsx +6 -1
  512. package/src/Timer/Timer.doc.mjs +183 -0
  513. package/src/Timer/Timer.spec.md +215 -0
  514. package/src/Timer/Timer.test.tsx +302 -0
  515. package/src/Timer/Timer.tsx +258 -0
  516. package/src/Timer/index.ts +11 -0
  517. package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
  518. package/src/ToggleButton/ToggleButton.test.tsx +299 -4
  519. package/src/ToggleButton/ToggleButton.tsx +26 -33
  520. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
  521. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  522. package/src/Token/Token.doc.mjs +2 -2
  523. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  524. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  525. package/src/TopNav/TopNavMenu.test.tsx +30 -0
  526. package/src/TopNav/TopNavMenu.tsx +7 -2
  527. package/src/TreeList/TreeList.doc.mjs +3 -3
  528. package/src/TreeList/TreeList.spec.md +123 -75
  529. package/src/TreeList/TreeList.test.tsx +29 -0
  530. package/src/TreeList/TreeList.tsx +3 -0
  531. package/src/TreeList/TreeListItem.tsx +15 -1
  532. package/src/TreeList/TreeListTypes.ts +11 -1
  533. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  534. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  535. package/src/Typeahead/Typeahead.test.tsx +53 -0
  536. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  537. package/src/__tests__/pressState.ts +93 -0
  538. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  539. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  540. package/src/hooks/useClickableContainer.test.tsx +236 -0
  541. package/src/hooks/useClickableContainer.ts +20 -4
  542. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  543. package/src/hooks/useScrollableArea.test.tsx +59 -1
  544. package/src/hooks/useScrollableArea.ts +34 -10
  545. package/src/index.ts +3 -0
  546. package/src/inputFontFloor.test.ts +106 -0
  547. package/src/tailwind-theme.css +0 -6
  548. package/src/theme/Theme.test.tsx +32 -0
  549. package/src/theme/declarationBoundary.test.ts +126 -0
  550. package/src/theme/declarationBoundary.ts +347 -0
  551. package/src/theme/derivedVarRegistry.test.ts +7 -0
  552. package/src/theme/derivedVarRegistry.ts +1 -0
  553. package/src/theme/generateThemeRules.test.ts +526 -1
  554. package/src/theme/generateThemeRules.ts +228 -50
  555. package/src/theme/tokenValueCompat.test.ts +4 -1
  556. package/src/utils/interactionOverlay.stylex.ts +20 -0
  557. package/src/utils/safeUrl.test.ts +88 -0
  558. package/src/utils/safeUrl.ts +85 -0
@@ -0,0 +1,1522 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ChatSendButton.a11y.chromium.spec.ts
5
+ * @input Exact-head Storybook build plus checked-in ChatSendButton audit fixtures
6
+ * @output Chromium PNGs, 10-sensor receipts, D7 contrast pairs, coarse hit pairs,
7
+ * and a fail-closed manifest
8
+ * @position Real-browser evidence for the ChatSendButton component audit
9
+ */
10
+
11
+ import {createHash} from 'node:crypto';
12
+ import {execFileSync} from 'node:child_process';
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import {
16
+ expect,
17
+ test,
18
+ type Browser,
19
+ type BrowserContext,
20
+ type Locator,
21
+ type Page,
22
+ } from '@playwright/test';
23
+ // @ts-expect-error -- pngjs ships no declarations; runtime support is pinned.
24
+ import {PNG} from 'pngjs';
25
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
26
+ import {
27
+ DEFAULT_STORYBOOK_DIR,
28
+ serveStorybook,
29
+ type StaticServer,
30
+ } from '@astryxdesign/a11y-spec/storybook';
31
+
32
+ const OUTPUT = path.resolve('test-results/chat-send-button-audit-evidence');
33
+ const NARROW_STORY = 'core-chatcomposer--disabled-streaming-narrow';
34
+ const SUBJECT = '.astryx-chat-send-button';
35
+
36
+ // A stale prior run must never fill a missing receipt. Run this file serially
37
+ // without retries, and clear its output once before the exact-head matrix starts.
38
+ test.describe.configure({mode: 'serial', retries: 0});
39
+
40
+ type Mode = 'light' | 'dark';
41
+ type Direction = 'ltr' | 'rtl';
42
+ type Interaction = 'rest' | 'hover' | 'focus-visible' | 'pressed';
43
+ type State = 'send' | 'disabled-send' | 'stop';
44
+ type Size = 'sm' | 'md';
45
+ type Rgba = {r: number; g: number; b: number; a: number};
46
+
47
+ interface ContrastPair {
48
+ part: 'icon' | 'focus-indicator' | 'boundary';
49
+ meaningful: boolean;
50
+ foreground: string;
51
+ backdrop: string;
52
+ resolvedBackdrop: string;
53
+ ratio: number | null;
54
+ threshold: number | null;
55
+ exception: string | null;
56
+ passed: boolean;
57
+ }
58
+
59
+ interface Fixture {
60
+ key: string;
61
+ storyId: string;
62
+ name: 'Send' | 'Stop';
63
+ state: State;
64
+ size: Size;
65
+ disabled: boolean;
66
+ }
67
+
68
+ const FIXTURES: Record<string, Fixture> = {
69
+ sendMd: {
70
+ key: 'send-md',
71
+ storyId: 'a11y-button-pattern--chat-send',
72
+ name: 'Send',
73
+ state: 'send',
74
+ size: 'md',
75
+ disabled: false,
76
+ },
77
+ disabledMd: {
78
+ key: 'send-disabled-md',
79
+ storyId: 'a11y-button-pattern--chat-send-disabled',
80
+ name: 'Send',
81
+ state: 'disabled-send',
82
+ size: 'md',
83
+ disabled: true,
84
+ },
85
+ stopMd: {
86
+ key: 'stop-md',
87
+ storyId: 'a11y-button-pattern--chat-send-stop',
88
+ name: 'Stop',
89
+ state: 'stop',
90
+ size: 'md',
91
+ disabled: false,
92
+ },
93
+ sendSm: {
94
+ key: 'send-sm',
95
+ storyId: 'a11y-chatsendbutton-audit--chat-send-small',
96
+ name: 'Send',
97
+ state: 'send',
98
+ size: 'sm',
99
+ disabled: false,
100
+ },
101
+ stopSm: {
102
+ key: 'stop-sm',
103
+ storyId: 'a11y-chatsendbutton-audit--chat-send-stop-small',
104
+ name: 'Stop',
105
+ state: 'stop',
106
+ size: 'sm',
107
+ disabled: false,
108
+ },
109
+ };
110
+
111
+ interface Shot {
112
+ file: string;
113
+ sha256: string;
114
+ width: number;
115
+ height: number;
116
+ distinctColors: number;
117
+ nonBlank: boolean;
118
+ }
119
+
120
+ interface Receipt {
121
+ frame: string;
122
+ passed: boolean;
123
+ failures: string[];
124
+ build: {expected: string; checkout: string; storybook: string};
125
+ story: {id: string; expectedName: string};
126
+ theme: {requested: 'neutral'; observed: string | null};
127
+ mode: {
128
+ requested: Mode;
129
+ observed: string | null;
130
+ colorScheme: string;
131
+ surfaceBackground: string;
132
+ surfaceLuminanceClass: 'light' | 'dark' | 'unknown';
133
+ };
134
+ direction: {requested: Direction; observed: string};
135
+ viewport: {width: number; height: number};
136
+ devicePixelRatio: number;
137
+ media: {
138
+ reducedMotion: boolean;
139
+ forcedColors: boolean;
140
+ coarsePointer: boolean;
141
+ hoverCapable: boolean;
142
+ touchPoints: number;
143
+ };
144
+ semantics: {
145
+ role: string | null;
146
+ accessibleName: string | null;
147
+ disabled: boolean;
148
+ size: string | null;
149
+ variant: string | null;
150
+ };
151
+ geometry: {
152
+ selectorCount: number;
153
+ visibleCount: number;
154
+ width: number;
155
+ height: number;
156
+ circular: boolean;
157
+ };
158
+ interaction: {
159
+ requested: Interaction;
160
+ matchesHover: boolean;
161
+ matchesFocusVisible: boolean;
162
+ matchesActive: boolean;
163
+ cursor: string;
164
+ color: string;
165
+ backgroundColor: string;
166
+ backgroundImage: string;
167
+ opacity: string;
168
+ transform: string;
169
+ outlineColor: string;
170
+ outlineOffset: string;
171
+ outlineStyle: string;
172
+ outlineWidth: string;
173
+ representation: string;
174
+ tokenSignaturePresent: boolean;
175
+ restSha256?: string;
176
+ changedPixels?: boolean;
177
+ };
178
+ contrastPairs: ContrastPair[];
179
+ sensors: Record<
180
+ | 'Build'
181
+ | 'Story'
182
+ | 'Theme'
183
+ | 'Color mode'
184
+ | 'Direction'
185
+ | 'Viewport/media'
186
+ | 'Rendered state'
187
+ | 'Subject geometry'
188
+ | 'Settled render'
189
+ | 'Image',
190
+ {expected: unknown; observed: unknown; passed: boolean}
191
+ >;
192
+ settled: {fontsReady: boolean; pageErrors: string[]; storyError: boolean};
193
+ image: Shot;
194
+ }
195
+
196
+ interface CoarseHitPair {
197
+ fixture: string;
198
+ size: Size;
199
+ state: 'send' | 'stop';
200
+ direction: Direction;
201
+ coarsePointer: boolean;
202
+ touchPoints: number;
203
+ selectorCount: number;
204
+ visibleCount: number;
205
+ target: {x: number; y: number; width: number; height: number};
206
+ hit: boolean;
207
+ centered: boolean;
208
+ passed: boolean;
209
+ }
210
+
211
+ let server: StaticServer;
212
+ let head = '';
213
+ let build = '';
214
+ let browserVersion = 'unknown';
215
+ const receipts: Receipt[] = [];
216
+ const coarseHitPairs: CoarseHitPair[] = [];
217
+
218
+ function pngShot(file: string, bytes: Buffer): Shot {
219
+ const png = PNG.sync.read(bytes);
220
+ const colors = new Set<string>();
221
+ for (let index = 0; index < png.data.length; index += 4) {
222
+ colors.add(
223
+ `${png.data[index]},${png.data[index + 1]},${png.data[index + 2]},${png.data[index + 3]}`,
224
+ );
225
+ }
226
+ return {
227
+ file,
228
+ sha256: createHash('sha256').update(bytes).digest('hex'),
229
+ width: png.width,
230
+ height: png.height,
231
+ distinctColors: colors.size,
232
+ nonBlank: colors.size > 1,
233
+ };
234
+ }
235
+
236
+ function parseRgb(value: string): Rgba | null {
237
+ const match = value.match(/rgba?\(([^)]+)\)/);
238
+ const channels = match?.[1].match(/[\d.]+/g)?.map(Number) ?? [];
239
+ if (channels.length < 3) {
240
+ return null;
241
+ }
242
+ return {r: channels[0], g: channels[1], b: channels[2], a: channels[3] ?? 1};
243
+ }
244
+
245
+ function composite(foreground: Rgba, background: Rgba): Rgba {
246
+ const alpha = foreground.a + background.a * (1 - foreground.a);
247
+ const channel = (front: number, back: number) =>
248
+ alpha === 0
249
+ ? 0
250
+ : (front * foreground.a + back * background.a * (1 - foreground.a)) /
251
+ alpha;
252
+ return {
253
+ r: channel(foreground.r, background.r),
254
+ g: channel(foreground.g, background.g),
255
+ b: channel(foreground.b, background.b),
256
+ a: alpha,
257
+ };
258
+ }
259
+
260
+ function luminance(color: Rgba): number {
261
+ const linear = [color.r, color.g, color.b].map(channel => {
262
+ const value = channel / 255;
263
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
264
+ });
265
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
266
+ }
267
+
268
+ function ratio(
269
+ foreground: Rgba | null,
270
+ background: Rgba | null,
271
+ ): number | null {
272
+ if (foreground == null || background == null) {
273
+ return null;
274
+ }
275
+ const first = luminance(foreground);
276
+ const second = luminance(background);
277
+ return Number(
278
+ (
279
+ (Math.max(first, second) + 0.05) /
280
+ (Math.min(first, second) + 0.05)
281
+ ).toFixed(2),
282
+ );
283
+ }
284
+
285
+ function colorString(color: Rgba): string {
286
+ return `rgb(${Math.round(color.r)}, ${Math.round(color.g)}, ${Math.round(color.b)})`;
287
+ }
288
+
289
+ function sameColor(first: Rgba | null, second: Rgba | null): boolean {
290
+ return (
291
+ first != null &&
292
+ second != null &&
293
+ Math.abs(first.r - second.r) < 0.01 &&
294
+ Math.abs(first.g - second.g) < 0.01 &&
295
+ Math.abs(first.b - second.b) < 0.01 &&
296
+ Math.abs(first.a - second.a) < 0.001
297
+ );
298
+ }
299
+
300
+ function isIdentityTransform(transform: string): boolean {
301
+ if (transform === 'none') {
302
+ return true;
303
+ }
304
+ const values = transform
305
+ .slice(transform.indexOf('(') + 1, transform.lastIndexOf(')'))
306
+ .match(/-?[\d.]+/g)
307
+ ?.map(Number);
308
+ const close = (actual: number, expected: number) =>
309
+ Math.abs(actual - expected) < 0.001;
310
+ if (transform.startsWith('matrix3d(') && values?.length === 16) {
311
+ const expected = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
312
+ return values.every((value, index) => close(value, expected[index]));
313
+ }
314
+ if (transform.startsWith('matrix(') && values?.length === 6) {
315
+ const expected = [1, 0, 0, 1, 0, 0];
316
+ return values.every((value, index) => close(value, expected[index]));
317
+ }
318
+ return false;
319
+ }
320
+
321
+ function isScale098(transform: string): boolean {
322
+ const values = transform
323
+ .slice(transform.indexOf('(') + 1, transform.lastIndexOf(')'))
324
+ .match(/-?[\d.]+/g)
325
+ ?.map(Number);
326
+ const close = (actual: number, expected: number) =>
327
+ Math.abs(actual - expected) < 0.001;
328
+ if (transform.startsWith('matrix3d(') && values?.length === 16) {
329
+ const expected = [0.98, 0, 0, 0, 0, 0.98, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
330
+ return values.every((value, index) => close(value, expected[index]));
331
+ }
332
+ if (transform.startsWith('matrix(') && values?.length === 6) {
333
+ const expected = [0.98, 0, 0, 0.98, 0, 0];
334
+ return values.every((value, index) => close(value, expected[index]));
335
+ }
336
+ return false;
337
+ }
338
+
339
+ function resolveBackdrop(
340
+ backgroundColor: string,
341
+ backgroundImage: string,
342
+ surfaceBackground: string,
343
+ ): {
344
+ color: Rgba | null;
345
+ css: string;
346
+ overlay: Rgba | null;
347
+ } {
348
+ const base = parseRgb(backgroundColor);
349
+ const surface = parseRgb(surfaceBackground);
350
+ const resolvedBase =
351
+ base != null && base.a < 1 && surface != null
352
+ ? composite(base, surface)
353
+ : base;
354
+ const gradientColor = backgroundImage.match(/rgba?\([^)]+\)/)?.[0];
355
+ const overlay = gradientColor ? parseRgb(gradientColor) : null;
356
+ const color =
357
+ resolvedBase != null && overlay != null
358
+ ? composite(overlay, resolvedBase)
359
+ : resolvedBase;
360
+ return {
361
+ color,
362
+ css: color == null ? backgroundColor : colorString(color),
363
+ overlay,
364
+ };
365
+ }
366
+
367
+ function contrastPairs(
368
+ actual: Awaited<ReturnType<typeof readSensors>>,
369
+ fixture: Fixture,
370
+ interaction: Interaction,
371
+ ): ContrastPair[] {
372
+ const backdrop = resolveBackdrop(
373
+ actual.backgroundColor,
374
+ actual.backgroundImage,
375
+ actual.surfaceBackground,
376
+ );
377
+ const surface = parseRgb(actual.surfaceBackground);
378
+ const iconRatio = ratio(parseRgb(actual.color), backdrop.color);
379
+ const inactive = fixture.disabled;
380
+ const pairs: ContrastPair[] = [
381
+ {
382
+ part: 'icon',
383
+ meaningful: !inactive,
384
+ foreground: actual.color,
385
+ backdrop: `${actual.backgroundColor}; overlay ${actual.backgroundImage}`,
386
+ resolvedBackdrop: backdrop.css,
387
+ ratio: iconRatio,
388
+ threshold: inactive ? null : 3,
389
+ exception: inactive ? 'WCAG inactive-control exception' : null,
390
+ passed: inactive || (iconRatio != null && iconRatio >= 3),
391
+ },
392
+ {
393
+ part: 'boundary',
394
+ meaningful: false,
395
+ foreground: backdrop.css,
396
+ backdrop: actual.surfaceBackground,
397
+ resolvedBackdrop: actual.surfaceBackground,
398
+ ratio: ratio(backdrop.color, surface),
399
+ threshold: null,
400
+ exception:
401
+ 'Decorative boundary: the meaningful icon and accessible name identify the control.',
402
+ passed: true,
403
+ },
404
+ ];
405
+ if (interaction === 'focus-visible') {
406
+ const focusRatio = ratio(parseRgb(actual.outlineColor), surface);
407
+ pairs.push({
408
+ part: 'focus-indicator',
409
+ meaningful: true,
410
+ foreground: actual.outlineColor,
411
+ backdrop: actual.surfaceBackground,
412
+ resolvedBackdrop: actual.surfaceBackground,
413
+ ratio: focusRatio,
414
+ threshold: 3,
415
+ exception: null,
416
+ passed: focusRatio != null && focusRatio >= 3,
417
+ });
418
+ }
419
+ return pairs;
420
+ }
421
+
422
+ function storyUrl(
423
+ storyId: string,
424
+ mode: Mode,
425
+ direction: Direction = 'ltr',
426
+ ): string {
427
+ return (
428
+ `${server.origin}/iframe.html?id=${storyId}&viewMode=story` +
429
+ `&globals=colorMode:${mode};astryxTheme:neutral;direction:${direction}`
430
+ );
431
+ }
432
+
433
+ async function openFixture(
434
+ page: Page,
435
+ fixture: Fixture,
436
+ mode: Mode,
437
+ direction: Direction = 'ltr',
438
+ ): Promise<{target: Locator; errors: string[]}> {
439
+ const errors: string[] = [];
440
+ page.removeAllListeners('pageerror');
441
+ page.on('pageerror', error => errors.push(String(error)));
442
+ browserVersion = page.context().browser()?.version() ?? 'unknown';
443
+ await page.goto(storyUrl(fixture.storyId, mode, direction));
444
+ await page.locator(SUBJECT).first().waitFor();
445
+ await page.waitForFunction(
446
+ expected =>
447
+ document.documentElement.getAttribute('data-theme') === expected,
448
+ mode,
449
+ );
450
+ await holdMotionStill(page);
451
+ await page.evaluate(async () => {
452
+ await document.fonts.ready;
453
+ if (document.activeElement instanceof HTMLElement) {
454
+ document.activeElement.blur();
455
+ }
456
+ });
457
+ await page.mouse.move(0, 0);
458
+ return {target: page.locator(SUBJECT), errors};
459
+ }
460
+
461
+ async function settleStoryPlayAndBlur(page: Page): Promise<void> {
462
+ await page.waitForFunction(
463
+ () =>
464
+ document
465
+ .querySelector('[data-testid="disabled-streaming-fixture"]')
466
+ ?.getAttribute('data-stop-requests') === '1',
467
+ );
468
+ await page.evaluate(() => {
469
+ if (document.activeElement instanceof HTMLElement) {
470
+ document.activeElement.blur();
471
+ }
472
+ });
473
+ await page
474
+ .locator(SUBJECT)
475
+ .first()
476
+ .evaluate(element => {
477
+ if (element.matches(':focus-visible')) {
478
+ throw new Error(
479
+ 'ChatSendButton retained focus-visible after story play',
480
+ );
481
+ }
482
+ });
483
+ }
484
+
485
+ async function readSensors(page: Page, interaction: Interaction) {
486
+ return page.locator(SUBJECT).evaluateAll((elements, expected) => {
487
+ const visible = elements.filter(element => {
488
+ const style = getComputedStyle(element);
489
+ const box = element.getBoundingClientRect();
490
+ return (
491
+ box.width > 0 &&
492
+ box.height > 0 &&
493
+ style.display !== 'none' &&
494
+ style.visibility !== 'hidden' &&
495
+ style.opacity !== '0'
496
+ );
497
+ });
498
+ const element = visible[0] as HTMLButtonElement | undefined;
499
+ const style = element ? getComputedStyle(element) : null;
500
+ const box = element?.getBoundingClientRect();
501
+ const radius = style ? Number.parseFloat(style.borderTopLeftRadius) : 0;
502
+ const parseRgb = (value: string) => {
503
+ const channels = value.match(/[\d.]+/g)?.map(Number) ?? [];
504
+ return channels.length >= 3
505
+ ? {r: channels[0], g: channels[1], b: channels[2], a: channels[3] ?? 1}
506
+ : null;
507
+ };
508
+ const relativeLuminance = (color: {r: number; g: number; b: number}) => {
509
+ const linear = [color.r, color.g, color.b].map(channel => {
510
+ const value = channel / 255;
511
+ return value <= 0.04045
512
+ ? value / 12.92
513
+ : ((value + 0.055) / 1.055) ** 2.4;
514
+ });
515
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
516
+ };
517
+ const resolveColorToken = (name: string) => {
518
+ if (element == null) {
519
+ return '';
520
+ }
521
+ const probe = document.createElement('span');
522
+ probe.style.backgroundColor = `var(${name})`;
523
+ probe.style.display = 'none';
524
+ element.appendChild(probe);
525
+ const resolved = getComputedStyle(probe).backgroundColor;
526
+ probe.remove();
527
+ return resolved;
528
+ };
529
+ const ancestors: Element[] = [];
530
+ for (
531
+ let current = element?.parentElement;
532
+ current != null;
533
+ current = current.parentElement
534
+ ) {
535
+ ancestors.push(current);
536
+ }
537
+ const surfaceBackground = [
538
+ ...ancestors,
539
+ document.querySelector('[data-astryx-theme]'),
540
+ document.querySelector('#storybook-root'),
541
+ document.body,
542
+ document.documentElement,
543
+ ]
544
+ .map(candidate =>
545
+ candidate == null ? null : getComputedStyle(candidate).backgroundColor,
546
+ )
547
+ .find(value => {
548
+ const color = value == null ? null : parseRgb(value);
549
+ return color != null && color.a > 0.99;
550
+ });
551
+ const surfaceColor = surfaceBackground ? parseRgb(surfaceBackground) : null;
552
+ const surfaceLuminanceClass: 'light' | 'dark' | 'unknown' =
553
+ surfaceColor == null
554
+ ? 'unknown'
555
+ : relativeLuminance(surfaceColor) > 0.5
556
+ ? 'light'
557
+ : 'dark';
558
+ return {
559
+ selectorCount: elements.length,
560
+ visibleCount: visible.length,
561
+ role:
562
+ element?.getAttribute('role') ?? element?.tagName.toLowerCase() ?? null,
563
+ accessibleName: element?.getAttribute('aria-label') ?? null,
564
+ disabled: element?.disabled ?? false,
565
+ size: element?.getAttribute('data-size') ?? null,
566
+ variant: element?.getAttribute('data-variant') ?? null,
567
+ direction: style?.direction ?? '',
568
+ width: box?.width ?? 0,
569
+ height: box?.height ?? 0,
570
+ circular:
571
+ box != null &&
572
+ Math.abs(box.width - box.height) < 0.5 &&
573
+ radius >= Math.min(box.width, box.height) / 2 - 0.5,
574
+ matchesHover: element?.matches(':hover') ?? false,
575
+ matchesFocusVisible: element?.matches(':focus-visible') ?? false,
576
+ matchesActive: element?.matches(':active') ?? false,
577
+ cursor: style?.cursor ?? '',
578
+ color: style?.color ?? '',
579
+ backgroundColor: style?.backgroundColor ?? '',
580
+ backgroundImage: style?.backgroundImage ?? '',
581
+ hoverOverlayToken: resolveColorToken('--color-overlay-hover'),
582
+ pressedOverlayToken: resolveColorToken('--color-overlay-pressed'),
583
+ opacity: style?.opacity ?? '',
584
+ transform: style?.transform ?? '',
585
+ outlineColor: style?.outlineColor ?? '',
586
+ outlineOffset: style?.outlineOffset ?? '',
587
+ outlineStyle: style?.outlineStyle ?? '',
588
+ outlineWidth: style?.outlineWidth ?? '',
589
+ mode: document.documentElement.getAttribute('data-theme'),
590
+ theme:
591
+ document
592
+ .querySelector('[data-astryx-theme]')
593
+ ?.getAttribute('data-astryx-theme') ?? null,
594
+ colorScheme: getComputedStyle(document.documentElement).colorScheme,
595
+ surfaceBackground: surfaceBackground ?? 'transparent',
596
+ surfaceLuminanceClass,
597
+ fontsReady: document.fonts.status === 'loaded',
598
+ reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
599
+ forcedColors: matchMedia('(forced-colors: active)').matches,
600
+ coarsePointer: matchMedia('(pointer: coarse)').matches,
601
+ hoverCapable: matchMedia('(hover: hover)').matches,
602
+ touchPoints: navigator.maxTouchPoints,
603
+ storyError: [
604
+ ...document.querySelectorAll(
605
+ '.sb-errordisplay, [data-testid="story-error"]',
606
+ ),
607
+ ].some(node => {
608
+ const nodeStyle = getComputedStyle(node);
609
+ const nodeBox = node.getBoundingClientRect();
610
+ return (
611
+ nodeBox.width > 0 &&
612
+ nodeBox.height > 0 &&
613
+ nodeStyle.display !== 'none' &&
614
+ nodeStyle.visibility !== 'hidden' &&
615
+ nodeStyle.opacity !== '0'
616
+ );
617
+ }),
618
+ expectedInteraction: expected,
619
+ storyId: new URL(location.href).searchParams.get('id'),
620
+ };
621
+ }, interaction);
622
+ }
623
+
624
+ async function screenshotTarget(page: Page, frame: string): Promise<Shot> {
625
+ const target = page.locator(SUBJECT).first();
626
+ await page.locator('[data-a11y-activations] > p').evaluateAll(nodes => {
627
+ for (const node of nodes) {
628
+ (node as HTMLElement).style.visibility = 'hidden';
629
+ }
630
+ });
631
+ const box = await target.boundingBox();
632
+ if (box == null) {
633
+ throw new Error(`${frame}: ChatSendButton has no layout box`);
634
+ }
635
+ const padding = 8;
636
+ const viewport = page.viewportSize();
637
+ const clip = {
638
+ x: Math.max(0, box.x - padding),
639
+ y: Math.max(0, box.y - padding),
640
+ width: Math.min(
641
+ (viewport?.width ?? box.x + box.width) - Math.max(0, box.x - padding),
642
+ box.width + padding * 2,
643
+ ),
644
+ height: Math.min(
645
+ (viewport?.height ?? box.y + box.height) - Math.max(0, box.y - padding),
646
+ box.height + padding * 2,
647
+ ),
648
+ };
649
+ const bytes = await page.screenshot({animations: 'disabled', clip});
650
+ const file = `chat-send-button__${frame}.png`;
651
+ fs.writeFileSync(path.join(OUTPUT, file), bytes);
652
+ return pngShot(file, bytes);
653
+ }
654
+
655
+ async function writeContactSheet(page: Page): Promise<void> {
656
+ const cards = receipts
657
+ .map(receipt => {
658
+ const bytes = fs.readFileSync(path.join(OUTPUT, receipt.image.file));
659
+ const source = `data:image/png;base64,${bytes.toString('base64')}`;
660
+ return `<figure><img src="${source}" alt=""><figcaption>${receipt.frame}</figcaption></figure>`;
661
+ })
662
+ .join('');
663
+ await page.setViewportSize({width: 1200, height: 900});
664
+ await page.setContent(`<!doctype html>
665
+ <style>
666
+ html { color-scheme: light; background: #f3f4f6; }
667
+ body { margin: 20px; font: 12px/1.35 system-ui, sans-serif; color: #111827; }
668
+ h1 { margin: 0 0 16px; font-size: 20px; }
669
+ main { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
670
+ figure { margin: 0; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; }
671
+ img { display: block; width: 100%; height: 96px; object-fit: contain; }
672
+ figcaption { margin-top: 8px; overflow-wrap: anywhere; }
673
+ </style>
674
+ <h1>ChatSendButton exact-head state evidence</h1>
675
+ <main>${cards}</main>`);
676
+ await page.locator('img').evaluateAll(async images => {
677
+ await Promise.all(
678
+ images.map(async image => {
679
+ const candidate = image as HTMLImageElement;
680
+ if (candidate.complete) {
681
+ return;
682
+ }
683
+ await new Promise<void>((resolve, reject) => {
684
+ candidate.addEventListener('load', () => resolve(), {once: true});
685
+ candidate.addEventListener(
686
+ 'error',
687
+ () => reject(new Error('image failed')),
688
+ {once: true},
689
+ );
690
+ });
691
+ }),
692
+ );
693
+ });
694
+ await page.screenshot({
695
+ path: path.join(OUTPUT, 'chat-send-button__contact-sheet.png'),
696
+ fullPage: true,
697
+ animations: 'disabled',
698
+ });
699
+ }
700
+
701
+ async function capture(
702
+ page: Page,
703
+ fixture: Fixture,
704
+ mode: Mode,
705
+ interaction: Interaction,
706
+ errors: string[],
707
+ options: {
708
+ direction?: Direction;
709
+ restSha256?: string;
710
+ expectChangedPixels?: boolean;
711
+ expectCoarse?: boolean;
712
+ expectedViewport?: {width: number; height: number};
713
+ frameSuffix?: string;
714
+ } = {},
715
+ ): Promise<Receipt> {
716
+ const direction = options.direction ?? 'ltr';
717
+ const frame = `${fixture.key}__${interaction}__${mode}${options.frameSuffix ?? ''}`;
718
+ const actual = await readSensors(page, interaction);
719
+ const image = await screenshotTarget(page, frame);
720
+ const viewport = await page.evaluate(() => ({
721
+ width: innerWidth,
722
+ height: innerHeight,
723
+ dpr: devicePixelRatio,
724
+ }));
725
+ const expectedViewport = options.expectedViewport ?? {
726
+ width: 1280,
727
+ height: 720,
728
+ };
729
+ const expectedCoarse = options.expectCoarse ?? false;
730
+ const changedPixels =
731
+ options.restSha256 == null
732
+ ? undefined
733
+ : image.sha256 !== options.restSha256;
734
+ const resolvedBackdrop = resolveBackdrop(
735
+ actual.backgroundColor,
736
+ actual.backgroundImage,
737
+ actual.surfaceBackground,
738
+ );
739
+ const overlayIsAlpha =
740
+ resolvedBackdrop.overlay != null &&
741
+ resolvedBackdrop.overlay.a > 0 &&
742
+ resolvedBackdrop.overlay.a < 1;
743
+ const hoverOverlayMatches = sameColor(
744
+ resolvedBackdrop.overlay,
745
+ parseRgb(actual.hoverOverlayToken),
746
+ );
747
+ const pressedOverlayMatches = sameColor(
748
+ resolvedBackdrop.overlay,
749
+ parseRgb(actual.pressedOverlayToken),
750
+ );
751
+ const representation = fixture.disabled
752
+ ? 'Disabled: native disabled, muted and non-interactive'
753
+ : interaction === 'hover'
754
+ ? 'Hovered: overlay style'
755
+ : interaction === 'focus-visible'
756
+ ? 'Focused: outline style'
757
+ : interaction === 'pressed'
758
+ ? 'Pressed: scale(0.98) plus pressed overlay'
759
+ : 'Rest: base tokens with no interaction';
760
+ const interactionStateIsExclusive =
761
+ interaction === 'rest'
762
+ ? !actual.matchesHover &&
763
+ !actual.matchesFocusVisible &&
764
+ !actual.matchesActive &&
765
+ actual.outlineStyle === 'none'
766
+ : interaction === 'hover'
767
+ ? actual.matchesHover &&
768
+ !actual.matchesFocusVisible &&
769
+ !actual.matchesActive &&
770
+ actual.outlineStyle === 'none'
771
+ : interaction === 'focus-visible'
772
+ ? !actual.matchesHover &&
773
+ actual.matchesFocusVisible &&
774
+ !actual.matchesActive &&
775
+ actual.outlineStyle !== 'none'
776
+ : actual.matchesHover &&
777
+ !actual.matchesFocusVisible &&
778
+ actual.matchesActive &&
779
+ actual.outlineStyle === 'none';
780
+ const tokenSignaturePresent = fixture.disabled
781
+ ? actual.disabled &&
782
+ actual.cursor === 'default' &&
783
+ Number.parseFloat(actual.opacity) < 1 &&
784
+ actual.backgroundImage === 'none' &&
785
+ isIdentityTransform(actual.transform) &&
786
+ interactionStateIsExclusive
787
+ : interaction === 'hover'
788
+ ? overlayIsAlpha && hoverOverlayMatches && interactionStateIsExclusive
789
+ : interaction === 'focus-visible'
790
+ ? actual.outlineStyle !== 'none' &&
791
+ Number.parseFloat(actual.outlineWidth) >= 2 &&
792
+ Number.parseFloat(actual.outlineOffset) >= 3 &&
793
+ interactionStateIsExclusive
794
+ : interaction === 'pressed'
795
+ ? overlayIsAlpha &&
796
+ pressedOverlayMatches &&
797
+ isScale098(actual.transform) &&
798
+ interactionStateIsExclusive
799
+ : interactionStateIsExclusive;
800
+ const renderedContrastPairs = contrastPairs(actual, fixture, interaction);
801
+ const failures: string[] = [];
802
+ const check = (condition: boolean, message: string) => {
803
+ if (!condition) {
804
+ failures.push(message);
805
+ }
806
+ };
807
+
808
+ check(
809
+ actual.selectorCount === 1,
810
+ 'expected exactly one ChatSendButton selector match',
811
+ );
812
+ check(
813
+ actual.visibleCount === 1,
814
+ 'expected exactly one visible ChatSendButton',
815
+ );
816
+ check(actual.storyId === fixture.storyId, 'Storybook story id drifted');
817
+ check(actual.role === 'button', 'native button role drifted');
818
+ check(actual.accessibleName === fixture.name, 'accessible name drifted');
819
+ check(actual.disabled === fixture.disabled, 'disabled state drifted');
820
+ check(actual.size === fixture.size, 'size attribute drifted');
821
+ check(
822
+ actual.variant === (fixture.state === 'stop' ? 'secondary' : 'primary'),
823
+ 'state variant drifted',
824
+ );
825
+ check(actual.direction === direction, 'computed direction drifted');
826
+ check(actual.circular, 'target is not circular');
827
+ check(
828
+ actual.width >= 24 && actual.height >= 24,
829
+ 'target does not contain a 24px square',
830
+ );
831
+ check(
832
+ image.nonBlank && image.width > 0 && image.height > 0,
833
+ 'PNG is blank or has no painted dimensions',
834
+ );
835
+ check(actual.mode === mode, 'color mode did not settle');
836
+ check(actual.theme === 'neutral', 'neutral theme did not settle');
837
+ check(
838
+ actual.surfaceLuminanceClass === mode,
839
+ 'rendered surface luminance does not match color mode',
840
+ );
841
+ check(actual.colorScheme.includes(mode), 'computed color-scheme drifted');
842
+ check(actual.fontsReady, 'fonts did not settle');
843
+ check(actual.reducedMotion, 'motion was not held still');
844
+ check(!actual.forcedColors, 'forced-colors unexpectedly active');
845
+ check(
846
+ actual.hoverCapable === !actual.coarsePointer,
847
+ 'pointer and hover media disagree',
848
+ );
849
+ check(actual.coarsePointer === expectedCoarse, 'pointer modality drifted');
850
+ check(
851
+ viewport.width === expectedViewport.width &&
852
+ viewport.height === expectedViewport.height,
853
+ 'viewport drifted',
854
+ );
855
+ check(viewport.dpr === 1, 'device pixel ratio drifted');
856
+ check(tokenSignaturePresent, `${representation} token signature is absent`);
857
+ for (const pair of renderedContrastPairs) {
858
+ check(pair.passed, `${pair.part} contrast pair failed`);
859
+ }
860
+ check(!actual.storyError, 'Storybook rendered an error surface');
861
+ check(errors.length === 0, 'page emitted an error');
862
+ if (options.expectCoarse) {
863
+ check(actual.touchPoints > 0, 'coarse-pointer context has no touch points');
864
+ }
865
+ check(
866
+ interactionStateIsExclusive,
867
+ `${interaction} state is not interaction-exclusive`,
868
+ );
869
+ if (options.expectChangedPixels != null) {
870
+ check(
871
+ changedPixels === options.expectChangedPixels,
872
+ options.expectChangedPixels
873
+ ? 'interaction pixels are identical to rest'
874
+ : 'disabled hover changed pixels',
875
+ );
876
+ }
877
+ if (fixture.disabled) {
878
+ check(actual.cursor === 'default', 'disabled cursor is not default');
879
+ check(
880
+ actual.backgroundImage === 'none',
881
+ 'disabled state paints a hover image',
882
+ );
883
+ check(Number.parseFloat(actual.opacity) < 1, 'disabled state is not muted');
884
+ check(
885
+ isIdentityTransform(actual.transform),
886
+ 'disabled state paints a non-identity transform',
887
+ );
888
+ }
889
+
890
+ const receipt: Receipt = {
891
+ frame,
892
+ passed: failures.length === 0,
893
+ failures,
894
+ build: {expected: head, checkout: head, storybook: build},
895
+ story: {id: fixture.storyId, expectedName: fixture.name},
896
+ theme: {requested: 'neutral', observed: actual.theme},
897
+ mode: {
898
+ requested: mode,
899
+ observed: actual.mode,
900
+ colorScheme: actual.colorScheme,
901
+ surfaceBackground: actual.surfaceBackground,
902
+ surfaceLuminanceClass: actual.surfaceLuminanceClass,
903
+ },
904
+ direction: {requested: direction, observed: actual.direction},
905
+ viewport: {width: viewport.width, height: viewport.height},
906
+ devicePixelRatio: viewport.dpr,
907
+ media: {
908
+ reducedMotion: actual.reducedMotion,
909
+ forcedColors: actual.forcedColors,
910
+ coarsePointer: actual.coarsePointer,
911
+ hoverCapable: actual.hoverCapable,
912
+ touchPoints: actual.touchPoints,
913
+ },
914
+ semantics: {
915
+ role: actual.role,
916
+ accessibleName: actual.accessibleName,
917
+ disabled: actual.disabled,
918
+ size: actual.size,
919
+ variant: actual.variant,
920
+ },
921
+ geometry: {
922
+ selectorCount: actual.selectorCount,
923
+ visibleCount: actual.visibleCount,
924
+ width: actual.width,
925
+ height: actual.height,
926
+ circular: actual.circular,
927
+ },
928
+ interaction: {
929
+ requested: interaction,
930
+ matchesHover: actual.matchesHover,
931
+ matchesFocusVisible: actual.matchesFocusVisible,
932
+ matchesActive: actual.matchesActive,
933
+ cursor: actual.cursor,
934
+ color: actual.color,
935
+ backgroundColor: actual.backgroundColor,
936
+ backgroundImage: actual.backgroundImage,
937
+ opacity: actual.opacity,
938
+ transform: actual.transform,
939
+ outlineColor: actual.outlineColor,
940
+ outlineOffset: actual.outlineOffset,
941
+ outlineStyle: actual.outlineStyle,
942
+ outlineWidth: actual.outlineWidth,
943
+ representation,
944
+ tokenSignaturePresent,
945
+ restSha256: options.restSha256,
946
+ changedPixels,
947
+ },
948
+ contrastPairs: renderedContrastPairs,
949
+ sensors: {
950
+ Build: {
951
+ expected: {headSha: head},
952
+ observed: {checkoutSha: head, storybookSha: build},
953
+ passed: build === head,
954
+ },
955
+ Story: {
956
+ expected: {id: fixture.storyId, accessibleName: fixture.name},
957
+ observed: {id: actual.storyId, accessibleName: actual.accessibleName},
958
+ passed:
959
+ actual.storyId === fixture.storyId &&
960
+ actual.accessibleName === fixture.name,
961
+ },
962
+ Theme: {
963
+ expected: 'neutral',
964
+ observed: actual.theme,
965
+ passed: actual.theme === 'neutral',
966
+ },
967
+ 'Color mode': {
968
+ expected: {
969
+ global: mode,
970
+ colorScheme: mode,
971
+ surfaceLuminanceClass: mode,
972
+ },
973
+ observed: {
974
+ global: actual.mode,
975
+ colorScheme: actual.colorScheme,
976
+ surfaceBackground: actual.surfaceBackground,
977
+ surfaceLuminanceClass: actual.surfaceLuminanceClass,
978
+ },
979
+ passed:
980
+ actual.mode === mode &&
981
+ actual.colorScheme.includes(mode) &&
982
+ actual.surfaceLuminanceClass === mode,
983
+ },
984
+ Direction: {
985
+ expected: direction,
986
+ observed: actual.direction,
987
+ passed: actual.direction === direction,
988
+ },
989
+ 'Viewport/media': {
990
+ expected: {
991
+ viewport: expectedViewport,
992
+ devicePixelRatio: 1,
993
+ forcedColors: false,
994
+ reducedMotion: true,
995
+ coarsePointer: expectedCoarse,
996
+ hoverCapable: !expectedCoarse,
997
+ },
998
+ observed: {
999
+ viewport: {width: viewport.width, height: viewport.height},
1000
+ devicePixelRatio: viewport.dpr,
1001
+ forcedColors: actual.forcedColors,
1002
+ reducedMotion: actual.reducedMotion,
1003
+ coarsePointer: actual.coarsePointer,
1004
+ hoverCapable: actual.hoverCapable,
1005
+ touchPoints: actual.touchPoints,
1006
+ },
1007
+ passed:
1008
+ viewport.width === expectedViewport.width &&
1009
+ viewport.height === expectedViewport.height &&
1010
+ viewport.dpr === 1 &&
1011
+ !actual.forcedColors &&
1012
+ actual.reducedMotion &&
1013
+ actual.coarsePointer === expectedCoarse &&
1014
+ actual.hoverCapable === !expectedCoarse &&
1015
+ (!expectedCoarse || actual.touchPoints > 0),
1016
+ },
1017
+ 'Rendered state': {
1018
+ expected: {
1019
+ role: 'button',
1020
+ accessibleName: fixture.name,
1021
+ disabled: fixture.disabled,
1022
+ size: fixture.size,
1023
+ variant: fixture.state === 'stop' ? 'secondary' : 'primary',
1024
+ interaction,
1025
+ stateSignature: {
1026
+ hover: interaction === 'hover' || interaction === 'pressed',
1027
+ focusVisible: interaction === 'focus-visible',
1028
+ active: interaction === 'pressed',
1029
+ outline: interaction === 'focus-visible' ? 'visible' : 'none',
1030
+ },
1031
+ representation,
1032
+ },
1033
+ observed: {
1034
+ role: actual.role,
1035
+ accessibleName: actual.accessibleName,
1036
+ disabled: actual.disabled,
1037
+ size: actual.size,
1038
+ variant: actual.variant,
1039
+ interaction,
1040
+ stateSignature: {
1041
+ hover: actual.matchesHover,
1042
+ focusVisible: actual.matchesFocusVisible,
1043
+ active: actual.matchesActive,
1044
+ outline: actual.outlineStyle,
1045
+ },
1046
+ representation,
1047
+ tokenSignaturePresent,
1048
+ contrastPairs: renderedContrastPairs,
1049
+ changedPixels,
1050
+ },
1051
+ passed:
1052
+ actual.role === 'button' &&
1053
+ actual.accessibleName === fixture.name &&
1054
+ actual.disabled === fixture.disabled &&
1055
+ actual.size === fixture.size &&
1056
+ actual.variant ===
1057
+ (fixture.state === 'stop' ? 'secondary' : 'primary') &&
1058
+ interactionStateIsExclusive &&
1059
+ tokenSignaturePresent &&
1060
+ renderedContrastPairs.every(pair => pair.passed) &&
1061
+ (options.expectChangedPixels == null ||
1062
+ changedPixels === options.expectChangedPixels),
1063
+ },
1064
+ 'Subject geometry': {
1065
+ expected: {
1066
+ selectorCount: 1,
1067
+ visibleCount: 1,
1068
+ nonZero: true,
1069
+ circular: true,
1070
+ contains24pxSquare: true,
1071
+ },
1072
+ observed: {
1073
+ selectorCount: actual.selectorCount,
1074
+ visibleCount: actual.visibleCount,
1075
+ width: actual.width,
1076
+ height: actual.height,
1077
+ circular: actual.circular,
1078
+ },
1079
+ passed:
1080
+ actual.selectorCount === 1 &&
1081
+ actual.visibleCount === 1 &&
1082
+ actual.width >= 24 &&
1083
+ actual.height >= 24 &&
1084
+ actual.circular,
1085
+ },
1086
+ 'Settled render': {
1087
+ expected: {
1088
+ fontsReady: true,
1089
+ reducedMotion: true,
1090
+ storyError: false,
1091
+ pageErrors: 0,
1092
+ },
1093
+ observed: {
1094
+ fontsReady: actual.fontsReady,
1095
+ reducedMotion: actual.reducedMotion,
1096
+ storyError: actual.storyError,
1097
+ pageErrors: errors,
1098
+ },
1099
+ passed:
1100
+ actual.fontsReady &&
1101
+ actual.reducedMotion &&
1102
+ !actual.storyError &&
1103
+ errors.length === 0,
1104
+ },
1105
+ Image: {
1106
+ expected: {path: frame, nonBlankHash: true, nonZeroPixels: true},
1107
+ observed: image,
1108
+ passed:
1109
+ image.sha256.length === 64 &&
1110
+ image.width > 0 &&
1111
+ image.height > 0 &&
1112
+ image.nonBlank,
1113
+ },
1114
+ },
1115
+ settled: {
1116
+ fontsReady: actual.fontsReady,
1117
+ pageErrors: [...errors],
1118
+ storyError: actual.storyError,
1119
+ },
1120
+ image,
1121
+ };
1122
+ receipts.push(receipt);
1123
+ expect(failures, `${frame}: fail-closed sensor receipt`).toEqual([]);
1124
+ return receipt;
1125
+ }
1126
+
1127
+ async function tabToSubject(page: Page): Promise<void> {
1128
+ for (let press = 0; press < 8; press += 1) {
1129
+ await page.keyboard.press('Tab');
1130
+ if (
1131
+ await page
1132
+ .locator(SUBJECT)
1133
+ .first()
1134
+ .evaluate(element => document.activeElement === element)
1135
+ ) {
1136
+ return;
1137
+ }
1138
+ }
1139
+ throw new Error('Tab did not reach ChatSendButton');
1140
+ }
1141
+
1142
+ async function driveInteraction(
1143
+ page: Page,
1144
+ fixture: Fixture,
1145
+ interaction: Exclude<Interaction, 'rest'>,
1146
+ ): Promise<void> {
1147
+ const target = page.locator(SUBJECT).first();
1148
+ if (interaction === 'hover') {
1149
+ await target.hover();
1150
+ return;
1151
+ }
1152
+ if (interaction === 'focus-visible') {
1153
+ await tabToSubject(page);
1154
+ return;
1155
+ }
1156
+ const box = await target.boundingBox();
1157
+ if (box == null) {
1158
+ throw new Error(`${fixture.key}: target has no layout box`);
1159
+ }
1160
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
1161
+ await page.mouse.down();
1162
+ }
1163
+
1164
+ async function coarseContext(browser: Browser): Promise<BrowserContext> {
1165
+ return browser.newContext({
1166
+ viewport: {width: 320, height: 640},
1167
+ deviceScaleFactor: 1,
1168
+ hasTouch: true,
1169
+ isMobile: true,
1170
+ });
1171
+ }
1172
+
1173
+ async function readCoarseHitPair(
1174
+ page: Page,
1175
+ fixture: Fixture,
1176
+ direction: Direction,
1177
+ ): Promise<CoarseHitPair> {
1178
+ await openFixture(page, fixture, 'light', direction);
1179
+ return page.locator(SUBJECT).evaluateAll(
1180
+ (elements, expected) => {
1181
+ const visible = elements.filter(element => {
1182
+ const box = element.getBoundingClientRect();
1183
+ const style = getComputedStyle(element);
1184
+ return (
1185
+ box.width > 0 &&
1186
+ box.height > 0 &&
1187
+ style.display !== 'none' &&
1188
+ style.visibility !== 'hidden' &&
1189
+ style.opacity !== '0'
1190
+ );
1191
+ });
1192
+ const target = visible[0];
1193
+ const box = target?.getBoundingClientRect() ?? new DOMRect();
1194
+ const cx = box.x + box.width / 2;
1195
+ const cy = box.y + box.height / 2;
1196
+ const hit = document.elementFromPoint(cx, cy);
1197
+ const hitTarget =
1198
+ target != null && (hit === target || target.contains(hit));
1199
+ const targetCenter = target?.getBoundingClientRect();
1200
+ const centered =
1201
+ targetCenter != null &&
1202
+ Math.abs(targetCenter.x + targetCenter.width / 2 - cx) < 0.5 &&
1203
+ Math.abs(targetCenter.y + targetCenter.height / 2 - cy) < 0.5;
1204
+ const coarsePointer = matchMedia('(pointer: coarse)').matches;
1205
+ const touchPoints = navigator.maxTouchPoints;
1206
+ const passed =
1207
+ elements.length === 1 &&
1208
+ visible.length === 1 &&
1209
+ box.width >= 24 &&
1210
+ box.height >= 24 &&
1211
+ coarsePointer &&
1212
+ touchPoints > 0 &&
1213
+ hitTarget &&
1214
+ centered;
1215
+ return {
1216
+ fixture: expected.key,
1217
+ size: expected.size,
1218
+ state: expected.state,
1219
+ direction: expected.direction,
1220
+ coarsePointer,
1221
+ touchPoints,
1222
+ selectorCount: elements.length,
1223
+ visibleCount: visible.length,
1224
+ target: {x: box.x, y: box.y, width: box.width, height: box.height},
1225
+ hit: hitTarget,
1226
+ centered,
1227
+ passed,
1228
+ };
1229
+ },
1230
+ {
1231
+ key: fixture.key,
1232
+ size: fixture.size,
1233
+ state: fixture.state as 'send' | 'stop',
1234
+ direction,
1235
+ },
1236
+ );
1237
+ }
1238
+
1239
+ test.beforeAll(async () => {
1240
+ head = execFileSync('git', ['rev-parse', 'HEAD'], {encoding: 'utf8'}).trim();
1241
+ const dirtyTracked = execFileSync(
1242
+ 'git',
1243
+ ['status', '--porcelain', '--untracked-files=no'],
1244
+ {encoding: 'utf8'},
1245
+ ).trim();
1246
+ const dirtyEvidenceFiles = execFileSync(
1247
+ 'git',
1248
+ [
1249
+ 'status',
1250
+ '--porcelain',
1251
+ '--untracked-files=all',
1252
+ '--',
1253
+ 'apps/storybook/stories/ChatSendButtonAudit.stories.tsx',
1254
+ 'packages/core/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts',
1255
+ ],
1256
+ {encoding: 'utf8'},
1257
+ ).trim();
1258
+ const dirty = [dirtyTracked, dirtyEvidenceFiles].filter(Boolean).join('\n');
1259
+ if (dirty !== '') {
1260
+ throw new Error(
1261
+ `browser evidence requires a clean committed source set: ${dirty}`,
1262
+ );
1263
+ }
1264
+ if (process.env.ASTRYX_HEAD_SHA && process.env.ASTRYX_HEAD_SHA !== head) {
1265
+ throw new Error('PR head differs from the checked out source');
1266
+ }
1267
+ fs.rmSync(OUTPUT, {recursive: true, force: true});
1268
+ fs.mkdirSync(OUTPUT, {recursive: true});
1269
+ server = await serveStorybook(
1270
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
1271
+ );
1272
+ const stamp = await fetch(`${server.origin}/astryx-build-sha.txt`);
1273
+ if (!stamp.ok) {
1274
+ throw new Error('Storybook build is missing its source stamp');
1275
+ }
1276
+ build = (await stamp.text()).trim();
1277
+ if (build !== head) {
1278
+ throw new Error('Storybook bytes differ from the PR head');
1279
+ }
1280
+ });
1281
+
1282
+ test.afterAll(async () => {
1283
+ if (!head || !build) {
1284
+ await server?.close();
1285
+ return;
1286
+ }
1287
+ const manifestPath = path.join(OUTPUT, 'manifest.json');
1288
+ const contactSheetFile = 'chat-send-button__contact-sheet.png';
1289
+ const contactSheetPath = path.join(OUTPUT, contactSheetFile);
1290
+ const contactSheet = fs.existsSync(contactSheetPath)
1291
+ ? pngShot(contactSheetFile, fs.readFileSync(contactSheetPath))
1292
+ : null;
1293
+ const mergedFrames = [...receipts];
1294
+ const mergedCoarseHitPairs = [...coarseHitPairs];
1295
+ const d7ContrastPairs = mergedFrames.flatMap(frame =>
1296
+ frame.contrastPairs.map(pair => ({frame: frame.frame, ...pair})),
1297
+ );
1298
+ const expectedFrames: string[] = [];
1299
+ for (const mode of ['light', 'dark'] as const) {
1300
+ for (const fixture of [FIXTURES.sendMd, FIXTURES.stopMd]) {
1301
+ for (const interaction of [
1302
+ 'rest',
1303
+ 'hover',
1304
+ 'focus-visible',
1305
+ 'pressed',
1306
+ ] as const) {
1307
+ expectedFrames.push(`${fixture.key}__${interaction}__${mode}`);
1308
+ }
1309
+ }
1310
+ expectedFrames.push(
1311
+ `${FIXTURES.disabledMd.key}__rest__${mode}`,
1312
+ `${FIXTURES.disabledMd.key}__hover__${mode}`,
1313
+ `${FIXTURES.sendSm.key}__rest__${mode}`,
1314
+ `${FIXTURES.stopSm.key}__rest__${mode}`,
1315
+ `stop-md-narrow-coarse__rest__${mode}__320px-coarse`,
1316
+ );
1317
+ }
1318
+ const expectedPairs = [
1319
+ FIXTURES.sendSm,
1320
+ FIXTURES.sendMd,
1321
+ FIXTURES.stopSm,
1322
+ FIXTURES.stopMd,
1323
+ ].flatMap(fixture =>
1324
+ (['ltr', 'rtl'] as const).map(direction => `${fixture.key}:${direction}`),
1325
+ );
1326
+ const matrixFailures = [
1327
+ ...(contactSheet?.nonBlank ? [] : ['missing or blank contact sheet']),
1328
+ ...expectedFrames
1329
+ .filter(frame => !mergedFrames.some(receipt => receipt.frame === frame))
1330
+ .map(frame => `missing frame: ${frame}`),
1331
+ ...mergedFrames
1332
+ .filter(receipt => !receipt.passed)
1333
+ .map(receipt => `failed frame: ${receipt.frame}`),
1334
+ ...expectedPairs
1335
+ .filter(
1336
+ pair =>
1337
+ !mergedCoarseHitPairs.some(
1338
+ receipt => `${receipt.fixture}:${receipt.direction}` === pair,
1339
+ ),
1340
+ )
1341
+ .map(pair => `missing coarse hit pair: ${pair}`),
1342
+ ...mergedCoarseHitPairs
1343
+ .filter(pair => !pair.passed)
1344
+ .map(pair => `failed coarse hit pair: ${pair.fixture}:${pair.direction}`),
1345
+ ...d7ContrastPairs
1346
+ .filter(pair => !pair.passed)
1347
+ .map(pair => `failed D7 contrast pair: ${pair.frame}/${pair.part}`),
1348
+ ...(['light', 'dark'] as const).flatMap(mode =>
1349
+ (['send', 'stop'] as const).flatMap(state => {
1350
+ const small = mergedFrames.find(
1351
+ frame => frame.frame === `${state}-sm__rest__${mode}`,
1352
+ );
1353
+ const medium = mergedFrames.find(
1354
+ frame => frame.frame === `${state}-md__rest__${mode}`,
1355
+ );
1356
+ return small != null &&
1357
+ medium != null &&
1358
+ medium.geometry.width > small.geometry.width &&
1359
+ medium.geometry.height > small.geometry.height
1360
+ ? []
1361
+ : [`size geometry did not increase: ${state}/${mode}`];
1362
+ }),
1363
+ ),
1364
+ ];
1365
+ fs.writeFileSync(
1366
+ manifestPath,
1367
+ `${JSON.stringify(
1368
+ {
1369
+ version: 1,
1370
+ component: 'core/ChatSendButton',
1371
+ headSha: head,
1372
+ storybookSha: build,
1373
+ browser: browserVersion,
1374
+ failClosed: true,
1375
+ matrixComplete: matrixFailures.length === 0,
1376
+ matrixFailures,
1377
+ sensorCount: 10,
1378
+ requiredSensors: [
1379
+ 'Build',
1380
+ 'Story',
1381
+ 'Theme',
1382
+ 'Color mode',
1383
+ 'Direction',
1384
+ 'Viewport/media',
1385
+ 'Rendered state',
1386
+ 'Subject geometry',
1387
+ 'Settled render',
1388
+ 'Image',
1389
+ ],
1390
+ visualEvidence: {
1391
+ requiredPair: false,
1392
+ reason:
1393
+ 'The runtime fix changes event composition only and has no rendering delta, so Q12 requires exact-head state evidence rather than a before/after pixel pair.',
1394
+ contactSheet,
1395
+ },
1396
+ notApplicable: {
1397
+ loading: 'ChatSendButton exposes no loading state.',
1398
+ status: 'ChatSendButton exposes no status-message state.',
1399
+ selected: 'ChatSendButton is an action, not a selectable control.',
1400
+ empty:
1401
+ 'The icon-only action has no empty-content state; its translated name is always present.',
1402
+ overflow:
1403
+ 'The icon-only circular target contains no variable text content that can overflow.',
1404
+ },
1405
+ stateVisualConformance: {
1406
+ enabledSendMd: ['rest', 'hover', 'focus-visible', 'pressed'],
1407
+ disabledSendMd: ['rest', 'hover-no-paint'],
1408
+ stopMd: ['rest', 'hover', 'focus-visible', 'pressed'],
1409
+ sendSm: ['rest'],
1410
+ stopSm: ['rest'],
1411
+ colorModes: ['light', 'dark'],
1412
+ },
1413
+ frames: mergedFrames,
1414
+ d7ContrastPairs,
1415
+ coarseHitPairs: mergedCoarseHitPairs,
1416
+ },
1417
+ null,
1418
+ 2,
1419
+ )}\n`,
1420
+ );
1421
+ await server?.close();
1422
+ expect(matrixFailures, 'complete exact-head evidence matrix').toEqual([]);
1423
+ });
1424
+
1425
+ for (const mode of ['light', 'dark'] as const) {
1426
+ test(`captures the ${mode} interaction, disabled, and size matrix`, async ({
1427
+ page,
1428
+ }) => {
1429
+ for (const fixture of [FIXTURES.sendMd, FIXTURES.stopMd]) {
1430
+ const {errors: restErrors} = await openFixture(page, fixture, mode);
1431
+ const rest = await capture(page, fixture, mode, 'rest', restErrors);
1432
+
1433
+ for (const interaction of [
1434
+ 'hover',
1435
+ 'focus-visible',
1436
+ 'pressed',
1437
+ ] as const) {
1438
+ const {errors} = await openFixture(page, fixture, mode);
1439
+ await driveInteraction(page, fixture, interaction);
1440
+ await capture(page, fixture, mode, interaction, errors, {
1441
+ restSha256: rest.image.sha256,
1442
+ expectChangedPixels: true,
1443
+ });
1444
+ if (interaction === 'pressed') {
1445
+ await page.mouse.up();
1446
+ }
1447
+ }
1448
+ }
1449
+
1450
+ const disabled = FIXTURES.disabledMd;
1451
+ const {errors: disabledRestErrors} = await openFixture(
1452
+ page,
1453
+ disabled,
1454
+ mode,
1455
+ );
1456
+ const disabledRest = await capture(
1457
+ page,
1458
+ disabled,
1459
+ mode,
1460
+ 'rest',
1461
+ disabledRestErrors,
1462
+ );
1463
+ const {errors: disabledHoverErrors} = await openFixture(
1464
+ page,
1465
+ disabled,
1466
+ mode,
1467
+ );
1468
+ await page.locator(SUBJECT).hover({force: true});
1469
+ await capture(page, disabled, mode, 'hover', disabledHoverErrors, {
1470
+ restSha256: disabledRest.image.sha256,
1471
+ expectChangedPixels: false,
1472
+ });
1473
+
1474
+ for (const fixture of [FIXTURES.sendSm, FIXTURES.stopSm]) {
1475
+ const {errors} = await openFixture(page, fixture, mode);
1476
+ await capture(page, fixture, mode, 'rest', errors);
1477
+ }
1478
+ });
1479
+ }
1480
+
1481
+ test('captures 320px coarse-pointer evidence from DisabledStreamingNarrow', async ({
1482
+ browser,
1483
+ }) => {
1484
+ const context = await coarseContext(browser);
1485
+ const page = await context.newPage();
1486
+ const fixture: Fixture = {
1487
+ ...FIXTURES.stopMd,
1488
+ key: 'stop-md-narrow-coarse',
1489
+ storyId: NARROW_STORY,
1490
+ };
1491
+ for (const mode of ['light', 'dark'] as const) {
1492
+ const {errors} = await openFixture(page, fixture, mode);
1493
+ await settleStoryPlayAndBlur(page);
1494
+ await capture(page, fixture, mode, 'rest', errors, {
1495
+ expectCoarse: true,
1496
+ expectedViewport: {width: 320, height: 640},
1497
+ frameSuffix: '__320px-coarse',
1498
+ });
1499
+ }
1500
+ await context.close();
1501
+ });
1502
+
1503
+ test('records the complete coarse hit-target matrix', async ({browser}) => {
1504
+ const context = await coarseContext(browser);
1505
+ const page = await context.newPage();
1506
+ for (const fixture of [
1507
+ FIXTURES.sendSm,
1508
+ FIXTURES.sendMd,
1509
+ FIXTURES.stopSm,
1510
+ FIXTURES.stopMd,
1511
+ ]) {
1512
+ for (const direction of ['ltr', 'rtl'] as const) {
1513
+ const pair = await readCoarseHitPair(page, fixture, direction);
1514
+ coarseHitPairs.push(pair);
1515
+ expect(pair.passed, `${fixture.key}/${direction}: coarse hit pair`).toBe(
1516
+ true,
1517
+ );
1518
+ }
1519
+ }
1520
+ await writeContactSheet(page);
1521
+ await context.close();
1522
+ });