@assistant-ui/react-generative-ui 0.0.24 → 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 +4 -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 -153
  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 -584
  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 -337
  306. package/src/temporal.ts +0 -263
  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 -1457
  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 -1109
  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,1897 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { CHILDREN_CAP, NODE_BUDGET } from "../convert/boundSpec";
3
- import { toAdaptiveCard } from "./toAdaptiveCard";
4
- import { toTeamsAttachments } from "./toTeamsAttachments";
5
- import {
6
- CAROUSEL_ATTACHMENT_CAP,
7
- CHOICE_OPTION_CAP,
8
- PAYLOAD_SOFT_CAP,
9
- PRIMARY_ACTION_CAP,
10
- TABLE_COLUMN_CAP,
11
- TABLE_ROW_CAP,
12
- } from "./constants";
13
- import type {
14
- TeamsActionSet,
15
- TeamsColumnSet,
16
- TeamsContainer,
17
- TeamsFactSet,
18
- TeamsImage,
19
- TeamsInputChoiceSet,
20
- TeamsInputDate,
21
- TeamsInputText,
22
- TeamsInputToggle,
23
- TeamsTable,
24
- TeamsCardElement,
25
- TeamsTextBlock,
26
- } from "./types";
27
-
28
- const hostileSpecies = <T>(
29
- source: T[],
30
- injected: unknown[],
31
- onDispatch: () => void,
32
- ): T[] => {
33
- function HostileCtor() {
34
- return {
35
- map: () => {
36
- onDispatch();
37
- return injected;
38
- },
39
- [Symbol.iterator]: function* () {
40
- onDispatch();
41
- yield* injected;
42
- },
43
- };
44
- }
45
- const constructor = { [Symbol.species]: HostileCtor };
46
- return new Proxy(source, {
47
- get: (target, prop, receiver) =>
48
- prop === "constructor"
49
- ? constructor
50
- : Reflect.get(target, prop, receiver),
51
- });
52
- };
53
-
54
- describe("toAdaptiveCard", () => {
55
- describe("Header", () => {
56
- it("renders a heading TextBlock", () => {
57
- const { card } = toAdaptiveCard({ $type: "Header", text: "Title" });
58
- expect(card.body).toEqual([
59
- {
60
- type: "TextBlock",
61
- text: "Title",
62
- wrap: true,
63
- size: "large",
64
- weight: "bolder",
65
- style: "heading",
66
- },
67
- ]);
68
- });
69
- });
70
-
71
- describe("Text", () => {
72
- it("renders a wrapped TextBlock with no size/weight/isSubtle by default", () => {
73
- const { card } = toAdaptiveCard({ $type: "Text", value: "Hello" });
74
- expect(card.body).toEqual([
75
- { type: "TextBlock", text: "Hello", wrap: true },
76
- ]);
77
- });
78
-
79
- it.each([
80
- ["sm", "small"],
81
- ["lg", "medium"],
82
- ["xl", "large"],
83
- ["2xl", "extraLarge"],
84
- ["3xl", "extraLarge"],
85
- ] as const)("maps size %s to %s", (size, expected) => {
86
- const { card } = toAdaptiveCard({ $type: "Text", value: "x", size });
87
- expect((card.body[0] as TeamsTextBlock).size).toBe(expected);
88
- });
89
-
90
- it("omits size for md", () => {
91
- const { card } = toAdaptiveCard({
92
- $type: "Text",
93
- value: "x",
94
- size: "md",
95
- });
96
- expect((card.body[0] as TeamsTextBlock).size).toBeUndefined();
97
- });
98
-
99
- it.each(["medium", "semibold", "bold"] as const)(
100
- "maps weight %s to bolder",
101
- (weight) => {
102
- const { card } = toAdaptiveCard({ $type: "Text", value: "x", weight });
103
- expect((card.body[0] as TeamsTextBlock).weight).toBe("bolder");
104
- },
105
- );
106
-
107
- it("omits weight for normal", () => {
108
- const { card } = toAdaptiveCard({
109
- $type: "Text",
110
- value: "x",
111
- weight: "normal",
112
- });
113
- expect((card.body[0] as TeamsTextBlock).weight).toBeUndefined();
114
- });
115
-
116
- it.each([
117
- "secondary",
118
- "alpha-70",
119
- "white",
120
- "white-70",
121
- "white-50",
122
- ] as const)("marks color %s as isSubtle", (color) => {
123
- const { card } = toAdaptiveCard({ $type: "Text", value: "x", color });
124
- expect((card.body[0] as TeamsTextBlock).isSubtle).toBe(true);
125
- });
126
-
127
- it("does not mark emphasis as isSubtle", () => {
128
- const { card } = toAdaptiveCard({
129
- $type: "Text",
130
- value: "x",
131
- color: "emphasis",
132
- });
133
- expect((card.body[0] as TeamsTextBlock).isSubtle).toBeUndefined();
134
- });
135
- });
136
-
137
- describe("Caption", () => {
138
- it("renders a small subtle TextBlock", () => {
139
- const { card } = toAdaptiveCard({
140
- $type: "Caption",
141
- value: "Fine print",
142
- });
143
- expect(card.body).toEqual([
144
- {
145
- type: "TextBlock",
146
- text: "Fine print",
147
- wrap: true,
148
- size: "small",
149
- isSubtle: true,
150
- },
151
- ]);
152
- });
153
- });
154
-
155
- describe("Badge", () => {
156
- it("degrades to a small subtle TextBlock", () => {
157
- const { card } = toAdaptiveCard({ $type: "Badge", value: "New" });
158
- expect(card.body).toEqual([
159
- {
160
- type: "TextBlock",
161
- text: "New",
162
- wrap: true,
163
- size: "small",
164
- isSubtle: true,
165
- },
166
- ]);
167
- });
168
- });
169
-
170
- describe("Markdown", () => {
171
- it("passes text through unwrapped, with no syntax transformation", () => {
172
- const { card } = toAdaptiveCard({
173
- $type: "Markdown",
174
- value: "# Heading\n**bold**",
175
- });
176
- expect(card.body).toEqual([
177
- { type: "TextBlock", text: "# Heading\n**bold**", wrap: true },
178
- ]);
179
- });
180
- });
181
-
182
- describe("Fact", () => {
183
- it("renders a single fact as a one-entry FactSet", () => {
184
- const { card } = toAdaptiveCard({
185
- $type: "Fact",
186
- label: "Status",
187
- value: "Active",
188
- });
189
- expect(card.body).toEqual([
190
- { type: "FactSet", facts: [{ title: "Status", value: "Active" }] },
191
- ]);
192
- });
193
-
194
- it("merges consecutive Fact siblings into one FactSet", () => {
195
- const facts = Array.from({ length: 5 }, (_, i) => ({
196
- $type: "Fact",
197
- label: `L${i}`,
198
- value: `V${i}`,
199
- }));
200
- const { card } = toAdaptiveCard(facts);
201
- expect(card.body).toHaveLength(1);
202
- expect((card.body[0] as TeamsFactSet).facts).toHaveLength(5);
203
- });
204
-
205
- it("appends a directional fact delta to the value text", () => {
206
- const { card } = toAdaptiveCard({
207
- $type: "Fact",
208
- label: "Revenue",
209
- value: "$12.4k",
210
- delta: "8%",
211
- trend: "down",
212
- });
213
- expect(card.body).toEqual([
214
- {
215
- type: "FactSet",
216
- facts: [{ title: "Revenue", value: "$12.4k (↓ 8%)" }],
217
- },
218
- ]);
219
- });
220
-
221
- it("breaks Fact merging on a non-Fact sibling", () => {
222
- const { card } = toAdaptiveCard([
223
- { $type: "Fact", label: "A", value: "1" },
224
- { $type: "Fact", label: "B", value: "2" },
225
- { $type: "Text", value: "between" },
226
- { $type: "Fact", label: "C", value: "3" },
227
- ]);
228
- expect(card.body).toHaveLength(3);
229
- expect((card.body[0] as TeamsFactSet).facts).toHaveLength(2);
230
- expect((card.body[1] as TeamsTextBlock).text).toBe("between");
231
- expect((card.body[2] as TeamsFactSet).facts).toHaveLength(1);
232
- });
233
- });
234
-
235
- describe("Image", () => {
236
- it.each([
237
- ["sm", "small"],
238
- ["md", "medium"],
239
- ["lg", "large"],
240
- ] as const)("maps size %s to %s", (size, expected) => {
241
- const { card } = toAdaptiveCard({
242
- $type: "Image",
243
- src: "https://x/y.png",
244
- alt: "A photo",
245
- size,
246
- });
247
- expect(card.body).toEqual([
248
- {
249
- type: "Image",
250
- url: "https://x/y.png",
251
- altText: "A photo",
252
- size: expected,
253
- },
254
- ]);
255
- });
256
-
257
- it("drops a numeric size with a warning", () => {
258
- const { card, warnings } = toAdaptiveCard({
259
- $type: "Image",
260
- src: "https://x/y.png",
261
- alt: "A photo",
262
- size: 48,
263
- });
264
- expect((card.body[0] as TeamsImage).size).toBeUndefined();
265
- expect(warnings).toContainEqual({
266
- code: "dropped",
267
- component: "Image",
268
- detail: "A numeric size was dropped.",
269
- });
270
- });
271
-
272
- it("drops round silently", () => {
273
- const { warnings } = toAdaptiveCard({
274
- $type: "Image",
275
- src: "https://x/y.png",
276
- alt: "A photo",
277
- round: true,
278
- });
279
- expect(warnings).toEqual([]);
280
- });
281
- });
282
-
283
- describe("Divider", () => {
284
- it("sets separator on the next sibling instead of emitting its own element", () => {
285
- const { card } = toAdaptiveCard([
286
- { $type: "Divider" },
287
- { $type: "Text", value: "After" },
288
- ]);
289
- expect(card.body).toEqual([
290
- { type: "TextBlock", text: "After", wrap: true, separator: true },
291
- ]);
292
- });
293
-
294
- it("drops a trailing Divider silently", () => {
295
- const { card, warnings } = toAdaptiveCard([
296
- { $type: "Text", value: "Before" },
297
- { $type: "Divider" },
298
- ]);
299
- expect(card.body).toEqual([
300
- { type: "TextBlock", text: "Before", wrap: true },
301
- ]);
302
- expect(warnings).toEqual([]);
303
- });
304
-
305
- it("carries the separator past a dropped child to the next emitted sibling", () => {
306
- const { card, warnings } = toAdaptiveCard([
307
- { $type: "Divider" },
308
- { $type: "Mystery" },
309
- { $type: "Text", value: "After" },
310
- ]);
311
- expect(card.body).toEqual([
312
- { type: "TextBlock", text: "After", wrap: true, separator: true },
313
- ]);
314
- expect(warnings).toContainEqual({
315
- code: "dropped",
316
- component: "Mystery",
317
- detail: "Unknown component type was dropped.",
318
- });
319
- });
320
- });
321
-
322
- describe("Spacer", () => {
323
- it("sets spacing large on the next sibling instead of emitting its own element", () => {
324
- const { card } = toAdaptiveCard([
325
- { $type: "Spacer" },
326
- { $type: "Text", value: "After" },
327
- ]);
328
- expect(card.body).toEqual([
329
- { type: "TextBlock", text: "After", wrap: true, spacing: "large" },
330
- ]);
331
- });
332
-
333
- it("drops a trailing Spacer silently", () => {
334
- const { card, warnings } = toAdaptiveCard([
335
- { $type: "Text", value: "Before" },
336
- { $type: "Spacer" },
337
- ]);
338
- expect(card.body).toEqual([
339
- { type: "TextBlock", text: "Before", wrap: true },
340
- ]);
341
- expect(warnings).toEqual([]);
342
- });
343
- });
344
-
345
- describe("Button", () => {
346
- it("renders a single Button as a one-action ActionSet with the aui envelope", () => {
347
- const { card } = toAdaptiveCard({
348
- $type: "Button",
349
- label: "Buy",
350
- $action: { type: "buy", sku: "abc" },
351
- });
352
- expect(card.body).toEqual([
353
- {
354
- type: "ActionSet",
355
- actions: [
356
- {
357
- type: "Action.Submit",
358
- title: "Buy",
359
- data: { aui: { type: "buy", payload: { sku: "abc" } } },
360
- },
361
- ],
362
- },
363
- ]);
364
- });
365
-
366
- it('falls back to action type "action" and omits payload when $action is absent', () => {
367
- const { card } = toAdaptiveCard({ $type: "Button", label: "Go" });
368
- expect(card.body).toEqual([
369
- {
370
- type: "ActionSet",
371
- actions: [
372
- {
373
- type: "Action.Submit",
374
- title: "Go",
375
- data: { aui: { type: "action" } },
376
- },
377
- ],
378
- },
379
- ]);
380
- });
381
-
382
- it("never emits Teams style for buttonStyle (Teams ignores it)", () => {
383
- const { card } = toAdaptiveCard({
384
- $type: "Button",
385
- label: "Delete",
386
- buttonStyle: "danger",
387
- $action: { type: "del" },
388
- });
389
- const actionSet = card.body[0] as TeamsActionSet;
390
- expect(actionSet.actions[0]).not.toHaveProperty("style");
391
- });
392
-
393
- it("merges consecutive Buttons into one ActionSet, capping primary actions and setting secondary mode beyond the cap", () => {
394
- const buttons = Array.from(
395
- { length: PRIMARY_ACTION_CAP + 2 },
396
- (_, i) => ({
397
- $type: "Button",
398
- label: `B${i}`,
399
- $action: { type: `act${i}` },
400
- }),
401
- );
402
- const { card, warnings } = toAdaptiveCard(buttons);
403
- expect(card.body).toHaveLength(1);
404
- const actionSet = card.body[0] as TeamsActionSet;
405
- expect(actionSet.actions).toHaveLength(PRIMARY_ACTION_CAP + 2);
406
- for (const action of actionSet.actions.slice(0, PRIMARY_ACTION_CAP)) {
407
- expect(action.mode).toBeUndefined();
408
- }
409
- for (const action of actionSet.actions.slice(PRIMARY_ACTION_CAP)) {
410
- expect(action.mode).toBe("secondary");
411
- }
412
- expect(warnings).toContainEqual(
413
- expect.objectContaining({ code: "fallback", component: "Button" }),
414
- );
415
- });
416
- });
417
-
418
- describe("nested arrays flatten into one sibling sequence", () => {
419
- it("carries a Divider's separator across a nested array boundary", () => {
420
- const { card } = toAdaptiveCard([
421
- [{ $type: "Divider" }],
422
- { $type: "Text", value: "After" },
423
- ]);
424
- expect(card.body).toEqual([
425
- { type: "TextBlock", text: "After", wrap: true, separator: true },
426
- ]);
427
- });
428
-
429
- it("carries a Spacer's spacing across a nested array boundary", () => {
430
- const { card } = toAdaptiveCard([
431
- [{ $type: "Spacer" }],
432
- { $type: "Text", value: "After" },
433
- ]);
434
- expect(card.body).toEqual([
435
- { type: "TextBlock", text: "After", wrap: true, spacing: "large" },
436
- ]);
437
- });
438
-
439
- it("merges Fact siblings split across a nested array into one FactSet", () => {
440
- const { card } = toAdaptiveCard([
441
- { $type: "Fact", label: "A", value: "1" },
442
- [{ $type: "Fact", label: "B", value: "2" }],
443
- ]);
444
- expect(card.body).toHaveLength(1);
445
- expect((card.body[0] as TeamsFactSet).facts).toHaveLength(2);
446
- });
447
-
448
- it("merges Button siblings split across singleton arrays, still capping primary actions", () => {
449
- const buttons = Array.from({ length: PRIMARY_ACTION_CAP + 2 }, (_, i) => [
450
- { $type: "Button", label: `B${i}`, $action: { type: `act${i}` } },
451
- ]);
452
- const { card, warnings } = toAdaptiveCard(buttons);
453
- expect(card.body).toHaveLength(1);
454
- const actionSet = card.body[0] as TeamsActionSet;
455
- expect(actionSet.actions).toHaveLength(PRIMARY_ACTION_CAP + 2);
456
- for (const action of actionSet.actions.slice(0, PRIMARY_ACTION_CAP)) {
457
- expect(action.mode).toBeUndefined();
458
- }
459
- for (const action of actionSet.actions.slice(PRIMARY_ACTION_CAP)) {
460
- expect(action.mode).toBe("secondary");
461
- }
462
- expect(warnings).toContainEqual(
463
- expect.objectContaining({ code: "fallback", component: "Button" }),
464
- );
465
- });
466
- });
467
-
468
- describe("Select", () => {
469
- it("renders a compact Input.ChoiceSet", () => {
470
- const { card } = toAdaptiveCard({
471
- $type: "Select",
472
- name: "color",
473
- label: "Color",
474
- placeholder: "Pick one",
475
- options: [
476
- { label: "Red", value: "red" },
477
- { label: "Blue", value: "blue" },
478
- ],
479
- });
480
- expect(card.body).toEqual([
481
- {
482
- type: "Input.ChoiceSet",
483
- id: "color",
484
- style: "compact",
485
- choices: [
486
- { title: "Red", value: "red" },
487
- { title: "Blue", value: "blue" },
488
- ],
489
- placeholder: "Pick one",
490
- label: "Color",
491
- },
492
- ]);
493
- });
494
-
495
- it('falls back to id "select" when name is absent', () => {
496
- const { card } = toAdaptiveCard({ $type: "Select", options: [] });
497
- expect((card.body[0] as TeamsInputChoiceSet).id).toBe("select");
498
- });
499
-
500
- it("sets value from a non-empty defaultValue", () => {
501
- const { card } = toAdaptiveCard({
502
- $type: "Select",
503
- defaultValue: "blue",
504
- options: [{ label: "Blue", value: "blue" }],
505
- });
506
- expect((card.body[0] as TeamsInputChoiceSet).value).toBe("blue");
507
- });
508
-
509
- it("omits value when defaultValue is empty", () => {
510
- const { card } = toAdaptiveCard({
511
- $type: "Select",
512
- defaultValue: "",
513
- options: [{ label: "Blue", value: "blue" }],
514
- });
515
- expect((card.body[0] as TeamsInputChoiceSet).value).toBeUndefined();
516
- });
517
-
518
- it("does not dispatch a prop array through Symbol.species", () => {
519
- let dispatches = 0;
520
- const injected = Array.from(
521
- { length: CHOICE_OPTION_CAP + 1 },
522
- (_, i) => ({ label: `injected-${i}`, value: `injected-${i}` }),
523
- );
524
- const { card, warnings } = toAdaptiveCard({
525
- $type: "Select",
526
- options: hostileSpecies(
527
- [{ label: "Kept", value: "kept" }],
528
- injected,
529
- () => {
530
- dispatches += 1;
531
- },
532
- ),
533
- });
534
- expect((card.body[0] as TeamsInputChoiceSet).choices).toEqual([
535
- { title: "Kept", value: "kept" },
536
- ]);
537
- expect(dispatches).toBe(0);
538
- expect(warnings).toEqual([]);
539
- });
540
-
541
- it("appends a companion submit ActionSet with a fallback warning when $action is present", () => {
542
- const { card, warnings } = toAdaptiveCard({
543
- $type: "Select",
544
- name: "color",
545
- options: [{ label: "Red", value: "red" }],
546
- $action: { type: "pick", scope: "color" },
547
- });
548
- expect(card.body).toHaveLength(2);
549
- expect(card.body[0]?.type).toBe("Input.ChoiceSet");
550
- expect(card.body[1]).toEqual({
551
- type: "ActionSet",
552
- actions: [
553
- {
554
- type: "Action.Submit",
555
- title: "Submit",
556
- data: { aui: { type: "pick", payload: { scope: "color" } } },
557
- },
558
- ],
559
- });
560
- expect(warnings).toContainEqual({
561
- code: "fallback",
562
- component: "Select",
563
- detail:
564
- "Teams inputs cannot dispatch on change; a companion submit action was appended.",
565
- });
566
- });
567
- });
568
-
569
- describe("RadioGroup", () => {
570
- it("renders an expanded Input.ChoiceSet with the default value", () => {
571
- const { card } = toAdaptiveCard({
572
- $type: "RadioGroup",
573
- name: "plan",
574
- defaultValue: "pro",
575
- options: [
576
- { label: "Free", value: "free" },
577
- { label: "Pro", value: "pro" },
578
- ],
579
- });
580
- expect(card.body).toEqual([
581
- {
582
- type: "Input.ChoiceSet",
583
- id: "plan",
584
- style: "expanded",
585
- choices: [
586
- { title: "Free", value: "free" },
587
- { title: "Pro", value: "pro" },
588
- ],
589
- value: "pro",
590
- },
591
- ]);
592
- });
593
-
594
- it("appends option descriptions to choice titles", () => {
595
- const { card } = toAdaptiveCard({
596
- $type: "RadioGroup",
597
- options: [
598
- {
599
- label: "Free",
600
- description: "For personal projects",
601
- value: "free",
602
- },
603
- ],
604
- });
605
- expect((card.body[0] as TeamsInputChoiceSet).choices).toEqual([
606
- { title: "Free: For personal projects", value: "free" },
607
- ]);
608
- });
609
- });
610
-
611
- describe("CheckboxGroup", () => {
612
- it("renders an expanded multi-select Input.ChoiceSet with the default values joined by commas", () => {
613
- const { card } = toAdaptiveCard({
614
- $type: "CheckboxGroup",
615
- name: "toppings",
616
- label: "Toppings",
617
- defaultValue: ["basil", 7, "onion"],
618
- options: [
619
- { label: "Basil", value: "basil" },
620
- { label: "Olives", value: "olives" },
621
- { label: "Onion", value: "onion" },
622
- ],
623
- });
624
- expect(card.body).toEqual([
625
- {
626
- type: "Input.ChoiceSet",
627
- id: "toppings",
628
- style: "expanded",
629
- isMultiSelect: true,
630
- choices: [
631
- { title: "Basil", value: "basil" },
632
- { title: "Olives", value: "olives" },
633
- { title: "Onion", value: "onion" },
634
- ],
635
- value: "basil,onion",
636
- label: "Toppings",
637
- },
638
- ]);
639
- });
640
-
641
- it("falls back to the checkboxgroup id and appends a companion submit for an action", () => {
642
- const { card, warnings } = toAdaptiveCard({
643
- $type: "CheckboxGroup",
644
- options: [{ label: "Basil", value: "basil" }],
645
- $action: { type: "pick" },
646
- });
647
- expect(card.body).toEqual([
648
- {
649
- type: "Input.ChoiceSet",
650
- id: "checkboxgroup",
651
- style: "expanded",
652
- isMultiSelect: true,
653
- choices: [{ title: "Basil", value: "basil" }],
654
- },
655
- {
656
- type: "ActionSet",
657
- actions: [
658
- {
659
- type: "Action.Submit",
660
- title: "Submit",
661
- data: { aui: { type: "pick" } },
662
- },
663
- ],
664
- },
665
- ]);
666
- expect(warnings).toContainEqual({
667
- code: "fallback",
668
- component: "CheckboxGroup",
669
- detail:
670
- "Teams inputs cannot dispatch on change; a companion submit action was appended.",
671
- });
672
- });
673
- });
674
-
675
- describe("Checkbox", () => {
676
- it("renders an Input.Toggle with true/false values", () => {
677
- const { card } = toAdaptiveCard({
678
- $type: "Checkbox",
679
- label: "Subscribe",
680
- name: "subscribe",
681
- defaultChecked: true,
682
- });
683
- expect(card.body).toEqual([
684
- {
685
- type: "Input.Toggle",
686
- id: "subscribe",
687
- title: "Subscribe",
688
- value: "true",
689
- valueOn: "true",
690
- valueOff: "false",
691
- },
692
- ]);
693
- });
694
-
695
- it("derives the id from the label when name is absent", () => {
696
- const { card } = toAdaptiveCard({ $type: "Checkbox", label: "Agree" });
697
- expect((card.body[0] as TeamsInputToggle).id).toBe("Agree");
698
- expect((card.body[0] as TeamsInputToggle).value).toBe("false");
699
- });
700
-
701
- it("maps a switch like a checkbox", () => {
702
- const { card } = toAdaptiveCard({
703
- $type: "Checkbox",
704
- label: "Subscribe",
705
- variant: "switch",
706
- });
707
- expect(card.body[0]).toMatchObject({
708
- type: "Input.Toggle",
709
- title: "Subscribe",
710
- });
711
- });
712
- });
713
-
714
- describe("Slider", () => {
715
- it("renders an Input.Number with its bounds, value, and label", () => {
716
- const { card } = toAdaptiveCard({
717
- $type: "Slider",
718
- name: "quantity",
719
- label: "Quantity",
720
- min: 1,
721
- max: 12,
722
- defaultValue: 3,
723
- });
724
- expect(card.body[0]).toEqual({
725
- type: "Input.Number",
726
- id: "quantity",
727
- label: "Quantity",
728
- min: 1,
729
- max: 12,
730
- value: 3,
731
- });
732
- });
733
- });
734
-
735
- describe("Input", () => {
736
- it("maps password and number inputs while preserving string values", () => {
737
- const { card } = toAdaptiveCard([
738
- {
739
- $type: "Input",
740
- name: "quantity",
741
- inputType: "number",
742
- defaultValue: "2.5",
743
- },
744
- { $type: "Input", name: "password", inputType: "password" },
745
- ]);
746
- expect(card.body).toEqual([
747
- { type: "Input.Text", id: "quantity", value: "2.5" },
748
- { type: "Input.Text", id: "password", style: "password" },
749
- ]);
750
- });
751
-
752
- it("renders an Input.Text", () => {
753
- const { card } = toAdaptiveCard({
754
- $type: "Input",
755
- name: "email",
756
- label: "Email",
757
- placeholder: "you@example.com",
758
- });
759
- expect(card.body).toEqual([
760
- {
761
- type: "Input.Text",
762
- id: "email",
763
- label: "Email",
764
- placeholder: "you@example.com",
765
- },
766
- ]);
767
- });
768
-
769
- it("sets isMultiline for a multiline input", () => {
770
- const { card } = toAdaptiveCard({
771
- $type: "Input",
772
- name: "notes",
773
- multiline: true,
774
- });
775
- expect((card.body[0] as TeamsInputText).isMultiline).toBe(true);
776
- });
777
-
778
- it("keeps a multiline password input single-line", () => {
779
- const { card } = toAdaptiveCard({
780
- $type: "Input",
781
- name: "password",
782
- inputType: "password",
783
- multiline: true,
784
- });
785
- expect(card.body[0]).toEqual({
786
- type: "Input.Text",
787
- id: "password",
788
- style: "password",
789
- });
790
- });
791
-
792
- it("sets value from a non-empty defaultValue", () => {
793
- const { card } = toAdaptiveCard({
794
- $type: "Input",
795
- name: "notes",
796
- defaultValue: "Draft reply",
797
- });
798
- expect((card.body[0] as TeamsInputText).value).toBe("Draft reply");
799
- });
800
-
801
- it("omits value when defaultValue is empty", () => {
802
- const { card } = toAdaptiveCard({
803
- $type: "Input",
804
- name: "notes",
805
- defaultValue: "",
806
- });
807
- expect((card.body[0] as TeamsInputText).value).toBeUndefined();
808
- });
809
-
810
- it("emits no ActionSet when $action is absent", () => {
811
- const { card } = toAdaptiveCard({ $type: "Input", name: "email" });
812
- expect(card.body).toHaveLength(1);
813
- });
814
- });
815
-
816
- describe("DatePicker", () => {
817
- it("keeps a value matching YYYY-MM-DD", () => {
818
- const { card } = toAdaptiveCard({
819
- $type: "DatePicker",
820
- name: "date",
821
- value: "2026-07-15",
822
- });
823
- expect((card.body[0] as TeamsInputDate).value).toBe("2026-07-15");
824
- });
825
-
826
- it("drops a value that does not match YYYY-MM-DD", () => {
827
- const { card } = toAdaptiveCard({
828
- $type: "DatePicker",
829
- name: "date",
830
- value: "not-a-date",
831
- });
832
- expect((card.body[0] as TeamsInputDate).value).toBeUndefined();
833
- });
834
-
835
- it("keeps min and max matching YYYY-MM-DD", () => {
836
- const { card } = toAdaptiveCard({
837
- $type: "DatePicker",
838
- name: "date",
839
- min: "2026-01-01",
840
- max: "2026-12-31",
841
- });
842
- expect((card.body[0] as TeamsInputDate).min).toBe("2026-01-01");
843
- expect((card.body[0] as TeamsInputDate).max).toBe("2026-12-31");
844
- });
845
-
846
- it("drops a min that does not match YYYY-MM-DD", () => {
847
- const { card } = toAdaptiveCard({
848
- $type: "DatePicker",
849
- name: "date",
850
- min: "tomorrow",
851
- });
852
- expect((card.body[0] as TeamsInputDate).min).toBeUndefined();
853
- });
854
- });
855
-
856
- describe('reserved "aui" input id', () => {
857
- it.each([
858
- ["Input", { $type: "Input", name: "aui" }],
859
- ["Select", { $type: "Select", name: "aui", options: [] }],
860
- ["RadioGroup", { $type: "RadioGroup", name: "aui", options: [] }],
861
- ["CheckboxGroup", { $type: "CheckboxGroup", name: "aui", options: [] }],
862
- ["Checkbox", { $type: "Checkbox", name: "aui", label: "Agree" }],
863
- ["Slider", { $type: "Slider", name: "aui", min: 0, max: 10 }],
864
- ["DatePicker", { $type: "DatePicker", name: "aui" }],
865
- ] as const)(
866
- "renames a %s named aui to aui_ with a warning",
867
- (type, node) => {
868
- const { card, warnings } = toAdaptiveCard(node);
869
- const element = card.body[0] as { id: string };
870
- expect(element.id).toBe("aui_");
871
- expect(warnings).toContainEqual(
872
- expect.objectContaining({ code: "fallback", component: type }),
873
- );
874
- },
875
- );
876
-
877
- it("leaves a non-colliding id untouched", () => {
878
- const { card, warnings } = toAdaptiveCard({
879
- $type: "Input",
880
- name: "email",
881
- });
882
- expect((card.body[0] as TeamsInputText).id).toBe("email");
883
- expect(warnings).toEqual([]);
884
- });
885
- });
886
-
887
- describe("per-card input id uniqueness", () => {
888
- it('uniquifies duplicate unnamed Input ids with a "_2" suffix and a warning', () => {
889
- const { card, warnings } = toAdaptiveCard([
890
- { $type: "Input" },
891
- { $type: "Input" },
892
- ]);
893
- const ids = (card.body as TeamsInputText[]).map((element) => element.id);
894
- expect(ids).toEqual(["input", "input_2"]);
895
- expect(warnings).toContainEqual(
896
- expect.objectContaining({ code: "fallback", component: "Input" }),
897
- );
898
- });
899
-
900
- it('keeps ids for "aui", "aui_", and an unnamed input mutually distinct', () => {
901
- const { card, warnings } = toAdaptiveCard([
902
- { $type: "Input", name: "aui" },
903
- { $type: "Input", name: "aui_" },
904
- { $type: "Input" },
905
- ]);
906
- const ids = (card.body as TeamsInputText[]).map((element) => element.id);
907
- expect(new Set(ids).size).toBe(3);
908
- expect(warnings.some((warning) => warning.code === "fallback")).toBe(
909
- true,
910
- );
911
- });
912
-
913
- it("names the actually-emitted id in the reserved-key warning, and fires only that one warning, when the rename target also collides", () => {
914
- const { card, warnings } = toAdaptiveCard([
915
- { $type: "Input", name: "aui_" },
916
- { $type: "Input", name: "aui" },
917
- ]);
918
- const ids = (card.body as TeamsInputText[]).map((element) => element.id);
919
- expect(ids).toEqual(["aui_", "aui__2"]);
920
- expect(warnings).toEqual([
921
- {
922
- code: "fallback",
923
- component: "Input",
924
- detail:
925
- 'the input id "aui" collides with the submit envelope\'s reserved key and was renamed to "aui__2".',
926
- },
927
- ]);
928
- });
929
- });
930
-
931
- describe("Form", () => {
932
- it("converts children inline and appends a Submit ActionSet with the aui envelope", () => {
933
- const { card } = toAdaptiveCard({
934
- $type: "Form",
935
- $action: { type: "save", scope: "profile" },
936
- children: [{ $type: "Input", name: "email", label: "Email" }],
937
- });
938
- expect(card.body).toEqual([
939
- { type: "Input.Text", id: "email", label: "Email" },
940
- {
941
- type: "ActionSet",
942
- actions: [
943
- {
944
- type: "Action.Submit",
945
- title: "Submit",
946
- data: { aui: { type: "save", payload: { scope: "profile" } } },
947
- },
948
- ],
949
- },
950
- ]);
951
- });
952
- });
953
-
954
- describe("Card", () => {
955
- it("renders a titled Container followed by a confirm/cancel ActionSet", () => {
956
- const { card } = toAdaptiveCard({
957
- $type: "Card",
958
- title: "Approve request?",
959
- children: { $type: "Text", value: "Details" },
960
- confirm: { label: "Approve", $action: { type: "approve" } },
961
- cancel: { label: "Reject", $action: { type: "reject" } },
962
- });
963
- expect(card.body).toEqual([
964
- {
965
- type: "Container",
966
- style: "default",
967
- items: [
968
- {
969
- type: "TextBlock",
970
- text: "Approve request?",
971
- wrap: true,
972
- size: "large",
973
- weight: "bolder",
974
- style: "heading",
975
- },
976
- { type: "TextBlock", text: "Details", wrap: true },
977
- ],
978
- },
979
- {
980
- type: "ActionSet",
981
- actions: [
982
- {
983
- type: "Action.Submit",
984
- title: "Approve",
985
- data: { aui: { type: "approve" } },
986
- },
987
- {
988
- type: "Action.Submit",
989
- title: "Reject",
990
- data: { aui: { type: "reject" } },
991
- },
992
- ],
993
- },
994
- ]);
995
- });
996
-
997
- it("omits the footer ActionSet when there is no confirm/cancel", () => {
998
- const { card } = toAdaptiveCard({ $type: "Card", title: "Plain" });
999
- expect(card.body).toHaveLength(1);
1000
- });
1001
- });
1002
-
1003
- describe("Col / Box", () => {
1004
- it.each(["Col", "Box"] as const)("renders %s as a Container", ($type) => {
1005
- const { card } = toAdaptiveCard({
1006
- $type,
1007
- children: { $type: "Text", value: "Inside" },
1008
- });
1009
- expect(card.body).toEqual([
1010
- {
1011
- type: "Container",
1012
- items: [{ type: "TextBlock", text: "Inside", wrap: true }],
1013
- },
1014
- ]);
1015
- });
1016
- });
1017
-
1018
- describe("Row", () => {
1019
- it("renders each child as its own auto-width Column", () => {
1020
- const { card, warnings } = toAdaptiveCard({
1021
- $type: "Row",
1022
- children: [
1023
- { $type: "Text", value: "A" },
1024
- { $type: "Text", value: "B" },
1025
- ],
1026
- });
1027
- const columnSet = card.body[0] as TeamsColumnSet;
1028
- expect(columnSet.type).toBe("ColumnSet");
1029
- expect(columnSet.columns).toEqual([
1030
- {
1031
- type: "Column",
1032
- width: "auto",
1033
- items: [{ type: "TextBlock", text: "A", wrap: true }],
1034
- },
1035
- {
1036
- type: "Column",
1037
- width: "auto",
1038
- items: [{ type: "TextBlock", text: "B", wrap: true }],
1039
- },
1040
- ]);
1041
- expect(warnings).toEqual([]);
1042
- });
1043
-
1044
- it("warns but keeps every column when there are more than 3 children", () => {
1045
- const { card, warnings } = toAdaptiveCard({
1046
- $type: "Row",
1047
- children: [1, 2, 3, 4].map((n) => ({ $type: "Text", value: `${n}` })),
1048
- });
1049
- const columnSet = card.body[0] as TeamsColumnSet;
1050
- expect(columnSet.columns).toHaveLength(4);
1051
- expect(warnings).toContainEqual(
1052
- expect.objectContaining({ code: "advisory", component: "Row" }),
1053
- );
1054
- });
1055
- });
1056
-
1057
- describe("ListView / ListViewItem", () => {
1058
- it("converts items into Containers with selectAction and separators between items", () => {
1059
- const { card } = toAdaptiveCard({
1060
- $type: "ListView",
1061
- children: [
1062
- {
1063
- $type: "ListViewItem",
1064
- $action: { type: "open", id: "1" },
1065
- children: { $type: "Text", value: "Row 1" },
1066
- },
1067
- {
1068
- $type: "ListViewItem",
1069
- children: { $type: "Text", value: "Row 2" },
1070
- },
1071
- ],
1072
- });
1073
- expect(card.body).toEqual([
1074
- {
1075
- type: "Container",
1076
- items: [{ type: "TextBlock", text: "Row 1", wrap: true }],
1077
- selectAction: {
1078
- type: "Action.Submit",
1079
- title: "Open",
1080
- data: { aui: { type: "open", payload: { id: "1" } } },
1081
- },
1082
- },
1083
- {
1084
- type: "Container",
1085
- items: [{ type: "TextBlock", text: "Row 2", wrap: true }],
1086
- separator: true,
1087
- },
1088
- ]);
1089
- });
1090
-
1091
- it("routes a non-ListViewItem child through the normal warning path instead of filtering it silently", () => {
1092
- const { card, warnings } = toAdaptiveCard({
1093
- $type: "ListView",
1094
- children: [
1095
- { $type: "Mystery" },
1096
- {
1097
- $type: "ListViewItem",
1098
- children: { $type: "Text", value: "Kept" },
1099
- },
1100
- ],
1101
- });
1102
- expect(card.body).toEqual([
1103
- {
1104
- type: "Container",
1105
- items: [{ type: "TextBlock", text: "Kept", wrap: true }],
1106
- },
1107
- ]);
1108
- expect(warnings).toContainEqual({
1109
- code: "dropped",
1110
- component: "Mystery",
1111
- detail: "Unknown component type was dropped.",
1112
- });
1113
- });
1114
-
1115
- it("keeps a discarded child's clamp warning out of the result", () => {
1116
- const { warnings } = toAdaptiveCard({
1117
- $type: "ListView",
1118
- children: [
1119
- {
1120
- $type: "Table",
1121
- rows: Array.from({ length: TABLE_ROW_CAP + 5 }, () => ["x"]),
1122
- },
1123
- { $type: "ListViewItem", title: "row" },
1124
- ],
1125
- });
1126
- expect(warnings.some((warning) => warning.code === "clamped")).toBe(
1127
- false,
1128
- );
1129
- expect(warnings).toContainEqual({
1130
- code: "dropped",
1131
- component: "ListView",
1132
- detail: "1 non-item child was dropped.",
1133
- });
1134
- });
1135
-
1136
- it("reports a renderable non-item child whose output it discards", () => {
1137
- const { card, warnings } = toAdaptiveCard({
1138
- $type: "ListView",
1139
- children: [
1140
- { $type: "Text", value: "stray" },
1141
- { $type: "ListViewItem", children: { $type: "Text", value: "Kept" } },
1142
- ],
1143
- });
1144
- expect(card.body).toEqual([
1145
- {
1146
- type: "Container",
1147
- items: [{ type: "TextBlock", text: "Kept", wrap: true }],
1148
- },
1149
- ]);
1150
- expect(warnings).toContainEqual({
1151
- code: "dropped",
1152
- component: "ListView",
1153
- detail: "1 non-item child was dropped.",
1154
- });
1155
- });
1156
-
1157
- it.each([
1158
- ["before", 0],
1159
- ["after", 1],
1160
- ] as const)(
1161
- "does not let a discarded non-item child claim or report an input id, surviving control %s",
1162
- (_position, listViewIndex) => {
1163
- const listView = {
1164
- $type: "ListView",
1165
- children: [
1166
- { $type: "Input", name: "email" },
1167
- { $type: "ListViewItem", title: "row" },
1168
- ],
1169
- };
1170
- const survivor = { $type: "Input", name: "email", label: "Real" };
1171
- const { card, warnings } = toAdaptiveCard(
1172
- listViewIndex === 0 ? [listView, survivor] : [survivor, listView],
1173
- );
1174
- const ids = card.body.map((element) => (element as { id?: string }).id);
1175
- expect(ids).toContain("email");
1176
- expect(
1177
- warnings.some((warning) => warning.detail.includes("email_2")),
1178
- ).toBe(false);
1179
- },
1180
- );
1181
- });
1182
-
1183
- describe("Carousel", () => {
1184
- it("reports the non-card children a nested carousel filters out", () => {
1185
- const { warnings } = toAdaptiveCard({
1186
- $type: "Col",
1187
- children: {
1188
- $type: "Carousel",
1189
- children: [
1190
- { $type: "Text", value: "lost" },
1191
- { $type: "Card", title: "kept" },
1192
- ],
1193
- },
1194
- });
1195
- expect(warnings).toContainEqual({
1196
- code: "dropped",
1197
- component: "Carousel",
1198
- detail: "1 non-card child was dropped.",
1199
- });
1200
- });
1201
-
1202
- it("stays silent for a nested-carousel child that renders nothing anyway", () => {
1203
- const { warnings } = toAdaptiveCard({
1204
- $type: "Col",
1205
- children: {
1206
- $type: "Carousel",
1207
- children: [{ $type: "Spacer" }, { $type: "Card", title: "kept" }],
1208
- },
1209
- });
1210
- expect(warnings.some((warning) => warning.code === "dropped")).toBe(
1211
- false,
1212
- );
1213
- });
1214
-
1215
- it("forwards a nested-carousel child's own dropped warning", () => {
1216
- const { warnings } = toAdaptiveCard({
1217
- $type: "Col",
1218
- children: {
1219
- $type: "Carousel",
1220
- children: [{ $type: "Mystery" }, { $type: "Card", title: "kept" }],
1221
- },
1222
- });
1223
- expect(warnings).toContainEqual({
1224
- code: "dropped",
1225
- component: "Mystery",
1226
- detail: "Unknown component type was dropped.",
1227
- });
1228
- });
1229
- });
1230
-
1231
- describe("choices", () => {
1232
- it("reports options dropped for want of a string value", () => {
1233
- const { card, warnings } = toAdaptiveCard({
1234
- $type: "Select",
1235
- name: "s",
1236
- options: [{ label: "ok", value: "a" }, { label: "bad" }, "nope"],
1237
- });
1238
- const select = card.body[0];
1239
- if (select?.type !== "Input.ChoiceSet") {
1240
- throw new Error("Expected an input choice set.");
1241
- }
1242
- expect(select.choices).toEqual([{ title: "ok", value: "a" }]);
1243
- expect(warnings).toContainEqual({
1244
- code: "dropped",
1245
- component: "Select",
1246
- detail: "2 options were dropped for want of a string value.",
1247
- });
1248
- });
1249
-
1250
- it("names RadioGroup when the same loss happens there", () => {
1251
- const { warnings } = toAdaptiveCard({
1252
- $type: "RadioGroup",
1253
- name: "r",
1254
- options: [{ label: "ok", value: "a" }, { label: "bad" }],
1255
- });
1256
- expect(warnings).toContainEqual({
1257
- code: "dropped",
1258
- component: "RadioGroup",
1259
- detail: "1 option was dropped for want of a string value.",
1260
- });
1261
- });
1262
- });
1263
-
1264
- describe("Table", () => {
1265
- it.each([["A"], [{}], [{ label: 42 }]])(
1266
- "keeps an unlabeled column's position in the header and reports it: %j",
1267
- (column) => {
1268
- const { card, warnings } = toAdaptiveCard({
1269
- $type: "Table",
1270
- columns: [column, { label: "B" }],
1271
- rows: [["1", "2"]],
1272
- });
1273
- const table = card.body[0] as TeamsTable;
1274
- expect(
1275
- table.rows[0]?.cells.map(
1276
- (cell) => (cell.items[0] as TeamsTextBlock).text,
1277
- ),
1278
- ).toEqual(["", "B"]);
1279
- expect(warnings).toContainEqual({
1280
- code: "dropped",
1281
- component: "Table",
1282
- detail: "1 column header was left blank for want of a string label.",
1283
- });
1284
- },
1285
- );
1286
-
1287
- it("renders firstRowAsHeaders true and a header row when columns are present", () => {
1288
- const { card } = toAdaptiveCard({
1289
- $type: "Table",
1290
- columns: [{ label: "Name" }, { label: "Age" }],
1291
- rows: [
1292
- ["Alice", 30],
1293
- ["Bob", 25],
1294
- ],
1295
- });
1296
- const table = card.body[0] as TeamsTable;
1297
- expect(table.firstRowAsHeaders).toBe(true);
1298
- expect(table.columns).toEqual([{ width: 1 }, { width: 1 }]);
1299
- expect(table.rows).toEqual([
1300
- {
1301
- type: "TableRow",
1302
- cells: [
1303
- {
1304
- type: "TableCell",
1305
- items: [{ type: "TextBlock", text: "Name", wrap: true }],
1306
- },
1307
- {
1308
- type: "TableCell",
1309
- items: [{ type: "TextBlock", text: "Age", wrap: true }],
1310
- },
1311
- ],
1312
- },
1313
- {
1314
- type: "TableRow",
1315
- cells: [
1316
- {
1317
- type: "TableCell",
1318
- items: [{ type: "TextBlock", text: "Alice", wrap: true }],
1319
- },
1320
- {
1321
- type: "TableCell",
1322
- items: [{ type: "TextBlock", text: "30", wrap: true }],
1323
- },
1324
- ],
1325
- },
1326
- {
1327
- type: "TableRow",
1328
- cells: [
1329
- {
1330
- type: "TableCell",
1331
- items: [{ type: "TextBlock", text: "Bob", wrap: true }],
1332
- },
1333
- {
1334
- type: "TableCell",
1335
- items: [{ type: "TextBlock", text: "25", wrap: true }],
1336
- },
1337
- ],
1338
- },
1339
- ]);
1340
- });
1341
-
1342
- it("formats table cells from their columns", () => {
1343
- const { card } = toAdaptiveCard({
1344
- $type: "Table",
1345
- columns: [
1346
- { label: "Number", format: { kind: "number", decimals: 1 } },
1347
- {
1348
- label: "Revenue",
1349
- format: { kind: "currency", currency: "USD", decimals: 2 },
1350
- },
1351
- { label: "Share", format: { kind: "percent", decimals: 0 } },
1352
- { label: "Date", format: { kind: "date" } },
1353
- ],
1354
- rows: [[1234.5, 12.4, 0.08, "2024-01-02"]],
1355
- });
1356
- const table = card.body[0] as TeamsTable;
1357
- expect(
1358
- table.rows[1]?.cells.map(
1359
- (cell) => (cell.items[0] as TeamsTextBlock).text,
1360
- ),
1361
- ).toEqual(["1,234.5", "$12.40", "8%", "Jan 2, 2024"]);
1362
- });
1363
-
1364
- it("sets firstRowAsHeaders false and emits no header row when there are no columns", () => {
1365
- const { card } = toAdaptiveCard({
1366
- $type: "Table",
1367
- rows: [["Alice", 30]],
1368
- });
1369
- const table = card.body[0] as TeamsTable;
1370
- expect(table.firstRowAsHeaders).toBe(false);
1371
- expect(table.columns).toEqual([]);
1372
- expect(table.rows).toHaveLength(1);
1373
- });
1374
-
1375
- it(`clamps columns to ${TABLE_COLUMN_CAP} and rows to ${TABLE_ROW_CAP}, warning for each`, () => {
1376
- const columns = Array.from({ length: TABLE_COLUMN_CAP + 5 }, (_, i) => ({
1377
- label: `C${i}`,
1378
- }));
1379
- const rows = Array.from({ length: TABLE_ROW_CAP + 5 }, (_, i) => [
1380
- `R${i}`,
1381
- ]);
1382
- const { card, warnings } = toAdaptiveCard({
1383
- $type: "Table",
1384
- columns,
1385
- rows,
1386
- });
1387
- const table = card.body[0] as TeamsTable;
1388
- expect(table.columns).toHaveLength(TABLE_COLUMN_CAP);
1389
- expect(table.rows).toHaveLength(TABLE_ROW_CAP + 1);
1390
- expect(warnings).toContainEqual({
1391
- code: "clamped",
1392
- component: "Table",
1393
- detail: `columns were clamped to ${TABLE_COLUMN_CAP} entries.`,
1394
- });
1395
- expect(warnings).toContainEqual({
1396
- code: "clamped",
1397
- component: "Table",
1398
- detail: `rows were clamped to ${TABLE_ROW_CAP} entries.`,
1399
- });
1400
- });
1401
-
1402
- it("bounds a hostile Proxy array with a fabricated length instead of hanging", () => {
1403
- const hostileRows = new Proxy([], {
1404
- get(target, prop) {
1405
- if (prop === "length") return 2 ** 31;
1406
- if (typeof prop === "string" && /^\d+$/.test(prop)) return ["x"];
1407
- return Reflect.get(target, prop);
1408
- },
1409
- has(target, prop) {
1410
- if (prop === "length") return true;
1411
- if (typeof prop === "string" && /^\d+$/.test(prop)) return true;
1412
- return Reflect.has(target, prop);
1413
- },
1414
- });
1415
- const { card, warnings } = toAdaptiveCard({
1416
- $type: "Table",
1417
- rows: hostileRows,
1418
- });
1419
- const table = card.body[0] as TeamsTable;
1420
- expect(table.rows).toHaveLength(TABLE_ROW_CAP);
1421
- expect(warnings).toContainEqual({
1422
- code: "clamped",
1423
- component: "Table",
1424
- detail: `rows were clamped to ${TABLE_ROW_CAP} entries.`,
1425
- });
1426
- });
1427
-
1428
- it("does not dispatch a row array through Symbol.species", () => {
1429
- let dispatches = 0;
1430
- const row = hostileSpecies(
1431
- ["kept"],
1432
- ["injected", "also-injected"],
1433
- () => {
1434
- dispatches += 1;
1435
- },
1436
- );
1437
- const { card } = toAdaptiveCard({
1438
- $type: "Table",
1439
- columns: [{ label: "A" }],
1440
- rows: [row],
1441
- });
1442
- const table = card.body[0] as TeamsTable;
1443
- expect(table.rows).toHaveLength(2);
1444
- expect(table.rows[1]?.cells).toHaveLength(1);
1445
- expect(table.rows[1]?.cells[0]?.items).toEqual([
1446
- { type: "TextBlock", text: "kept", wrap: true },
1447
- ]);
1448
- expect(dispatches).toBe(0);
1449
- });
1450
- });
1451
-
1452
- describe("Alert", () => {
1453
- it.each([
1454
- ["info", "accent"],
1455
- ["success", "good"],
1456
- ["warning", "warning"],
1457
- ["danger", "attention"],
1458
- [undefined, "accent"],
1459
- ] as const)("maps tone %s to style %s", (tone, style) => {
1460
- const { card } = toAdaptiveCard({
1461
- $type: "Alert",
1462
- title: "Heads up",
1463
- description: "Details",
1464
- ...(tone !== undefined ? { tone } : {}),
1465
- });
1466
- const container = card.body[0] as TeamsContainer;
1467
- expect(container.style).toBe(style);
1468
- expect(container.items).toEqual([
1469
- { type: "TextBlock", text: "Heads up", wrap: true, weight: "bolder" },
1470
- { type: "TextBlock", text: "Details", wrap: true },
1471
- ]);
1472
- });
1473
- });
1474
-
1475
- describe("Carousel (in body)", () => {
1476
- it("falls back to sequential Card containers with a fallback warning", () => {
1477
- const { card, warnings } = toAdaptiveCard({
1478
- $type: "Col",
1479
- children: {
1480
- $type: "Carousel",
1481
- children: [
1482
- { $type: "Card", title: "A" },
1483
- { $type: "Card", title: "B" },
1484
- ],
1485
- },
1486
- });
1487
- const outer = card.body[0] as TeamsContainer;
1488
- expect(outer.items).toHaveLength(2);
1489
- expect(warnings).toContainEqual(
1490
- expect.objectContaining({ code: "fallback", component: "Carousel" }),
1491
- );
1492
- });
1493
-
1494
- it("bounds a hostile Proxy Carousel children array with a fabricated length instead of hanging", () => {
1495
- const hostileChildren = new Proxy([], {
1496
- get(target, prop) {
1497
- if (prop === "length") return 2 ** 31;
1498
- if (typeof prop === "string" && /^\d+$/.test(prop)) {
1499
- return { $type: "Card", title: "x" };
1500
- }
1501
- return Reflect.get(target, prop);
1502
- },
1503
- has(target, prop) {
1504
- if (prop === "length") return true;
1505
- if (typeof prop === "string" && /^\d+$/.test(prop)) return true;
1506
- return Reflect.has(target, prop);
1507
- },
1508
- });
1509
- const { card, warnings } = toAdaptiveCard({
1510
- $type: "Col",
1511
- children: { $type: "Carousel", children: hostileChildren },
1512
- });
1513
- const outer = card.body[0] as TeamsContainer;
1514
- expect(outer.items.length).toBeLessThanOrEqual(200);
1515
- expect(warnings).toContainEqual(
1516
- expect.objectContaining({ code: "clamped", component: "Root" }),
1517
- );
1518
- });
1519
- });
1520
-
1521
- describe("Chart", () => {
1522
- it("drops the chart and emits an omission note with a warning", () => {
1523
- const { card, warnings } = toAdaptiveCard({
1524
- $type: "Chart",
1525
- variant: "bar",
1526
- data: [{ value: 1 }],
1527
- });
1528
- expect(card.body).toEqual([
1529
- {
1530
- type: "TextBlock",
1531
- text: "Chart omitted on Teams.",
1532
- wrap: true,
1533
- isSubtle: true,
1534
- size: "small",
1535
- },
1536
- ]);
1537
- expect(warnings).toContainEqual({
1538
- code: "dropped",
1539
- component: "Chart",
1540
- detail: "Chart was replaced by a Teams omission note.",
1541
- });
1542
- });
1543
- });
1544
-
1545
- describe("Icon", () => {
1546
- it("drops silently", () => {
1547
- const { card, warnings } = toAdaptiveCard({
1548
- $type: "Icon",
1549
- name: "star",
1550
- });
1551
- expect(card.body).toEqual([]);
1552
- expect(warnings).toEqual([]);
1553
- });
1554
- });
1555
-
1556
- describe("bare strings and numbers", () => {
1557
- it("flattens into wrapped TextBlocks", () => {
1558
- const { card } = toAdaptiveCard(["Hello", 42]);
1559
- expect(card.body).toEqual([
1560
- { type: "TextBlock", text: "Hello", wrap: true },
1561
- { type: "TextBlock", text: "42", wrap: true },
1562
- ]);
1563
- });
1564
- });
1565
-
1566
- describe("root handling", () => {
1567
- it("drops an unknown $type with a warning", () => {
1568
- const { card, warnings } = toAdaptiveCard({
1569
- $type: "Mystery",
1570
- foo: "bar",
1571
- });
1572
- expect(card.body).toEqual([]);
1573
- expect(warnings).toEqual([
1574
- {
1575
- code: "dropped",
1576
- component: "Mystery",
1577
- detail: "Unknown component type was dropped.",
1578
- },
1579
- ]);
1580
- });
1581
-
1582
- it("does not throw on 65 levels of nesting", () => {
1583
- let node: unknown = { $type: "Text", value: "leaf" };
1584
- for (let i = 0; i < 65; i++) {
1585
- node = { $type: "Col", children: node };
1586
- }
1587
- expect(() => toAdaptiveCard(node)).not.toThrow();
1588
- });
1589
-
1590
- it("keeps the leaf at exactly 64 nested Cols", () => {
1591
- let node: unknown = { $type: "Text", value: "leaf" };
1592
- for (let i = 0; i < 64; i++) {
1593
- node = { $type: "Col", children: node };
1594
- }
1595
- const { card } = toAdaptiveCard(node);
1596
- let current: TeamsCardElement | undefined = card.body[0];
1597
- for (let i = 0; i < 63; i++) {
1598
- current = (current as TeamsContainer).items[0];
1599
- }
1600
- expect((current as TeamsContainer).items[0]).toEqual({
1601
- type: "TextBlock",
1602
- text: "leaf",
1603
- wrap: true,
1604
- });
1605
- });
1606
-
1607
- it("drops the leaf past 64 nested Cols", () => {
1608
- let node: unknown = { $type: "Text", value: "leaf" };
1609
- for (let i = 0; i < 65; i++) {
1610
- node = { $type: "Col", children: node };
1611
- }
1612
- const { card } = toAdaptiveCard(node);
1613
- let current: TeamsCardElement | undefined = card.body[0];
1614
- for (let i = 0; i < 64; i++) {
1615
- current = (current as TeamsContainer).items[0];
1616
- }
1617
- expect((current as TeamsContainer).items).toEqual([]);
1618
- });
1619
-
1620
- it(`warns when the serialized card exceeds the ${PAYLOAD_SOFT_CAP}-byte soft cap`, () => {
1621
- const value = "x".repeat(PAYLOAD_SOFT_CAP + 1000);
1622
- const { warnings } = toAdaptiveCard({ $type: "Text", value });
1623
- expect(warnings).toContainEqual(
1624
- expect.objectContaining({
1625
- code: "advisory",
1626
- component: "Root",
1627
- detail: expect.stringContaining(
1628
- `over the ${PAYLOAD_SOFT_CAP}-byte soft budget`,
1629
- ),
1630
- }),
1631
- );
1632
- });
1633
-
1634
- it("measures the soft cap in UTF-8 bytes rather than UTF-16 code units", () => {
1635
- const value = "汉".repeat(30000);
1636
- const { warnings } = toAdaptiveCard({ $type: "Markdown", value });
1637
- expect(warnings).toContainEqual(
1638
- expect.objectContaining({ code: "advisory", component: "Root" }),
1639
- );
1640
- });
1641
-
1642
- it("always includes the fixed card envelope", () => {
1643
- const { card } = toAdaptiveCard({ $type: "Text", value: "x" });
1644
- expect(card.$schema).toBe(
1645
- "http://adaptivecards.io/schemas/adaptive-card.json",
1646
- );
1647
- expect(card.type).toBe("AdaptiveCard");
1648
- expect(card.version).toBe("1.5");
1649
- expect(card.actions).toEqual([]);
1650
- });
1651
-
1652
- it("never throws, even on hostile input", () => {
1653
- const hostile = new Proxy(
1654
- {},
1655
- {
1656
- get() {
1657
- throw new Error("boom");
1658
- },
1659
- },
1660
- );
1661
- expect(() => toAdaptiveCard(hostile)).not.toThrow();
1662
- });
1663
-
1664
- it("bounds a hostile root-level Proxy array with a fabricated length instead of hanging", () => {
1665
- const hostileRoot = new Proxy([], {
1666
- get(target, prop) {
1667
- if (prop === "length") return 2 ** 31;
1668
- if (typeof prop === "string" && /^\d+$/.test(prop)) {
1669
- return { $type: "Text", value: "x" };
1670
- }
1671
- return Reflect.get(target, prop);
1672
- },
1673
- has(target, prop) {
1674
- if (prop === "length") return true;
1675
- if (typeof prop === "string" && /^\d+$/.test(prop)) return true;
1676
- return Reflect.has(target, prop);
1677
- },
1678
- });
1679
- const { card, warnings } = toAdaptiveCard(hostileRoot);
1680
- expect(card.body.length).toBeLessThanOrEqual(200);
1681
- expect(warnings).toContainEqual(
1682
- expect.objectContaining({ code: "clamped", component: "Root" }),
1683
- );
1684
- });
1685
- });
1686
-
1687
- describe("node budget and cycle guard", () => {
1688
- it("bounds a self-referential children array instead of looping forever", () => {
1689
- const arr: unknown[] = [];
1690
- const el = { $type: "Card", children: arr };
1691
- arr.push(el);
1692
- const { warnings } = toAdaptiveCard(el);
1693
- expect(warnings).toContainEqual({
1694
- code: "clamped",
1695
- component: "Root",
1696
- detail: "a self-referencing node was dropped.",
1697
- });
1698
- });
1699
-
1700
- it("bounds a shared-reference fan-out across nested levels with a budget warning instead of doing exponential work", () => {
1701
- let shared: unknown = { $type: "Text", value: "leaf" };
1702
- for (let level = 0; level < 3; level++) {
1703
- shared = {
1704
- $type: "Card",
1705
- children: Array.from({ length: 200 }, () => shared),
1706
- };
1707
- }
1708
- const { warnings } = toAdaptiveCard(shared);
1709
- expect(warnings).toContainEqual({
1710
- code: "clamped",
1711
- component: "Root",
1712
- detail: `the tree was truncated after ${NODE_BUDGET} nodes.`,
1713
- });
1714
- });
1715
-
1716
- it("clamps a benign tree past the node budget with exactly one budget warning", () => {
1717
- const rows = Array.from({ length: 60 }, (_, r) => ({
1718
- $type: "Col",
1719
- children: Array.from({ length: 100 }, (_, c) => ({
1720
- $type: "Caption",
1721
- value: `r${r}c${c}`,
1722
- })),
1723
- }));
1724
- const { card, warnings } = toAdaptiveCard(rows);
1725
- const totalCaptions = card.body.reduce(
1726
- (sum, element) => sum + (element as TeamsContainer).items.length,
1727
- 0,
1728
- );
1729
- expect(totalCaptions).toBeLessThan(6000);
1730
- const budgetWarnings = warnings.filter(
1731
- (warning) =>
1732
- warning.code === "clamped" &&
1733
- warning.component === "Root" &&
1734
- warning.detail ===
1735
- `the tree was truncated after ${NODE_BUDGET} nodes.`,
1736
- );
1737
- expect(budgetWarnings).toHaveLength(1);
1738
- });
1739
-
1740
- it("clamps a shared-reference array past the children cap without treating the reuse as a cycle", () => {
1741
- const card = { $type: "Card", title: "shared" };
1742
- let node: unknown = Array.from({ length: 201 }, () => card);
1743
- for (let level = 0; level < 3; level++) {
1744
- node = { $type: "Col", children: node };
1745
- }
1746
- const { warnings } = toAdaptiveCard(node);
1747
- expect(warnings).toContainEqual({
1748
- code: "clamped",
1749
- component: "Root",
1750
- detail: `children were clamped to ${CHILDREN_CAP} entries.`,
1751
- });
1752
- expect(
1753
- warnings.some((warning) => warning.detail.includes("self-referencing")),
1754
- ).toBe(false);
1755
- });
1756
-
1757
- it("converts a 70-level-deep chain successfully and reports the depth detail", () => {
1758
- let node: unknown = { $type: "Caption", value: "x" };
1759
- for (let i = 0; i < 70; i++) {
1760
- node = { $type: "Card", children: [node] };
1761
- }
1762
- expect(() => toAdaptiveCard(node)).not.toThrow();
1763
- const { warnings } = toAdaptiveCard(node);
1764
- expect(warnings).toContainEqual({
1765
- code: "clamped",
1766
- component: "Root",
1767
- detail: "nodes deeper than 32 levels were dropped.",
1768
- });
1769
- });
1770
-
1771
- it("reports the depth detail at the level it actually starts dropping", () => {
1772
- const chain = (levels: number) => {
1773
- let node: unknown = { $type: "Caption", value: "x" };
1774
- for (let i = 0; i < levels; i++) {
1775
- node = { $type: "Card", children: [node] };
1776
- }
1777
- return node;
1778
- };
1779
- const dropped = (node: unknown) =>
1780
- toAdaptiveCard(node).warnings.some((warning) =>
1781
- warning.detail.includes("deeper than"),
1782
- );
1783
- expect(dropped(chain(32))).toBe(false);
1784
- expect(dropped(chain(33))).toBe(true);
1785
- expect(dropped([chain(31)])).toBe(false);
1786
- expect(dropped([chain(32)])).toBe(true);
1787
- });
1788
- });
1789
- });
1790
-
1791
- describe("toTeamsAttachments", () => {
1792
- it("converts a root Carousel into up to the cap of attachments with a carousel layout", () => {
1793
- const cards = Array.from(
1794
- { length: CAROUSEL_ATTACHMENT_CAP + 2 },
1795
- (_, i) => ({
1796
- $type: "Card",
1797
- title: `Card ${i}`,
1798
- }),
1799
- );
1800
- const { attachments, attachmentLayout, warnings } = toTeamsAttachments({
1801
- $type: "Carousel",
1802
- children: cards,
1803
- });
1804
- expect(attachments).toHaveLength(CAROUSEL_ATTACHMENT_CAP);
1805
- expect(attachmentLayout).toBe("carousel");
1806
- expect(warnings).toContainEqual({
1807
- code: "clamped",
1808
- component: "Carousel",
1809
- detail: `cards were clamped to ${CAROUSEL_ATTACHMENT_CAP} entries.`,
1810
- });
1811
- for (const attachment of attachments) {
1812
- expect(attachment.contentType).toBe(
1813
- "application/vnd.microsoft.card.adaptive",
1814
- );
1815
- expect(attachment.content.type).toBe("AdaptiveCard");
1816
- }
1817
- });
1818
-
1819
- it("routes an unknown child through the normal warning path instead of filtering it silently", () => {
1820
- const { attachments, warnings } = toTeamsAttachments({
1821
- $type: "Carousel",
1822
- children: [{ $type: "Card", title: "Kept" }, { $type: "Mystery" }],
1823
- });
1824
- expect(attachments).toHaveLength(1);
1825
- expect(warnings).toContainEqual({
1826
- code: "dropped",
1827
- component: "Mystery",
1828
- detail: "Unknown component type was dropped.",
1829
- });
1830
- });
1831
-
1832
- it("wraps anything else as a single attachment with no attachmentLayout", () => {
1833
- const { attachments, attachmentLayout, warnings } = toTeamsAttachments({
1834
- $type: "Text",
1835
- value: "Hi",
1836
- });
1837
- expect(attachments).toHaveLength(1);
1838
- expect(attachmentLayout).toBeUndefined();
1839
- expect(attachments[0]?.content.body).toEqual([
1840
- { type: "TextBlock", text: "Hi", wrap: true },
1841
- ]);
1842
- expect(warnings).toEqual([]);
1843
- });
1844
-
1845
- it("never throws, even on hostile input", () => {
1846
- const hostile = new Proxy(
1847
- {},
1848
- {
1849
- get() {
1850
- throw new Error("boom");
1851
- },
1852
- },
1853
- );
1854
- expect(() => toTeamsAttachments(hostile)).not.toThrow();
1855
- });
1856
-
1857
- it("keeps the same unnamed input id across separate carousel cards", () => {
1858
- const { attachments, warnings } = toTeamsAttachments({
1859
- $type: "Carousel",
1860
- children: [
1861
- { $type: "Card", title: "A", children: { $type: "Input" } },
1862
- { $type: "Card", title: "B", children: { $type: "Input" } },
1863
- ],
1864
- });
1865
- const ids = attachments.map((attachment) => {
1866
- const container = attachment.content.body.find(
1867
- (element) => element.type === "Container",
1868
- ) as TeamsContainer;
1869
- const input = container.items.find(
1870
- (item) => item.type === "Input.Text",
1871
- ) as TeamsInputText;
1872
- return input.id;
1873
- });
1874
- expect(ids).toEqual(["input", "input"]);
1875
- expect(
1876
- warnings.some(
1877
- (warning) =>
1878
- warning.code === "clamped" && warning.detail.includes("already used"),
1879
- ),
1880
- ).toBe(false);
1881
- });
1882
- });
1883
-
1884
- it("stringifies boolean table cells", () => {
1885
- const { card } = toAdaptiveCard({
1886
- $type: "Table",
1887
- columns: [{ label: "Flag" }],
1888
- rows: [[true], [false]],
1889
- });
1890
- const table = card.body.find(
1891
- (element) => element.type === "Table",
1892
- ) as Extract<TeamsCardElement, { type: "Table" }>;
1893
- const texts = table.rows
1894
- .slice(1)
1895
- .map((row) => (row.cells[0]!.items[0] as { text: string }).text);
1896
- expect(texts).toEqual(["true", "false"]);
1897
- });