@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1

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 (348) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/Button/Button.d.ts +1 -1
  3. package/dist/Button/Button.d.ts.map +1 -1
  4. package/dist/Button/Button.js +5 -2
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +191 -32
  7. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +5 -0
  10. package/dist/Chat/ChatMessage.d.ts +11 -0
  11. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  12. package/dist/Chat/ChatMessage.js +18 -9
  13. package/dist/Chat/useTriggerMenu.d.ts +9 -2
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +222 -50
  16. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  17. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  18. package/dist/CodeBlock/CodeBlock.js +158 -38
  19. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  20. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  21. package/dist/CodeBlock/tokenizer.js +108 -0
  22. package/dist/CommandPalette/CommandPalette.d.ts +9 -5
  23. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  24. package/dist/CommandPalette/CommandPalette.js +38 -20
  25. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
  26. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
  28. package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
  29. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPaletteInput.js +35 -18
  31. package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
  32. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  33. package/dist/CommandPalette/CommandPaletteItem.js +8 -9
  34. package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
  35. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  36. package/dist/CommandPalette/CommandPaletteList.js +10 -5
  37. package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
  38. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  39. package/dist/ComplexSelector/ComplexSelector.js +12 -3
  40. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  41. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  42. package/dist/ContextMenu/ContextMenu.js +62 -48
  43. package/dist/DateInput/DateInput.js +1 -1
  44. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  45. package/dist/DateInput/MonthScroller.js +12 -4
  46. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  47. package/dist/DateRangeInput/DateRangeInput.js +12 -2
  48. package/dist/Dialog/Dialog.d.ts +1 -1
  49. package/dist/Dialog/Dialog.d.ts.map +1 -1
  50. package/dist/Dialog/Dialog.js +16 -5
  51. package/dist/Dialog/DialogHeader.d.ts +1 -1
  52. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  53. package/dist/Dialog/DialogHeader.js +7 -7
  54. package/dist/Divider/Divider.d.ts +1 -0
  55. package/dist/Divider/Divider.d.ts.map +1 -1
  56. package/dist/Divider/Divider.js +5 -4
  57. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.js +161 -65
  59. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
  60. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
  62. package/dist/Field/InputClearButton.d.ts +8 -4
  63. package/dist/Field/InputClearButton.d.ts.map +1 -1
  64. package/dist/Field/InputClearButton.js +23 -7
  65. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  66. package/dist/FormLayout/FormLayout.js +1 -0
  67. package/dist/Grid/Grid.d.ts +13 -4
  68. package/dist/Grid/Grid.d.ts.map +1 -1
  69. package/dist/Grid/Grid.js +7 -1
  70. package/dist/HoverCard/useHoverCard.d.ts +1 -1
  71. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  72. package/dist/HoverCard/useHoverCard.js +67 -49
  73. package/dist/InputGroup/InputGroup.d.ts +2 -4
  74. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  75. package/dist/InputGroup/InputGroup.js +2 -4
  76. package/dist/InputGroup/groupStyles.d.ts +10 -2
  77. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  78. package/dist/InputGroup/groupStyles.js +19 -2
  79. package/dist/Layer/useLayerDismissal.d.ts +9 -9
  80. package/dist/Layer/useLayerDismissal.js +9 -9
  81. package/dist/Markdown/Markdown.d.ts.map +1 -1
  82. package/dist/Markdown/Markdown.js +25 -4
  83. package/dist/MobileNav/MobileNav.d.ts +7 -0
  84. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  85. package/dist/MobileNav/MobileNav.js +45 -2
  86. package/dist/MultiSelector/MultiSelector.d.ts +7 -4
  87. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  88. package/dist/MultiSelector/MultiSelector.js +24 -3
  89. package/dist/NumberInput/NumberInput.js +1 -1
  90. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  91. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
  92. package/dist/Resizable/ResizeHandle.d.ts +1 -1
  93. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  94. package/dist/Resizable/ResizeHandle.js +83 -15
  95. package/dist/Resizable/useResizable.d.ts +23 -0
  96. package/dist/Resizable/useResizable.d.ts.map +1 -1
  97. package/dist/Resizable/useResizable.js +78 -0
  98. package/dist/Selector/Selector.d.ts +6 -3
  99. package/dist/Selector/Selector.d.ts.map +1 -1
  100. package/dist/Selector/Selector.js +28 -6
  101. package/dist/SideNav/SideNav.d.ts.map +1 -1
  102. package/dist/SideNav/SideNav.js +10 -2
  103. package/dist/Slider/Slider.d.ts.map +1 -1
  104. package/dist/Slider/Slider.js +18 -7
  105. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  106. package/dist/Tokenizer/Tokenizer.js +16 -4
  107. package/dist/Toolbar/Toolbar.d.ts +2 -1
  108. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  109. package/dist/Toolbar/Toolbar.js +3 -1
  110. package/dist/TreeList/TreeList.d.ts.map +1 -1
  111. package/dist/TreeList/TreeList.js +6 -3
  112. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  113. package/dist/TreeList/TreeListItem.js +7 -1
  114. package/dist/astryx.css +22 -0
  115. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  116. package/dist/hooks/useScrollOverflow.js +55 -12
  117. package/dist/hooks/useTreeFocus.d.ts +21 -0
  118. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  119. package/dist/hooks/useTreeFocus.js +40 -2
  120. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  121. package/dist/i18n/generated-locales/en.generated.js +3 -0
  122. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  123. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  124. package/dist/reset.a11y.chromium.spec.js +6 -1
  125. package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
  126. package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
  127. package/dist/reset.until-found.a11y.browser.spec.js +71 -0
  128. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  129. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  130. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  131. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  132. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  133. package/dist/theme/syntax/index.d.ts +1 -1
  134. package/dist/theme/syntax/index.d.ts.map +1 -1
  135. package/dist/theme/syntax/tokens.d.ts +12 -2
  136. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  137. package/dist/theme/syntax/tokens.js +25 -4
  138. package/dist/utils/plainDate.d.ts +12 -0
  139. package/dist/utils/plainDate.d.ts.map +1 -1
  140. package/dist/utils/plainDate.js +63 -10
  141. package/locales/en.json +13 -1
  142. package/locales/pseudo.json +9 -0
  143. package/package.json +4 -4
  144. package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
  145. package/src/AlertDialog/AlertDialog.spec.md +205 -0
  146. package/src/AlertDialog/AlertDialog.test.tsx +3 -14
  147. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
  148. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
  149. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
  150. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
  151. package/src/Button/Button.test.tsx +37 -0
  152. package/src/Button/Button.tsx +5 -1
  153. package/src/Calendar/Calendar.doc.mjs +234 -60
  154. package/src/Calendar/Calendar.spec.md +209 -0
  155. package/src/Calendar/Calendar.test.tsx +559 -20
  156. package/src/Calendar/Calendar.tsx +258 -41
  157. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
  158. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
  159. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
  160. package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
  161. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
  162. package/src/Carousel/Carousel.test.tsx +54 -71
  163. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
  164. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
  165. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
  166. package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
  167. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
  168. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  169. package/src/Chat/ChatComposerInput.spec.md +94 -55
  170. package/src/Chat/ChatComposerInput.test.tsx +202 -1
  171. package/src/Chat/ChatComposerInput.tsx +13 -0
  172. package/src/Chat/ChatMessage.doc.mjs +31 -16
  173. package/src/Chat/ChatMessage.spec.md +265 -0
  174. package/src/Chat/ChatMessage.test.tsx +246 -0
  175. package/src/Chat/ChatMessage.tsx +30 -7
  176. package/src/Chat/ChatSystemMessage.spec.md +36 -37
  177. package/src/Chat/ChatSystemMessage.test.tsx +4 -4
  178. package/src/Chat/useTriggerMenu.test.tsx +994 -0
  179. package/src/Chat/useTriggerMenu.tsx +278 -46
  180. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  181. package/src/CodeBlock/CodeBlock.spec.md +40 -41
  182. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  183. package/src/CodeBlock/CodeBlock.tsx +205 -25
  184. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  185. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  186. package/src/CodeBlock/tokenizer.test.ts +112 -0
  187. package/src/CodeBlock/tokenizer.ts +106 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
  189. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
  190. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
  191. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
  192. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
  193. package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
  194. package/src/CommandPalette/CommandPalette.spec.md +100 -49
  195. package/src/CommandPalette/CommandPalette.test.tsx +241 -2
  196. package/src/CommandPalette/CommandPalette.tsx +47 -21
  197. package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
  198. package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
  199. package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
  200. package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
  201. package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
  202. package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
  203. package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
  204. package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
  205. package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
  206. package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
  207. package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
  208. package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
  209. package/src/CommandPalette/CommandPaletteList.tsx +10 -5
  210. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
  211. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
  212. package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
  213. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
  214. package/src/ComplexSelector/ComplexSelector.tsx +37 -8
  215. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  216. package/src/ContextMenu/ContextMenu.spec.md +51 -53
  217. package/src/ContextMenu/ContextMenu.test.tsx +132 -1
  218. package/src/ContextMenu/ContextMenu.tsx +78 -48
  219. package/src/DateInput/DateInput.doc.mjs +52 -0
  220. package/src/DateInput/DateInput.spec.md +27 -15
  221. package/src/DateInput/DateInput.test.tsx +38 -0
  222. package/src/DateInput/DateInput.tsx +1 -1
  223. package/src/DateInput/DateInputTouch.test.tsx +22 -0
  224. package/src/DateInput/MonthScroller.tsx +12 -4
  225. package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
  226. package/src/DateRangeInput/DateRangeInput.tsx +20 -2
  227. package/src/Dialog/Dialog.test.tsx +27 -1
  228. package/src/Dialog/Dialog.tsx +16 -5
  229. package/src/Dialog/DialogHeader.test.tsx +27 -2
  230. package/src/Dialog/DialogHeader.tsx +7 -8
  231. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  232. package/src/Divider/Divider.doc.mjs +5 -3
  233. package/src/Divider/Divider.spec.md +140 -54
  234. package/src/Divider/Divider.test.tsx +15 -0
  235. package/src/Divider/Divider.tsx +6 -4
  236. package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
  237. package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
  238. package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
  239. package/src/DropdownMenu/DropdownMenu.tsx +212 -81
  240. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
  241. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
  242. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
  243. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
  244. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
  245. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
  246. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
  247. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
  248. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
  249. package/src/Field/InputClearButton.test.tsx +29 -4
  250. package/src/Field/InputClearButton.tsx +26 -8
  251. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  252. package/src/FormLayout/FormLayout.tsx +1 -0
  253. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
  254. package/src/Grid/Grid.doc.mjs +14 -11
  255. package/src/Grid/Grid.spec.md +50 -32
  256. package/src/Grid/Grid.tsx +13 -4
  257. package/src/HoverCard/HoverCard.test.tsx +46 -1
  258. package/src/HoverCard/useHoverCard.tsx +68 -48
  259. package/src/InputGroup/InputGroup.doc.mjs +11 -1
  260. package/src/InputGroup/InputGroup.tsx +2 -4
  261. package/src/InputGroup/groupStyles.ts +40 -2
  262. package/src/Layer/useLayerDismissal.ts +9 -9
  263. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
  264. package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
  265. package/src/Link/__tests__/Link.a11y.states.ts +104 -0
  266. package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
  267. package/src/Markdown/Markdown.doc.mjs +1 -1
  268. package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
  269. package/src/Markdown/Markdown.spec.md +45 -23
  270. package/src/Markdown/Markdown.test.tsx +38 -0
  271. package/src/Markdown/Markdown.tsx +24 -2
  272. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  273. package/src/MobileNav/MobileNav.tsx +50 -2
  274. package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
  275. package/src/MoreMenu/MoreMenu.spec.md +18 -7
  276. package/src/MoreMenu/MoreMenu.test.tsx +38 -0
  277. package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
  278. package/src/MultiSelector/MultiSelector.spec.md +1 -1
  279. package/src/MultiSelector/MultiSelector.test.tsx +188 -21
  280. package/src/MultiSelector/MultiSelector.tsx +50 -11
  281. package/src/NumberInput/NumberInput.test.tsx +24 -0
  282. package/src/NumberInput/NumberInput.tsx +1 -1
  283. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  284. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
  285. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
  286. package/src/Resizable/Resizable.spec.md +41 -9
  287. package/src/Resizable/ResizeHandle.test.tsx +565 -2
  288. package/src/Resizable/ResizeHandle.tsx +126 -21
  289. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
  290. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
  291. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
  292. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
  293. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
  294. package/src/Resizable/useResizable.ts +95 -0
  295. package/src/Selector/Selector.doc.mjs +7 -6
  296. package/src/Selector/Selector.spec.md +37 -24
  297. package/src/Selector/Selector.test.tsx +176 -10
  298. package/src/Selector/Selector.tsx +59 -14
  299. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
  300. package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
  301. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
  302. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
  303. package/src/SideNav/SideNav.test.tsx +35 -0
  304. package/src/SideNav/SideNav.tsx +16 -2
  305. package/src/Slider/Slider.spec.md +43 -17
  306. package/src/Slider/Slider.test.tsx +134 -0
  307. package/src/Slider/Slider.tsx +29 -6
  308. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
  309. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
  310. package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
  311. package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
  312. package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
  313. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  314. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  315. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  316. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  317. package/src/Tokenizer/Tokenizer.test.tsx +87 -0
  318. package/src/Tokenizer/Tokenizer.tsx +31 -3
  319. package/src/Toolbar/Toolbar.test.tsx +131 -4
  320. package/src/Toolbar/Toolbar.tsx +3 -1
  321. package/src/TreeList/TreeList.spec.md +33 -8
  322. package/src/TreeList/TreeList.test.tsx +72 -0
  323. package/src/TreeList/TreeList.tsx +10 -6
  324. package/src/TreeList/TreeListItem.tsx +5 -0
  325. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
  326. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
  327. package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
  328. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
  329. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
  330. package/src/hooks/useScrollOverflow.test.tsx +222 -0
  331. package/src/hooks/useScrollOverflow.ts +55 -13
  332. package/src/hooks/useTreeFocus.test.tsx +37 -0
  333. package/src/hooks/useTreeFocus.ts +55 -2
  334. package/src/i18n/generated-locales/en.generated.ts +3 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
  336. package/src/reset.a11y.chromium.spec.ts +9 -1
  337. package/src/reset.css +8 -3
  338. package/src/reset.until-found.a11y.browser.spec.ts +75 -0
  339. package/src/reset.until-found.test.ts +32 -0
  340. package/src/stylexValidation.source-build.test.mjs +42 -0
  341. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  342. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  343. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  344. package/src/theme/syntax/index.ts +5 -1
  345. package/src/theme/syntax/tokens.ts +32 -2
  346. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  347. package/src/utils/plainDate.test.ts +147 -0
  348. package/src/utils/plainDate.ts +75 -10
