@assistant-ui/react-generative-ui 0.0.20 → 0.0.22

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 (189) hide show
  1. package/LICENSE +1 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +30 -13
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/RadioGroupScope.d.ts +6 -0
  6. package/dist/RadioGroupScope.d.ts.map +1 -0
  7. package/dist/RadioGroupScope.js +25 -0
  8. package/dist/RadioGroupScope.js.map +1 -0
  9. package/dist/a2ui/BindingContext.d.ts +11 -0
  10. package/dist/a2ui/BindingContext.d.ts.map +1 -0
  11. package/dist/a2ui/BindingContext.js +12 -0
  12. package/dist/a2ui/BindingContext.js.map +1 -0
  13. package/dist/a2ui/PresentRenderer.d.ts +11 -0
  14. package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
  15. package/dist/a2ui/PresentRenderer.js +39 -0
  16. package/dist/a2ui/PresentRenderer.js.map +1 -0
  17. package/dist/a2ui/convert.d.ts +6 -0
  18. package/dist/a2ui/convert.d.ts.map +1 -1
  19. package/dist/a2ui/convert.js +369 -87
  20. package/dist/a2ui/convert.js.map +1 -1
  21. package/dist/a2ui/dataModel.d.ts +9 -0
  22. package/dist/a2ui/dataModel.d.ts.map +1 -0
  23. package/dist/a2ui/dataModel.js +44 -0
  24. package/dist/a2ui/dataModel.js.map +1 -0
  25. package/dist/a2ui/reducer.d.ts +5 -0
  26. package/dist/a2ui/reducer.d.ts.map +1 -1
  27. package/dist/a2ui/reducer.js +3 -3
  28. package/dist/a2ui/reducer.js.map +1 -1
  29. package/dist/a2ui/types.d.ts +8 -2
  30. package/dist/a2ui/types.d.ts.map +1 -1
  31. package/dist/a2ui/types.js.map +1 -1
  32. package/dist/a2ui/valueFunctions.d.ts +16 -0
  33. package/dist/a2ui/valueFunctions.d.ts.map +1 -0
  34. package/dist/a2ui/valueFunctions.js +332 -0
  35. package/dist/a2ui/valueFunctions.js.map +1 -0
  36. package/dist/actionRegistry.d.ts +1 -1
  37. package/dist/actionRegistry.js.map +1 -1
  38. package/dist/answeredValues.d.ts +16 -0
  39. package/dist/answeredValues.d.ts.map +1 -0
  40. package/dist/answeredValues.js +37 -0
  41. package/dist/answeredValues.js.map +1 -0
  42. package/dist/constants.d.ts +2 -0
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +3 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/fieldReferences.d.ts +9 -0
  47. package/dist/fieldReferences.d.ts.map +1 -0
  48. package/dist/fieldReferences.js +53 -0
  49. package/dist/fieldReferences.js.map +1 -0
  50. package/dist/generativeUIToJSX.js +3 -3
  51. package/dist/generativeUIToJSX.js.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.server.js +2 -2
  54. package/dist/index.shared.js +2 -2
  55. package/dist/ir.d.ts +1 -1
  56. package/dist/ir.js.map +1 -1
  57. package/dist/renderGenerativeUI.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.js +12 -1
  59. package/dist/renderGenerativeUI.js.map +1 -1
  60. package/dist/slack/constants.d.ts +2 -0
  61. package/dist/slack/constants.d.ts.map +1 -1
  62. package/dist/slack/constants.js +3 -1
  63. package/dist/slack/constants.js.map +1 -1
  64. package/dist/slack/decodeBlockAction.d.ts +3 -1
  65. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.js +58 -7
  67. package/dist/slack/decodeBlockAction.js.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  69. package/dist/slack/fromSlackBlocks.js +20 -3
  70. package/dist/slack/fromSlackBlocks.js.map +1 -1
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +187 -19
  73. package/dist/slack/toSlackBlocks.js.map +1 -1
  74. package/dist/slack/types.d.ts +4 -0
  75. package/dist/slack/types.d.ts.map +1 -1
  76. package/dist/teams/constants.d.ts +1 -1
  77. package/dist/teams/constants.d.ts.map +1 -1
  78. package/dist/teams/constants.js +1 -1
  79. package/dist/teams/constants.js.map +1 -1
  80. package/dist/teams/decodeSubmitData.d.ts +4 -0
  81. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  82. package/dist/teams/decodeSubmitData.js +6 -1
  83. package/dist/teams/decodeSubmitData.js.map +1 -1
  84. package/dist/teams/toAdaptiveCard.d.ts +5 -4
  85. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  86. package/dist/teams/toAdaptiveCard.js +56 -17
  87. package/dist/teams/toAdaptiveCard.js.map +1 -1
  88. package/dist/teams/types.d.ts +4 -1
  89. package/dist/teams/types.d.ts.map +1 -1
  90. package/dist/vocabulary/alert.d.ts +9 -2
  91. package/dist/vocabulary/alert.d.ts.map +1 -1
  92. package/dist/vocabulary/alert.js +116 -19
  93. package/dist/vocabulary/alert.js.map +1 -1
  94. package/dist/vocabulary/collectFormValues.d.ts +2 -1
  95. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  96. package/dist/vocabulary/collectFormValues.js +10 -3
  97. package/dist/vocabulary/collectFormValues.js.map +1 -1
  98. package/dist/vocabulary/data.d.ts +22 -5
  99. package/dist/vocabulary/data.d.ts.map +1 -1
  100. package/dist/vocabulary/data.js +278 -83
  101. package/dist/vocabulary/data.js.map +1 -1
  102. package/dist/vocabulary/dispatch.d.ts +3 -2
  103. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  104. package/dist/vocabulary/dispatch.js +33 -4
  105. package/dist/vocabulary/dispatch.js.map +1 -1
  106. package/dist/vocabulary/fact.d.ts +8 -1
  107. package/dist/vocabulary/fact.d.ts.map +1 -1
  108. package/dist/vocabulary/fact.js +37 -11
  109. package/dist/vocabulary/fact.js.map +1 -1
  110. package/dist/vocabulary/form.d.ts.map +1 -1
  111. package/dist/vocabulary/form.js +3 -3
  112. package/dist/vocabulary/form.js.map +1 -1
  113. package/dist/vocabulary/formatValue.d.ts +7 -0
  114. package/dist/vocabulary/formatValue.d.ts.map +1 -0
  115. package/dist/vocabulary/formatValue.js +76 -0
  116. package/dist/vocabulary/formatValue.js.map +1 -0
  117. package/dist/vocabulary/icon.d.ts +1 -1
  118. package/dist/vocabulary/interactive.d.ts +137 -8
  119. package/dist/vocabulary/interactive.d.ts.map +1 -1
  120. package/dist/vocabulary/interactive.js +404 -103
  121. package/dist/vocabulary/interactive.js.map +1 -1
  122. package/dist/vocabulary/layout.d.ts.map +1 -1
  123. package/dist/vocabulary/layout.js +4 -4
  124. package/dist/vocabulary/layout.js.map +1 -1
  125. package/dist/vocabulary/list.js +2 -2
  126. package/dist/vocabulary/list.js.map +1 -1
  127. package/dist/vocabulary/text.d.ts +1 -1
  128. package/package.json +6 -3
  129. package/src/JSONGenerativeUI.client.tsx +84 -40
  130. package/src/JSONGenerativeUI.test.tsx +964 -68
  131. package/src/RadioGroupScope.server.test.tsx +24 -0
  132. package/src/RadioGroupScope.tsx +34 -0
  133. package/src/a2ui/BindingContext.ts +26 -0
  134. package/src/a2ui/PresentRenderer.tsx +83 -0
  135. package/src/a2ui/convert.test.ts +1982 -145
  136. package/src/a2ui/convert.ts +677 -137
  137. package/src/a2ui/dataModel.ts +69 -0
  138. package/src/a2ui/reducer.test.ts +35 -0
  139. package/src/a2ui/reducer.ts +5 -4
  140. package/src/a2ui/types.ts +9 -2
  141. package/src/a2ui/valueFunctions.test.ts +271 -0
  142. package/src/a2ui/valueFunctions.ts +523 -0
  143. package/src/actionRegistry.ts +1 -1
  144. package/src/answeredValues.test.ts +33 -0
  145. package/src/answeredValues.tsx +70 -0
  146. package/src/constants.ts +4 -0
  147. package/src/fieldReferences.test.ts +66 -0
  148. package/src/fieldReferences.ts +83 -0
  149. package/src/generativeUIToJSX.test.ts +18 -0
  150. package/src/generativeUIToJSX.ts +4 -6
  151. package/src/index.server.test.ts +106 -0
  152. package/src/ir.ts +1 -1
  153. package/src/renderGenerativeUI.test.tsx +14 -5
  154. package/src/renderGenerativeUI.tsx +30 -1
  155. package/src/slack/a2uiSlack.test.ts +198 -0
  156. package/src/slack/constants.ts +3 -0
  157. package/src/slack/decodeBlockAction.test.ts +98 -0
  158. package/src/slack/decodeBlockAction.ts +110 -17
  159. package/src/slack/fromSlackBlocks.test.ts +222 -3
  160. package/src/slack/fromSlackBlocks.ts +53 -2
  161. package/src/slack/toSlackBlocks.test.ts +682 -3
  162. package/src/slack/toSlackBlocks.ts +374 -23
  163. package/src/slack/types.ts +4 -0
  164. package/src/teams/constants.ts +1 -1
  165. package/src/teams/decodeSubmitData.test.ts +33 -0
  166. package/src/teams/decodeSubmitData.ts +9 -1
  167. package/src/teams/toAdaptiveCard.test.ts +219 -0
  168. package/src/teams/toAdaptiveCard.ts +93 -19
  169. package/src/teams/types.ts +4 -1
  170. package/src/vocabulary/alert.test.tsx +288 -7
  171. package/src/vocabulary/alert.tsx +168 -26
  172. package/src/vocabulary/collectFormValues.test.ts +28 -1
  173. package/src/vocabulary/collectFormValues.ts +25 -5
  174. package/src/vocabulary/data.test.tsx +248 -6
  175. package/src/vocabulary/data.tsx +398 -100
  176. package/src/vocabulary/dispatch.ts +63 -2
  177. package/src/vocabulary/fact.test.tsx +67 -0
  178. package/src/vocabulary/fact.tsx +40 -9
  179. package/src/vocabulary/form.tsx +7 -2
  180. package/src/vocabulary/formatValue.test.ts +72 -0
  181. package/src/vocabulary/formatValue.ts +127 -0
  182. package/src/vocabulary/index.test.ts +2 -0
  183. package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
  184. package/src/vocabulary/interactive.dom.test.tsx +1699 -0
  185. package/src/vocabulary/interactive.test.tsx +60 -4
  186. package/src/vocabulary/interactive.tsx +770 -151
  187. package/src/vocabulary/layout.test.tsx +6 -4
  188. package/src/vocabulary/layout.tsx +13 -2
  189. package/src/vocabulary/list.tsx +2 -2
