@assistant-ui/react-generative-ui 0.0.21 → 0.0.23
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.
- package/LICENSE +1 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +30 -13
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/RadioGroupScope.d.ts +6 -0
- package/dist/RadioGroupScope.d.ts.map +1 -0
- package/dist/RadioGroupScope.js +25 -0
- package/dist/RadioGroupScope.js.map +1 -0
- package/dist/a2ui/BindingContext.d.ts +11 -0
- package/dist/a2ui/BindingContext.d.ts.map +1 -0
- package/dist/a2ui/BindingContext.js +12 -0
- package/dist/a2ui/BindingContext.js.map +1 -0
- package/dist/a2ui/PresentRenderer.d.ts +11 -0
- package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
- package/dist/a2ui/PresentRenderer.js +39 -0
- package/dist/a2ui/PresentRenderer.js.map +1 -0
- package/dist/a2ui/convert.d.ts +6 -0
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +363 -85
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/dataModel.d.ts +9 -0
- package/dist/a2ui/dataModel.d.ts.map +1 -0
- package/dist/a2ui/dataModel.js +44 -0
- package/dist/a2ui/dataModel.js.map +1 -0
- package/dist/a2ui/reducer.d.ts +5 -0
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +3 -3
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/types.d.ts +8 -2
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/a2ui/types.js.map +1 -1
- package/dist/a2ui/valueFunctions.d.ts +16 -0
- package/dist/a2ui/valueFunctions.d.ts.map +1 -0
- package/dist/a2ui/valueFunctions.js +332 -0
- package/dist/a2ui/valueFunctions.js.map +1 -0
- package/dist/actionRegistry.d.ts +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/answeredValues.d.ts +16 -0
- package/dist/answeredValues.d.ts.map +1 -0
- package/dist/answeredValues.js +37 -0
- package/dist/answeredValues.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -1
- package/dist/constants.js.map +1 -1
- package/dist/fieldReferences.d.ts +9 -0
- package/dist/fieldReferences.d.ts.map +1 -0
- package/dist/fieldReferences.js +53 -0
- package/dist/fieldReferences.js.map +1 -0
- package/dist/generativeUIToJSX.js +3 -3
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.server.js +2 -2
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +12 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +2 -0
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +3 -1
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +3 -1
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.js +96 -7
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +43 -3
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +244 -20
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +24 -1
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +1 -1
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +1 -1
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +4 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.js +29 -3
- package/dist/teams/decodeSubmitData.js.map +1 -1
- package/dist/teams/temporalId.d.ts +5 -0
- package/dist/teams/temporalId.d.ts.map +1 -0
- package/dist/teams/temporalId.js +28 -0
- package/dist/teams/temporalId.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +5 -4
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +136 -18
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/teams/types.d.ts +23 -2
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/teams.d.ts +1 -1
- package/dist/teams.d.ts.map +1 -1
- package/dist/temporal.d.ts +33 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +152 -0
- package/dist/temporal.js.map +1 -0
- package/dist/vocabulary/alert.d.ts +9 -2
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +116 -19
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +2 -1
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +15 -4
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +22 -5
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +278 -83
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +3 -2
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/dispatch.js +33 -4
- package/dist/vocabulary/dispatch.js.map +1 -1
- package/dist/vocabulary/fact.d.ts +8 -1
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js +37 -11
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/form.js +3 -3
- package/dist/vocabulary/form.js.map +1 -1
- package/dist/vocabulary/formatValue.d.ts +7 -0
- package/dist/vocabulary/formatValue.d.ts.map +1 -0
- package/dist/vocabulary/formatValue.js +76 -0
- package/dist/vocabulary/formatValue.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +1 -1
- package/dist/vocabulary/interactive.d.ts +143 -8
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +458 -107
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +4 -4
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.js +2 -2
- package/dist/vocabulary/list.js.map +1 -1
- package/dist/vocabulary/text.d.ts +1 -1
- package/package.json +8 -5
- package/src/JSONGenerativeUI.client.tsx +84 -40
- package/src/JSONGenerativeUI.test.tsx +964 -68
- package/src/RadioGroupScope.server.test.tsx +24 -0
- package/src/RadioGroupScope.tsx +34 -0
- package/src/a2ui/BindingContext.ts +26 -0
- package/src/a2ui/PresentRenderer.tsx +83 -0
- package/src/a2ui/convert.test.ts +2054 -229
- package/src/a2ui/convert.ts +665 -134
- package/src/a2ui/dataModel.ts +69 -0
- package/src/a2ui/reducer.test.ts +35 -0
- package/src/a2ui/reducer.ts +5 -4
- package/src/a2ui/types.ts +9 -2
- package/src/a2ui/valueFunctions.test.ts +271 -0
- package/src/a2ui/valueFunctions.ts +523 -0
- package/src/actionRegistry.ts +1 -1
- package/src/answeredValues.test.ts +33 -0
- package/src/answeredValues.tsx +70 -0
- package/src/constants.ts +5 -0
- package/src/fieldReferences.test.ts +66 -0
- package/src/fieldReferences.ts +83 -0
- package/src/generativeUIToJSX.test.ts +18 -0
- package/src/generativeUIToJSX.ts +4 -6
- package/src/index.server.test.ts +106 -0
- package/src/ir.ts +1 -1
- package/src/renderGenerativeUI.test.tsx +14 -5
- package/src/renderGenerativeUI.tsx +30 -1
- package/src/slack/a2uiSlack.test.ts +198 -0
- package/src/slack/constants.ts +3 -0
- package/src/slack/datePicker.test.ts +612 -0
- package/src/slack/decodeBlockAction.test.ts +98 -0
- package/src/slack/decodeBlockAction.ts +182 -16
- package/src/slack/fromSlackBlocks.test.ts +222 -3
- package/src/slack/fromSlackBlocks.ts +82 -2
- package/src/slack/toSlackBlocks.test.ts +682 -3
- package/src/slack/toSlackBlocks.ts +463 -25
- package/src/slack/types.ts +28 -0
- package/src/slack.ts +2 -0
- package/src/teams/constants.ts +1 -1
- package/src/teams/decodeSubmitData.test.ts +249 -0
- package/src/teams/decodeSubmitData.ts +57 -4
- package/src/teams/temporal.test.ts +766 -0
- package/src/teams/temporalId.ts +56 -0
- package/src/teams/toAdaptiveCard.test.ts +219 -0
- package/src/teams/toAdaptiveCard.ts +259 -21
- package/src/teams/toTeamsAttachments.ts +4 -1
- package/src/teams/types.ts +21 -1
- package/src/teams.ts +1 -0
- package/src/temporal.test.ts +319 -0
- package/src/temporal.ts +259 -0
- package/src/vocabulary/alert.test.tsx +288 -7
- package/src/vocabulary/alert.tsx +168 -26
- package/src/vocabulary/collectFormValues.test.ts +225 -1
- package/src/vocabulary/collectFormValues.ts +54 -5
- package/src/vocabulary/data.test.tsx +248 -6
- package/src/vocabulary/data.tsx +398 -100
- package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
- package/src/vocabulary/dispatch.ts +63 -2
- package/src/vocabulary/fact.test.tsx +67 -0
- package/src/vocabulary/fact.tsx +40 -9
- package/src/vocabulary/form.tsx +7 -2
- package/src/vocabulary/formatValue.test.ts +72 -0
- package/src/vocabulary/formatValue.ts +127 -0
- package/src/vocabulary/index.test.ts +2 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
- package/src/vocabulary/interactive.dom.test.tsx +1747 -0
- package/src/vocabulary/interactive.test.tsx +60 -4
- package/src/vocabulary/interactive.tsx +910 -155
- package/src/vocabulary/layout.test.tsx +6 -4
- package/src/vocabulary/layout.tsx +13 -2
- package/src/vocabulary/list.tsx +2 -2
|
@@ -5,7 +5,16 @@ import {
|
|
|
5
5
|
collectFormValues,
|
|
6
6
|
type FormControlElementLike,
|
|
7
7
|
} from "./collectFormValues";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
CHECKBOX_GROUP_ATTR,
|
|
10
|
+
FIELD_VALUE_ATTR,
|
|
11
|
+
GENERATED_NAME_ATTR,
|
|
12
|
+
} from "../constants";
|
|
13
|
+
import {
|
|
14
|
+
normalizeTemporalInputValue,
|
|
15
|
+
toLocalDateTime,
|
|
16
|
+
toPickerLocalDateTime,
|
|
17
|
+
} from "../temporal";
|
|
9
18
|
|
|
10
19
|
const el = (
|
|
11
20
|
partial: Partial<FormControlElementLike>,
|
|
@@ -19,6 +28,194 @@ const el = (
|
|
|
19
28
|
});
|
|
20
29
|
|
|
21
30
|
describe("collectFormValues", () => {
|
|
31
|
+
it.each([
|
|
32
|
+
["time", "13:45:30.500", "13:45:30.5"],
|
|
33
|
+
["time", "13:45:30.000", "13:45:30"],
|
|
34
|
+
["datetime-local", "2026-07-16T09:45:30.500", "2026-07-16T09:45:30.5"],
|
|
35
|
+
["datetime-local", "2026-07-16T09:45:30.000", "2026-07-16T09:45:30"],
|
|
36
|
+
])("collects an edited %s in shortest form", (type, value, expected) => {
|
|
37
|
+
expect(collectFormValues([el({ name: "when", type, value })])).toEqual({
|
|
38
|
+
when: expected,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("retains an untouched instant's offset spelling and precision", () => {
|
|
43
|
+
const anchor = "2025-12-15T17:00:00.5Z";
|
|
44
|
+
const displayed = toPickerLocalDateTime(anchor);
|
|
45
|
+
const getAttribute = (name: string) =>
|
|
46
|
+
name === FIELD_VALUE_ATTR ? anchor : null;
|
|
47
|
+
expect(
|
|
48
|
+
collectFormValues([
|
|
49
|
+
el({
|
|
50
|
+
name: "when",
|
|
51
|
+
type: "datetime-local",
|
|
52
|
+
value: displayed,
|
|
53
|
+
getAttribute,
|
|
54
|
+
}),
|
|
55
|
+
]),
|
|
56
|
+
).toEqual({ when: "2025-12-15T17:00:00.5Z" });
|
|
57
|
+
expect(
|
|
58
|
+
collectFormValues([
|
|
59
|
+
el({
|
|
60
|
+
name: "when",
|
|
61
|
+
type: "datetime-local",
|
|
62
|
+
value: displayed.replace(".500", ".5"),
|
|
63
|
+
getAttribute,
|
|
64
|
+
}),
|
|
65
|
+
]),
|
|
66
|
+
).toEqual({ when: "2025-12-15T17:00:00.5Z" });
|
|
67
|
+
expect(
|
|
68
|
+
collectFormValues([
|
|
69
|
+
el({
|
|
70
|
+
name: "when",
|
|
71
|
+
type: "datetime-local",
|
|
72
|
+
value: displayed.replace(".500", ".750"),
|
|
73
|
+
getAttribute,
|
|
74
|
+
}),
|
|
75
|
+
]),
|
|
76
|
+
).toEqual({ when: "2025-12-15T17:00:00.75Z" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses an instant anchor only for datetime-local controls", () => {
|
|
80
|
+
expect(
|
|
81
|
+
collectFormValues([
|
|
82
|
+
el({
|
|
83
|
+
name: "canonical",
|
|
84
|
+
type: "datetime-local",
|
|
85
|
+
value: normalizeTemporalInputValue(
|
|
86
|
+
toLocalDateTime("2025-12-15T17:00:00Z"),
|
|
87
|
+
),
|
|
88
|
+
getAttribute: (name) =>
|
|
89
|
+
name === FIELD_VALUE_ATTR ? "2025-12-15T17:00:00Z" : null,
|
|
90
|
+
}),
|
|
91
|
+
el({
|
|
92
|
+
name: "ordinary",
|
|
93
|
+
value: "live",
|
|
94
|
+
getAttribute: (name) => (name === FIELD_VALUE_ATTR ? "stale" : null),
|
|
95
|
+
}),
|
|
96
|
+
]),
|
|
97
|
+
).toEqual({ canonical: "2025-12-15T17:00:00Z", ordinary: "live" });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("uses the declared datetime-local type when the browser reports text", () => {
|
|
101
|
+
const anchor = "2025-12-15T17:00:00.250Z";
|
|
102
|
+
const displayed = toPickerLocalDateTime(anchor);
|
|
103
|
+
const getAttribute = (name: string) =>
|
|
104
|
+
name === "type"
|
|
105
|
+
? "datetime-local"
|
|
106
|
+
: name === FIELD_VALUE_ATTR
|
|
107
|
+
? anchor
|
|
108
|
+
: null;
|
|
109
|
+
const control = (value: string) =>
|
|
110
|
+
el({ name: "when", type: "text", value, getAttribute });
|
|
111
|
+
expect(collectFormValues([control(displayed)])).toEqual({
|
|
112
|
+
when: anchor,
|
|
113
|
+
});
|
|
114
|
+
expect(
|
|
115
|
+
collectFormValues([control(displayed.replace(".250", ".750"))]),
|
|
116
|
+
).toEqual({
|
|
117
|
+
when: "2025-12-15T17:00:00.750Z",
|
|
118
|
+
});
|
|
119
|
+
expect(collectFormValues([control(displayed.slice(0, -4))])).toEqual({
|
|
120
|
+
when: "2025-12-15T17:00:00.000Z",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("uses the declared time type when the browser reports text", () => {
|
|
125
|
+
expect(
|
|
126
|
+
collectFormValues([
|
|
127
|
+
el({
|
|
128
|
+
name: "when",
|
|
129
|
+
type: "text",
|
|
130
|
+
value: "12:00",
|
|
131
|
+
getAttribute: (name) =>
|
|
132
|
+
name === "type"
|
|
133
|
+
? "time"
|
|
134
|
+
: name === FIELD_VALUE_ATTR
|
|
135
|
+
? "12:00:00"
|
|
136
|
+
: null,
|
|
137
|
+
}),
|
|
138
|
+
]),
|
|
139
|
+
).toEqual({ when: "12:00:00" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it.each([
|
|
143
|
+
["time", "17:00:00", "17:00", "08:30:45"],
|
|
144
|
+
[
|
|
145
|
+
"datetime-local",
|
|
146
|
+
"2025-12-15T17:00:00",
|
|
147
|
+
"2025-12-15T17:00",
|
|
148
|
+
"2025-12-16T08:30:45",
|
|
149
|
+
],
|
|
150
|
+
])(
|
|
151
|
+
"uses the canonical %s value only while its projection matches",
|
|
152
|
+
(type, canonical, displayed, edited) => {
|
|
153
|
+
const getAttribute = (name: string) =>
|
|
154
|
+
name === FIELD_VALUE_ATTR ? canonical : null;
|
|
155
|
+
expect(
|
|
156
|
+
collectFormValues([
|
|
157
|
+
el({ name: "value", type, value: displayed, getAttribute }),
|
|
158
|
+
]),
|
|
159
|
+
).toEqual({ value: canonical });
|
|
160
|
+
expect(
|
|
161
|
+
collectFormValues([
|
|
162
|
+
el({ name: "value", type, value: edited, getAttribute }),
|
|
163
|
+
]),
|
|
164
|
+
).toEqual({ value: edited });
|
|
165
|
+
expect(
|
|
166
|
+
collectFormValues([
|
|
167
|
+
el({ name: "value", type, value: "", getAttribute }),
|
|
168
|
+
]),
|
|
169
|
+
).toEqual({ value: "" });
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
it("ignores instant anchors on other control types", () => {
|
|
174
|
+
const getAttribute = (name: string) =>
|
|
175
|
+
name === FIELD_VALUE_ATTR ? "2025-12-15T17:00:00Z" : null;
|
|
176
|
+
expect(
|
|
177
|
+
collectFormValues([
|
|
178
|
+
el({ name: "date", type: "date", value: "", getAttribute }),
|
|
179
|
+
el({ name: "time", type: "time", value: "12:34", getAttribute }),
|
|
180
|
+
el({ name: "text", value: "live", getAttribute }),
|
|
181
|
+
el({
|
|
182
|
+
name: "select",
|
|
183
|
+
type: "select-one",
|
|
184
|
+
value: "chosen",
|
|
185
|
+
getAttribute,
|
|
186
|
+
}),
|
|
187
|
+
el({ name: "textarea", type: "textarea", value: "text", getAttribute }),
|
|
188
|
+
el({
|
|
189
|
+
name: "radio",
|
|
190
|
+
type: "radio",
|
|
191
|
+
value: "chosen",
|
|
192
|
+
checked: true,
|
|
193
|
+
getAttribute,
|
|
194
|
+
}),
|
|
195
|
+
el({ name: "checkbox", type: "checkbox", checked: true, getAttribute }),
|
|
196
|
+
el({
|
|
197
|
+
name: "group",
|
|
198
|
+
type: "checkbox",
|
|
199
|
+
value: "chosen",
|
|
200
|
+
checked: true,
|
|
201
|
+
hasAttribute: (name) => name === CHECKBOX_GROUP_ATTR,
|
|
202
|
+
getAttribute,
|
|
203
|
+
}),
|
|
204
|
+
el({ name: "range", type: "range", value: "7", getAttribute }),
|
|
205
|
+
]),
|
|
206
|
+
).toEqual({
|
|
207
|
+
date: "",
|
|
208
|
+
time: "12:34",
|
|
209
|
+
text: "live",
|
|
210
|
+
select: "chosen",
|
|
211
|
+
textarea: "text",
|
|
212
|
+
radio: "chosen",
|
|
213
|
+
checkbox: true,
|
|
214
|
+
group: ["chosen"],
|
|
215
|
+
range: 7,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
22
219
|
it("resolves a checkbox to its checked boolean", () => {
|
|
23
220
|
expect(
|
|
24
221
|
collectFormValues([
|
|
@@ -51,6 +248,27 @@ describe("collectFormValues", () => {
|
|
|
51
248
|
expect(values.size).toBeUndefined();
|
|
52
249
|
});
|
|
53
250
|
|
|
251
|
+
it("resolves a checkbox group to its checked values in document order", () => {
|
|
252
|
+
const option = (value: string, checked: boolean) =>
|
|
253
|
+
el({
|
|
254
|
+
name: "toppings",
|
|
255
|
+
type: "checkbox",
|
|
256
|
+
value,
|
|
257
|
+
checked,
|
|
258
|
+
hasAttribute: (name) => name === CHECKBOX_GROUP_ATTR,
|
|
259
|
+
});
|
|
260
|
+
expect(
|
|
261
|
+
collectFormValues([
|
|
262
|
+
option("basil", true),
|
|
263
|
+
option("olives", false),
|
|
264
|
+
option("onion", true),
|
|
265
|
+
]),
|
|
266
|
+
).toEqual({ toppings: ["basil", "onion"] });
|
|
267
|
+
expect(collectFormValues([option("basil", false)])).toEqual({
|
|
268
|
+
toppings: [],
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
54
272
|
it("resolves select/input/textarea/date controls to their string value", () => {
|
|
55
273
|
expect(
|
|
56
274
|
collectFormValues([
|
|
@@ -67,6 +285,12 @@ describe("collectFormValues", () => {
|
|
|
67
285
|
});
|
|
68
286
|
});
|
|
69
287
|
|
|
288
|
+
it("resolves a range input to its numeric value", () => {
|
|
289
|
+
expect(
|
|
290
|
+
collectFormValues([el({ name: "volume", type: "range", value: "7" })]),
|
|
291
|
+
).toEqual({ volume: 7 });
|
|
292
|
+
});
|
|
293
|
+
|
|
70
294
|
it("collects repeated non-radio names into an array in document order", () => {
|
|
71
295
|
expect(
|
|
72
296
|
collectFormValues([
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { FormEvent } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CHECKBOX_GROUP_ATTR,
|
|
4
|
+
FIELD_NAME_ATTR,
|
|
5
|
+
FIELD_VALUE_ATTR,
|
|
6
|
+
GENERATED_NAME_ATTR,
|
|
7
|
+
} from "../constants";
|
|
8
|
+
import {
|
|
9
|
+
fromLocalDateTime,
|
|
10
|
+
normalizeTemporalInputValue,
|
|
11
|
+
toPickerLocalDateTime,
|
|
12
|
+
} from "../temporal";
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
15
|
* The subset of `HTMLInputElement`/`HTMLSelectElement`/`HTMLTextAreaElement` that {@link collectFormValues} reads. A structural type rather than the DOM interfaces themselves, so a plain object can stand in for a form control in tests.
|
|
@@ -11,11 +21,12 @@ export type FormControlElementLike = {
|
|
|
11
21
|
readonly checked?: boolean;
|
|
12
22
|
readonly disabled: boolean;
|
|
13
23
|
readonly hasAttribute: (name: string) => boolean;
|
|
24
|
+
readonly getAttribute?: ((name: string) => string | null) | undefined;
|
|
14
25
|
readonly matches?: ((selector: string) => boolean) | undefined;
|
|
15
26
|
};
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
|
-
* Collects a submitted form's named control values into a plain object, keyed by `name`, in document order. Reads each control's live DOM state rather than `FormData`, so a checkbox resolves to its `checked` boolean instead of an on/off string. A radio group resolves to its checked option's `value`, or `undefined` if none is checked. Any other repeated `name` resolves to an array of its controls' values, in document order. Controls without a `name`, that carry `data-aui-generated-name`, or that are effectively disabled, including through an ancestor disabled fieldset outside its first legend, are skipped entirely.
|
|
29
|
+
* Collects a submitted form's named control values into a plain object, keyed by `name`, in document order. Reads each control's live DOM state rather than `FormData`, so a checkbox resolves to its `checked` boolean instead of an on/off string. A radio group resolves to its checked option's `value`, or `undefined` if none is checked. A checkbox group resolves to its checked options' values in document order, or an empty array if none is checked. Any other repeated `name` resolves to an array of its controls' values, in document order. A time or datetime-local control's `data-aui-field-value` preserves the canonical string when its local projection is unchanged; an edited instant retains its original offset and precision. Controls without a `name`, that carry `data-aui-generated-name`, or that are effectively disabled, including through an ancestor disabled fieldset outside its first legend, are skipped entirely.
|
|
19
30
|
*/
|
|
20
31
|
export function collectFormValues(
|
|
21
32
|
elements: ArrayLike<FormControlElementLike>,
|
|
@@ -23,7 +34,8 @@ export function collectFormValues(
|
|
|
23
34
|
const values: Record<string, unknown> = Object.create(null);
|
|
24
35
|
|
|
25
36
|
for (const element of Array.from(elements)) {
|
|
26
|
-
const {
|
|
37
|
+
const { disabled } = element;
|
|
38
|
+
const name = element.getAttribute?.(FIELD_NAME_ATTR) ?? element.name;
|
|
27
39
|
if (
|
|
28
40
|
!name ||
|
|
29
41
|
disabled ||
|
|
@@ -38,8 +50,45 @@ export function collectFormValues(
|
|
|
38
50
|
continue;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
element.type === "checkbox"
|
|
53
|
+
if (
|
|
54
|
+
element.type === "checkbox" &&
|
|
55
|
+
element.hasAttribute(CHECKBOX_GROUP_ATTR)
|
|
56
|
+
) {
|
|
57
|
+
const existing = values[name];
|
|
58
|
+
const checked = Array.isArray(existing) ? existing : [];
|
|
59
|
+
values[name] = element.checked ? [...checked, element.value] : checked;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const temporalType =
|
|
64
|
+
element.type === "datetime-local" || element.type === "time"
|
|
65
|
+
? element.type
|
|
66
|
+
: element.getAttribute?.("type");
|
|
67
|
+
const inputValue =
|
|
68
|
+
temporalType === "datetime-local" || temporalType === "time"
|
|
69
|
+
? normalizeTemporalInputValue(element.value)
|
|
70
|
+
: element.value;
|
|
71
|
+
const fieldValue =
|
|
72
|
+
temporalType === "datetime-local" || temporalType === "time"
|
|
73
|
+
? element.getAttribute?.(FIELD_VALUE_ATTR)
|
|
74
|
+
: undefined;
|
|
75
|
+
const value: string | number | boolean =
|
|
76
|
+
element.type === "checkbox"
|
|
77
|
+
? (element.checked ?? false)
|
|
78
|
+
: element.type === "range"
|
|
79
|
+
? Number(element.value)
|
|
80
|
+
: fieldValue && inputValue !== ""
|
|
81
|
+
? normalizeTemporalInputValue(inputValue) ===
|
|
82
|
+
normalizeTemporalInputValue(
|
|
83
|
+
temporalType === "time"
|
|
84
|
+
? fieldValue
|
|
85
|
+
: toPickerLocalDateTime(fieldValue),
|
|
86
|
+
)
|
|
87
|
+
? fieldValue
|
|
88
|
+
: temporalType === "datetime-local"
|
|
89
|
+
? fromLocalDateTime(inputValue, fieldValue)
|
|
90
|
+
: inputValue
|
|
91
|
+
: inputValue;
|
|
43
92
|
|
|
44
93
|
if (Object.hasOwn(values, name)) {
|
|
45
94
|
const existing = values[name];
|
|
@@ -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(
|
|
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
|
|
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: [
|
|
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(
|
|
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(
|
|
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(
|
|
809
|
+
expect((html.match(/cy="0"/g) ?? []).length).toBe(4);
|
|
568
810
|
});
|
|
569
811
|
|
|
570
812
|
it("cycles series colors past the palette size", () => {
|