@assistant-ui/react-generative-ui 0.0.6 → 0.0.8

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 (201) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +4 -2
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +9 -2
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.d.ts +8 -1
  8. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  9. package/dist/JSONGenerativeUI.shared.js.map +1 -1
  10. package/dist/actionRegistry.d.ts +32 -0
  11. package/dist/actionRegistry.d.ts.map +1 -0
  12. package/dist/actionRegistry.js +41 -0
  13. package/dist/actionRegistry.js.map +1 -0
  14. package/dist/buildPresentParameters.d.ts +5 -5
  15. package/dist/buildPresentParameters.d.ts.map +1 -1
  16. package/dist/buildPresentParameters.js +20 -6
  17. package/dist/buildPresentParameters.js.map +1 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/defineGenerativeComponents.d.ts +0 -1
  20. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  21. package/dist/generativeUIToJSX.d.ts +15 -10
  22. package/dist/generativeUIToJSX.d.ts.map +1 -1
  23. package/dist/generativeUIToJSX.js +47 -17
  24. package/dist/generativeUIToJSX.js.map +1 -1
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +5 -2
  27. package/dist/index.server.d.ts +13 -0
  28. package/dist/index.server.js +10 -0
  29. package/dist/index.shared.d.ts +11 -0
  30. package/dist/index.shared.js +9 -0
  31. package/dist/ir.d.ts +115 -0
  32. package/dist/ir.d.ts.map +1 -0
  33. package/dist/ir.js +175 -0
  34. package/dist/ir.js.map +1 -0
  35. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  36. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  37. package/dist/renderGenerativeUI.d.ts +5 -5
  38. package/dist/renderGenerativeUI.d.ts.map +1 -1
  39. package/dist/renderGenerativeUI.js +18 -45
  40. package/dist/renderGenerativeUI.js.map +1 -1
  41. package/dist/slack/constants.d.ts +111 -0
  42. package/dist/slack/constants.d.ts.map +1 -0
  43. package/dist/slack/constants.js +168 -0
  44. package/dist/slack/constants.js.map +1 -0
  45. package/dist/slack/decodeBlockAction.d.ts +11 -0
  46. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  47. package/dist/slack/decodeBlockAction.js +40 -0
  48. package/dist/slack/decodeBlockAction.js.map +1 -0
  49. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  50. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  51. package/dist/slack/fromSlackBlocks.js +447 -0
  52. package/dist/slack/fromSlackBlocks.js.map +1 -0
  53. package/dist/slack/toSlackBlocks.d.ts +7 -0
  54. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  55. package/dist/slack/toSlackBlocks.js +649 -0
  56. package/dist/slack/toSlackBlocks.js.map +1 -0
  57. package/dist/slack/types.d.ts +178 -0
  58. package/dist/slack/types.d.ts.map +1 -0
  59. package/dist/slack/types.js +0 -0
  60. package/dist/slack.d.ts +5 -0
  61. package/dist/slack.js +4 -0
  62. package/dist/teams/boundSpec.d.ts +20 -0
  63. package/dist/teams/boundSpec.d.ts.map +1 -0
  64. package/dist/teams/boundSpec.js +67 -0
  65. package/dist/teams/boundSpec.js.map +1 -0
  66. package/dist/teams/constants.d.ts +57 -0
  67. package/dist/teams/constants.d.ts.map +1 -0
  68. package/dist/teams/constants.js +92 -0
  69. package/dist/teams/constants.js.map +1 -0
  70. package/dist/teams/decodeSubmitData.d.ts +25 -0
  71. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  72. package/dist/teams/decodeSubmitData.js +44 -0
  73. package/dist/teams/decodeSubmitData.js.map +1 -0
  74. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  75. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  76. package/dist/teams/toAdaptiveCard.js +496 -0
  77. package/dist/teams/toAdaptiveCard.js.map +1 -0
  78. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  79. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  80. package/dist/teams/toTeamsAttachments.js +95 -0
  81. package/dist/teams/toTeamsAttachments.js.map +1 -0
  82. package/dist/teams/types.d.ts +196 -0
  83. package/dist/teams/types.d.ts.map +1 -0
  84. package/dist/teams/types.js +0 -0
  85. package/dist/teams.d.ts +5 -0
  86. package/dist/teams.js +4 -0
  87. package/dist/types.d.ts +41 -34
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vocabulary/alert.d.ts +28 -0
  90. package/dist/vocabulary/alert.d.ts.map +1 -0
  91. package/dist/vocabulary/alert.js +58 -0
  92. package/dist/vocabulary/alert.js.map +1 -0
  93. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  94. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  95. package/dist/vocabulary/collectFormValues.js +32 -0
  96. package/dist/vocabulary/collectFormValues.js.map +1 -0
  97. package/dist/vocabulary/data.d.ts +52 -0
  98. package/dist/vocabulary/data.d.ts.map +1 -0
  99. package/dist/vocabulary/data.js +294 -0
  100. package/dist/vocabulary/data.js.map +1 -0
  101. package/dist/vocabulary/dispatch.d.ts +11 -0
  102. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  103. package/dist/vocabulary/dispatch.js +21 -0
  104. package/dist/vocabulary/dispatch.js.map +1 -0
  105. package/dist/vocabulary/fact.d.ts +15 -0
  106. package/dist/vocabulary/fact.d.ts.map +1 -0
  107. package/dist/vocabulary/fact.js +24 -0
  108. package/dist/vocabulary/fact.js.map +1 -0
  109. package/dist/vocabulary/form.d.ts +14 -0
  110. package/dist/vocabulary/form.d.ts.map +1 -0
  111. package/dist/vocabulary/form.js +23 -0
  112. package/dist/vocabulary/form.js.map +1 -0
  113. package/dist/vocabulary/icon.d.ts +44 -0
  114. package/dist/vocabulary/icon.d.ts.map +1 -0
  115. package/dist/vocabulary/icon.js +272 -0
  116. package/dist/vocabulary/icon.js.map +1 -0
  117. package/dist/vocabulary/index.d.ts +15 -0
  118. package/dist/vocabulary/index.d.ts.map +1 -0
  119. package/dist/vocabulary/index.js +36 -0
  120. package/dist/vocabulary/index.js.map +1 -0
  121. package/dist/vocabulary/interactive.d.ts +93 -0
  122. package/dist/vocabulary/interactive.d.ts.map +1 -0
  123. package/dist/vocabulary/interactive.js +172 -0
  124. package/dist/vocabulary/interactive.js.map +1 -0
  125. package/dist/vocabulary/layout.d.ts +82 -0
  126. package/dist/vocabulary/layout.d.ts.map +1 -0
  127. package/dist/vocabulary/layout.js +136 -0
  128. package/dist/vocabulary/layout.js.map +1 -0
  129. package/dist/vocabulary/list.d.ts +17 -0
  130. package/dist/vocabulary/list.d.ts.map +1 -0
  131. package/dist/vocabulary/list.js +48 -0
  132. package/dist/vocabulary/list.js.map +1 -0
  133. package/dist/vocabulary/media.d.ts +28 -0
  134. package/dist/vocabulary/media.d.ts.map +1 -0
  135. package/dist/vocabulary/media.js +42 -0
  136. package/dist/vocabulary/media.js.map +1 -0
  137. package/dist/vocabulary/text.d.ts +60 -0
  138. package/dist/vocabulary/text.d.ts.map +1 -0
  139. package/dist/vocabulary/text.js +48 -0
  140. package/dist/vocabulary/text.js.map +1 -0
  141. package/package.json +21 -13
  142. package/src/JSONGenerativeUI.client.tsx +10 -2
  143. package/src/JSONGenerativeUI.shared.ts +8 -0
  144. package/src/actionRegistry.test.ts +90 -0
  145. package/src/actionRegistry.ts +88 -0
  146. package/src/buildPresentParameters.ts +38 -16
  147. package/src/generativeUIToJSX.test.ts +186 -0
  148. package/src/generativeUIToJSX.ts +112 -21
  149. package/src/index.server.ts +2 -0
  150. package/src/index.shared.ts +61 -0
  151. package/src/index.ts +2 -26
  152. package/src/ir.test.ts +203 -0
  153. package/src/ir.ts +279 -0
  154. package/src/renderGenerativeUI.test.tsx +69 -2
  155. package/src/renderGenerativeUI.tsx +49 -82
  156. package/src/slack/constants.ts +217 -0
  157. package/src/slack/decodeBlockAction.test.ts +135 -0
  158. package/src/slack/decodeBlockAction.ts +66 -0
  159. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  160. package/src/slack/fromSlackBlocks.ts +709 -0
  161. package/src/slack/toSlackBlocks.test.ts +1936 -0
  162. package/src/slack/toSlackBlocks.ts +1319 -0
  163. package/src/slack/types.ts +230 -0
  164. package/src/slack.ts +37 -0
  165. package/src/teams/boundSpec.ts +102 -0
  166. package/src/teams/constants.ts +127 -0
  167. package/src/teams/decodeSubmitData.test.ts +148 -0
  168. package/src/teams/decodeSubmitData.ts +57 -0
  169. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  170. package/src/teams/toAdaptiveCard.ts +762 -0
  171. package/src/teams/toTeamsAttachments.test.ts +29 -0
  172. package/src/teams/toTeamsAttachments.ts +137 -0
  173. package/src/teams/types.ts +237 -0
  174. package/src/teams.ts +34 -0
  175. package/src/types.ts +57 -42
  176. package/src/vocabulary/alert.test.tsx +136 -0
  177. package/src/vocabulary/alert.tsx +66 -0
  178. package/src/vocabulary/collectFormValues.test.ts +127 -0
  179. package/src/vocabulary/collectFormValues.ts +58 -0
  180. package/src/vocabulary/data.test.tsx +567 -0
  181. package/src/vocabulary/data.tsx +494 -0
  182. package/src/vocabulary/dispatch.ts +22 -0
  183. package/src/vocabulary/fact.test.tsx +15 -0
  184. package/src/vocabulary/fact.tsx +22 -0
  185. package/src/vocabulary/form.test.tsx +112 -0
  186. package/src/vocabulary/form.tsx +34 -0
  187. package/src/vocabulary/icon.test.tsx +64 -0
  188. package/src/vocabulary/icon.tsx +174 -0
  189. package/src/vocabulary/index.test.ts +63 -0
  190. package/src/vocabulary/index.ts +33 -0
  191. package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
  192. package/src/vocabulary/interactive.test.tsx +248 -0
  193. package/src/vocabulary/interactive.tsx +269 -0
  194. package/src/vocabulary/layout.test.tsx +289 -0
  195. package/src/vocabulary/layout.tsx +229 -0
  196. package/src/vocabulary/list.test.tsx +195 -0
  197. package/src/vocabulary/list.tsx +48 -0
  198. package/src/vocabulary/media.test.tsx +85 -0
  199. package/src/vocabulary/media.tsx +56 -0
  200. package/src/vocabulary/text.test.tsx +73 -0
  201. package/src/vocabulary/text.tsx +59 -0