@@ -1,3 +1,7 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
1
5
  import { describe, it, expect } from "vitest";
2
6
  import { renderToStaticMarkup } from "react-dom/server";
3
7
  import { renderGenerativeUI } from "../renderGenerativeUI";
@@ -6,6 +10,10 @@ import { dataVocabulary } from "./data";
6
10
  const render = (node: unknown) =>
7
11
  renderToStaticMarkup(<>{renderGenerativeUI(node, dataVocabulary)}</>);
8
12
 
13
+ (
14
+ globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
15
+ ).IS_REACT_ACT_ENVIRONMENT = true;
16
+
9
17
  describe("dataVocabulary", () => {
10
18
  it("Table renders columns and rows", () => {
11
19
  const html = render({
@@ -32,6 +40,109 @@ describe("dataVocabulary", () => {
32
40
  expect(html).toContain("<td>only</td>");
33
41
  });
34
42
 
43
+ it("Table formats typed columns and end-aligns numeric formats", () => {
44
+ const html = render({
45
+ $type: "Table",
46
+ columns: [
47
+ { label: "Number", format: { kind: "number", decimals: 1 } },
48
+ {
49
+ label: "Revenue",
50
+ format: { kind: "currency", currency: "USD", decimals: 2 },
51
+ },
52
+ { label: "Share", format: { kind: "percent", decimals: 0 } },
53
+ { label: "Date", format: { kind: "date" } },
54
+ ],
55
+ rows: [[1234.5, 12.4, 0.08, "2024-01-02"]],
56
+ });
57
+ expect(html).toContain('data-aui-align="end">Number</th>');
58
+ expect(html).toContain('data-aui-align="end">Revenue</th>');
59
+ expect(html).toContain('data-aui-align="end">Share</th>');
60
+ expect(html).toContain('<td data-aui-align="end">1,234.5</td>');
61
+ expect(html).toContain('<td data-aui-align="end">$12.40</td>');
62
+ expect(html).toContain('<td data-aui-align="end">8%</td>');
63
+ expect(html).toContain("<td>Jan 2, 2024</td>");
64
+ });
65
+
66
+ it("Table keeps wrong-typed formatted cells as raw text", () => {
67
+ const html = render({
68
+ $type: "Table",
69
+ columns: [
70
+ { label: "Number", format: { kind: "number" } },
71
+ { label: "Date", format: { kind: "date" } },
72
+ {
73
+ label: "Currency",
74
+ format: { kind: "currency", currency: "not-a-currency" },
75
+ },
76
+ ],
77
+ rows: [["not a number", "not a date", 12]],
78
+ });
79
+ expect(html).toContain('<td data-aui-align="end">not a number</td>');
80
+ expect(html).toContain("<td>not a date</td>");
81
+ expect(html).toContain('<td data-aui-align="end">12</td>');
82
+ });
83
+
84
+ it("Table sorts through ascending, descending, and original row order", async () => {
85
+ const container = document.createElement("div");
86
+ document.body.append(container);
87
+ let root: Root | undefined;
88
+ try {
89
+ root = createRoot(container);
90
+ await act(async () => {
91
+ root!.render(
92
+ renderGenerativeUI(
93
+ {
94
+ $type: "Table",
95
+ sortable: true,
96
+ columns: [
97
+ { label: "Name" },
98
+ { label: "Score", format: { kind: "number" } },
99
+ ],
100
+ rows: [
101
+ ["Item 10", 2],
102
+ ["Item 2", 10],
103
+ ["Item 1", 1],
104
+ ],
105
+ },
106
+ dataVocabulary,
107
+ ),
108
+ );
109
+ });
110
+
111
+ const headers = container.querySelectorAll("th");
112
+ const nameButton = headers[0]?.querySelector("button");
113
+ const scoreButton = headers[1]?.querySelector("button");
114
+ const rowNames = () =>
115
+ Array.from(container.querySelectorAll("tbody tr")).map(
116
+ (row) => row.querySelector("td")?.textContent,
117
+ );
118
+ expect(headers[0]?.hasAttribute("aria-sort")).toBe(false);
119
+ expect(headers[1]?.hasAttribute("aria-sort")).toBe(false);
120
+
121
+ await act(async () => scoreButton?.click());
122
+ expect(headers[0]?.hasAttribute("aria-sort")).toBe(false);
123
+ expect(headers[1]?.getAttribute("aria-sort")).toBe("ascending");
124
+ expect(scoreButton?.textContent).toBe("Score↑");
125
+ expect(rowNames()).toEqual(["Item 1", "Item 10", "Item 2"]);
126
+
127
+ await act(async () => scoreButton?.click());
128
+ expect(headers[1]?.getAttribute("aria-sort")).toBe("descending");
129
+ expect(scoreButton?.textContent).toBe("Score↓");
130
+ expect(rowNames()).toEqual(["Item 2", "Item 10", "Item 1"]);
131
+
132
+ await act(async () => scoreButton?.click());
133
+ expect(headers[1]?.hasAttribute("aria-sort")).toBe(false);
134
+ expect(scoreButton?.textContent).toBe("Score");
135
+ expect(rowNames()).toEqual(["Item 10", "Item 2", "Item 1"]);
136
+
137
+ await act(async () => nameButton?.click());
138
+ expect(headers[0]?.getAttribute("aria-sort")).toBe("ascending");
139
+ expect(rowNames()).toEqual(["Item 1", "Item 2", "Item 10"]);
140
+ } finally {
141
+ await act(async () => root?.unmount());
142
+ container.remove();
143
+ }
144
+ });
145
+
35
146
  it("Table ignores malformed collections instead of throwing", () => {
36
147
  expect(
37
148
  render({
@@ -125,7 +236,8 @@ describe("dataVocabulary", () => {
125
236
  data: [{ value: 20 }],
126
237
  });
127
238
  expect(html).not.toContain("<polyline");
128
- expect((html.match(/<circle/g) ?? []).length).toBe(1);
239
+ expect((html.match(/<circle/g) ?? []).length).toBe(2);
240
+ expect(html).toContain('data-aui="chart-point"');
129
241
  expect(html).toContain('cx="50"');
130
242
  expect(html).toContain('cy="0"');
131
243
  expect(html).toContain('r="2"');
@@ -169,15 +281,38 @@ describe("dataVocabulary", () => {
169
281
  expect(html).not.toContain("<rect");
170
282
  });
171
283
 
172
- it("Chart clamps negative and non-finite values to 0", () => {
284
+ it("Chart maps negative bars from the zero line and non-finite values to 0", () => {
173
285
  const html = render({
174
286
  $type: "Chart",
175
287
  variant: "bar",
176
- data: [{ value: -5 }, { value: Number.NaN }, { value: 10 }],
288
+ data: [
289
+ { label: "Loss", value: -10 },
290
+ { value: Number.NaN },
291
+ { label: "Gain", value: 10 },
292
+ ],
177
293
  });
178
294
  expect((html.match(/<rect/g) ?? []).length).toBe(3);
295
+ expect(html).toContain('data-aui="chart-zero"');
296
+ expect(html).toContain('y1="20" y2="20"');
297
+ expect(html).toMatch(/<rect[^>]*y="20"[^>]*height="20"/);
179
298
  expect(html).toContain('height="0"');
180
- expect(html).toContain('height="40"');
299
+ expect(html).toContain("<title>Loss: -10</title>");
300
+ expect(html).toContain("<title>2: 0</title>");
301
+ });
302
+
303
+ it("Chart omits the zero baseline when values stay on one side of zero", () => {
304
+ const positive = render({
305
+ $type: "Chart",
306
+ variant: "bar",
307
+ data: [{ value: 1 }],
308
+ });
309
+ const negative = render({
310
+ $type: "Chart",
311
+ variant: "bar",
312
+ data: [{ value: -1 }],
313
+ });
314
+ expect(positive).not.toContain('data-aui="chart-zero"');
315
+ expect(negative).not.toContain('data-aui="chart-zero"');
181
316
  });
182
317
 
183
318
  it("Chart with all-zero data renders flat baseline marks", () => {
@@ -307,6 +442,22 @@ describe("dataVocabulary Chart stacking", () => {
307
442
  expect((html.match(/y="0"/g) ?? []).length).toBe(1); // only the tallest bar touches y=0
308
443
  });
309
444
 
445
+ it("stacks positive and negative bars independently from zero", () => {
446
+ const html = render({
447
+ $type: "Chart",
448
+ variant: "bar",
449
+ stacked: true,
450
+ series: [
451
+ { data: [{ value: -1 }, { value: 1 }] },
452
+ { data: [{ value: -1 }, { value: 1 }] },
453
+ ],
454
+ });
455
+ expect(html).toContain('x="5" y="20" width="40" height="10"');
456
+ expect(html).toContain('x="5" y="30" width="40" height="10"');
457
+ expect(html).toContain('x="55" y="10" width="40" height="10"');
458
+ expect(html).toContain('x="55" y="0" width="40" height="10"');
459
+ });
460
+
310
461
  it("stacked areas build cumulative top/bottom polygons per series", () => {
311
462
  const html = render({
312
463
  $type: "Chart",
@@ -324,6 +475,20 @@ describe("dataVocabulary Chart stacking", () => {
324
475
  expect(html).toContain('points="0,0 100,0 100,20 0,20"');
325
476
  });
326
477
 
478
+ it("stacks positive and negative areas independently from zero", () => {
479
+ const html = render({
480
+ $type: "Chart",
481
+ variant: "area",
482
+ stacked: true,
483
+ series: [
484
+ { data: [{ value: -1 }, { value: 1 }] },
485
+ { data: [{ value: -1 }, { value: 1 }] },
486
+ ],
487
+ });
488
+ expect(html).toContain('points="0,30 100,10 100,20 0,20"');
489
+ expect(html).toContain('points="0,40 100,0 100,10 0,30"');
490
+ });
491
+
327
492
  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)", () => {
328
493
  const html = render({
329
494
  $type: "Chart",
@@ -392,6 +557,21 @@ describe("dataVocabulary Chart area variant", () => {
392
557
  expect(html).toMatch(/<polyline[^>]*vector-effect="non-scaling-stroke"/);
393
558
  });
394
559
 
560
+ it("fills negative and positive values to the zero line", () => {
561
+ const html = render({
562
+ $type: "Chart",
563
+ variant: "area",
564
+ data: [
565
+ { label: "Loss", value: -10 },
566
+ { label: "Gain", value: 10 },
567
+ ],
568
+ });
569
+ expect(html).toContain('data-aui="chart-zero"');
570
+ expect(html).toContain('points="0,40 100,0 100,20 0,20"');
571
+ expect((html.match(/data-aui="chart-point"/g) ?? []).length).toBe(2);
572
+ expect(html).toContain("<title>Loss: -10</title>");
573
+ });
574
+
395
575
  it("renders a circle instead of a degenerate polygon for a single point", () => {
396
576
  const html = render({
397
577
  $type: "Chart",
@@ -399,7 +579,7 @@ describe("dataVocabulary Chart area variant", () => {
399
579
  data: [{ value: 20 }],
400
580
  });
401
581
  expect(html).not.toContain("<polygon");
402
- expect((html.match(/<circle/g) ?? []).length).toBe(1);
582
+ expect((html.match(/<circle/g) ?? []).length).toBe(2);
403
583
  });
404
584
 
405
585
  it("renders an empty series group without throwing when data is empty", () => {
@@ -447,6 +627,21 @@ describe("dataVocabulary Chart showAxis niceMax ticks", () => {
447
627
  expect(html).toContain('height="33.6"');
448
628
  });
449
629
 
630
+ it("uses round ticks that include 0 for a domain crossing zero", () => {
631
+ const html = render({
632
+ $type: "Chart",
633
+ variant: "line",
634
+ showAxis: true,
635
+ data: [{ value: -3 }, { value: 5 }],
636
+ });
637
+ expect(html).toContain("<div>10</div>");
638
+ expect(html).toContain("<div>5</div>");
639
+ expect(html).toContain("<div>0</div>");
640
+ expect(html).toContain("<div>-5</div>");
641
+ expect(html).toContain("<div>-10</div>");
642
+ expect(html).toContain('data-aui="chart-zero"');
643
+ });
644
+
450
645
  it("stays a bare svg (no frame) when neither showAxis nor showLegend is set, even with new props", () => {
451
646
  const html = render({
452
647
  $type: "Chart",
@@ -509,6 +704,53 @@ describe("dataVocabulary Chart legend and x-labels", () => {
509
704
  );
510
705
  });
511
706
 
707
+ it("adds titles to line points with and without a series label", () => {
708
+ const standalone = render({
709
+ $type: "Chart",
710
+ variant: "line",
711
+ data: [{ label: "Jan", value: 1.5 }, { value: 2 }],
712
+ });
713
+ const named = render({
714
+ $type: "Chart",
715
+ variant: "line",
716
+ series: [{ label: "Revenue", data: [{ label: "Jan", value: 1.5 }] }],
717
+ });
718
+ expect(standalone).toContain("<title>Jan: 1.5</title>");
719
+ expect(standalone).toContain("<title>2: 2</title>");
720
+ expect(named).toContain("<title>Revenue, Jan: 1.5</title>");
721
+ });
722
+
723
+ it("emits per-series colors on chart series and legend items", () => {
724
+ const html = render({
725
+ $type: "Chart",
726
+ variant: "bar",
727
+ showLegend: true,
728
+ series: [
729
+ { label: "Revenue", color: "white", data: [{ value: 1 }] },
730
+ { label: "Cost", color: "alpha-70", data: [{ value: 2 }] },
731
+ ],
732
+ });
733
+ expect(html).toContain(
734
+ '<g data-aui="chart-series" data-aui-series="0" data-aui-color="white">',
735
+ );
736
+ expect(html).toContain(
737
+ '<span data-aui="chart-legend-item" data-aui-series="1" data-aui-color="alpha-70">',
738
+ );
739
+ });
740
+
741
+ it("uses the single-series color for its legend swatch", () => {
742
+ const html = render({
743
+ $type: "Chart",
744
+ variant: "bar",
745
+ color: "white",
746
+ showLegend: true,
747
+ data: [{ value: 1 }],
748
+ });
749
+ expect(html).toContain(
750
+ '<span data-aui="chart-legend-item" data-aui-series="0" data-aui-color="white"><span data-aui="chart-legend-swatch"></span>',
751
+ );
752
+ });
753
+
512
754
  it("falls back to a positional label when a series has no label", () => {
513
755
  const html = render({
514
756
  $type: "Chart",
@@ -564,7 +806,7 @@ describe("dataVocabulary Chart legend and x-labels", () => {
564
806
  stacked: true,
565
807
  series: [{ data: [{ value: 100 }] }, { data: [{ value: 100 }] }],
566
808
  });
567
- expect((html.match(/cy="0"/g) ?? []).length).toBe(2);
809
+ expect((html.match(/cy="0"/g) ?? []).length).toBe(4);
568
810
  });
569
811
 
570
812
  it("cycles series colors past the palette size", () => {