@assistant-ui/react-generative-ui 0.0.23 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/README.md +2 -0
  2. package/dist/a2ui.d.ts +1 -4
  3. package/dist/a2ui.d.ts.map +1 -1
  4. package/dist/a2ui.js +1 -4
  5. package/dist/index.d.ts +1 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -10
  8. package/dist/index.server.d.ts +1 -2
  9. package/dist/index.server.d.ts.map +1 -1
  10. package/dist/index.server.js +1 -10
  11. package/dist/ir.d.ts +1 -111
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +1 -175
  14. package/dist/slack.d.ts +1 -4
  15. package/dist/slack.d.ts.map +1 -1
  16. package/dist/slack.js +1 -4
  17. package/dist/teams.d.ts +1 -4
  18. package/dist/teams.d.ts.map +1 -1
  19. package/dist/teams.js +1 -4
  20. package/package.json +5 -5
  21. package/src/a2ui.ts +1 -22
  22. package/src/index.server.ts +1 -2
  23. package/src/index.test.ts +75 -0
  24. package/src/index.ts +1 -2
  25. package/src/ir.ts +1 -279
  26. package/src/slack.ts +1 -39
  27. package/src/teams.ts +1 -35
  28. package/dist/JSONGenerativeUI.client.d.ts +0 -28
  29. package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
  30. package/dist/JSONGenerativeUI.client.js +0 -101
  31. package/dist/JSONGenerativeUI.client.js.map +0 -1
  32. package/dist/JSONGenerativeUI.server.d.ts +0 -21
  33. package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
  34. package/dist/JSONGenerativeUI.server.js +0 -32
  35. package/dist/JSONGenerativeUI.server.js.map +0 -1
  36. package/dist/JSONGenerativeUI.shared.d.ts +0 -63
  37. package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
  38. package/dist/JSONGenerativeUI.shared.js +0 -31
  39. package/dist/JSONGenerativeUI.shared.js.map +0 -1
  40. package/dist/RadioGroupScope.d.ts +0 -6
  41. package/dist/RadioGroupScope.d.ts.map +0 -1
  42. package/dist/RadioGroupScope.js +0 -25
  43. package/dist/RadioGroupScope.js.map +0 -1
  44. package/dist/a2ui/BindingContext.d.ts +0 -11
  45. package/dist/a2ui/BindingContext.d.ts.map +0 -1
  46. package/dist/a2ui/BindingContext.js +0 -12
  47. package/dist/a2ui/BindingContext.js.map +0 -1
  48. package/dist/a2ui/PresentRenderer.d.ts +0 -11
  49. package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
  50. package/dist/a2ui/PresentRenderer.js +0 -39
  51. package/dist/a2ui/PresentRenderer.js.map +0 -1
  52. package/dist/a2ui/convert.d.ts +0 -15
  53. package/dist/a2ui/convert.d.ts.map +0 -1
  54. package/dist/a2ui/convert.js +0 -728
  55. package/dist/a2ui/convert.js.map +0 -1
  56. package/dist/a2ui/dataModel.d.ts +0 -9
  57. package/dist/a2ui/dataModel.d.ts.map +0 -1
  58. package/dist/a2ui/dataModel.js +0 -44
  59. package/dist/a2ui/dataModel.js.map +0 -1
  60. package/dist/a2ui/reducer.d.ts +0 -8
  61. package/dist/a2ui/reducer.d.ts.map +0 -1
  62. package/dist/a2ui/reducer.js +0 -242
  63. package/dist/a2ui/reducer.js.map +0 -1
  64. package/dist/a2ui/snapshot.d.ts +0 -3
  65. package/dist/a2ui/snapshot.d.ts.map +0 -1
  66. package/dist/a2ui/snapshot.js +0 -33
  67. package/dist/a2ui/snapshot.js.map +0 -1
  68. package/dist/a2ui/types.d.ts +0 -88
  69. package/dist/a2ui/types.d.ts.map +0 -1
  70. package/dist/a2ui/types.js +0 -6
  71. package/dist/a2ui/types.js.map +0 -1
  72. package/dist/a2ui/valueFunctions.d.ts +0 -16
  73. package/dist/a2ui/valueFunctions.d.ts.map +0 -1
  74. package/dist/a2ui/valueFunctions.js +0 -332
  75. package/dist/a2ui/valueFunctions.js.map +0 -1
  76. package/dist/actionRegistry.d.ts +0 -29
  77. package/dist/actionRegistry.d.ts.map +0 -1
  78. package/dist/actionRegistry.js +0 -41
  79. package/dist/actionRegistry.js.map +0 -1
  80. package/dist/answeredValues.d.ts +0 -16
  81. package/dist/answeredValues.d.ts.map +0 -1
  82. package/dist/answeredValues.js +0 -37
  83. package/dist/answeredValues.js.map +0 -1
  84. package/dist/buildPresentParameters.d.ts +0 -21
  85. package/dist/buildPresentParameters.d.ts.map +0 -1
  86. package/dist/buildPresentParameters.js +0 -95
  87. package/dist/buildPresentParameters.js.map +0 -1
  88. package/dist/constants.d.ts +0 -15
  89. package/dist/constants.d.ts.map +0 -1
  90. package/dist/constants.js +0 -19
  91. package/dist/constants.js.map +0 -1
  92. package/dist/convert/boundSpec.d.ts +0 -48
  93. package/dist/convert/boundSpec.d.ts.map +0 -1
  94. package/dist/convert/boundSpec.js +0 -108
  95. package/dist/convert/boundSpec.js.map +0 -1
  96. package/dist/convert/copyBounded.d.ts +0 -25
  97. package/dist/convert/copyBounded.d.ts.map +0 -1
  98. package/dist/convert/copyBounded.js +0 -38
  99. package/dist/convert/copyBounded.js.map +0 -1
  100. package/dist/convert/isElement.d.ts +0 -3
  101. package/dist/convert/isElement.d.ts.map +0 -1
  102. package/dist/convert/isElement.js +0 -6
  103. package/dist/convert/isElement.js.map +0 -1
  104. package/dist/convert/takeRun.d.ts +0 -6
  105. package/dist/convert/takeRun.d.ts.map +0 -1
  106. package/dist/convert/takeRun.js +0 -20
  107. package/dist/convert/takeRun.js.map +0 -1
  108. package/dist/defineGenerativeComponents.d.ts +0 -29
  109. package/dist/defineGenerativeComponents.d.ts.map +0 -1
  110. package/dist/defineGenerativeComponents.js +0 -34
  111. package/dist/defineGenerativeComponents.js.map +0 -1
  112. package/dist/fieldReferences.d.ts +0 -9
  113. package/dist/fieldReferences.d.ts.map +0 -1
  114. package/dist/fieldReferences.js +0 -53
  115. package/dist/fieldReferences.js.map +0 -1
  116. package/dist/generativeUIToJSX.d.ts +0 -18
  117. package/dist/generativeUIToJSX.d.ts.map +0 -1
  118. package/dist/generativeUIToJSX.js +0 -76
  119. package/dist/generativeUIToJSX.js.map +0 -1
  120. package/dist/index.shared.d.ts +0 -15
  121. package/dist/index.shared.d.ts.map +0 -1
  122. package/dist/index.shared.js +0 -9
  123. package/dist/ir.js.map +0 -1
  124. package/dist/renderGenerativeUI.d.ts +0 -14
  125. package/dist/renderGenerativeUI.d.ts.map +0 -1
  126. package/dist/renderGenerativeUI.js +0 -83
  127. package/dist/renderGenerativeUI.js.map +0 -1
  128. package/dist/scopeSchema.d.ts +0 -6
  129. package/dist/scopeSchema.d.ts.map +0 -1
  130. package/dist/scopeSchema.js +0 -56
  131. package/dist/scopeSchema.js.map +0 -1
  132. package/dist/slack/constants.d.ts +0 -93
  133. package/dist/slack/constants.d.ts.map +0 -1
  134. package/dist/slack/constants.js +0 -146
  135. package/dist/slack/constants.js.map +0 -1
  136. package/dist/slack/decodeBlockAction.d.ts +0 -10
  137. package/dist/slack/decodeBlockAction.d.ts.map +0 -1
  138. package/dist/slack/decodeBlockAction.js +0 -129
  139. package/dist/slack/decodeBlockAction.js.map +0 -1
  140. package/dist/slack/fromSlackBlocks.d.ts +0 -27
  141. package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
  142. package/dist/slack/fromSlackBlocks.js +0 -483
  143. package/dist/slack/fromSlackBlocks.js.map +0 -1
  144. package/dist/slack/toSlackBlocks.d.ts +0 -4
  145. package/dist/slack/toSlackBlocks.d.ts.map +0 -1
  146. package/dist/slack/toSlackBlocks.js +0 -901
  147. package/dist/slack/toSlackBlocks.js.map +0 -1
  148. package/dist/slack/types.d.ts +0 -204
  149. package/dist/slack/types.d.ts.map +0 -1
  150. package/dist/slack/types.js +0 -0
  151. package/dist/teams/constants.d.ts +0 -34
  152. package/dist/teams/constants.d.ts.map +0 -1
  153. package/dist/teams/constants.js +0 -68
  154. package/dist/teams/constants.js.map +0 -1
  155. package/dist/teams/decodeSubmitData.d.ts +0 -26
  156. package/dist/teams/decodeSubmitData.d.ts.map +0 -1
  157. package/dist/teams/decodeSubmitData.js +0 -70
  158. package/dist/teams/decodeSubmitData.js.map +0 -1
  159. package/dist/teams/temporalId.d.ts +0 -5
  160. package/dist/teams/temporalId.d.ts.map +0 -1
  161. package/dist/teams/temporalId.js +0 -28
  162. package/dist/teams/temporalId.js.map +0 -1
  163. package/dist/teams/toAdaptiveCard.d.ts +0 -45
  164. package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
  165. package/dist/teams/toAdaptiveCard.js +0 -642
  166. package/dist/teams/toAdaptiveCard.js.map +0 -1
  167. package/dist/teams/toTeamsAttachments.d.ts +0 -10
  168. package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
  169. package/dist/teams/toTeamsAttachments.js +0 -99
  170. package/dist/teams/toTeamsAttachments.js.map +0 -1
  171. package/dist/teams/types.d.ts +0 -224
  172. package/dist/teams/types.d.ts.map +0 -1
  173. package/dist/teams/types.js +0 -0
  174. package/dist/temporal.d.ts +0 -33
  175. package/dist/temporal.d.ts.map +0 -1
  176. package/dist/temporal.js +0 -152
  177. package/dist/temporal.js.map +0 -1
  178. package/dist/types.d.ts +0 -99
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -0
  181. package/dist/vocabulary/alert.d.ts +0 -32
  182. package/dist/vocabulary/alert.d.ts.map +0 -1
  183. package/dist/vocabulary/alert.js +0 -160
  184. package/dist/vocabulary/alert.js.map +0 -1
  185. package/dist/vocabulary/collectFormValues.d.ts +0 -23
  186. package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
  187. package/dist/vocabulary/collectFormValues.js +0 -44
  188. package/dist/vocabulary/collectFormValues.js.map +0 -1
  189. package/dist/vocabulary/data.d.ts +0 -66
  190. package/dist/vocabulary/data.d.ts.map +0 -1
  191. package/dist/vocabulary/data.js +0 -503
  192. package/dist/vocabulary/data.js.map +0 -1
  193. package/dist/vocabulary/dispatch.d.ts +0 -9
  194. package/dist/vocabulary/dispatch.d.ts.map +0 -1
  195. package/dist/vocabulary/dispatch.js +0 -50
  196. package/dist/vocabulary/dispatch.js.map +0 -1
  197. package/dist/vocabulary/fact.d.ts +0 -19
  198. package/dist/vocabulary/fact.d.ts.map +0 -1
  199. package/dist/vocabulary/fact.js +0 -51
  200. package/dist/vocabulary/fact.js.map +0 -1
  201. package/dist/vocabulary/form.d.ts +0 -11
  202. package/dist/vocabulary/form.d.ts.map +0 -1
  203. package/dist/vocabulary/form.js +0 -23
  204. package/dist/vocabulary/form.js.map +0 -1
  205. package/dist/vocabulary/formatValue.d.ts +0 -7
  206. package/dist/vocabulary/formatValue.d.ts.map +0 -1
  207. package/dist/vocabulary/formatValue.js +0 -76
  208. package/dist/vocabulary/formatValue.js.map +0 -1
  209. package/dist/vocabulary/icon.d.ts +0 -41
  210. package/dist/vocabulary/icon.d.ts.map +0 -1
  211. package/dist/vocabulary/icon.js +0 -272
  212. package/dist/vocabulary/icon.js.map +0 -1
  213. package/dist/vocabulary/index.d.ts +0 -12
  214. package/dist/vocabulary/index.d.ts.map +0 -1
  215. package/dist/vocabulary/index.js +0 -36
  216. package/dist/vocabulary/index.js.map +0 -1
  217. package/dist/vocabulary/interactive.d.ts +0 -230
  218. package/dist/vocabulary/interactive.d.ts.map +0 -1
  219. package/dist/vocabulary/interactive.js +0 -530
  220. package/dist/vocabulary/interactive.js.map +0 -1
  221. package/dist/vocabulary/layout.d.ts +0 -79
  222. package/dist/vocabulary/layout.d.ts.map +0 -1
  223. package/dist/vocabulary/layout.js +0 -139
  224. package/dist/vocabulary/layout.js.map +0 -1
  225. package/dist/vocabulary/list.d.ts +0 -14
  226. package/dist/vocabulary/list.d.ts.map +0 -1
  227. package/dist/vocabulary/list.js +0 -50
  228. package/dist/vocabulary/list.js.map +0 -1
  229. package/dist/vocabulary/media.d.ts +0 -25
  230. package/dist/vocabulary/media.d.ts.map +0 -1
  231. package/dist/vocabulary/media.js +0 -42
  232. package/dist/vocabulary/media.js.map +0 -1
  233. package/dist/vocabulary/text.d.ts +0 -57
  234. package/dist/vocabulary/text.d.ts.map +0 -1
  235. package/dist/vocabulary/text.js +0 -49
  236. package/dist/vocabulary/text.js.map +0 -1
  237. package/dist/vocabulary/toTextContent.d.ts +0 -2
  238. package/dist/vocabulary/toTextContent.d.ts.map +0 -1
  239. package/dist/vocabulary/toTextContent.js +0 -6
  240. package/dist/vocabulary/toTextContent.js.map +0 -1
  241. package/src/JSONGenerativeUI.client.tsx +0 -168
  242. package/src/JSONGenerativeUI.server.tsx +0 -39
  243. package/src/JSONGenerativeUI.shared.ts +0 -90
  244. package/src/JSONGenerativeUI.test.tsx +0 -1298
  245. package/src/RadioGroupScope.server.test.tsx +0 -24
  246. package/src/RadioGroupScope.tsx +0 -34
  247. package/src/a2ui/BindingContext.ts +0 -26
  248. package/src/a2ui/PresentRenderer.tsx +0 -83
  249. package/src/a2ui/convert.test.ts +0 -2923
  250. package/src/a2ui/convert.ts +0 -1207
  251. package/src/a2ui/dataModel.ts +0 -69
  252. package/src/a2ui/reducer.test.ts +0 -422
  253. package/src/a2ui/reducer.ts +0 -363
  254. package/src/a2ui/snapshot.test.ts +0 -52
  255. package/src/a2ui/snapshot.ts +0 -52
  256. package/src/a2ui/types.ts +0 -114
  257. package/src/a2ui/valueFunctions.test.ts +0 -271
  258. package/src/a2ui/valueFunctions.ts +0 -523
  259. package/src/actionRegistry.test.ts +0 -90
  260. package/src/actionRegistry.ts +0 -88
  261. package/src/answeredValues.test.ts +0 -33
  262. package/src/answeredValues.tsx +0 -70
  263. package/src/buildPresentParameters.test.ts +0 -246
  264. package/src/buildPresentParameters.ts +0 -130
  265. package/src/constants.ts +0 -17
  266. package/src/convert/boundSpec.test.ts +0 -192
  267. package/src/convert/boundSpec.ts +0 -134
  268. package/src/convert/copyBounded.test.ts +0 -131
  269. package/src/convert/copyBounded.ts +0 -38
  270. package/src/convert/isElement.ts +0 -6
  271. package/src/convert/takeRun.test.ts +0 -64
  272. package/src/convert/takeRun.ts +0 -19
  273. package/src/defineGenerativeComponents.ts +0 -38
  274. package/src/fieldReferences.test.ts +0 -66
  275. package/src/fieldReferences.ts +0 -83
  276. package/src/generativeUIToJSX.test.ts +0 -268
  277. package/src/generativeUIToJSX.ts +0 -152
  278. package/src/index.server.test.ts +0 -106
  279. package/src/index.shared.ts +0 -61
  280. package/src/ir.test.ts +0 -203
  281. package/src/renderGenerativeUI.test.tsx +0 -373
  282. package/src/renderGenerativeUI.tsx +0 -165
  283. package/src/scopeSchema.test.ts +0 -72
  284. package/src/scopeSchema.ts +0 -71
  285. package/src/slack/a2uiSlack.test.ts +0 -198
  286. package/src/slack/constants.ts +0 -194
  287. package/src/slack/datePicker.test.ts +0 -612
  288. package/src/slack/decodeBlockAction.test.ts +0 -233
  289. package/src/slack/decodeBlockAction.ts +0 -232
  290. package/src/slack/fromSlackBlocks.test.ts +0 -1465
  291. package/src/slack/fromSlackBlocks.ts +0 -789
  292. package/src/slack/toSlackBlocks.test.ts +0 -3065
  293. package/src/slack/toSlackBlocks.ts +0 -1818
  294. package/src/slack/types.ts +0 -264
  295. package/src/teams/constants.ts +0 -93
  296. package/src/teams/decodeSubmitData.test.ts +0 -397
  297. package/src/teams/decodeSubmitData.ts +0 -110
  298. package/src/teams/temporal.test.ts +0 -766
  299. package/src/teams/temporalId.ts +0 -56
  300. package/src/teams/toAdaptiveCard.test.ts +0 -1897
  301. package/src/teams/toAdaptiveCard.ts +0 -1057
  302. package/src/teams/toTeamsAttachments.test.ts +0 -74
  303. package/src/teams/toTeamsAttachments.ts +0 -133
  304. package/src/teams/types.ts +0 -266
  305. package/src/temporal.test.ts +0 -319
  306. package/src/temporal.ts +0 -259
  307. package/src/types.ts +0 -119
  308. package/src/vocabulary/alert.test.tsx +0 -427
  309. package/src/vocabulary/alert.tsx +0 -217
  310. package/src/vocabulary/collectFormValues.test.ts +0 -389
  311. package/src/vocabulary/collectFormValues.ts +0 -116
  312. package/src/vocabulary/data.test.tsx +0 -858
  313. package/src/vocabulary/data.tsx +0 -819
  314. package/src/vocabulary/datepicker.dom.test.tsx +0 -1148
  315. package/src/vocabulary/dispatch.ts +0 -83
  316. package/src/vocabulary/fact.test.tsx +0 -94
  317. package/src/vocabulary/fact.tsx +0 -54
  318. package/src/vocabulary/form.test.tsx +0 -113
  319. package/src/vocabulary/form.tsx +0 -39
  320. package/src/vocabulary/formatValue.test.ts +0 -72
  321. package/src/vocabulary/formatValue.ts +0 -127
  322. package/src/vocabulary/icon.test.tsx +0 -64
  323. package/src/vocabulary/icon.tsx +0 -174
  324. package/src/vocabulary/index.test.ts +0 -65
  325. package/src/vocabulary/index.ts +0 -33
  326. package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
  327. package/src/vocabulary/interactive.dom.test.tsx +0 -1747
  328. package/src/vocabulary/interactive.test.tsx +0 -377
  329. package/src/vocabulary/interactive.tsx +0 -1048
  330. package/src/vocabulary/layout.test.tsx +0 -323
  331. package/src/vocabulary/layout.tsx +0 -247
  332. package/src/vocabulary/list.test.tsx +0 -222
  333. package/src/vocabulary/list.tsx +0 -50
  334. package/src/vocabulary/media.test.tsx +0 -85
  335. package/src/vocabulary/media.tsx +0 -56
  336. package/src/vocabulary/text.test.tsx +0 -102
  337. package/src/vocabulary/text.tsx +0 -60
  338. package/src/vocabulary/toTextContent.ts +0 -2