@@ -0,0 +1,112 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Slider.a11y.chromium.spec.ts
5
+ * @input Built Slider stories and SLIDER_PATTERN in real Chromium
6
+ * @output Browser evidence for every slider expectation across the binding's states, reconciled against the exact known failures
7
+ * @position Chromium lane of the Slider binding (component:Slider AR1)
8
+ */
9
+
10
+ import {
11
+ expect,
12
+ test,
13
+ type CDPSession,
14
+ type Locator,
15
+ type Page,
16
+ } from '@playwright/test';
17
+ import {
18
+ SLIDER_PATTERN,
19
+ blockingResults,
20
+ checkAccessibilitySpec,
21
+ formatFailures,
22
+ neverExercised,
23
+ summarize,
24
+ unmatchedKnownFailures,
25
+ type BindingResult,
26
+ } from '@astryxdesign/a11y-spec';
27
+ import {
28
+ createChromiumHarness,
29
+ holdMotionStill,
30
+ } from '@astryxdesign/a11y-spec/chromium';
31
+ import {
32
+ DEFAULT_STORYBOOK_DIR,
33
+ serveStorybook,
34
+ type StaticServer,
35
+ } from '@astryxdesign/a11y-spec/storybook';
36
+ import {SLIDER_KNOWN_FAILURES} from './Slider.a11y.known-failures';
37
+ import {
38
+ SLIDER_BINDING_STATES,
39
+ type SliderBindingState,
40
+ } from './Slider.a11y.states';
41
+
42
+ let storybook: StaticServer;
43
+
44
+ test.beforeAll(async () => {
45
+ storybook = await serveStorybook(
46
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
47
+ );
48
+ });
49
+
50
+ test.afterAll(async () => {
51
+ await storybook?.close();
52
+ });
53
+
54
+ function storyUrl(storyId: string): string {
55
+ return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
56
+ }
57
+
58
+ async function mountState(
59
+ page: Page,
60
+ state: SliderBindingState,
61
+ ): Promise<{subject: Locator; related: Record<string, Locator>}> {
62
+ await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
63
+ await holdMotionStill(page);
64
+ const subject = page
65
+ .locator('#storybook-root')
66
+ .getByRole('slider', {name: state.label});
67
+ await subject.waitFor({state: 'attached'});
68
+ return {subject, related: {}};
69
+ }
70
+
71
+ async function runState(
72
+ page: Page,
73
+ cdp: CDPSession,
74
+ state: SliderBindingState,
75
+ ): Promise<BindingResult> {
76
+ return checkAccessibilitySpec({
77
+ spec: SLIDER_PATTERN,
78
+ binding: 'Slider',
79
+ state: state.id,
80
+ facts: state.facts,
81
+ knownFailures: SLIDER_KNOWN_FAILURES,
82
+ mount: async () => {
83
+ const {subject, related} = await mountState(page, state);
84
+ return createChromiumHarness({page, cdp, subject, related});
85
+ },
86
+ });
87
+ }
88
+
89
+ test('every slider expectation is exercised by a Slider state', async ({
90
+ page,
91
+ }) => {
92
+ const cdp = await page.context().newCDPSession(page);
93
+ const results: BindingResult[] = [];
94
+ for (const state of SLIDER_BINDING_STATES) {
95
+ results.push(await runState(page, cdp, state));
96
+ }
97
+ expect(neverExercised(results)).toEqual([]);
98
+ expect(unmatchedKnownFailures(SLIDER_KNOWN_FAILURES, results)).toEqual([]);
99
+ });
100
+
101
+ for (const state of SLIDER_BINDING_STATES) {
102
+ test(`Slider [${state.id}] — ${state.summary}`, async ({page}) => {
103
+ const cdp = await page.context().newCDPSession(page);
104
+ const result = await runState(page, cdp, state);
105
+ const report = summarize(SLIDER_PATTERN, [result]);
106
+ // Required rows block (AST-020 FR9); advisory rows and exact known
107
+ // failures report.
108
+ expect(formatFailures(blockingResults([result]))).toBe('');
109
+ expect(report.unrunLayers).toEqual([]);
110
+ expect(report.counts.unexpectedPass).toBe(0);
111
+ });
112
+ }
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Slider.a11y.known-failures.ts
5
+ * @input The exact slider-contract failures the shipped Slider produces today
6
+ * @output SLIDER_KNOWN_FAILURES — records both lanes reconcile against, so a fix shows up as an unexpected pass and any other failure stays a failure
7
+ * @position Known-failure ledger for the Slider binding
8
+ */
9
+
10
+ import type {KnownFailure} from '@astryxdesign/a11y-spec';
11
+
12
+ export const SLIDER_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [];
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file Slider.a11y.renders.tsx
7
+ * @input One Slider binding state
8
+ * @output The React tree every lane renders for that state (the jsdom test directly, the story through Storybook)
9
+ * @position Shared fixture for the Slider binding
10
+ */
11
+
12
+ import {useState, type ReactElement} from 'react';
13
+ import {Slider} from '../Slider';
14
+ import type {SliderBindingState} from './Slider.a11y.states';
15
+
16
+ function BoundSlider({state}: {state: SliderBindingState}) {
17
+ const [value, setValue] = useState(state.facts.value);
18
+ return (
19
+ <Slider
20
+ label={state.label}
21
+ min={state.facts.min}
22
+ max={state.facts.max}
23
+ step={state.facts.step}
24
+ value={value}
25
+ onChange={setValue}
26
+ orientation={state.facts.orientation}
27
+ isDisabled={!state.facts.operable}
28
+ disabledMessage={state.disabledMessage}
29
+ formatValue={
30
+ state.formatValue ? current => `${current} percent` : undefined
31
+ }
32
+ />
33
+ );
34
+ }
35
+
36
+ export function renderSliderState(state: SliderBindingState): ReactElement {
37
+ return (
38
+ <div dir={state.direction ?? 'ltr'} style={{padding: 24, width: 320}}>
39
+ <BoundSlider state={state} />
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,106 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Slider.a11y.states.ts
5
+ * @input Slider's representative thumb states and the facts SLIDER_PATTERN reads
6
+ * @output SLIDER_BINDING_STATES and the explicit ownership exclusions for this binding
7
+ * @position Binding inventory shared by the jsdom test, the Chromium spec, and the story (component:Slider AR1)
8
+ */
9
+
10
+ import type {SliderStateFacts} from '@astryxdesign/a11y-spec';
11
+
12
+ export interface SliderBindingState {
13
+ readonly id: string;
14
+ readonly summary: string;
15
+ readonly storyId: string;
16
+ /** The thumb's accessible name (the visible label). */
17
+ readonly label: string;
18
+ readonly facts: SliderStateFacts;
19
+ /** Props beyond label/min/max/step/value that shape the state. */
20
+ readonly disabledMessage?: string;
21
+ readonly formatValue?: boolean;
22
+ readonly direction?: 'ltr' | 'rtl';
23
+ }
24
+
25
+ function facts(overrides: Partial<SliderStateFacts> = {}): SliderStateFacts {
26
+ return {
27
+ min: 0,
28
+ max: 100,
29
+ step: 1,
30
+ value: 40,
31
+ orientation: 'horizontal',
32
+ rtl: false,
33
+ operable: true,
34
+ focusable: true,
35
+ valueText: null,
36
+ largeStep: 10,
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ const story = (id: string) => `a11y-slider--${id}`;
42
+
43
+ export const SLIDER_BINDING_STATES = [
44
+ {
45
+ id: 'horizontal',
46
+ summary: 'a horizontal single-thumb Slider at 40 of 0–100',
47
+ storyId: story('horizontal'),
48
+ label: 'Volume',
49
+ facts: facts(),
50
+ },
51
+ {
52
+ id: 'vertical',
53
+ summary: 'a vertical single-thumb Slider',
54
+ storyId: story('vertical'),
55
+ label: 'Temperature',
56
+ facts: facts({orientation: 'vertical'}),
57
+ },
58
+ {
59
+ id: 'horizontal-rtl',
60
+ summary:
61
+ 'a horizontal Slider on a right-to-left page: ArrowLeft must raise the value',
62
+ storyId: story('horizontal-rtl'),
63
+ label: 'Volume',
64
+ facts: facts({rtl: true}),
65
+ direction: 'rtl',
66
+ },
67
+ {
68
+ id: 'formatted',
69
+ summary: 'a Slider that speaks a formatted value',
70
+ storyId: story('formatted'),
71
+ label: 'Brightness',
72
+ facts: facts({valueText: '40 percent'}),
73
+ formatValue: true,
74
+ },
75
+ {
76
+ id: 'disabled-with-message',
77
+ summary:
78
+ 'a disabled Slider with a disabled message: the thumb keeps focus and its value',
79
+ storyId: story('disabled-with-message'),
80
+ label: 'Volume',
81
+ facts: facts({operable: false}),
82
+ disabledMessage: 'Volume is locked while sharing your screen',
83
+ },
84
+ {
85
+ id: 'step-three',
86
+ summary: 'a Slider with step 3 on a 0–100 range: End must reach 100',
87
+ storyId: story('step-three'),
88
+ label: 'Zoom',
89
+ facts: facts({step: 3, value: 48, largeStep: 30}),
90
+ },
91
+ ] as const satisfies ReadonlyArray<SliderBindingState>;
92
+
93
+ export const SLIDER_EXCLUSIONS = [
94
+ {
95
+ id: 'range-thumb-separation',
96
+ owner: 'component:Slider AR1 and Slider.test.tsx',
97
+ reason:
98
+ "A range Slider renders two thumbs whose aria-valuemin/aria-valuemax narrow to the other thumb minus the minimum separation; that two-thumb coupling and the group semantics around them are the binding's own tests. This binding proves the single-thumb pattern each thumb follows.",
99
+ },
100
+ {
101
+ id: 'pointer-drag-model',
102
+ owner: 'Slider.test.tsx pointer suites and the Chromium pointer evidence',
103
+ reason:
104
+ "Dragging, track clicks (mirrored under RTL), and the value tooltip are pointer behavior outside the keyboard-and-semantics pattern; the component's own suites prove them.",
105
+ },
106
+ ] as const;
@@ -0,0 +1,105 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @vitest-environment jsdom */
3
+
4
+ /**
5
+ * @file Slider.a11y.test.tsx
6
+ * @input SLIDER_PATTERN bound to Slider's representative states
7
+ * @output jsdom-lane proof of the DOM expectations and an honest "unrun" report for the browser layers
8
+ * @position Binding test for component:Slider AR1 (the Chromium spec covers the rest)
9
+ */
10
+
11
+ import {cleanup, render, screen} from '@testing-library/react';
12
+ import {describe, expect, it} from 'vitest';
13
+ import {
14
+ SLIDER_PATTERN,
15
+ checkAccessibilitySpec,
16
+ createJsdomHarness,
17
+ expectAccessibilitySpec,
18
+ summarize,
19
+ type BindingResult,
20
+ } from '@astryxdesign/a11y-spec';
21
+ import {SLIDER_KNOWN_FAILURES} from './Slider.a11y.known-failures';
22
+ import {renderSliderState} from './Slider.a11y.renders';
23
+ import {
24
+ SLIDER_BINDING_STATES,
25
+ SLIDER_EXCLUSIONS,
26
+ type SliderBindingState,
27
+ } from './Slider.a11y.states';
28
+
29
+ function thumb(state: SliderBindingState): HTMLElement {
30
+ return screen.getByRole('slider', {name: state.label});
31
+ }
32
+
33
+ async function checkState(state: SliderBindingState): Promise<BindingResult> {
34
+ return checkAccessibilitySpec({
35
+ spec: SLIDER_PATTERN,
36
+ binding: 'Slider',
37
+ state: state.id,
38
+ facts: state.facts,
39
+ knownFailures: SLIDER_KNOWN_FAILURES,
40
+ mount: async () => {
41
+ render(renderSliderState(state));
42
+ return createJsdomHarness({subject: thumb(state), related: {}});
43
+ },
44
+ unmount: cleanup,
45
+ });
46
+ }
47
+
48
+ describe('Slider — slider pattern, jsdom lane', () => {
49
+ it.each(
50
+ SLIDER_BINDING_STATES.map(
51
+ state => [state.id, state.summary, state] as const,
52
+ ),
53
+ )('%s — %s', async (_id, _summary, state) => {
54
+ await expectAccessibilitySpec({
55
+ spec: SLIDER_PATTERN,
56
+ binding: 'Slider',
57
+ state: state.id,
58
+ facts: state.facts,
59
+ knownFailures: SLIDER_KNOWN_FAILURES,
60
+ render: () => {
61
+ render(renderSliderState(state));
62
+ },
63
+ subject: () => thumb(state),
64
+ related: () => ({}),
65
+ cleanup,
66
+ });
67
+ });
68
+
69
+ it('runs the DOM layer and reports the browser-owned outcomes as unrun', async () => {
70
+ const results: BindingResult[] = [];
71
+ for (const state of SLIDER_BINDING_STATES) {
72
+ results.push(await checkState(state));
73
+ }
74
+ const report = summarize(SLIDER_PATTERN, results);
75
+ expect(report.counts.pass).toBeGreaterThan(0);
76
+ expect(report.unrunLayers).toEqual(['accessibility-tree', 'real-browser']);
77
+ expect(report.counts.unexpectedPass).toBe(0);
78
+ });
79
+ });
80
+
81
+ describe('Slider binding inventory', () => {
82
+ it('gives every representative state a distinct checked-in story', () => {
83
+ const stories = SLIDER_BINDING_STATES.map(state => state.storyId);
84
+ expect(new Set(stories).size).toBe(stories.length);
85
+ });
86
+
87
+ it('keeps range coupling and pointer ownership explicit', () => {
88
+ expect(SLIDER_EXCLUSIONS.map(exclusion => exclusion.id)).toEqual([
89
+ 'range-thumb-separation',
90
+ 'pointer-drag-model',
91
+ ]);
92
+ for (const exclusion of SLIDER_EXCLUSIONS) {
93
+ expect(exclusion.owner.length, exclusion.id).toBeGreaterThan(20);
94
+ expect(exclusion.reason.length, exclusion.id).toBeGreaterThan(80);
95
+ }
96
+ });
97
+
98
+ it('renders exactly one named thumb for every state', () => {
99
+ for (const state of SLIDER_BINDING_STATES) {
100
+ render(renderSliderState(state));
101
+ expect(thumb(state), state.id).toBeTruthy();
102
+ cleanup();
103
+ }
104
+ });
105
+ });
@@ -166,29 +166,17 @@ test('the state inventory describes the labels the page actually renders', async
166
166
  expect(wrong).toEqual([]);
167
167
  });
168
168
 
169
- test('every expectation is exercised by at least one bound state', async ({
170
- page,
171
- }) => {
172
- // A whole-binding sweep: every state, every expectation, a fresh mount each
173
- // time. That is minutes of real browser work, not the seconds a single-state
174
- // test takes.
175
- test.setTimeout(5 * 60 * 1000);
176
- const cdp = await page.context().newCDPSession(page);
177
- const results: BindingResult[] = [];
178
- for (const state of SWITCH_BINDING_STATES) {
179
- results.push(await runState(page, cdp, state));
180
- }
181
- // An expectation that applies to nothing is not coverage, however green it
182
- // looks. This is the only place that can notice: the contract never sees the
183
- // states, so whether a condition matches a real one is a binding-side fact.
184
- expect(neverExercised(results)).toEqual([]);
185
- expect(unmatchedKnownFailures(SWITCH_KNOWN_FAILURES, results)).toEqual([]);
186
- });
169
+ // Each state's result, recorded by its own test below so the whole-suite check
170
+ // at the end reads it instead of measuring every state a second time. A state
171
+ // whose test did not run in this worker (a filtered run, or a worker restarted
172
+ // after a failure) is measured by the whole-suite check itself.
173
+ const recorded = new Map<string, BindingResult>();
187
174
 
188
175
  for (const state of SWITCH_BINDING_STATES) {
189
176
  test(`Switch [${state.id}] — ${state.summary}`, async ({page}) => {
190
177
  const cdp = await page.context().newCDPSession(page);
191
178
  const result = await runState(page, cdp, state);
179
+ recorded.set(state.id, result);
192
180
  const report = summarize(SWITCH_PATTERN, [result]);
193
181
  // eslint-disable-next-line no-console -- the report is this run's artifact
194
182
  console.log(formatReport(report));
@@ -201,3 +189,22 @@ for (const state of SWITCH_BINDING_STATES) {
201
189
  expect(report.counts.unexpectedPass).toBe(0);
202
190
  });
203
191
  }
192
+
193
+ test('every expectation is exercised by at least one bound state', async ({
194
+ page,
195
+ }) => {
196
+ // A whole-binding sweep: every state's result, read from its own test or,
197
+ // when that test did not run in this worker, measured here (minutes of real
198
+ // browser work in that case, so the budget stays).
199
+ test.setTimeout(5 * 60 * 1000);
200
+ const cdp = await page.context().newCDPSession(page);
201
+ const results: BindingResult[] = [];
202
+ for (const state of SWITCH_BINDING_STATES) {
203
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
204
+ }
205
+ // An expectation that applies to nothing is not coverage, however green it
206
+ // looks. This is the only place that can notice: the contract never sees the
207
+ // states, so whether a condition matches a real one is a binding-side fact.
208
+ expect(neverExercised(results)).toEqual([]);
209
+ expect(unmatchedKnownFailures(SWITCH_KNOWN_FAILURES, results)).toEqual([]);
210
+ });
@@ -163,24 +163,18 @@ test('every binding fact matches the rendered semantic state', async ({
163
163
  expect(wrong).toEqual([]);
164
164
  });
165
165
 
166
- test('every expectation is exercised by at least one bound state', async ({
167
- page,
168
- }) => {
169
- test.setTimeout(5 * 60 * 1000);
170
- const cdp = await page.context().newCDPSession(page);
171
- const results: BindingResult[] = [];
172
- for (const state of TABS_BINDING_STATES) {
173
- results.push(await runState(page, cdp, state));
174
- }
175
- expect(neverExercised(results)).toEqual([]);
176
- expect(unmatchedKnownFailures(TABS_KNOWN_FAILURES, results)).toEqual([]);
177
- });
166
+ // Each state's result, recorded by its own test below so the whole-suite check
167
+ // at the end reads it instead of measuring every state a second time. A state
168
+ // whose test did not run in this worker (a filtered run, or a worker restarted
169
+ // after a failure) is measured by the whole-suite check itself.
170
+ const recorded = new Map<string, BindingResult>();
178
171
 
179
172
  for (const state of TABS_BINDING_STATES) {
180
173
  test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
181
174
  test.setTimeout(3 * 60 * 1000);
182
175
  const cdp = await page.context().newCDPSession(page);
183
176
  const result = await runState(page, cdp, state);
177
+ recorded.set(state.id, result);
184
178
  const report = summarize(TABS_PATTERN, [result]);
185
179
  // eslint-disable-next-line no-console -- the report is this run's artifact
186
180
  console.log(formatReport(report));
@@ -189,3 +183,16 @@ for (const state of TABS_BINDING_STATES) {
189
183
  expect(report.counts.unexpectedPass).toBe(0);
190
184
  });
191
185
  }
186
+
187
+ test('every expectation is exercised by at least one bound state', async ({
188
+ page,
189
+ }) => {
190
+ test.setTimeout(5 * 60 * 1000);
191
+ const cdp = await page.context().newCDPSession(page);
192
+ const results: BindingResult[] = [];
193
+ for (const state of TABS_BINDING_STATES) {
194
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
195
+ }
196
+ expect(neverExercised(results)).toEqual([]);
197
+ expect(unmatchedKnownFailures(TABS_KNOWN_FAILURES, results)).toEqual([]);
198
+ });
@@ -176,26 +176,18 @@ test('every state declaration matches the browser accessibility node', async ({
176
176
  expect(wrong).toEqual([]);
177
177
  });
178
178
 
179
- test('every expectation is exercised and every known failure matches once', async ({
180
- page,
181
- }) => {
182
- test.setTimeout(8 * 60 * 1000);
183
- const cdp = await page.context().newCDPSession(page);
184
- const results: BindingResult[] = [];
185
- for (const state of TEXT_INPUT_BINDING_STATES) {
186
- results.push(await runState(page, cdp, state));
187
- }
188
- expect(neverExercised(results)).toEqual([]);
189
- expect(unmatchedKnownFailures(TEXT_INPUT_KNOWN_FAILURES, results)).toEqual(
190
- [],
191
- );
192
- });
179
+ // Each state's result, recorded by its own test below so the whole-suite check
180
+ // at the end reads it instead of measuring every state a second time. A state
181
+ // whose test did not run in this worker (a filtered run, or a worker restarted
182
+ // after a failure) is measured by the whole-suite check itself.
183
+ const recorded = new Map<string, BindingResult>();
193
184
 
194
185
  for (const state of TEXT_INPUT_BINDING_STATES) {
195
186
  test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
196
187
  test.setTimeout(3 * 60 * 1000);
197
188
  const cdp = await page.context().newCDPSession(page);
198
189
  const result = await runState(page, cdp, state);
190
+ recorded.set(state.id, result);
199
191
  const report = summarize(TEXT_INPUT_PATTERN, [result]);
200
192
  // eslint-disable-next-line no-console -- the report is this run's artifact
201
193
  console.log(formatReport(report));
@@ -204,3 +196,18 @@ for (const state of TEXT_INPUT_BINDING_STATES) {
204
196
  expect(report.counts.unexpectedPass).toBe(0);
205
197
  });
206
198
  }
199
+
200
+ test('every expectation is exercised and every known failure matches once', async ({
201
+ page,
202
+ }) => {
203
+ test.setTimeout(8 * 60 * 1000);
204
+ const cdp = await page.context().newCDPSession(page);
205
+ const results: BindingResult[] = [];
206
+ for (const state of TEXT_INPUT_BINDING_STATES) {
207
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
208
+ }
209
+ expect(neverExercised(results)).toEqual([]);
210
+ expect(unmatchedKnownFailures(TEXT_INPUT_KNOWN_FAILURES, results)).toEqual(
211
+ [],
212
+ );
213
+ });
@@ -143,22 +143,17 @@ test('the state inventory matches rendered labels and applicability facts', asyn
143
143
  expect(wrong).toEqual([]);
144
144
  });
145
145
 
146
- test('every expectation is exercised by at least one ToggleButton state', async ({
147
- page,
148
- }) => {
149
- test.setTimeout(5 * 60 * 1000);
150
- const cdp = await page.context().newCDPSession(page);
151
- const results: BindingResult[] = [];
152
- for (const state of TOGGLE_BUTTON_BINDING_STATES) {
153
- results.push(await runState(page, cdp, state));
154
- }
155
- expect(neverExercised(results)).toEqual([]);
156
- });
146
+ // Each state's result, recorded by its own test below so the whole-suite check
147
+ // at the end reads it instead of measuring every state a second time. A state
148
+ // whose test did not run in this worker (a filtered run, or a worker restarted
149
+ // after a failure) is measured by the whole-suite check itself.
150
+ const recorded = new Map<string, BindingResult>();
157
151
 
158
152
  for (const state of TOGGLE_BUTTON_BINDING_STATES) {
159
153
  test(`ToggleButton [${state.id}] — ${state.summary}`, async ({page}) => {
160
154
  const cdp = await page.context().newCDPSession(page);
161
155
  const result = await runState(page, cdp, state);
156
+ recorded.set(state.id, result);
162
157
  const report = summarize(TOGGLE_BUTTON_PATTERN, [result]);
163
158
  // eslint-disable-next-line no-console -- the report is this run's evidence
164
159
  console.log(formatReport(report));
@@ -167,3 +162,15 @@ for (const state of TOGGLE_BUTTON_BINDING_STATES) {
167
162
  expect(report.counts.unexpectedPass).toBe(0);
168
163
  });
169
164
  }
165
+
166
+ test('every expectation is exercised by at least one ToggleButton state', async ({
167
+ page,
168
+ }) => {
169
+ test.setTimeout(5 * 60 * 1000);
170
+ const cdp = await page.context().newCDPSession(page);
171
+ const results: BindingResult[] = [];
172
+ for (const state of TOGGLE_BUTTON_BINDING_STATES) {
173
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
174
+ }
175
+ expect(neverExercised(results)).toEqual([]);
176
+ });
@@ -920,6 +920,93 @@ describe('Tokenizer', () => {
920
920
  expect(screen.queryByText('Create "existing"')).not.toBeInTheDocument();
921
921
  });
922
922
 
923
+ it('does not show Create option for an already-selected label whose id differs from it', async () => {
924
+ // Real selections usually carry an opaque id (`u1` under the label
925
+ // "Alice"). The duplicate check used to compare the typed text against
926
+ // selected ids only, and the search results could not catch the match
927
+ // because filteredSource had already removed the selected item — so
928
+ // typing the label of an existing token offered "Create", which added a
929
+ // duplicate token under a new id.
930
+ const source: SearchSource = {
931
+ search: (query: string) =>
932
+ [{id: 'u1', label: 'Alice'}].filter(u =>
933
+ u.label.toLowerCase().includes(query.toLowerCase()),
934
+ ),
935
+ bootstrap: () => [],
936
+ };
937
+ const onChange = vi.fn();
938
+ render(
939
+ <Tokenizer
940
+ label="Tags"
941
+ searchSource={source}
942
+ value={[{id: 'u1', label: 'Alice'}]}
943
+ onChange={onChange}
944
+ hasCreate
945
+ debounceMs={0}
946
+ />,
947
+ );
948
+
949
+ const input = screen.getByRole('combobox');
950
+ await act(async () => {
951
+ fireEvent.change(input, {target: {value: 'Alice'}});
952
+ });
953
+ await act(async () => {
954
+ await new Promise(r => setTimeout(r, 50));
955
+ });
956
+
957
+ expect(screen.queryByText('Create "Alice"')).not.toBeInTheDocument();
958
+ });
959
+
960
+ it('does not add a duplicate token when a stale Create entry is committed', async () => {
961
+ // The guard in handleAdd must apply the same label check: a Create
962
+ // entry rendered before the selection changed still commits through
963
+ // here, and an opaque id must not let a same-label token through.
964
+ const source: SearchSource = {
965
+ search: (query: string) =>
966
+ // A label that contains the query but is not exactly it, so the
967
+ // Create entry for the query itself is legitimately offered.
968
+ [{id: 'u2', label: 'Alice Smith'}].filter(u =>
969
+ u.label.toLowerCase().includes(query.toLowerCase()),
970
+ ),
971
+ bootstrap: () => [],
972
+ };
973
+ const onChange = vi.fn();
974
+ const {rerender} = render(
975
+ <Tokenizer
976
+ label="Tags"
977
+ searchSource={source}
978
+ value={[]}
979
+ onChange={onChange}
980
+ hasCreate
981
+ debounceMs={0}
982
+ />,
983
+ );
984
+
985
+ const input = screen.getByRole('combobox');
986
+ await act(async () => {
987
+ fireEvent.change(input, {target: {value: 'Alice'}});
988
+ });
989
+ await act(async () => {
990
+ await new Promise(r => setTimeout(r, 50));
991
+ });
992
+ // The Create entry exists while the selection is empty...
993
+ expect(screen.queryByText('Create "Alice"')).toBeInTheDocument();
994
+ // ...then the selection gains Alice (under an opaque id) before the
995
+ // entry is committed.
996
+ rerender(
997
+ <Tokenizer
998
+ label="Tags"
999
+ searchSource={source}
1000
+ value={[{id: 'u1', label: 'Alice'}]}
1001
+ onChange={onChange}
1002
+ hasCreate
1003
+ debounceMs={0}
1004
+ />,
1005
+ );
1006
+ fireEvent.click(screen.getByText('Create "Alice"'));
1007
+ expect(onChange).not.toHaveBeenCalled();
1008
+ });
1009
+
923
1010
  it('does not show Create option when hasCreate is false', async () => {
924
1011
  render(
925
1012
  <Tokenizer