@@ -0,0 +1,567 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { renderGenerativeUI } from "../renderGenerativeUI";
4
+ import { dataVocabulary } from "./data";
5
+
6
+ const render = (node: unknown) =>
7
+ renderToStaticMarkup(<>{renderGenerativeUI(node, dataVocabulary)}</>);
8
+
9
+ describe("dataVocabulary", () => {
10
+ it("Table renders columns and rows", () => {
11
+ const html = render({
12
+ $type: "Table",
13
+ columns: [{ label: "Name" }, { label: "Age" }],
14
+ rows: [
15
+ ["Ada", 36],
16
+ ["Bob", 24],
17
+ ],
18
+ });
19
+ expect(html).toContain('data-aui="table"');
20
+ expect(html).toContain('<th data-aui="table-col">Name</th>');
21
+ expect(html).toContain('<th data-aui="table-col">Age</th>');
22
+ expect(html).toContain("<td>Ada</td>");
23
+ expect(html).toContain("<td>36</td>");
24
+ expect(html).toContain("<td>Bob</td>");
25
+ expect(html).toContain("<td>24</td>");
26
+ });
27
+
28
+ it("Table renders with only rows (no header)", () => {
29
+ const html = render({ $type: "Table", rows: [["only"]] });
30
+ expect(html).toContain('data-aui="table"');
31
+ expect(html).not.toContain("<thead>");
32
+ expect(html).toContain("<td>only</td>");
33
+ });
34
+
35
+ it("Markdown renders the value in a div", () => {
36
+ expect(render({ $type: "Markdown", value: "# hi" })).toBe(
37
+ '<div data-aui="markdown"># hi</div>',
38
+ );
39
+ });
40
+
41
+ it("Markdown renders partial value while streaming", () => {
42
+ expect(
43
+ renderToStaticMarkup(
44
+ <>
45
+ {renderGenerativeUI(
46
+ { $type: "Markdown", value: "partial" },
47
+ dataVocabulary,
48
+ { status: "streaming" },
49
+ )}
50
+ </>,
51
+ ),
52
+ ).toBe('<div data-aui="markdown">partial</div>');
53
+ });
54
+
55
+ it("Chart bar variant renders one rect per data point", () => {
56
+ const html = render({
57
+ $type: "Chart",
58
+ variant: "bar",
59
+ data: [{ value: 1 }, { value: 2 }, { value: 3 }],
60
+ color: "#f00",
61
+ });
62
+ expect(html).toContain('data-aui="chart"');
63
+ expect(html).toContain('data-aui-variant="bar"');
64
+ expect(html).toContain('data-aui-color="#f00"');
65
+ expect((html.match(/<rect/g) ?? []).length).toBe(3);
66
+ expect(html).not.toContain("<polyline");
67
+ expect(html).not.toContain("data-aui-data");
68
+ });
69
+
70
+ it("Chart line variant renders a single polyline through all points", () => {
71
+ const html = render({
72
+ $type: "Chart",
73
+ variant: "line",
74
+ data: [{ value: 0 }, { value: 20 }, { value: 40 }],
75
+ });
76
+ expect((html.match(/<polyline/g) ?? []).length).toBe(1);
77
+ expect(html).not.toContain("<rect");
78
+ expect(html).toContain('points="0,40 50,20 100,0"');
79
+ });
80
+
81
+ it("Chart line variant with a single data point renders a circle, not a polyline", () => {
82
+ const html = render({
83
+ $type: "Chart",
84
+ variant: "line",
85
+ data: [{ value: 20 }],
86
+ });
87
+ expect(html).not.toContain("<polyline");
88
+ expect((html.match(/<circle/g) ?? []).length).toBe(1);
89
+ expect(html).toContain('cx="50"');
90
+ expect(html).toContain('cy="0"');
91
+ expect(html).toContain('r="2"');
92
+ });
93
+
94
+ it("Chart sparkline variant is structurally identical to line, differentiated by the variant attribute", () => {
95
+ const html = render({
96
+ $type: "Chart",
97
+ variant: "sparkline",
98
+ data: [{ value: 0 }, { value: 20 }, { value: 40 }],
99
+ });
100
+ expect((html.match(/<polyline/g) ?? []).length).toBe(1);
101
+ expect(html).toContain('data-aui-variant="sparkline"');
102
+ expect(html).toContain('points="0,40 50,20 100,0"');
103
+ });
104
+
105
+ it("Chart renders an aria-label describing the variant and point count", () => {
106
+ const html = render({
107
+ $type: "Chart",
108
+ variant: "bar",
109
+ data: [{ value: 1 }, { value: 2 }],
110
+ });
111
+ expect(html).toContain('aria-label="bar chart with 2 data points"');
112
+ });
113
+
114
+ it("Chart with empty data renders an empty svg without throwing", () => {
115
+ expect(() =>
116
+ render({ $type: "Chart", variant: "bar", data: [] }),
117
+ ).not.toThrow();
118
+ const html = render({ $type: "Chart", variant: "line", data: [] });
119
+ expect(html).toContain('data-aui="chart"');
120
+ expect(html).not.toContain("<rect");
121
+ expect(html).not.toContain("<polyline");
122
+ expect(html).toContain('aria-label="line chart with 0 data points"');
123
+ });
124
+
125
+ it("Chart with missing data renders an empty svg without throwing", () => {
126
+ expect(() => render({ $type: "Chart", variant: "bar" })).not.toThrow();
127
+ const html = render({ $type: "Chart", variant: "bar" });
128
+ expect(html).toContain('data-aui="chart"');
129
+ expect(html).not.toContain("<rect");
130
+ });
131
+
132
+ it("Chart clamps negative and non-finite values to 0", () => {
133
+ const html = render({
134
+ $type: "Chart",
135
+ variant: "bar",
136
+ data: [{ value: -5 }, { value: Number.NaN }, { value: 10 }],
137
+ });
138
+ expect((html.match(/<rect/g) ?? []).length).toBe(3);
139
+ expect(html).toContain('height="0"');
140
+ expect(html).toContain('height="40"');
141
+ });
142
+
143
+ it("Chart with all-zero data renders flat baseline marks", () => {
144
+ const barHtml = render({
145
+ $type: "Chart",
146
+ variant: "bar",
147
+ data: [{ value: 0 }, { value: 0 }],
148
+ });
149
+ expect((barHtml.match(/height="0"/g) ?? []).length).toBe(2);
150
+
151
+ const lineHtml = render({
152
+ $type: "Chart",
153
+ variant: "line",
154
+ data: [{ value: 0 }, { value: 0 }, { value: 0 }],
155
+ });
156
+ expect(lineHtml).toContain('points="0,40 50,40 100,40"');
157
+ });
158
+ });
159
+
160
+ describe("dataVocabulary Chart multi-series", () => {
161
+ it("wraps each series' marks in its own data-aui=chart-series group, indexed", () => {
162
+ const html = render({
163
+ $type: "Chart",
164
+ variant: "line",
165
+ series: [
166
+ { label: "A", data: [{ value: 1 }, { value: 2 }] },
167
+ { label: "B", data: [{ value: 3 }, { value: 4 }] },
168
+ { label: "C", data: [{ value: 5 }, { value: 6 }] },
169
+ ],
170
+ });
171
+ expect((html.match(/data-aui="chart-series"/g) ?? []).length).toBe(3);
172
+ expect(html).toContain('data-aui-series="0"');
173
+ expect(html).toContain('data-aui-series="1"');
174
+ expect(html).toContain('data-aui-series="2"');
175
+ expect((html.match(/<polyline/g) ?? []).length).toBe(3);
176
+ });
177
+
178
+ it("falls back to the data prop when series is an empty array", () => {
179
+ const withEmptySeries = render({
180
+ $type: "Chart",
181
+ variant: "bar",
182
+ series: [],
183
+ data: [{ value: 1 }, { value: 2 }],
184
+ });
185
+ const withDataOnly = render({
186
+ $type: "Chart",
187
+ variant: "bar",
188
+ data: [{ value: 1 }, { value: 2 }],
189
+ });
190
+ expect((withEmptySeries.match(/<rect/g) ?? []).length).toBe(2);
191
+ expect(withDataOnly).toContain('data-aui-variant="bar"');
192
+ });
193
+
194
+ it("series takes precedence over data when both are present", () => {
195
+ const html = render({
196
+ $type: "Chart",
197
+ variant: "bar",
198
+ data: [{ value: 999 }],
199
+ series: [{ data: [{ value: 1 }, { value: 2 }] }],
200
+ });
201
+ expect((html.match(/<rect/g) ?? []).length).toBe(2);
202
+ });
203
+
204
+ it("pads a shorter series with 0-valued points instead of throwing or misaligning", () => {
205
+ expect(() =>
206
+ render({
207
+ $type: "Chart",
208
+ variant: "bar",
209
+ series: [
210
+ { data: [{ value: 10 }, { value: 20 }, { value: 30 }] },
211
+ { data: [{ value: 5 }] },
212
+ ],
213
+ }),
214
+ ).not.toThrow();
215
+ const html = render({
216
+ $type: "Chart",
217
+ variant: "bar",
218
+ series: [
219
+ { data: [{ value: 10 }, { value: 20 }, { value: 30 }] },
220
+ { data: [{ value: 5 }] },
221
+ ],
222
+ });
223
+ expect((html.match(/<rect/g) ?? []).length).toBe(6);
224
+ expect(html).toContain('aria-label="bar chart with 3 data points"');
225
+ });
226
+
227
+ it("treats a missing series data array as empty rather than throwing", () => {
228
+ expect(() =>
229
+ render({
230
+ $type: "Chart",
231
+ variant: "bar",
232
+ series: [{ label: "A" }, { data: [{ value: 1 }] }] as never,
233
+ }),
234
+ ).not.toThrow();
235
+ });
236
+ });
237
+
238
+ describe("dataVocabulary Chart stacking", () => {
239
+ it("stacked bars accumulate per x across series (known input)", () => {
240
+ const html = render({
241
+ $type: "Chart",
242
+ variant: "bar",
243
+ stacked: true,
244
+ series: [
245
+ { data: [{ value: 10 }] },
246
+ { data: [{ value: 20 }] },
247
+ { data: [{ value: 30 }] },
248
+ ],
249
+ });
250
+ // total at the single x is 60 -> scaleMax (unstacked axis, plain max) = 60.
251
+ // series 0: height (10/60)*40 ≈ 6.667 at y = 40 - 0 - 6.667 ≈ 33.333
252
+ // series 1: height (20/60)*40 ≈ 13.333 at y = 40 - 6.667 - 13.333 ≈ 20
253
+ // series 2: height (30/60)*40 = 20 at y = 40 - 20 - 20 = 0
254
+ expect(html).toContain('y="33.333333333333336"');
255
+ expect(html).toContain('height="6.666666666666666"');
256
+ expect(html).toContain('y="20.000000000000004"');
257
+ expect(html).toContain('height="13.333333333333332"');
258
+ expect(html).toContain('y="0" width="80" height="20"');
259
+ });
260
+
261
+ it("unstacked (overlaid) bars all start from the baseline regardless of series order", () => {
262
+ const html = render({
263
+ $type: "Chart",
264
+ variant: "bar",
265
+ series: [{ data: [{ value: 10 }] }, { data: [{ value: 20 }] }],
266
+ });
267
+ expect((html.match(/y="0"/g) ?? []).length).toBe(1); // only the tallest bar touches y=0
268
+ });
269
+
270
+ it("stacked areas build cumulative top/bottom polygons per series", () => {
271
+ const html = render({
272
+ $type: "Chart",
273
+ variant: "area",
274
+ stacked: true,
275
+ series: [
276
+ { data: [{ value: 10 }, { value: 10 }] },
277
+ { data: [{ value: 10 }, { value: 10 }] },
278
+ ],
279
+ });
280
+ expect((html.match(/<polygon/g) ?? []).length).toBe(2);
281
+ // series 0 (bottom band): top=10/20*40=20 -> y=20, bottom=0 -> y=40
282
+ expect(html).toContain('points="0,20 100,20 100,40 0,40"');
283
+ // series 1 (top band): top=20/20*40=40 -> y=0, bottom=10/20*40=20 -> y=20
284
+ expect(html).toContain('points="0,0 100,0 100,20 0,20"');
285
+ });
286
+
287
+ it("stacked bars with showAxis keep the bottom segment on the baseline, abut exactly, and share one denominator with the ticks (totals crossing a tick boundary)", () => {
288
+ const html = render({
289
+ $type: "Chart",
290
+ variant: "bar",
291
+ stacked: true,
292
+ showAxis: true,
293
+ series: [
294
+ { data: [{ value: 50 }] },
295
+ { data: [{ value: 30 }] },
296
+ { data: [{ value: 25 }] },
297
+ ],
298
+ });
299
+ // total 105 crosses the 50*10^n boundary, so niceMax must fall back to the
300
+ // next power of ten (200) instead of resolving to an under-sized 100.
301
+ expect(html).toContain("<div>200</div><div>150</div>");
302
+ expect(html).toContain("<div>100</div><div>50</div><div>0</div>");
303
+
304
+ // series 0 (bottom): height=(50/200)*40=10 at y=40-0-10=30; bottom edge
305
+ // y+height=40, the chart baseline.
306
+ expect(html).toContain('y="30" width="80" height="10"');
307
+ // series 1: height=(30/200)*40=6 at y=40-10-6=24; abuts series 0's y=30
308
+ // exactly (24+6=30, no gap or overlap).
309
+ expect(html).toContain('y="24" width="80" height="6"');
310
+ // series 2 (top): height=(25/200)*40=5 at y=40-16-5=19; abuts series 1's
311
+ // y=24 exactly (19+5=24). The stack top (y=19) equals
312
+ // 40-(105/200)*40=19, the same denominator (200) the ticks above use.
313
+ expect(html).toContain('y="19" width="80" height="5"');
314
+ });
315
+
316
+ it("stacked areas keep the bottom band's baseline at the chart bottom regardless of scale", () => {
317
+ const html = render({
318
+ $type: "Chart",
319
+ variant: "area",
320
+ stacked: true,
321
+ showAxis: true,
322
+ series: [
323
+ { data: [{ value: 60 }, { value: 60 }] },
324
+ { data: [{ value: 60 }, { value: 60 }] },
325
+ ],
326
+ });
327
+ // total 120 crosses the same tick boundary (niceMax -> 200); the bottom
328
+ // band's own bottom edge must still sit at the chart baseline (y=40)
329
+ // regardless of scale, since it is built from an all-zero `bottoms` array.
330
+ expect(html).toContain("<div>200</div>");
331
+ expect(html).toContain('points="0,28 100,28 100,40 0,40"');
332
+ });
333
+ });
334
+
335
+ describe("dataVocabulary Chart area variant", () => {
336
+ it("renders a closed filled polygon with fill-opacity and a stroke on top", () => {
337
+ const html = render({
338
+ $type: "Chart",
339
+ variant: "area",
340
+ data: [{ value: 0 }, { value: 40 }],
341
+ });
342
+ expect(html).toContain("<polygon");
343
+ expect(html).toContain('fill="currentColor"');
344
+ expect(html).toContain('fill-opacity="0.25"');
345
+ expect(html).toContain('stroke="currentColor"');
346
+ // top line 0,40 -> 100,0 then closed back down to the baseline in reverse.
347
+ expect(html).toContain('points="0,40 100,0 100,40 0,40"');
348
+ });
349
+
350
+ it("renders a circle instead of a degenerate polygon for a single point", () => {
351
+ const html = render({
352
+ $type: "Chart",
353
+ variant: "area",
354
+ data: [{ value: 20 }],
355
+ });
356
+ expect(html).not.toContain("<polygon");
357
+ expect((html.match(/<circle/g) ?? []).length).toBe(1);
358
+ });
359
+
360
+ it("renders an empty series group without throwing when data is empty", () => {
361
+ expect(() =>
362
+ render({ $type: "Chart", variant: "area", data: [] }),
363
+ ).not.toThrow();
364
+ const html = render({ $type: "Chart", variant: "area", data: [] });
365
+ expect(html).not.toContain("<polygon");
366
+ expect(html).not.toContain("<circle");
367
+ });
368
+ });
369
+
370
+ describe("dataVocabulary Chart showAxis niceMax ticks", () => {
371
+ it("derives a round niceMax and renders 5 ticks from it down to 0", () => {
372
+ const html = render({
373
+ $type: "Chart",
374
+ variant: "bar",
375
+ showAxis: true,
376
+ data: [{ value: 42 }],
377
+ });
378
+ // niceMax(42): exponent=1, candidates 10,20,50 -> 50 is the first >= 42.
379
+ expect(html).toContain('data-aui="chart-frame"');
380
+ expect(html).toContain('data-aui="chart-ticks"');
381
+ const ticksMatch = html.match(
382
+ /<div data-aui="chart-ticks">(.*?)<\/div><svg/,
383
+ );
384
+ expect(ticksMatch).not.toBeNull();
385
+ const ticksHtml = ticksMatch![1]!;
386
+ expect((ticksHtml.match(/<div>/g) ?? []).length).toBe(5);
387
+ expect(ticksHtml).toContain("<div>50</div>");
388
+ expect(ticksHtml).toContain("<div>37.5</div>");
389
+ expect(ticksHtml).toContain("<div>25</div>");
390
+ expect(ticksHtml).toContain("<div>12.5</div>");
391
+ expect(ticksHtml).toContain("<div>0</div>");
392
+ });
393
+
394
+ it("scales marks against niceMax (not the raw max) when the axis is shown", () => {
395
+ const html = render({
396
+ $type: "Chart",
397
+ variant: "bar",
398
+ showAxis: true,
399
+ data: [{ value: 42 }],
400
+ });
401
+ // niceMax = 50, so height = (42/50)*40 = 33.6, not (42/42)*40 = 40.
402
+ expect(html).toContain('height="33.6"');
403
+ });
404
+
405
+ it("stays a bare svg (no frame) when neither showAxis nor showLegend is set, even with new props", () => {
406
+ const html = render({
407
+ $type: "Chart",
408
+ variant: "bar",
409
+ stacked: true,
410
+ series: [{ data: [{ value: 1 }] }],
411
+ });
412
+ expect(html).not.toContain("chart-frame");
413
+ expect(html.startsWith("<svg")).toBe(true);
414
+ });
415
+ });
416
+
417
+ describe("dataVocabulary Chart legend and x-labels", () => {
418
+ it("renders an x-label row from the first series' point labels", () => {
419
+ const html = render({
420
+ $type: "Chart",
421
+ variant: "line",
422
+ showAxis: true,
423
+ data: [
424
+ { label: "Mon", value: 1 },
425
+ { label: "Tue", value: 2 },
426
+ ],
427
+ });
428
+ expect(html).toContain('data-aui="chart-xlabels">');
429
+ expect(html).toContain("<div>Mon</div>");
430
+ expect(html).toContain("<div>Tue</div>");
431
+ });
432
+
433
+ it("omits the legend row when showLegend is not set", () => {
434
+ const html = render({
435
+ $type: "Chart",
436
+ variant: "line",
437
+ showAxis: true,
438
+ series: [{ label: "A", data: [{ value: 1 }] }],
439
+ });
440
+ expect(html).not.toContain("chart-legend");
441
+ });
442
+
443
+ it("renders a legend item per series with an indexed swatch when showLegend is set", () => {
444
+ const html = render({
445
+ $type: "Chart",
446
+ variant: "line",
447
+ showLegend: true,
448
+ series: [
449
+ { label: "Revenue", data: [{ value: 1 }] },
450
+ { label: "Cost", data: [{ value: 2 }] },
451
+ ],
452
+ });
453
+ expect((html.match(/data-aui="chart-legend-item"/g) ?? []).length).toBe(2);
454
+ expect(html).toContain(
455
+ '<span data-aui="chart-legend-item" data-aui-series="0">',
456
+ );
457
+ expect(html).toContain(
458
+ '<span data-aui="chart-legend-item" data-aui-series="1">',
459
+ );
460
+ expect(html).toContain("Revenue");
461
+ expect(html).toContain("Cost");
462
+ expect((html.match(/data-aui="chart-legend-swatch"/g) ?? []).length).toBe(
463
+ 2,
464
+ );
465
+ });
466
+
467
+ it("falls back to a positional label when a series has no label", () => {
468
+ const html = render({
469
+ $type: "Chart",
470
+ variant: "line",
471
+ showLegend: true,
472
+ series: [{ data: [{ value: 1 }] }],
473
+ });
474
+ expect(html).toContain("Series 1");
475
+ });
476
+
477
+ it("area series overlay from the baseline when not stacked", () => {
478
+ const html = render({
479
+ $type: "Chart",
480
+ variant: "area",
481
+ series: [
482
+ { data: [{ value: 10 }, { value: 20 }] },
483
+ { data: [{ value: 30 }, { value: 40 }] },
484
+ ],
485
+ });
486
+ const points = [...html.matchAll(/points="([^"]+)"/g)].map((m) => m[1]!);
487
+ expect(points).toHaveLength(2);
488
+ for (const polygon of points) {
489
+ expect(polygon).toContain("100,40");
490
+ expect(polygon).not.toContain("-");
491
+ }
492
+ expect(points[1]).toContain("100,0");
493
+ });
494
+
495
+ it("area series accumulate when stacked", () => {
496
+ const html = render({
497
+ $type: "Chart",
498
+ variant: "area",
499
+ stacked: true,
500
+ series: [
501
+ { data: [{ value: 10 }, { value: 20 }] },
502
+ { data: [{ value: 30 }, { value: 40 }] },
503
+ ],
504
+ });
505
+ const points = [...html.matchAll(/points="([^"]+)"/g)].map((m) => m[1]!);
506
+ expect(points).toHaveLength(2);
507
+ expect(points[1]).toContain("100,0");
508
+ expect(points[1]).not.toContain("100,40");
509
+ });
510
+
511
+ it("line charts ignore stacked for the scale", () => {
512
+ const html = render({
513
+ $type: "Chart",
514
+ variant: "line",
515
+ stacked: true,
516
+ series: [{ data: [{ value: 100 }] }, { data: [{ value: 100 }] }],
517
+ });
518
+ expect((html.match(/cy="0"/g) ?? []).length).toBe(2);
519
+ });
520
+
521
+ it("cycles series colors past the palette size", () => {
522
+ const html = render({
523
+ $type: "Chart",
524
+ variant: "bar",
525
+ showLegend: true,
526
+ series: Array.from({ length: 6 }, (_, i) => ({
527
+ label: `S${i + 1}`,
528
+ data: [{ value: i + 1 }],
529
+ })),
530
+ });
531
+ expect(html).toContain('<g data-aui="chart-series" data-aui-series="4">');
532
+ expect(
533
+ (html.match(/<g data-aui="chart-series" data-aui-series="0">/g) ?? [])
534
+ .length,
535
+ ).toBe(2);
536
+ expect(
537
+ (
538
+ html.match(
539
+ /<span data-aui="chart-legend-item" data-aui-series="0">/g,
540
+ ) ?? []
541
+ ).length,
542
+ ).toBe(2);
543
+ });
544
+
545
+ it("legend without axis renders no ticks or x labels", () => {
546
+ const html = render({
547
+ $type: "Chart",
548
+ variant: "bar",
549
+ showLegend: true,
550
+ series: [{ label: "A", data: [{ value: 1, label: "Jan" }] }],
551
+ });
552
+ expect(html).toContain('data-aui="chart-legend"');
553
+ expect(html).not.toContain('data-aui="chart-ticks"');
554
+ expect(html).not.toContain('data-aui="chart-xlabels"');
555
+ });
556
+
557
+ it("axis renders ticks and x labels", () => {
558
+ const html = render({
559
+ $type: "Chart",
560
+ variant: "bar",
561
+ showAxis: true,
562
+ series: [{ label: "A", data: [{ value: 1, label: "Jan" }] }],
563
+ });
564
+ expect(html).toContain('data-aui="chart-ticks"');
565
+ expect(html).toContain('data-aui="chart-xlabels"');
566
+ });
567
+ });