@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,1465 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { UIElement } from "../ir";
3
- import {
4
- CHECKBOX_OPTION_CAP,
5
- INBOUND_BLOCK_CAP,
6
- SELECT_OPTION_CAP,
7
- } from "./constants";
8
- import { fromSlackBlocks } from "./fromSlackBlocks";
9
- import { toSlackBlocks } from "./toSlackBlocks";
10
- import type { ToSlackBlocksOptions } from "./types";
11
-
12
- describe("fromSlackBlocks", () => {
13
- describe("malformed input", () => {
14
- it("returns empty nodes and a dropped warning for non-blocks input", () => {
15
- const malformed: unknown[] = [
16
- null,
17
- undefined,
18
- 42,
19
- "just text",
20
- true,
21
- {},
22
- { blocks: "nope" },
23
- ];
24
- for (const input of malformed) {
25
- const { nodes, warnings } = fromSlackBlocks(input);
26
- expect(nodes).toEqual([]);
27
- expect(warnings).toEqual([
28
- {
29
- code: "dropped",
30
- component: "Root",
31
- detail: "Input could not be converted.",
32
- },
33
- ]);
34
- }
35
- });
36
-
37
- it("accepts a bare blocks array", () => {
38
- const { nodes } = fromSlackBlocks([{ type: "divider" }]);
39
- expect(nodes).toEqual([{ $type: "Divider" }]);
40
- });
41
-
42
- it("accepts a { blocks } wrapper", () => {
43
- const { nodes } = fromSlackBlocks({ blocks: [{ type: "divider" }] });
44
- expect(nodes).toEqual([{ $type: "Divider" }]);
45
- });
46
-
47
- it("never throws for degenerate block entries", () => {
48
- const degenerate: unknown[] = [
49
- null,
50
- undefined,
51
- 42,
52
- "x",
53
- true,
54
- [],
55
- {},
56
- () => {},
57
- ];
58
- expect(() => fromSlackBlocks(degenerate)).not.toThrow();
59
- const { nodes, warnings } = fromSlackBlocks(degenerate);
60
- expect(nodes).toEqual([]);
61
- expect(warnings).toHaveLength(degenerate.length);
62
- });
63
- });
64
-
65
- describe("bounded arrays", () => {
66
- it("clamps a hostile proxied blocks array instead of hanging", () => {
67
- const hostileArray = new Proxy([], {
68
- get(target, prop) {
69
- if (prop === "length") return 2 ** 31;
70
- if (typeof prop === "string" && /^\d+$/.test(prop)) {
71
- return { type: "divider" };
72
- }
73
- return Reflect.get(target, prop);
74
- },
75
- has(target, prop) {
76
- if (prop === "length") return true;
77
- if (typeof prop === "string" && /^\d+$/.test(prop)) return true;
78
- return Reflect.has(target, prop);
79
- },
80
- });
81
- const { nodes, warnings } = fromSlackBlocks(hostileArray);
82
- expect(nodes).toHaveLength(INBOUND_BLOCK_CAP);
83
- expect(warnings).toContainEqual({
84
- code: "clamped",
85
- component: "Root",
86
- detail: `blocks were clamped to ${INBOUND_BLOCK_CAP} entries.`,
87
- });
88
- });
89
- });
90
-
91
- describe("unknown block types", () => {
92
- it("skips an unrecognized block with a dropped warning", () => {
93
- const { nodes, warnings } = fromSlackBlocks([
94
- { type: "totally_made_up" },
95
- ]);
96
- expect(nodes).toEqual([]);
97
- expect(warnings).toEqual([
98
- {
99
- code: "dropped",
100
- component: "totally_made_up",
101
- detail: "Unknown block type was dropped.",
102
- },
103
- ]);
104
- });
105
- });
106
-
107
- describe("Header", () => {
108
- it("inverts a header block", () => {
109
- const { nodes } = fromSlackBlocks([
110
- { type: "header", text: { type: "plain_text", text: "Title" } },
111
- ]);
112
- expect(nodes).toEqual([{ $type: "Header", text: "Title" }]);
113
- });
114
- });
115
-
116
- describe("section", () => {
117
- it("inverts section fields into one Fact per field", () => {
118
- const { nodes, warnings } = fromSlackBlocks([
119
- {
120
- type: "section",
121
- fields: [
122
- { type: "mrkdwn", text: "*Status*\nActive" },
123
- { type: "mrkdwn", text: "*Owner*\nAda" },
124
- ],
125
- },
126
- ]);
127
- expect(nodes).toEqual([
128
- { $type: "Fact", label: "Status", value: "Active" },
129
- { $type: "Fact", label: "Owner", value: "Ada" },
130
- ]);
131
- expect(warnings).toEqual([]);
132
- });
133
-
134
- it("falls back to a value-only Fact when a field does not match the label/value format", () => {
135
- const { nodes, warnings } = fromSlackBlocks([
136
- {
137
- type: "section",
138
- fields: [{ type: "mrkdwn", text: "no format here" }],
139
- },
140
- ]);
141
- expect(nodes).toEqual([
142
- { $type: "Fact", label: "", value: "no format here" },
143
- ]);
144
- expect(warnings).toEqual([
145
- {
146
- code: "fallback",
147
- component: "Fact",
148
- detail:
149
- "A section field that did not match the label/value format was kept as a value-only fact.",
150
- },
151
- ]);
152
- });
153
-
154
- it("inverts a section with mrkdwn text and a button accessory into a ListView of one ListViewItem", () => {
155
- const { nodes } = fromSlackBlocks([
156
- {
157
- type: "section",
158
- text: { type: "mrkdwn", text: "Clickable" },
159
- accessory: {
160
- type: "button",
161
- text: { type: "plain_text", text: "Open" },
162
- action_id: "open_row",
163
- },
164
- },
165
- ]);
166
- expect(nodes).toEqual([
167
- {
168
- $type: "ListView",
169
- children: [
170
- {
171
- $type: "ListViewItem",
172
- $action: { type: "open_row" },
173
- children: { $type: "Text", value: "Clickable" },
174
- },
175
- ],
176
- },
177
- ]);
178
- });
179
-
180
- it("inverts a plain mrkdwn section into Text", () => {
181
- const { nodes } = fromSlackBlocks([
182
- { type: "section", text: { type: "mrkdwn", text: "Hello" } },
183
- ]);
184
- expect(nodes).toEqual([{ $type: "Text", value: "Hello" }]);
185
- });
186
- });
187
-
188
- describe("context", () => {
189
- it("inverts a single-element context into a Caption", () => {
190
- const { nodes } = fromSlackBlocks([
191
- { type: "context", elements: [{ type: "mrkdwn", text: "Fine print" }] },
192
- ]);
193
- expect(nodes).toEqual([{ $type: "Caption", value: "Fine print" }]);
194
- });
195
-
196
- it("inverts a multi-element context into a Row of Captions, collapsing the Badge/Caption distinction", () => {
197
- const { nodes } = fromSlackBlocks([
198
- {
199
- type: "context",
200
- elements: [
201
- { type: "mrkdwn", text: "New" },
202
- { type: "mrkdwn", text: "since today" },
203
- ],
204
- },
205
- ]);
206
- expect(nodes).toEqual([
207
- {
208
- $type: "Row",
209
- children: [
210
- { $type: "Caption", value: "New" },
211
- { $type: "Caption", value: "since today" },
212
- ],
213
- },
214
- ]);
215
- });
216
- });
217
-
218
- describe("Image", () => {
219
- it("inverts an image block", () => {
220
- const { nodes } = fromSlackBlocks([
221
- { type: "image", image_url: "https://x/y.png", alt_text: "A photo" },
222
- ]);
223
- expect(nodes).toEqual([
224
- { $type: "Image", src: "https://x/y.png", alt: "A photo" },
225
- ]);
226
- });
227
- });
228
-
229
- describe("Divider", () => {
230
- it("inverts a divider block", () => {
231
- const { nodes } = fromSlackBlocks([{ type: "divider" }]);
232
- expect(nodes).toEqual([{ $type: "Divider" }]);
233
- });
234
- });
235
-
236
- describe("actions", () => {
237
- it("inverts a button, mapping style to buttonStyle only for primary/danger and deriving $action from the object-only value payload", () => {
238
- const { nodes: primary } = fromSlackBlocks([
239
- {
240
- type: "actions",
241
- elements: [
242
- {
243
- type: "button",
244
- text: { type: "plain_text", text: "Buy" },
245
- action_id: "buy",
246
- value: JSON.stringify({ sku: "abc" }),
247
- style: "primary",
248
- },
249
- ],
250
- },
251
- ]);
252
- expect(primary).toEqual([
253
- {
254
- $type: "Button",
255
- label: "Buy",
256
- buttonStyle: "primary",
257
- $action: { type: "buy", sku: "abc" },
258
- },
259
- ]);
260
-
261
- const { nodes: danger } = fromSlackBlocks([
262
- {
263
- type: "actions",
264
- elements: [
265
- {
266
- type: "button",
267
- text: { type: "plain_text", text: "Delete" },
268
- action_id: "del",
269
- style: "danger",
270
- },
271
- ],
272
- },
273
- ]);
274
- expect((danger[0] as UIElement)["buttonStyle"]).toBe("danger");
275
-
276
- const { nodes: plain } = fromSlackBlocks([
277
- {
278
- type: "actions",
279
- elements: [
280
- {
281
- type: "button",
282
- text: { type: "plain_text", text: "Go" },
283
- action_id: "go",
284
- },
285
- ],
286
- },
287
- ]);
288
- expect(plain[0]).not.toHaveProperty("buttonStyle");
289
- expect(plain[0]).toEqual({
290
- $type: "Button",
291
- label: "Go",
292
- $action: { type: "go" },
293
- });
294
- });
295
-
296
- it("inverts a static_select", () => {
297
- const { nodes } = fromSlackBlocks([
298
- {
299
- type: "actions",
300
- elements: [
301
- {
302
- type: "static_select",
303
- action_id: "pick",
304
- options: [
305
- { text: { type: "plain_text", text: "Alpha" }, value: "a" },
306
- { text: { type: "plain_text", text: "Beta" }, value: "b" },
307
- ],
308
- placeholder: { type: "plain_text", text: "Pick one" },
309
- },
310
- ],
311
- },
312
- ]);
313
- expect(nodes).toEqual([
314
- {
315
- $type: "Select",
316
- options: [
317
- { label: "Alpha", value: "a" },
318
- { label: "Beta", value: "b" },
319
- ],
320
- placeholder: "Pick one",
321
- $action: { type: "pick" },
322
- },
323
- ]);
324
- });
325
-
326
- it("reads defaultValue from a matching static_select initial_option", () => {
327
- const { nodes } = fromSlackBlocks([
328
- {
329
- type: "actions",
330
- elements: [
331
- {
332
- type: "static_select",
333
- action_id: "pick",
334
- options: [
335
- { text: { type: "plain_text", text: "Alpha" }, value: "a" },
336
- { text: { type: "plain_text", text: "Beta" }, value: "b" },
337
- ],
338
- initial_option: {
339
- text: { type: "plain_text", text: "Beta" },
340
- value: "b",
341
- },
342
- },
343
- ],
344
- },
345
- ]);
346
- expect(nodes[0]).toMatchObject({
347
- $type: "Select",
348
- defaultValue: "b",
349
- });
350
- });
351
-
352
- it("omits defaultValue when static_select initial_option is not an option", () => {
353
- const { nodes } = fromSlackBlocks([
354
- {
355
- type: "actions",
356
- elements: [
357
- {
358
- type: "static_select",
359
- action_id: "pick",
360
- options: [
361
- { text: { type: "plain_text", text: "Alpha" }, value: "a" },
362
- ],
363
- initial_option: {
364
- text: { type: "plain_text", text: "Missing" },
365
- value: "missing",
366
- },
367
- },
368
- ],
369
- },
370
- ]);
371
- expect(nodes[0]).not.toHaveProperty("defaultValue");
372
- });
373
-
374
- it("inverts a datepicker, reading value from initial_date and omitting it when absent", () => {
375
- const { nodes: withDate } = fromSlackBlocks([
376
- {
377
- type: "actions",
378
- elements: [
379
- {
380
- type: "datepicker",
381
- action_id: "pick_date",
382
- initial_date: "2026-07-15",
383
- },
384
- ],
385
- },
386
- ]);
387
- expect(withDate).toEqual([
388
- {
389
- $type: "DatePicker",
390
- value: "2026-07-15",
391
- $action: { type: "pick_date" },
392
- },
393
- ]);
394
-
395
- const { nodes: withoutDate } = fromSlackBlocks([
396
- {
397
- type: "actions",
398
- elements: [{ type: "datepicker", action_id: "pick_date" }],
399
- },
400
- ]);
401
- expect(withoutDate[0]).not.toHaveProperty("value");
402
- });
403
-
404
- it("inverts checkboxes, reading label/name from the first option and defaultChecked from initial_options", () => {
405
- const { nodes: checked } = fromSlackBlocks([
406
- {
407
- type: "actions",
408
- elements: [
409
- {
410
- type: "checkboxes",
411
- action_id: "toggle",
412
- options: [
413
- { text: { type: "plain_text", text: "Agree" }, value: "agree" },
414
- ],
415
- initial_options: [
416
- { text: { type: "plain_text", text: "Agree" }, value: "agree" },
417
- ],
418
- },
419
- ],
420
- },
421
- ]);
422
- expect(checked).toEqual([
423
- {
424
- $type: "Checkbox",
425
- label: "Agree",
426
- name: "agree",
427
- defaultChecked: true,
428
- $action: { type: "toggle" },
429
- },
430
- ]);
431
-
432
- const { nodes: unchecked } = fromSlackBlocks([
433
- {
434
- type: "actions",
435
- elements: [
436
- {
437
- type: "checkboxes",
438
- action_id: "toggle",
439
- options: [
440
- { text: { type: "plain_text", text: "Agree" }, value: "agree" },
441
- ],
442
- },
443
- ],
444
- },
445
- ]);
446
- expect(unchecked[0]).not.toHaveProperty("defaultChecked");
447
- });
448
-
449
- it("inverts multi-option checkboxes into CheckboxGroup with matching initial options", () => {
450
- const { nodes } = fromSlackBlocks([
451
- {
452
- type: "actions",
453
- elements: [
454
- {
455
- type: "checkboxes",
456
- action_id: "sizes",
457
- value: '{"source":"picker"}',
458
- options: [
459
- { text: { type: "plain_text", text: "Small" }, value: "s" },
460
- { text: { type: "plain_text", text: "Large" }, value: "l" },
461
- ],
462
- initial_options: [
463
- { text: { type: "plain_text", text: "Large" }, value: "l" },
464
- {
465
- text: { type: "plain_text", text: "Missing" },
466
- value: "missing",
467
- },
468
- ],
469
- },
470
- ],
471
- },
472
- ]);
473
- expect(nodes).toEqual([
474
- {
475
- $type: "CheckboxGroup",
476
- options: [
477
- { label: "Small", value: "s" },
478
- { label: "Large", value: "l" },
479
- ],
480
- defaultValue: ["l"],
481
- $action: { type: "sizes", source: "picker" },
482
- },
483
- ]);
484
- });
485
-
486
- it("drops malformed CheckboxGroup options", () => {
487
- const { nodes } = fromSlackBlocks([
488
- {
489
- type: "actions",
490
- elements: [
491
- {
492
- type: "checkboxes",
493
- action_id: "sizes",
494
- options: [
495
- { text: { type: "plain_text", text: "Small" }, value: "s" },
496
- { text: { type: "plain_text", text: "Missing value" } },
497
- ],
498
- },
499
- ],
500
- },
501
- ]);
502
- expect(nodes).toEqual([
503
- {
504
- $type: "CheckboxGroup",
505
- options: [{ label: "Small", value: "s" }],
506
- $action: { type: "sizes" },
507
- },
508
- ]);
509
- });
510
-
511
- it("inverts radio_buttons, reading defaultValue from initial_option and omitting it when absent", () => {
512
- const { nodes: withValue } = fromSlackBlocks([
513
- {
514
- type: "actions",
515
- elements: [
516
- {
517
- type: "radio_buttons",
518
- action_id: "size",
519
- options: [
520
- { text: { type: "plain_text", text: "Small" }, value: "s" },
521
- { text: { type: "plain_text", text: "Large" }, value: "l" },
522
- ],
523
- initial_option: {
524
- text: { type: "plain_text", text: "Large" },
525
- value: "l",
526
- },
527
- },
528
- ],
529
- },
530
- ]);
531
- expect(withValue).toEqual([
532
- {
533
- $type: "RadioGroup",
534
- options: [
535
- { label: "Small", value: "s" },
536
- { label: "Large", value: "l" },
537
- ],
538
- defaultValue: "l",
539
- $action: { type: "size" },
540
- },
541
- ]);
542
-
543
- const { nodes: withoutValue } = fromSlackBlocks([
544
- {
545
- type: "actions",
546
- elements: [
547
- {
548
- type: "radio_buttons",
549
- action_id: "size",
550
- options: [
551
- { text: { type: "plain_text", text: "Small" }, value: "s" },
552
- ],
553
- },
554
- ],
555
- },
556
- ]);
557
- expect(withoutValue[0]).not.toHaveProperty("defaultValue");
558
- });
559
-
560
- it("skips an unrecognized action element with a dropped warning, keeping the rest", () => {
561
- const { nodes, warnings } = fromSlackBlocks([
562
- {
563
- type: "actions",
564
- elements: [
565
- { type: "mystery_widget" },
566
- {
567
- type: "button",
568
- text: { type: "plain_text", text: "Go" },
569
- action_id: "go",
570
- },
571
- ],
572
- },
573
- ]);
574
- expect(nodes).toEqual([
575
- { $type: "Button", label: "Go", $action: { type: "go" } },
576
- ]);
577
- expect(warnings).toContainEqual({
578
- code: "dropped",
579
- component: "mystery_widget",
580
- detail: "Unknown action element type was dropped.",
581
- });
582
- });
583
- });
584
-
585
- describe("Input", () => {
586
- it("inverts an input block, passing multiline and placeholder through", () => {
587
- const { nodes } = fromSlackBlocks([
588
- {
589
- type: "input",
590
- label: { type: "plain_text", text: "Notes" },
591
- element: {
592
- type: "plain_text_input",
593
- action_id: "notes",
594
- multiline: true,
595
- placeholder: { type: "plain_text", text: "Type here" },
596
- },
597
- },
598
- ]);
599
- expect(nodes).toEqual([
600
- {
601
- $type: "Input",
602
- label: "Notes",
603
- placeholder: "Type here",
604
- multiline: true,
605
- $action: { type: "notes" },
606
- },
607
- ]);
608
- });
609
-
610
- it("omits multiline when not set", () => {
611
- const { nodes } = fromSlackBlocks([
612
- {
613
- type: "input",
614
- label: { type: "plain_text", text: "Name" },
615
- element: { type: "plain_text_input", action_id: "name" },
616
- },
617
- ]);
618
- expect(nodes[0]).not.toHaveProperty("multiline");
619
- });
620
-
621
- it("reads defaultValue from a string initial_value, including empty text", () => {
622
- const withText = fromSlackBlocks([
623
- {
624
- type: "input",
625
- label: { type: "plain_text", text: "Notes" },
626
- element: {
627
- type: "plain_text_input",
628
- action_id: "notes",
629
- initial_value: "Draft reply",
630
- },
631
- },
632
- ]);
633
- const withEmptyText = fromSlackBlocks([
634
- {
635
- type: "input",
636
- label: { type: "plain_text", text: "Notes" },
637
- element: {
638
- type: "plain_text_input",
639
- action_id: "notes",
640
- initial_value: "",
641
- },
642
- },
643
- ]);
644
- expect(withText.nodes[0]).toMatchObject({ defaultValue: "Draft reply" });
645
- expect(withEmptyText.nodes[0]).toMatchObject({ defaultValue: "" });
646
- });
647
-
648
- it("drops an input block wrapping an unrecognized element", () => {
649
- const { nodes, warnings } = fromSlackBlocks([
650
- {
651
- type: "input",
652
- label: { type: "plain_text", text: "Name" },
653
- element: { type: "mystery" },
654
- },
655
- ]);
656
- expect(nodes).toEqual([]);
657
- expect(warnings).toContainEqual({
658
- code: "dropped",
659
- component: "Input",
660
- detail: "Unknown input element type was dropped.",
661
- });
662
- });
663
- });
664
-
665
- describe("Markdown", () => {
666
- it("inverts a markdown block", () => {
667
- const { nodes } = fromSlackBlocks([
668
- { type: "markdown", text: "# Title" },
669
- ]);
670
- expect(nodes).toEqual([{ $type: "Markdown", value: "# Title" }]);
671
- });
672
- });
673
-
674
- describe("Card", () => {
675
- it("inverts a clean card with hero image, body, subtext, and confirm/cancel actions", () => {
676
- const { nodes, warnings } = fromSlackBlocks([
677
- {
678
- type: "card",
679
- hero_image: {
680
- type: "image",
681
- image_url: "https://x/y.png",
682
- alt_text: "Package",
683
- },
684
- title: { type: "mrkdwn", text: "Order #42" },
685
- body: { type: "mrkdwn", text: "Shipped" },
686
- subtext: { type: "mrkdwn", text: "Updated just now" },
687
- actions: [
688
- {
689
- type: "button",
690
- text: { type: "plain_text", text: "Ship" },
691
- action_id: "ship",
692
- style: "primary",
693
- },
694
- {
695
- type: "button",
696
- text: { type: "plain_text", text: "Cancel" },
697
- action_id: "cancel",
698
- },
699
- ],
700
- },
701
- ]);
702
- expect(nodes).toEqual([
703
- {
704
- $type: "Card",
705
- title: "Order #42",
706
- children: [
707
- { $type: "Image", src: "https://x/y.png", alt: "Package" },
708
- { $type: "Text", value: "Shipped" },
709
- { $type: "Caption", value: "Updated just now" },
710
- ],
711
- confirm: { label: "Ship", $action: { type: "ship" } },
712
- cancel: { label: "Cancel", $action: { type: "cancel" } },
713
- },
714
- ]);
715
- expect(warnings).toEqual([]);
716
- });
717
-
718
- it("appends card actions beyond confirm/cancel as Button children with a fallback warning", () => {
719
- const { nodes, warnings } = fromSlackBlocks([
720
- {
721
- type: "card",
722
- title: { type: "mrkdwn", text: "Order" },
723
- actions: [
724
- {
725
- type: "button",
726
- text: { type: "plain_text", text: "Yes" },
727
- action_id: "yes",
728
- },
729
- {
730
- type: "button",
731
- text: { type: "plain_text", text: "No" },
732
- action_id: "no",
733
- },
734
- {
735
- type: "button",
736
- text: { type: "plain_text", text: "Maybe" },
737
- action_id: "maybe",
738
- },
739
- ],
740
- },
741
- ]);
742
- const card = nodes[0] as UIElement;
743
- expect(card["children"]).toEqual([
744
- { $type: "Button", label: "Maybe", $action: { type: "maybe" } },
745
- ]);
746
- expect(warnings).toContainEqual({
747
- code: "fallback",
748
- component: "Card",
749
- detail:
750
- "Card actions beyond confirm and cancel were appended as Button children.",
751
- });
752
- });
753
-
754
- it("reconstructs a single non-primary button as cancel, not confirm", () => {
755
- const { nodes, warnings } = fromSlackBlocks([
756
- {
757
- type: "card",
758
- title: { type: "mrkdwn", text: "Order" },
759
- actions: [
760
- {
761
- type: "button",
762
- text: { type: "plain_text", text: "Dismiss" },
763
- action_id: "dismiss",
764
- },
765
- ],
766
- },
767
- ]);
768
- expect(nodes).toEqual([
769
- {
770
- $type: "Card",
771
- title: "Order",
772
- cancel: { label: "Dismiss", $action: { type: "dismiss" } },
773
- },
774
- ]);
775
- expect(nodes[0]).not.toHaveProperty("confirm");
776
- expect(warnings).toEqual([]);
777
- });
778
- });
779
-
780
- describe("Alert", () => {
781
- it("maps alert level to tone, mapping error to danger and omitting tone for the default level", () => {
782
- const cases: readonly [string, string | undefined][] = [
783
- ["error", "danger"],
784
- ["success", "success"],
785
- ["warning", "warning"],
786
- ["info", "info"],
787
- ["default", undefined],
788
- ];
789
- for (const [level, tone] of cases) {
790
- const { nodes } = fromSlackBlocks([
791
- { type: "alert", text: { type: "mrkdwn", text: "x" }, level },
792
- ]);
793
- expect(nodes).toEqual([
794
- {
795
- $type: "Alert",
796
- description: "x",
797
- ...(tone !== undefined ? { tone } : {}),
798
- },
799
- ]);
800
- }
801
- });
802
-
803
- it("keeps a joined title and description together in description, since the two cannot be separated", () => {
804
- const { nodes } = fromSlackBlocks([
805
- {
806
- type: "alert",
807
- text: { type: "mrkdwn", text: "Heads up\nSomething happened." },
808
- level: "info",
809
- },
810
- ]);
811
- expect(nodes).toEqual([
812
- {
813
- $type: "Alert",
814
- description: "Heads up\nSomething happened.",
815
- tone: "info",
816
- },
817
- ]);
818
- });
819
- });
820
-
821
- describe("Carousel", () => {
822
- it("inverts a carousel's card elements", () => {
823
- const { nodes } = fromSlackBlocks([
824
- {
825
- type: "carousel",
826
- elements: [
827
- { type: "card", title: { type: "mrkdwn", text: "One" } },
828
- { type: "card", title: { type: "mrkdwn", text: "Two" } },
829
- ],
830
- },
831
- ]);
832
- expect(nodes).toEqual([
833
- {
834
- $type: "Carousel",
835
- children: [
836
- { $type: "Card", title: "One" },
837
- { $type: "Card", title: "Two" },
838
- ],
839
- },
840
- ]);
841
- });
842
- });
843
-
844
- describe("Table", () => {
845
- it("inverts columns and typed rows", () => {
846
- const { nodes } = fromSlackBlocks([
847
- {
848
- type: "data_table",
849
- caption: "Table",
850
- rows: [
851
- [
852
- { type: "raw_text", text: "Name" },
853
- { type: "raw_text", text: "Age" },
854
- ],
855
- [
856
- { type: "raw_text", text: "Ada" },
857
- { type: "raw_number", text: "36" },
858
- ],
859
- ],
860
- },
861
- ]);
862
- expect(nodes).toEqual([
863
- {
864
- $type: "Table",
865
- columns: [{ label: "Name" }, { label: "Age" }],
866
- rows: [["Ada", 36]],
867
- },
868
- ]);
869
- });
870
-
871
- it("omits columns when every header cell is empty, inverting the rows-only synthesis", () => {
872
- const { nodes } = fromSlackBlocks([
873
- {
874
- type: "data_table",
875
- caption: "Table",
876
- rows: [
877
- [
878
- { type: "raw_text", text: "" },
879
- { type: "raw_text", text: "" },
880
- ],
881
- [
882
- { type: "raw_text", text: "Ada" },
883
- { type: "raw_number", text: "36" },
884
- ],
885
- ],
886
- },
887
- ]);
888
- expect(nodes).toEqual([{ $type: "Table", rows: [["Ada", 36]] }]);
889
- });
890
-
891
- it("keeps a raw_number cell as its raw string when the text is not a finite number", () => {
892
- const { nodes } = fromSlackBlocks([
893
- {
894
- type: "data_table",
895
- caption: "Table",
896
- rows: [
897
- [{ type: "raw_text", text: "Name" }],
898
- [{ type: "raw_number", text: "N/A" }],
899
- ],
900
- },
901
- ]);
902
- expect(nodes).toEqual([
903
- { $type: "Table", columns: [{ label: "Name" }], rows: [["N/A"]] },
904
- ]);
905
- });
906
- });
907
-
908
- describe("round trip", () => {
909
- it("keeps Input and Select defaultValue props", () => {
910
- const tree = [
911
- {
912
- $type: "Input",
913
- label: "Notes",
914
- defaultValue: "Draft reply",
915
- $action: { type: "notes" },
916
- },
917
- {
918
- $type: "Select",
919
- options: [
920
- { label: "Alpha", value: "a" },
921
- { label: "Beta", value: "b" },
922
- ],
923
- defaultValue: "b",
924
- $action: { type: "pick" },
925
- },
926
- ];
927
- expect(fromSlackBlocks(toSlackBlocks(tree).blocks).nodes).toEqual(tree);
928
- });
929
-
930
- const fixtures: readonly {
931
- readonly name: string;
932
- readonly tree: unknown;
933
- readonly options?: ToSlackBlocksOptions;
934
- readonly expected: readonly UIElement[];
935
- }[] = [
936
- {
937
- name: "Header",
938
- tree: { $type: "Header", text: "Title" },
939
- expected: [{ $type: "Header", text: "Title" }],
940
- },
941
- {
942
- name: "Text",
943
- tree: { $type: "Text", value: "Hello" },
944
- expected: [{ $type: "Text", value: "Hello" }],
945
- },
946
- {
947
- name: "Caption",
948
- tree: { $type: "Caption", value: "Fine print" },
949
- expected: [{ $type: "Caption", value: "Fine print" }],
950
- },
951
- {
952
- name: "Image",
953
- tree: { $type: "Image", src: "https://x/y.png", alt: "A photo" },
954
- expected: [{ $type: "Image", src: "https://x/y.png", alt: "A photo" }],
955
- },
956
- {
957
- name: "Divider",
958
- tree: { $type: "Divider" },
959
- expected: [{ $type: "Divider" }],
960
- },
961
- {
962
- name: "Markdown",
963
- tree: { $type: "Markdown", value: "# Title" },
964
- expected: [{ $type: "Markdown", value: "# Title" }],
965
- },
966
- {
967
- name: "Button with payload",
968
- tree: {
969
- $type: "Button",
970
- label: "Buy",
971
- buttonStyle: "primary",
972
- $action: { type: "buy", sku: "abc" },
973
- },
974
- expected: [
975
- {
976
- $type: "Button",
977
- label: "Buy",
978
- buttonStyle: "primary",
979
- $action: { type: "buy", sku: "abc" },
980
- },
981
- ],
982
- },
983
- {
984
- name: "Select",
985
- tree: {
986
- $type: "Select",
987
- options: [
988
- { label: "Alpha", value: "a" },
989
- { label: "Beta", value: "b" },
990
- ],
991
- placeholder: "Pick one",
992
- $action: { type: "pick" },
993
- },
994
- expected: [
995
- {
996
- $type: "Select",
997
- options: [
998
- { label: "Alpha", value: "a" },
999
- { label: "Beta", value: "b" },
1000
- ],
1001
- placeholder: "Pick one",
1002
- $action: { type: "pick" },
1003
- },
1004
- ],
1005
- },
1006
- {
1007
- name: "DatePicker with initial date",
1008
- tree: {
1009
- $type: "DatePicker",
1010
- value: "2026-07-15",
1011
- $action: { type: "pick_date" },
1012
- },
1013
- expected: [
1014
- {
1015
- $type: "DatePicker",
1016
- value: "2026-07-15",
1017
- $action: { type: "pick_date" },
1018
- },
1019
- ],
1020
- },
1021
- {
1022
- name: "Checkbox with defaultChecked",
1023
- tree: {
1024
- $type: "Checkbox",
1025
- label: "Agree",
1026
- name: "agree",
1027
- defaultChecked: true,
1028
- $action: { type: "toggle" },
1029
- },
1030
- expected: [
1031
- {
1032
- $type: "Checkbox",
1033
- label: "Agree",
1034
- name: "agree",
1035
- defaultChecked: true,
1036
- $action: { type: "toggle" },
1037
- },
1038
- ],
1039
- },
1040
- {
1041
- name: "RadioGroup with defaultValue",
1042
- tree: {
1043
- $type: "RadioGroup",
1044
- options: [
1045
- { label: "Small", value: "s" },
1046
- { label: "Large", value: "l" },
1047
- ],
1048
- defaultValue: "l",
1049
- $action: { type: "size" },
1050
- },
1051
- expected: [
1052
- {
1053
- $type: "RadioGroup",
1054
- options: [
1055
- { label: "Small", value: "s" },
1056
- { label: "Large", value: "l" },
1057
- ],
1058
- defaultValue: "l",
1059
- $action: { type: "size" },
1060
- },
1061
- ],
1062
- },
1063
- {
1064
- name: "CheckboxGroup with defaultValue",
1065
- tree: {
1066
- $type: "CheckboxGroup",
1067
- options: [
1068
- { label: "Small", value: "s" },
1069
- { label: "Large", value: "l" },
1070
- ],
1071
- defaultValue: ["s", "l"],
1072
- $action: { type: "sizes" },
1073
- },
1074
- expected: [
1075
- {
1076
- $type: "CheckboxGroup",
1077
- options: [
1078
- { label: "Small", value: "s" },
1079
- { label: "Large", value: "l" },
1080
- ],
1081
- defaultValue: ["s", "l"],
1082
- $action: { type: "sizes" },
1083
- },
1084
- ],
1085
- },
1086
- {
1087
- name: "Input",
1088
- tree: {
1089
- $type: "Input",
1090
- label: "Notes",
1091
- placeholder: "Type here",
1092
- multiline: true,
1093
- $action: { type: "notes" },
1094
- },
1095
- expected: [
1096
- {
1097
- $type: "Input",
1098
- label: "Notes",
1099
- placeholder: "Type here",
1100
- multiline: true,
1101
- $action: { type: "notes" },
1102
- },
1103
- ],
1104
- },
1105
- {
1106
- name: "Fact pair",
1107
- tree: [
1108
- { $type: "Fact", label: "Status", value: "Active" },
1109
- { $type: "Fact", label: "Owner", value: "Ada" },
1110
- ],
1111
- expected: [
1112
- { $type: "Fact", label: "Status", value: "Active" },
1113
- { $type: "Fact", label: "Owner", value: "Ada" },
1114
- ],
1115
- },
1116
- {
1117
- name: "simple Card with title and body text",
1118
- tree: {
1119
- $type: "Card",
1120
- title: "Order #42",
1121
- children: [{ $type: "Text", value: "Shipped" }],
1122
- },
1123
- expected: [
1124
- {
1125
- $type: "Card",
1126
- title: "Order #42",
1127
- children: [{ $type: "Text", value: "Shipped" }],
1128
- },
1129
- ],
1130
- },
1131
- {
1132
- name: "Alert (modal surface, where the native alert block round-trips; the message-surface fallback is not invertible into an Alert)",
1133
- tree: {
1134
- $type: "Alert",
1135
- description: "Something happened.",
1136
- tone: "danger",
1137
- },
1138
- options: { surface: "modal" },
1139
- expected: [
1140
- {
1141
- $type: "Alert",
1142
- description: "Something happened.",
1143
- tone: "danger",
1144
- },
1145
- ],
1146
- },
1147
- {
1148
- name: "Carousel of two simple Cards",
1149
- tree: {
1150
- $type: "Carousel",
1151
- children: [
1152
- {
1153
- $type: "Card",
1154
- title: "One",
1155
- children: [{ $type: "Text", value: "First" }],
1156
- },
1157
- {
1158
- $type: "Card",
1159
- title: "Two",
1160
- children: [{ $type: "Text", value: "Second" }],
1161
- },
1162
- ],
1163
- },
1164
- expected: [
1165
- {
1166
- $type: "Carousel",
1167
- children: [
1168
- {
1169
- $type: "Card",
1170
- title: "One",
1171
- children: [{ $type: "Text", value: "First" }],
1172
- },
1173
- {
1174
- $type: "Card",
1175
- title: "Two",
1176
- children: [{ $type: "Text", value: "Second" }],
1177
- },
1178
- ],
1179
- },
1180
- ],
1181
- },
1182
- {
1183
- name: "ListView of two actionable items",
1184
- tree: {
1185
- $type: "ListView",
1186
- children: [
1187
- {
1188
- $type: "ListViewItem",
1189
- $action: { type: "open_one" },
1190
- children: { $type: "Text", value: "Row one" },
1191
- },
1192
- {
1193
- $type: "ListViewItem",
1194
- $action: { type: "open_two" },
1195
- children: { $type: "Text", value: "Row two" },
1196
- },
1197
- ],
1198
- },
1199
- expected: [
1200
- {
1201
- $type: "ListView",
1202
- children: [
1203
- {
1204
- $type: "ListViewItem",
1205
- $action: { type: "open_one" },
1206
- children: { $type: "Text", value: "Row one" },
1207
- },
1208
- {
1209
- $type: "ListViewItem",
1210
- $action: { type: "open_two" },
1211
- children: { $type: "Text", value: "Row two" },
1212
- },
1213
- ],
1214
- },
1215
- ],
1216
- },
1217
- {
1218
- name: "Table with columns and mixed string/number rows",
1219
- tree: {
1220
- $type: "Table",
1221
- columns: [{ label: "Name" }, { label: "Age" }],
1222
- rows: [
1223
- ["Ada", 36],
1224
- ["Bob", 24],
1225
- ],
1226
- },
1227
- expected: [
1228
- {
1229
- $type: "Table",
1230
- columns: [{ label: "Name" }, { label: "Age" }],
1231
- rows: [
1232
- ["Ada", 36],
1233
- ["Bob", 24],
1234
- ],
1235
- },
1236
- ],
1237
- },
1238
- ];
1239
-
1240
- for (const fixture of fixtures) {
1241
- it(`round-trips ${fixture.name}`, () => {
1242
- const { blocks } = toSlackBlocks(fixture.tree, fixture.options);
1243
- const { nodes } = fromSlackBlocks(blocks);
1244
- expect(nodes).toEqual(fixture.expected);
1245
- });
1246
- }
1247
- });
1248
- });
1249
-
1250
- describe("fromSlackBlocks checkbox and radio caps", () => {
1251
- it("marks defaultChecked only when its single option is initially selected", () => {
1252
- const base = {
1253
- type: "actions",
1254
- elements: [
1255
- {
1256
- type: "checkboxes",
1257
- action_id: "toggle",
1258
- options: [
1259
- { text: { type: "plain_text", text: "First" }, value: "first" },
1260
- ],
1261
- initial_options: [
1262
- { text: { type: "plain_text", text: "Second" }, value: "second" },
1263
- ],
1264
- },
1265
- ],
1266
- };
1267
- const { nodes } = fromSlackBlocks([base]);
1268
- expect(nodes[0]).not.toHaveProperty("defaultChecked");
1269
-
1270
- const checkedFirst = structuredClone(base);
1271
- checkedFirst.elements[0]!.initial_options = [
1272
- { text: { type: "plain_text", text: "First" }, value: "first" },
1273
- ];
1274
- const checked = fromSlackBlocks([checkedFirst]);
1275
- expect(checked.nodes[0]).toMatchObject({ defaultChecked: true });
1276
- });
1277
-
1278
- it("clamps inbound radio options to the radio cap", () => {
1279
- const { nodes, warnings } = fromSlackBlocks([
1280
- {
1281
- type: "actions",
1282
- elements: [
1283
- {
1284
- type: "radio_buttons",
1285
- action_id: "pick",
1286
- options: Array.from({ length: 30 }, (_, i) => ({
1287
- text: { type: "plain_text", text: `O${i}` },
1288
- value: `v${i}`,
1289
- })),
1290
- },
1291
- ],
1292
- },
1293
- ]);
1294
- const radio = nodes[0];
1295
- if (radio?.$type !== "RadioGroup" || !Array.isArray(radio.options)) {
1296
- throw new Error("Expected a RadioGroup with options.");
1297
- }
1298
- expect(radio.options).toHaveLength(10);
1299
- expect(
1300
- warnings.some(
1301
- (warning) =>
1302
- warning.component === "RadioGroup" && warning.code === "clamped",
1303
- ),
1304
- ).toBe(true);
1305
- });
1306
-
1307
- it("clamps inbound CheckboxGroup options to the checkbox cap", () => {
1308
- const { nodes, warnings } = fromSlackBlocks([
1309
- {
1310
- type: "actions",
1311
- elements: [
1312
- {
1313
- type: "checkboxes",
1314
- action_id: "sizes",
1315
- options: Array.from(
1316
- { length: CHECKBOX_OPTION_CAP + 3 },
1317
- (_, i) => ({
1318
- text: { type: "plain_text", text: `O${i}` },
1319
- value: `v${i}`,
1320
- }),
1321
- ),
1322
- },
1323
- ],
1324
- },
1325
- ]);
1326
- const checkboxGroup = nodes[0];
1327
- if (
1328
- checkboxGroup?.$type !== "CheckboxGroup" ||
1329
- !Array.isArray(checkboxGroup.options)
1330
- ) {
1331
- throw new Error("Expected a CheckboxGroup with options.");
1332
- }
1333
- expect(checkboxGroup.options).toHaveLength(CHECKBOX_OPTION_CAP);
1334
- expect(
1335
- warnings.some(
1336
- (warning) =>
1337
- warning.component === "CheckboxGroup" && warning.code === "clamped",
1338
- ),
1339
- ).toBe(true);
1340
- });
1341
- });
1342
-
1343
- it("bounds static-select options when an array replaces its slice", () => {
1344
- const hostile = new Proxy(
1345
- Array.from({ length: SELECT_OPTION_CAP + 1 }, (_, index) => ({
1346
- text: { type: "plain_text", text: `Option ${index}` },
1347
- value: `value-${index}`,
1348
- })),
1349
- {
1350
- get(target, prop, receiver) {
1351
- if (prop === "slice") {
1352
- return () =>
1353
- Array.from({ length: SELECT_OPTION_CAP * 2 }, () => ({
1354
- text: { type: "plain_text", text: "Injected" },
1355
- value: "injected",
1356
- }));
1357
- }
1358
- return Reflect.get(target, prop, receiver);
1359
- },
1360
- },
1361
- );
1362
-
1363
- const { nodes } = fromSlackBlocks([
1364
- {
1365
- type: "actions",
1366
- elements: [
1367
- { type: "static_select", action_id: "pick", options: hostile },
1368
- ],
1369
- },
1370
- ]);
1371
- const select = nodes[0] as unknown as { options: unknown[] };
1372
-
1373
- expect(select.options).toHaveLength(SELECT_OPTION_CAP);
1374
- expect(select.options[0]).toEqual({ label: "Option 0", value: "value-0" });
1375
- expect(select.options.at(-1)).toEqual({
1376
- label: `Option ${SELECT_OPTION_CAP - 1}`,
1377
- value: `value-${SELECT_OPTION_CAP - 1}`,
1378
- });
1379
- });
1380
-
1381
- it("skips a hole in a section fields array instead of decoding a blank fact", () => {
1382
- const fields: unknown[] = [];
1383
- fields[0] = { type: "mrkdwn", text: "*A*\nyes" };
1384
- fields[2] = { type: "mrkdwn", text: "*C*\nyes" };
1385
-
1386
- const { nodes, warnings } = fromSlackBlocks([{ type: "section", fields }]);
1387
-
1388
- expect(warnings).toEqual([]);
1389
- expect(nodes.filter(Boolean)).toEqual([
1390
- { $type: "Fact", label: "A", value: "yes" },
1391
- { $type: "Fact", label: "C", value: "yes" },
1392
- ]);
1393
- });
1394
-
1395
- it("bounds static-select options when an array redirects Symbol.species", () => {
1396
- function HostileCtor() {
1397
- return {
1398
- map: () =>
1399
- Array.from({ length: SELECT_OPTION_CAP * 5 }, () => ({
1400
- label: "Injected",
1401
- value: "injected",
1402
- })),
1403
- length: 0,
1404
- };
1405
- }
1406
- const constructor = { [Symbol.species]: HostileCtor };
1407
- const hostile = new Proxy(
1408
- Array.from({ length: SELECT_OPTION_CAP + 1 }, (_, index) => ({
1409
- text: { type: "plain_text", text: `Option ${index}` },
1410
- value: `value-${index}`,
1411
- })),
1412
- {
1413
- get: (target, prop, receiver) =>
1414
- prop === "constructor"
1415
- ? constructor
1416
- : Reflect.get(target, prop, receiver),
1417
- },
1418
- );
1419
-
1420
- const { nodes } = fromSlackBlocks([
1421
- {
1422
- type: "actions",
1423
- elements: [
1424
- { type: "static_select", action_id: "pick", options: hostile },
1425
- ],
1426
- },
1427
- ]);
1428
- const select = nodes[0] as unknown as { options: unknown[] };
1429
-
1430
- expect(select.options).toHaveLength(SELECT_OPTION_CAP);
1431
- expect(select.options[0]).toEqual({ label: "Option 0", value: "value-0" });
1432
- });
1433
-
1434
- it("bounds hostile initial_options arrays when deriving defaultChecked", () => {
1435
- const hostile = new Proxy(
1436
- [{ text: { type: "plain_text", text: "Other" }, value: "other" }],
1437
- {
1438
- get(target, prop, receiver) {
1439
- if (prop === "length") return 2 ** 31;
1440
- if (prop === "slice") {
1441
- return () => [
1442
- { text: { type: "plain_text", text: "First" }, value: "first" },
1443
- ];
1444
- }
1445
- return Reflect.get(target, prop, receiver);
1446
- },
1447
- },
1448
- );
1449
- const { nodes } = fromSlackBlocks([
1450
- {
1451
- type: "actions",
1452
- elements: [
1453
- {
1454
- type: "checkboxes",
1455
- action_id: "toggle",
1456
- options: [
1457
- { text: { type: "plain_text", text: "First" }, value: "first" },
1458
- ],
1459
- initial_options: hostile,
1460
- },
1461
- ],
1462
- },
1463
- ]);
1464
- expect(nodes[0]).not.toHaveProperty("defaultChecked");
1465
- });