@@ -1,454 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import {
3
- Children,
4
- isValidElement,
5
- type ReactElement,
6
- type ReactNode,
7
- } from "react";
8
- import { renderToStaticMarkup } from "react-dom/server";
9
- import { interactiveVocabulary } from "./interactive";
10
- import { createActionRegistry } from "../actionRegistry";
11
-
12
- /** The props shape `Button.render` receives when the renderer injects `$action`
13
- * and `$dispatch`. Used to call `render` directly so the test can invoke
14
- * `onClick` without a DOM. */
15
- type ButtonRenderProps = {
16
- label: string;
17
- $status: "done";
18
- $action?: { type: string; [k: string]: unknown };
19
- $dispatch?: (a: unknown) => unknown;
20
- };
21
-
22
- /**
23
- * Calls a `render` that itself uses hooks (e.g. `RadioGroup`'s `useId`) from inside an active React render pass, then hands back the resulting element with its handlers intact so the test can invoke them directly.
24
- */
25
- function renderWithHooks(render: () => ReactNode): ReactElement {
26
- let captured: ReactElement | undefined;
27
- function Capture() {
28
- captured = render() as ReactElement;
29
- return captured;
30
- }
31
- renderToStaticMarkup(<Capture />);
32
- return captured!;
33
- }
34
-
35
- const getRadioOptions = (out: ReactElement) =>
36
- Children.toArray((out.props as { children: ReactNode }).children)
37
- .filter(isValidElement)
38
- .filter(
39
- (child) =>
40
- (child.props as { "data-aui"?: string })["data-aui"] ===
41
- "radiogroup-option",
42
- ) as ReactElement[];
43
-
44
- describe("interactiveVocabulary $action dispatch", () => {
45
- it("Button render attaches an onClick that fires $dispatch with the $action payload", () => {
46
- const handler = vi.fn();
47
- const registry = createActionRegistry({ purchase: handler });
48
- const out = renderWithHooks(() =>
49
- interactiveVocabulary.Button.render({
50
- label: "Buy",
51
- $status: "done",
52
- $action: { type: "purchase", itemId: "sku-1" },
53
- $dispatch: registry.dispatch,
54
- } as ButtonRenderProps),
55
- );
56
- expect(isValidElement(out)).toBe(true);
57
- const onClick = (out as { props: { onClick: (e: object) => void } }).props
58
- .onClick;
59
- expect(typeof onClick).toBe("function");
60
- onClick({ currentTarget: {} });
61
- expect(handler).toHaveBeenCalledWith({
62
- payload: { type: "purchase", itemId: "sku-1" },
63
- });
64
- });
65
-
66
- it("Button onClick is a no-op when no $dispatch is wired (read-only render)", () => {
67
- const out = renderWithHooks(() =>
68
- interactiveVocabulary.Button.render({
69
- label: "Buy",
70
- $status: "done",
71
- $action: { type: "purchase", itemId: "sku-1" },
72
- } as ButtonRenderProps),
73
- );
74
- const onClick = (out as { props: { onClick: (e: object) => void } }).props
75
- .onClick;
76
- expect(typeof onClick).toBe("function");
77
- expect(() => onClick({ currentTarget: {} })).not.toThrow();
78
- });
79
-
80
- it("Button onClick is a no-op when $action is absent", () => {
81
- const handler = vi.fn();
82
- const registry = createActionRegistry({ purchase: handler });
83
- const out = renderWithHooks(() =>
84
- interactiveVocabulary.Button.render({
85
- label: "Go",
86
- $status: "done",
87
- $dispatch: registry.dispatch,
88
- } as ButtonRenderProps),
89
- );
90
- (out as { props: { onClick: (e: object) => void } }).props.onClick({
91
- currentTarget: {},
92
- });
93
- expect(handler).not.toHaveBeenCalled();
94
- });
95
-
96
- it("Select onChange merges the selected value into $input and preserves a model-supplied $action.value", () => {
97
- const handler = vi.fn();
98
- const registry = createActionRegistry({ pick: handler });
99
- const out = renderWithHooks(() =>
100
- interactiveVocabulary.Select.render({
101
- options: [{ label: "A", value: "a" }],
102
- $status: "done",
103
- $action: { type: "pick", value: "model-supplied" },
104
- $dispatch: registry.dispatch,
105
- }),
106
- ) as ReactNode;
107
- const onChange = (
108
- out as {
109
- props: { onChange: (e: { currentTarget: { value: string } }) => void };
110
- }
111
- ).props.onChange;
112
- onChange({ currentTarget: { value: "user-picked" } });
113
- expect(handler).toHaveBeenCalledWith({
114
- payload: {
115
- type: "pick",
116
- value: "model-supplied",
117
- $input: "user-picked",
118
- },
119
- });
120
- });
121
-
122
- it("Input onKeyDown (Enter) merges the entered value into $input", () => {
123
- const handler = vi.fn();
124
- const registry = createActionRegistry({ submit: handler });
125
- const out = renderWithHooks(() =>
126
- interactiveVocabulary.Input.render({
127
- $status: "done",
128
- $action: { type: "submit" },
129
- $dispatch: registry.dispatch,
130
- }),
131
- ) as ReactNode;
132
- const onKeyDown = (
133
- out as {
134
- props: {
135
- onKeyDown: (e: {
136
- key: string;
137
- nativeEvent: { isComposing: boolean };
138
- currentTarget: { value: string };
139
- }) => void;
140
- };
141
- }
142
- ).props.onKeyDown;
143
- onKeyDown({
144
- key: "Enter",
145
- nativeEvent: { isComposing: false },
146
- currentTarget: { value: "typed text" },
147
- });
148
- expect(handler).toHaveBeenCalledWith({
149
- payload: { type: "submit", $input: "typed text" },
150
- });
151
- });
152
-
153
- it.each(["text", "password", "number"] as const)(
154
- "Input renders its %s inputType",
155
- (inputType) => {
156
- expect(
157
- interactiveVocabulary.Input.properties.parse({ inputType }),
158
- ).toEqual({ inputType });
159
- const out = renderWithHooks(() =>
160
- interactiveVocabulary.Input.render({
161
- inputType,
162
- defaultValue: "12.5",
163
- $status: "done",
164
- }),
165
- );
166
-
167
- expect(out.type).toBe("input");
168
- expect(out.props).toMatchObject({
169
- type: inputType,
170
- defaultValue: "12.5",
171
- step: inputType === "number" ? "any" : undefined,
172
- });
173
- },
174
- );
175
-
176
- it.each(["12.5", ""])(
177
- "Input with inputType number submits %j as a string",
178
- (value) => {
179
- const handler = vi.fn();
180
- const registry = createActionRegistry({ submit: handler });
181
- const out = renderWithHooks(() =>
182
- interactiveVocabulary.Input.render({
183
- inputType: "number",
184
- $status: "done",
185
- $action: { type: "submit" },
186
- $dispatch: registry.dispatch,
187
- }),
188
- ) as ReactElement<{
189
- type: string;
190
- step: string;
191
- onKeyDown: (event: {
192
- key: string;
193
- nativeEvent: { isComposing: boolean };
194
- currentTarget: { value: string };
195
- }) => void;
196
- }>;
197
-
198
- expect(out.props.type).toBe("number");
199
- expect(out.props.step).toBe("any");
200
- out.props.onKeyDown({
201
- key: "Enter",
202
- nativeEvent: { isComposing: false },
203
- currentTarget: { value },
204
- });
205
- expect(handler).toHaveBeenCalledWith({
206
- payload: { type: "submit", $input: value },
207
- });
208
- },
209
- );
210
-
211
- it("Input onKeyDown (Enter) defers to an ancestor form instead of firing its own $action", () => {
212
- const handler = vi.fn();
213
- const registry = createActionRegistry({ submit: handler });
214
- const out = renderWithHooks(() =>
215
- interactiveVocabulary.Input.render({
216
- name: "email",
217
- $status: "done",
218
- $action: { type: "submit" },
219
- $dispatch: registry.dispatch,
220
- }),
221
- ) as ReactNode;
222
- const onKeyDown = (
223
- out as {
224
- props: {
225
- onKeyDown: (e: {
226
- key: string;
227
- nativeEvent: { isComposing: boolean };
228
- currentTarget: { value: string; form: unknown };
229
- }) => void;
230
- };
231
- }
232
- ).props.onKeyDown;
233
- onKeyDown({
234
- key: "Enter",
235
- nativeEvent: { isComposing: false },
236
- currentTarget: { value: "typed text", form: {} },
237
- });
238
- expect(handler).not.toHaveBeenCalled();
239
- });
240
-
241
- type TextareaKeyDownEvent = {
242
- key: string;
243
- ctrlKey: boolean;
244
- metaKey: boolean;
245
- nativeEvent: { isComposing: boolean };
246
- currentTarget: {
247
- value: string;
248
- form: { requestSubmit: () => void } | null;
249
- };
250
- preventDefault: () => void;
251
- };
252
-
253
- it("Input multiline (textarea) Ctrl+Enter inside a form calls HTMLFormElement.prototype.requestSubmit instead of firing its own $action", () => {
254
- const handler = vi.fn();
255
- const registry = createActionRegistry({ submit: handler });
256
- const out = renderWithHooks(() =>
257
- interactiveVocabulary.Input.render({
258
- multiline: true,
259
- name: "notes",
260
- $status: "done",
261
- $action: { type: "submit" },
262
- $dispatch: registry.dispatch,
263
- }),
264
- ) as ReactNode;
265
- const onKeyDown = (
266
- out as { props: { onKeyDown: (e: TextareaKeyDownEvent) => void } }
267
- ).props.onKeyDown;
268
-
269
- // This environment has no DOM, so `HTMLFormElement` is not a global; install a minimal stand-in so the fix's `HTMLFormElement.prototype.requestSubmit.call(...)` has a real, spyable target instead of throwing a ReferenceError.
270
- class FakeHTMLFormElement {
271
- requestSubmit(): void {}
272
- }
273
- const originalHTMLFormElement = globalThis.HTMLFormElement;
274
- globalThis.HTMLFormElement =
275
- FakeHTMLFormElement as unknown as typeof HTMLFormElement;
276
- const requestSubmitSpy = vi.spyOn(
277
- FakeHTMLFormElement.prototype,
278
- "requestSubmit",
279
- );
280
- try {
281
- const form = new FakeHTMLFormElement();
282
- const preventDefault = vi.fn();
283
- onKeyDown({
284
- key: "Enter",
285
- ctrlKey: true,
286
- metaKey: false,
287
- nativeEvent: { isComposing: false },
288
- currentTarget: { value: "typed text", form },
289
- preventDefault,
290
- });
291
- expect(preventDefault).toHaveBeenCalledTimes(1);
292
- expect(requestSubmitSpy).toHaveBeenCalledTimes(1);
293
- expect(requestSubmitSpy.mock.contexts[0]).toBe(form);
294
- expect(handler).not.toHaveBeenCalled();
295
- } finally {
296
- requestSubmitSpy.mockRestore();
297
- globalThis.HTMLFormElement = originalHTMLFormElement;
298
- }
299
- });
300
-
301
- it("Input multiline (textarea) Ctrl+Enter without a form still fires its own $action", () => {
302
- const handler = vi.fn();
303
- const registry = createActionRegistry({ submit: handler });
304
- const out = renderWithHooks(() =>
305
- interactiveVocabulary.Input.render({
306
- multiline: true,
307
- $status: "done",
308
- $action: { type: "submit" },
309
- $dispatch: registry.dispatch,
310
- }),
311
- ) as ReactNode;
312
- const onKeyDown = (
313
- out as { props: { onKeyDown: (e: TextareaKeyDownEvent) => void } }
314
- ).props.onKeyDown;
315
- const preventDefault = vi.fn();
316
- onKeyDown({
317
- key: "Enter",
318
- ctrlKey: true,
319
- metaKey: false,
320
- nativeEvent: { isComposing: false },
321
- currentTarget: { value: "typed text", form: null },
322
- preventDefault,
323
- });
324
- expect(preventDefault).not.toHaveBeenCalled();
325
- expect(handler).toHaveBeenCalledWith({
326
- payload: { type: "submit", $input: "typed text" },
327
- });
328
- });
329
-
330
- it("Button with submit set renders no onClick handler, so it never fires $action on click", () => {
331
- const handler = vi.fn();
332
- const registry = createActionRegistry({ purchase: handler });
333
- const out = renderWithHooks(() =>
334
- interactiveVocabulary.Button.render({
335
- label: "Buy",
336
- submit: true,
337
- $status: "done",
338
- $action: { type: "purchase" },
339
- $dispatch: registry.dispatch,
340
- } as ButtonRenderProps & { submit: boolean }),
341
- );
342
- expect(
343
- (out as { props: { onClick?: unknown } }).props.onClick,
344
- ).toBeUndefined();
345
- expect(handler).not.toHaveBeenCalled();
346
- });
347
-
348
- it("Button without submit keeps firing $action on click", () => {
349
- const handler = vi.fn();
350
- const registry = createActionRegistry({ purchase: handler });
351
- const out = renderWithHooks(() =>
352
- interactiveVocabulary.Button.render({
353
- label: "Buy",
354
- $status: "done",
355
- $action: { type: "purchase" },
356
- $dispatch: registry.dispatch,
357
- } as ButtonRenderProps),
358
- );
359
- const onClick = (out as { props: { onClick: (e: object) => void } }).props
360
- .onClick;
361
- onClick({ currentTarget: {} });
362
- expect(handler).toHaveBeenCalledWith({ payload: { type: "purchase" } });
363
- });
364
-
365
- it("Checkbox onChange fires $dispatch with the checked boolean as $input", () => {
366
- const handler = vi.fn();
367
- const registry = createActionRegistry({ toggle: handler });
368
- const out = renderWithHooks(() =>
369
- interactiveVocabulary.Checkbox.render({
370
- label: "Accept",
371
- $status: "done",
372
- $action: { type: "toggle" },
373
- $dispatch: registry.dispatch,
374
- }),
375
- );
376
- const input = (out.props as { children: ReactElement[] }).children[0];
377
- const onChange = (
378
- input as {
379
- props: {
380
- onChange: (e: { currentTarget: { checked: boolean } }) => void;
381
- };
382
- }
383
- ).props.onChange;
384
- onChange({ currentTarget: { checked: true } });
385
- expect(handler).toHaveBeenCalledWith({
386
- payload: { type: "toggle", $input: true },
387
- });
388
- });
389
-
390
- it("Checkbox onChange is a no-op when no $dispatch is wired", () => {
391
- const out = renderWithHooks(() =>
392
- interactiveVocabulary.Checkbox.render({
393
- label: "Accept",
394
- $status: "done",
395
- $action: { type: "toggle" },
396
- }),
397
- );
398
- const input = (out.props as { children: ReactElement[] }).children[0];
399
- const onChange = (
400
- input as {
401
- props: {
402
- onChange: (e: { currentTarget: { checked: boolean } }) => void;
403
- };
404
- }
405
- ).props.onChange;
406
- expect(() => onChange({ currentTarget: { checked: true } })).not.toThrow();
407
- });
408
-
409
- it("RadioGroup onChange fires $dispatch with the option's value as $input", () => {
410
- const handler = vi.fn();
411
- const registry = createActionRegistry({ pick: handler });
412
- const out = renderWithHooks(() =>
413
- interactiveVocabulary.RadioGroup.render({
414
- $status: "done",
415
- $action: { type: "pick" },
416
- $dispatch: registry.dispatch,
417
- options: [
418
- { label: "Small", value: "sm" },
419
- { label: "Large", value: "lg" },
420
- ],
421
- }),
422
- );
423
- const options = getRadioOptions(out);
424
- const secondRadio = (options[1]!.props as { children: ReactElement[] })
425
- .children[0] as ReactElement;
426
- const onChange = (secondRadio.props as { onChange: (e: object) => void })
427
- .onChange;
428
- onChange({ currentTarget: {} });
429
- expect(handler).toHaveBeenCalledWith({
430
- payload: { type: "pick", $input: "lg" },
431
- });
432
- });
433
-
434
- it("RadioGroup falls back to useId() for the shared radio name when name is omitted", () => {
435
- const out = renderWithHooks(() =>
436
- interactiveVocabulary.RadioGroup.render({
437
- $status: "done",
438
- options: [
439
- { label: "Small", value: "sm" },
440
- { label: "Large", value: "lg" },
441
- ],
442
- }),
443
- );
444
- const options = getRadioOptions(out);
445
- const firstRadio = (options[0]!.props as { children: ReactElement[] })
446
- .children[0] as ReactElement;
447
- const secondRadio = (options[1]!.props as { children: ReactElement[] })
448
- .children[0] as ReactElement;
449
- const firstName = (firstRadio.props as { name: string }).name;
450
- const secondName = (secondRadio.props as { name: string }).name;
451
- expect(firstName).toBeTruthy();
452
- expect(firstName).toBe(secondName);
453
- });
454
- });