@assistant-ui/react-generative-ui 0.0.12 → 0.0.14
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/dist/JSONGenerativeUI.client.d.ts +5 -0
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +12 -3
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/renderGenerativeUI.js +2 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +11 -4
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +11 -4
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/fromSlackBlocks.js +2 -2
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +114 -19
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +7 -1
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +9 -2
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +10 -3
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +10 -2
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +54 -15
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js +21 -13
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/teams/types.d.ts +11 -2
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/vocabulary/data.js +3 -2
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/layout.js +2 -1
- package/dist/vocabulary/layout.js.map +1 -1
- package/package.json +5 -5
- package/src/JSONGenerativeUI.client.tsx +13 -5
- package/src/JSONGenerativeUI.test.tsx +21 -2
- package/src/slack/constants.ts +11 -3
- package/src/slack/toSlackBlocks.test.ts +374 -8
- package/src/slack/toSlackBlocks.ts +181 -24
- package/src/slack/types.ts +7 -1
- package/src/teams/constants.ts +10 -2
- package/src/teams/toAdaptiveCard.test.ts +204 -10
- package/src/teams/toAdaptiveCard.ts +79 -15
- package/src/teams/toTeamsAttachments.test.ts +47 -2
- package/src/teams/toTeamsAttachments.ts +17 -11
- package/src/teams/types.ts +11 -2
- package/src/vocabulary/layout.test.tsx +16 -1
- package/src/vocabulary/layout.tsx +4 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
INPUT_LABEL_CAP,
|
|
27
27
|
INTERACTIVE_TEXT_CAP,
|
|
28
28
|
MARKDOWN_TEXT_BUDGET,
|
|
29
|
+
MAX_ELEMENT_DEPTH,
|
|
29
30
|
MAX_TRAVERSAL_DEPTH,
|
|
30
31
|
MESSAGE_BLOCK_CAP,
|
|
31
32
|
MODAL_BLOCK_CAP,
|
|
@@ -73,6 +74,14 @@ const INTERACTIVE_TYPES = new Set([
|
|
|
73
74
|
"RadioGroup",
|
|
74
75
|
]);
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Types whose control disappears when a card is reshaped to text. `Input` and
|
|
79
|
+
* `Form` are not in {@link INTERACTIVE_TYPES} because they emit their own
|
|
80
|
+
* block rather than an actions element, but a reshape loses them the same way,
|
|
81
|
+
* and a `Form` gets a Submit button whether or not it carries an action.
|
|
82
|
+
*/
|
|
83
|
+
const CONTROL_TYPES = new Set([...INTERACTIVE_TYPES, "Input", "Form"]);
|
|
84
|
+
|
|
76
85
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
77
86
|
|
|
78
87
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -201,6 +210,22 @@ const optionFrom = (
|
|
|
201
210
|
};
|
|
202
211
|
};
|
|
203
212
|
|
|
213
|
+
const warnDroppedOptions = (
|
|
214
|
+
kept: number,
|
|
215
|
+
taken: number,
|
|
216
|
+
component: string,
|
|
217
|
+
context: ConversionContext,
|
|
218
|
+
) => {
|
|
219
|
+
const dropped = taken - kept;
|
|
220
|
+
if (dropped === 0) return;
|
|
221
|
+
warn(
|
|
222
|
+
context,
|
|
223
|
+
"dropped",
|
|
224
|
+
component,
|
|
225
|
+
`${dropped} ${dropped === 1 ? "option was" : "options were"} dropped for want of a string label and value.`,
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
204
229
|
const toActionElement = (
|
|
205
230
|
element: NormalizedUIElement,
|
|
206
231
|
context: ConversionContext,
|
|
@@ -227,10 +252,16 @@ const toActionElement = (
|
|
|
227
252
|
`options were clamped to ${SELECT_OPTION_CAP} entries.`,
|
|
228
253
|
);
|
|
229
254
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
255
|
+
const takenOptions = rawOptions.slice(0, SELECT_OPTION_CAP);
|
|
256
|
+
const options = takenOptions
|
|
232
257
|
.map((option) => optionFrom(option, "Select", context))
|
|
233
258
|
.filter((option): option is SlackOption => option !== undefined);
|
|
259
|
+
warnDroppedOptions(
|
|
260
|
+
options.length,
|
|
261
|
+
takenOptions.length,
|
|
262
|
+
"Select",
|
|
263
|
+
context,
|
|
264
|
+
);
|
|
234
265
|
const placeholder = clampText(
|
|
235
266
|
asString(props["placeholder"]),
|
|
236
267
|
PLACEHOLDER_TEXT_CAP,
|
|
@@ -296,10 +327,16 @@ const toActionElement = (
|
|
|
296
327
|
`options were clamped to ${RADIO_OPTION_CAP} entries.`,
|
|
297
328
|
);
|
|
298
329
|
}
|
|
299
|
-
const
|
|
300
|
-
|
|
330
|
+
const takenOptions = rawOptions.slice(0, RADIO_OPTION_CAP);
|
|
331
|
+
const options = takenOptions
|
|
301
332
|
.map((option) => optionFrom(option, "RadioGroup", context))
|
|
302
333
|
.filter((option): option is SlackOption => option !== undefined);
|
|
334
|
+
warnDroppedOptions(
|
|
335
|
+
options.length,
|
|
336
|
+
takenOptions.length,
|
|
337
|
+
"RadioGroup",
|
|
338
|
+
context,
|
|
339
|
+
);
|
|
303
340
|
const selectedValue =
|
|
304
341
|
typeof props["value"] === "string"
|
|
305
342
|
? props["value"]
|
|
@@ -582,10 +619,54 @@ const convertCard = (
|
|
|
582
619
|
return [card];
|
|
583
620
|
};
|
|
584
621
|
|
|
622
|
+
/** The content kinds a reshaped carousel card cannot carry, in report order. */
|
|
623
|
+
const LOST_CONTENT_KINDS = ["images", "tables", "charts", "controls"] as const;
|
|
624
|
+
|
|
625
|
+
const listPhrase = (items: readonly string[]): string =>
|
|
626
|
+
items.length <= 1
|
|
627
|
+
? (items[0] ?? "")
|
|
628
|
+
: `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Collects the kinds of content a reshape drops, walking the same tree
|
|
632
|
+
* {@link collectText} does so a node nested below the top level counts too.
|
|
633
|
+
* These are exactly the pieces text cannot represent: an image has no text, a
|
|
634
|
+
* table and a chart carry theirs in array props, and a control is behavior,
|
|
635
|
+
* whether or not it carries an action of its own. An `$action` counts only on
|
|
636
|
+
* a node that renders a control from it, since a `Box` or a `Row` carrying one
|
|
637
|
+
* renders no control on the clean path either.
|
|
638
|
+
*/
|
|
639
|
+
const scanLostContent = (
|
|
640
|
+
node: NormalizedUINode | undefined,
|
|
641
|
+
into: Set<string>,
|
|
642
|
+
depth: number,
|
|
643
|
+
): void => {
|
|
644
|
+
if (depth > MAX_TRAVERSAL_DEPTH) return;
|
|
645
|
+
if (Array.isArray(node)) {
|
|
646
|
+
for (const child of node) scanLostContent(child, into, depth + 1);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (node === undefined || !isElement(node)) return;
|
|
650
|
+
if (
|
|
651
|
+
CONTROL_TYPES.has(node.type) ||
|
|
652
|
+
(node.type === "ListViewItem" && isRecord(node.action)) ||
|
|
653
|
+
(node.type === "Card" &&
|
|
654
|
+
(isRecord(node.props["confirm"]) || isRecord(node.props["cancel"])))
|
|
655
|
+
) {
|
|
656
|
+
into.add("controls");
|
|
657
|
+
}
|
|
658
|
+
if (node.type === "Image") into.add("images");
|
|
659
|
+
if (node.type === "Table") into.add("tables");
|
|
660
|
+
if (node.type === "Chart") into.add("charts");
|
|
661
|
+
scanLostContent(node.children, into, depth + 1);
|
|
662
|
+
};
|
|
663
|
+
|
|
585
664
|
/**
|
|
586
665
|
* Degrades a card that cannot map cleanly into a title-and-body card block.
|
|
587
|
-
* A carousel cannot fall back to a block sequence like a standalone card
|
|
588
|
-
*
|
|
666
|
+
* A carousel cannot fall back to a block sequence like a standalone card can,
|
|
667
|
+
* so the card is reshaped to the two text fields the block has. Text reachable
|
|
668
|
+
* by {@link collectText} survives at any depth; an image, a table, a chart,
|
|
669
|
+
* and a control do not, and are reported separately from the reshape itself.
|
|
589
670
|
*/
|
|
590
671
|
const degradeCard = (
|
|
591
672
|
element: NormalizedUIElement,
|
|
@@ -596,14 +677,40 @@ const degradeCard = (
|
|
|
596
677
|
const textChunks = collectText(element.children, depth + 1);
|
|
597
678
|
const titleSource = rawTitle || textChunks[0] || "";
|
|
598
679
|
const bodyChunks = rawTitle ? textChunks : textChunks.slice(1);
|
|
599
|
-
const titleText =
|
|
600
|
-
|
|
680
|
+
const titleText = clampText(
|
|
681
|
+
titleSource,
|
|
682
|
+
CARD_TITLE_CAP,
|
|
683
|
+
"Card",
|
|
684
|
+
"title",
|
|
685
|
+
context,
|
|
686
|
+
);
|
|
687
|
+
const bodyText = clampText(
|
|
688
|
+
bodyChunks.join("\n"),
|
|
689
|
+
CARD_BODY_CAP,
|
|
690
|
+
"Card",
|
|
691
|
+
"body",
|
|
692
|
+
context,
|
|
693
|
+
);
|
|
601
694
|
warn(
|
|
602
695
|
context,
|
|
603
|
-
"
|
|
696
|
+
"fallback",
|
|
604
697
|
"Card",
|
|
605
|
-
"A card inside a carousel was
|
|
698
|
+
"A card inside a carousel was reshaped to title and body.",
|
|
606
699
|
);
|
|
700
|
+
const lostKinds = new Set<string>();
|
|
701
|
+
if (isRecord(element.props["confirm"]) || isRecord(element.props["cancel"])) {
|
|
702
|
+
lostKinds.add("controls");
|
|
703
|
+
}
|
|
704
|
+
scanLostContent(element.children, lostKinds, depth + 1);
|
|
705
|
+
const lost = LOST_CONTENT_KINDS.filter((kind) => lostKinds.has(kind));
|
|
706
|
+
if (lost.length > 0) {
|
|
707
|
+
warn(
|
|
708
|
+
context,
|
|
709
|
+
"dropped",
|
|
710
|
+
"Card",
|
|
711
|
+
`A reshaped carousel card's ${listPhrase(lost)} were dropped.`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
607
714
|
return buildCardBlock({
|
|
608
715
|
...(titleText ? { title: { type: "mrkdwn", text: titleText } } : {}),
|
|
609
716
|
...(bodyText ? { body: { type: "mrkdwn", text: bodyText } } : {}),
|
|
@@ -698,15 +805,47 @@ const convertListItem = (
|
|
|
698
805
|
};
|
|
699
806
|
};
|
|
700
807
|
|
|
808
|
+
/**
|
|
809
|
+
* Converts a child whose output is thrown away, and reports whether anything
|
|
810
|
+
* was lost with it. A scratch context keeps the throwaway out of the shared
|
|
811
|
+
* markdown and data-table budgets that surviving blocks still need; only its
|
|
812
|
+
* `dropped` warnings are forwarded, because those describe the tree the caller
|
|
813
|
+
* wrote, while a clamp or a fallback would describe content never delivered.
|
|
814
|
+
*/
|
|
815
|
+
const discardedChild = (
|
|
816
|
+
child: NormalizedUINode,
|
|
817
|
+
context: ConversionContext,
|
|
818
|
+
depth: number,
|
|
819
|
+
): boolean => {
|
|
820
|
+
const scratch: ConversionContext = { ...context, warnings: [] };
|
|
821
|
+
const produced = convertSequence(child, scratch, depth).length > 0;
|
|
822
|
+
const lost = scratch.warnings.filter((warning) => warning.code === "dropped");
|
|
823
|
+
context.warnings.push(...lost);
|
|
824
|
+
return produced || lost.length > 0;
|
|
825
|
+
};
|
|
826
|
+
|
|
701
827
|
const convertListView = (
|
|
702
828
|
element: NormalizedUIElement,
|
|
703
829
|
context: ConversionContext,
|
|
704
830
|
depth: number,
|
|
705
831
|
): SlackBlock[] => {
|
|
706
|
-
const items =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
832
|
+
const items: NormalizedUIElement[] = [];
|
|
833
|
+
let discarded = 0;
|
|
834
|
+
for (const child of normalizedList(element.children)) {
|
|
835
|
+
if (isElement(child) && child.type === "ListViewItem") {
|
|
836
|
+
items.push(child);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (discardedChild(child, context, depth + 1)) discarded += 1;
|
|
840
|
+
}
|
|
841
|
+
if (discarded > 0) {
|
|
842
|
+
warn(
|
|
843
|
+
context,
|
|
844
|
+
"dropped",
|
|
845
|
+
"ListView",
|
|
846
|
+
`${discarded} non-item ${discarded === 1 ? "child was" : "children were"} dropped.`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
710
849
|
return items.flatMap((item, index) => [
|
|
711
850
|
...(index > 0 ? [{ type: "divider" as const }] : []),
|
|
712
851
|
convertListItem(item, context, depth + 1),
|
|
@@ -719,12 +858,21 @@ const convertCarousel = (
|
|
|
719
858
|
depth: number,
|
|
720
859
|
): SlackBlock[] => {
|
|
721
860
|
const cardChildren: NormalizedUIElement[] = [];
|
|
861
|
+
let droppedCards = 0;
|
|
722
862
|
for (const child of normalizedList(element.children)) {
|
|
723
863
|
if (isElement(child) && child.type === "Card") {
|
|
724
864
|
cardChildren.push(child);
|
|
725
|
-
|
|
726
|
-
warn(context, "dropped", "Carousel", "A non-card child was dropped.");
|
|
865
|
+
continue;
|
|
727
866
|
}
|
|
867
|
+
if (discardedChild(child, context, depth + 1)) droppedCards += 1;
|
|
868
|
+
}
|
|
869
|
+
if (droppedCards > 0) {
|
|
870
|
+
warn(
|
|
871
|
+
context,
|
|
872
|
+
"dropped",
|
|
873
|
+
"Carousel",
|
|
874
|
+
`${droppedCards} non-card ${droppedCards === 1 ? "child was" : "children were"} dropped.`,
|
|
875
|
+
);
|
|
728
876
|
}
|
|
729
877
|
if (cardChildren.length > CAROUSEL_CARD_CAP) {
|
|
730
878
|
warn(
|
|
@@ -790,13 +938,22 @@ const convertTable = (
|
|
|
790
938
|
);
|
|
791
939
|
}
|
|
792
940
|
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
941
|
+
const takenColumns = rawColumns.slice(0, DATA_TABLE_COLUMN_CAP);
|
|
942
|
+
const unlabeled = takenColumns.filter(
|
|
943
|
+
(column) => !isRecord(column) || typeof column["label"] !== "string",
|
|
944
|
+
).length;
|
|
945
|
+
if (unlabeled > 0) {
|
|
946
|
+
warn(
|
|
947
|
+
context,
|
|
948
|
+
"dropped",
|
|
949
|
+
"Table",
|
|
950
|
+
`${unlabeled} column ${unlabeled === 1 ? "header was" : "headers were"} left blank for want of a string label.`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const columnHeaderRow: SlackDataTableCell[] = takenColumns.map((column) => ({
|
|
954
|
+
type: "raw_text" as const,
|
|
955
|
+
text: isRecord(column) ? asString(column["label"]) : "",
|
|
956
|
+
}));
|
|
800
957
|
const dataRows: SlackDataTableCell[][] = rawRows
|
|
801
958
|
.slice(0, DATA_TABLE_ROW_CAP)
|
|
802
959
|
.map((row) =>
|
|
@@ -1283,7 +1440,7 @@ export function toSlackBlocks(
|
|
|
1283
1440
|
: reason === "cycle"
|
|
1284
1441
|
? "a self-referencing node was dropped."
|
|
1285
1442
|
: reason === "depth"
|
|
1286
|
-
? `nodes deeper than ${
|
|
1443
|
+
? `nodes deeper than ${MAX_ELEMENT_DEPTH} levels were dropped.`
|
|
1287
1444
|
: `children were clamped to ${CHILDREN_CAP} entries.`;
|
|
1288
1445
|
warn(context, "clamped", "Root", detail);
|
|
1289
1446
|
});
|
package/src/slack/types.ts
CHANGED
|
@@ -210,7 +210,13 @@ export interface ToSlackBlocksOptions {
|
|
|
210
210
|
readonly surface?: "message" | "modal";
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* A non-fatal note reported during Slack conversion.
|
|
215
|
+
*
|
|
216
|
+
* `clamped` removed content to fit a limit, `dropped` discarded a node or one
|
|
217
|
+
* of its props and may leave a placeholder note behind, and `fallback` emitted
|
|
218
|
+
* the node in a different form than requested.
|
|
219
|
+
*/
|
|
214
220
|
export interface SlackConversionWarning {
|
|
215
221
|
readonly code: "clamped" | "dropped" | "fallback";
|
|
216
222
|
readonly component: string;
|
package/src/teams/constants.ts
CHANGED
|
@@ -17,9 +17,17 @@ export const ADAPTIVE_CARD_SCHEMA =
|
|
|
17
17
|
export const ATTACHMENT_CONTENT_TYPE =
|
|
18
18
|
"application/vnd.microsoft.card.adaptive";
|
|
19
19
|
|
|
20
|
-
/** The maximum
|
|
20
|
+
/** The maximum traversal depth, counted in visited values, not tree levels. */
|
|
21
21
|
export const MAX_TRAVERSAL_DEPTH = 64;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The element-nesting ceiling a caller observes. `boundNode` visits an element
|
|
25
|
+
* and its `children` array separately, so one level of nesting spends two of
|
|
26
|
+
* {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
|
|
27
|
+
* the looser of the two. A root array costs one more level than a root object.
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_ELEMENT_DEPTH = MAX_TRAVERSAL_DEPTH / 2;
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* The maximum number of entries kept at any single level (root, or a
|
|
25
33
|
* `children` array at any depth) of the raw spec, before it ever reaches
|
|
@@ -46,7 +54,7 @@ export function clampReasonDetail(reason: ClampReason): string {
|
|
|
46
54
|
}
|
|
47
55
|
if (reason === "cycle") return "a self-referencing node was dropped.";
|
|
48
56
|
if (reason === "depth") {
|
|
49
|
-
return `nodes deeper than ${
|
|
57
|
+
return `nodes deeper than ${MAX_ELEMENT_DEPTH} levels were dropped.`;
|
|
50
58
|
}
|
|
51
59
|
return `children were clamped to ${CHILDREN_CAP} entries.`;
|
|
52
60
|
}
|
|
@@ -368,7 +368,7 @@ describe("toAdaptiveCard", () => {
|
|
|
368
368
|
expect(action.mode).toBe("secondary");
|
|
369
369
|
}
|
|
370
370
|
expect(warnings).toContainEqual(
|
|
371
|
-
expect.objectContaining({ code: "
|
|
371
|
+
expect.objectContaining({ code: "fallback", component: "Button" }),
|
|
372
372
|
);
|
|
373
373
|
});
|
|
374
374
|
});
|
|
@@ -418,7 +418,7 @@ describe("toAdaptiveCard", () => {
|
|
|
418
418
|
expect(action.mode).toBe("secondary");
|
|
419
419
|
}
|
|
420
420
|
expect(warnings).toContainEqual(
|
|
421
|
-
expect.objectContaining({ code: "
|
|
421
|
+
expect.objectContaining({ code: "fallback", component: "Button" }),
|
|
422
422
|
);
|
|
423
423
|
});
|
|
424
424
|
});
|
|
@@ -623,7 +623,7 @@ describe("toAdaptiveCard", () => {
|
|
|
623
623
|
const element = card.body[0] as { id: string };
|
|
624
624
|
expect(element.id).toBe("aui_");
|
|
625
625
|
expect(warnings).toContainEqual(
|
|
626
|
-
expect.objectContaining({ code: "
|
|
626
|
+
expect.objectContaining({ code: "fallback", component: type }),
|
|
627
627
|
);
|
|
628
628
|
},
|
|
629
629
|
);
|
|
@@ -647,7 +647,7 @@ describe("toAdaptiveCard", () => {
|
|
|
647
647
|
const ids = (card.body as TeamsInputText[]).map((element) => element.id);
|
|
648
648
|
expect(ids).toEqual(["input", "input_2"]);
|
|
649
649
|
expect(warnings).toContainEqual(
|
|
650
|
-
expect.objectContaining({ code: "
|
|
650
|
+
expect.objectContaining({ code: "fallback", component: "Input" }),
|
|
651
651
|
);
|
|
652
652
|
});
|
|
653
653
|
|
|
@@ -659,7 +659,9 @@ describe("toAdaptiveCard", () => {
|
|
|
659
659
|
]);
|
|
660
660
|
const ids = (card.body as TeamsInputText[]).map((element) => element.id);
|
|
661
661
|
expect(new Set(ids).size).toBe(3);
|
|
662
|
-
expect(warnings.some((warning) => warning.code === "
|
|
662
|
+
expect(warnings.some((warning) => warning.code === "fallback")).toBe(
|
|
663
|
+
true,
|
|
664
|
+
);
|
|
663
665
|
});
|
|
664
666
|
|
|
665
667
|
it("names the actually-emitted id in the reserved-key warning, and fires only that one warning, when the rename target also collides", () => {
|
|
@@ -671,7 +673,7 @@ describe("toAdaptiveCard", () => {
|
|
|
671
673
|
expect(ids).toEqual(["aui_", "aui__2"]);
|
|
672
674
|
expect(warnings).toEqual([
|
|
673
675
|
{
|
|
674
|
-
code: "
|
|
676
|
+
code: "fallback",
|
|
675
677
|
component: "Input",
|
|
676
678
|
detail:
|
|
677
679
|
'the input id "aui" collides with the submit envelope\'s reserved key and was renamed to "aui__2".',
|
|
@@ -801,7 +803,7 @@ describe("toAdaptiveCard", () => {
|
|
|
801
803
|
const columnSet = card.body[0] as TeamsColumnSet;
|
|
802
804
|
expect(columnSet.columns).toHaveLength(4);
|
|
803
805
|
expect(warnings).toContainEqual(
|
|
804
|
-
expect.objectContaining({ code: "
|
|
806
|
+
expect.objectContaining({ code: "advisory", component: "Row" }),
|
|
805
807
|
);
|
|
806
808
|
});
|
|
807
809
|
});
|
|
@@ -863,9 +865,177 @@ describe("toAdaptiveCard", () => {
|
|
|
863
865
|
detail: "Unknown component type was dropped.",
|
|
864
866
|
});
|
|
865
867
|
});
|
|
868
|
+
|
|
869
|
+
it("keeps a discarded child's clamp warning out of the result", () => {
|
|
870
|
+
const { warnings } = toAdaptiveCard({
|
|
871
|
+
$type: "ListView",
|
|
872
|
+
children: [
|
|
873
|
+
{
|
|
874
|
+
$type: "Table",
|
|
875
|
+
rows: Array.from({ length: TABLE_ROW_CAP + 5 }, () => ["x"]),
|
|
876
|
+
},
|
|
877
|
+
{ $type: "ListViewItem", title: "row" },
|
|
878
|
+
],
|
|
879
|
+
});
|
|
880
|
+
expect(warnings.some((warning) => warning.code === "clamped")).toBe(
|
|
881
|
+
false,
|
|
882
|
+
);
|
|
883
|
+
expect(warnings).toContainEqual({
|
|
884
|
+
code: "dropped",
|
|
885
|
+
component: "ListView",
|
|
886
|
+
detail: "1 non-item child was dropped.",
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("reports a renderable non-item child whose output it discards", () => {
|
|
891
|
+
const { card, warnings } = toAdaptiveCard({
|
|
892
|
+
$type: "ListView",
|
|
893
|
+
children: [
|
|
894
|
+
{ $type: "Text", value: "stray" },
|
|
895
|
+
{ $type: "ListViewItem", children: { $type: "Text", value: "Kept" } },
|
|
896
|
+
],
|
|
897
|
+
});
|
|
898
|
+
expect(card.body).toEqual([
|
|
899
|
+
{
|
|
900
|
+
type: "Container",
|
|
901
|
+
items: [{ type: "TextBlock", text: "Kept", wrap: true }],
|
|
902
|
+
},
|
|
903
|
+
]);
|
|
904
|
+
expect(warnings).toContainEqual({
|
|
905
|
+
code: "dropped",
|
|
906
|
+
component: "ListView",
|
|
907
|
+
detail: "1 non-item child was dropped.",
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it.each([
|
|
912
|
+
["before", 0],
|
|
913
|
+
["after", 1],
|
|
914
|
+
] as const)(
|
|
915
|
+
"does not let a discarded non-item child claim or report an input id, surviving control %s",
|
|
916
|
+
(_position, listViewIndex) => {
|
|
917
|
+
const listView = {
|
|
918
|
+
$type: "ListView",
|
|
919
|
+
children: [
|
|
920
|
+
{ $type: "Input", name: "email" },
|
|
921
|
+
{ $type: "ListViewItem", title: "row" },
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
const survivor = { $type: "Input", name: "email", label: "Real" };
|
|
925
|
+
const { card, warnings } = toAdaptiveCard(
|
|
926
|
+
listViewIndex === 0 ? [listView, survivor] : [survivor, listView],
|
|
927
|
+
);
|
|
928
|
+
const ids = card.body.map((element) => (element as { id?: string }).id);
|
|
929
|
+
expect(ids).toContain("email");
|
|
930
|
+
expect(
|
|
931
|
+
warnings.some((warning) => warning.detail.includes("email_2")),
|
|
932
|
+
).toBe(false);
|
|
933
|
+
},
|
|
934
|
+
);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
describe("Carousel", () => {
|
|
938
|
+
it("reports the non-card children a nested carousel filters out", () => {
|
|
939
|
+
const { warnings } = toAdaptiveCard({
|
|
940
|
+
$type: "Col",
|
|
941
|
+
children: {
|
|
942
|
+
$type: "Carousel",
|
|
943
|
+
children: [
|
|
944
|
+
{ $type: "Text", value: "lost" },
|
|
945
|
+
{ $type: "Card", title: "kept" },
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
expect(warnings).toContainEqual({
|
|
950
|
+
code: "dropped",
|
|
951
|
+
component: "Carousel",
|
|
952
|
+
detail: "1 non-card child was dropped.",
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("stays silent for a nested-carousel child that renders nothing anyway", () => {
|
|
957
|
+
const { warnings } = toAdaptiveCard({
|
|
958
|
+
$type: "Col",
|
|
959
|
+
children: {
|
|
960
|
+
$type: "Carousel",
|
|
961
|
+
children: [{ $type: "Spacer" }, { $type: "Card", title: "kept" }],
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
expect(warnings.some((warning) => warning.code === "dropped")).toBe(
|
|
965
|
+
false,
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("forwards a nested-carousel child's own dropped warning", () => {
|
|
970
|
+
const { warnings } = toAdaptiveCard({
|
|
971
|
+
$type: "Col",
|
|
972
|
+
children: {
|
|
973
|
+
$type: "Carousel",
|
|
974
|
+
children: [{ $type: "Mystery" }, { $type: "Card", title: "kept" }],
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
expect(warnings).toContainEqual({
|
|
978
|
+
code: "dropped",
|
|
979
|
+
component: "Mystery",
|
|
980
|
+
detail: "Unknown component type was dropped.",
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
describe("choices", () => {
|
|
986
|
+
it("reports options dropped for want of a string value", () => {
|
|
987
|
+
const { card, warnings } = toAdaptiveCard({
|
|
988
|
+
$type: "Select",
|
|
989
|
+
name: "s",
|
|
990
|
+
options: [{ label: "ok", value: "a" }, { label: "bad" }, "nope"],
|
|
991
|
+
});
|
|
992
|
+
expect((card.body[0] as { choices: unknown[] }).choices).toEqual([
|
|
993
|
+
{ title: "ok", value: "a" },
|
|
994
|
+
]);
|
|
995
|
+
expect(warnings).toContainEqual({
|
|
996
|
+
code: "dropped",
|
|
997
|
+
component: "Select",
|
|
998
|
+
detail: "2 options were dropped for want of a string value.",
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("names RadioGroup when the same loss happens there", () => {
|
|
1003
|
+
const { warnings } = toAdaptiveCard({
|
|
1004
|
+
$type: "RadioGroup",
|
|
1005
|
+
name: "r",
|
|
1006
|
+
options: [{ label: "ok", value: "a" }, { label: "bad" }],
|
|
1007
|
+
});
|
|
1008
|
+
expect(warnings).toContainEqual({
|
|
1009
|
+
code: "dropped",
|
|
1010
|
+
component: "RadioGroup",
|
|
1011
|
+
detail: "1 option was dropped for want of a string value.",
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
866
1014
|
});
|
|
867
1015
|
|
|
868
1016
|
describe("Table", () => {
|
|
1017
|
+
it.each([["A"], [{}], [{ label: 42 }]])(
|
|
1018
|
+
"keeps an unlabeled column's position in the header and reports it: %j",
|
|
1019
|
+
(column) => {
|
|
1020
|
+
const { card, warnings } = toAdaptiveCard({
|
|
1021
|
+
$type: "Table",
|
|
1022
|
+
columns: [column, { label: "B" }],
|
|
1023
|
+
rows: [["1", "2"]],
|
|
1024
|
+
});
|
|
1025
|
+
const table = card.body[0] as TeamsTable;
|
|
1026
|
+
expect(
|
|
1027
|
+
table.rows[0]?.cells.map(
|
|
1028
|
+
(cell) => (cell.items[0] as TeamsTextBlock).text,
|
|
1029
|
+
),
|
|
1030
|
+
).toEqual(["", "B"]);
|
|
1031
|
+
expect(warnings).toContainEqual({
|
|
1032
|
+
code: "dropped",
|
|
1033
|
+
component: "Table",
|
|
1034
|
+
detail: "1 column header was left blank for want of a string label.",
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
|
|
869
1039
|
it("renders firstRowAsHeaders true and a header row when columns are present", () => {
|
|
870
1040
|
const { card } = toAdaptiveCard({
|
|
871
1041
|
$type: "Table",
|
|
@@ -1158,7 +1328,13 @@ describe("toAdaptiveCard", () => {
|
|
|
1158
1328
|
const value = "x".repeat(PAYLOAD_SOFT_CAP + 1000);
|
|
1159
1329
|
const { warnings } = toAdaptiveCard({ $type: "Text", value });
|
|
1160
1330
|
expect(warnings).toContainEqual(
|
|
1161
|
-
expect.objectContaining({
|
|
1331
|
+
expect.objectContaining({
|
|
1332
|
+
code: "advisory",
|
|
1333
|
+
component: "Root",
|
|
1334
|
+
detail: expect.stringContaining(
|
|
1335
|
+
`over the ${PAYLOAD_SOFT_CAP}-byte soft budget`,
|
|
1336
|
+
),
|
|
1337
|
+
}),
|
|
1162
1338
|
);
|
|
1163
1339
|
});
|
|
1164
1340
|
|
|
@@ -1166,7 +1342,7 @@ describe("toAdaptiveCard", () => {
|
|
|
1166
1342
|
const value = "汉".repeat(30000);
|
|
1167
1343
|
const { warnings } = toAdaptiveCard({ $type: "Markdown", value });
|
|
1168
1344
|
expect(warnings).toContainEqual(
|
|
1169
|
-
expect.objectContaining({ code: "
|
|
1345
|
+
expect.objectContaining({ code: "advisory", component: "Root" }),
|
|
1170
1346
|
);
|
|
1171
1347
|
});
|
|
1172
1348
|
|
|
@@ -1295,9 +1471,27 @@ describe("toAdaptiveCard", () => {
|
|
|
1295
1471
|
expect(warnings).toContainEqual({
|
|
1296
1472
|
code: "clamped",
|
|
1297
1473
|
component: "Root",
|
|
1298
|
-
detail: "nodes deeper than
|
|
1474
|
+
detail: "nodes deeper than 32 levels were dropped.",
|
|
1299
1475
|
});
|
|
1300
1476
|
});
|
|
1477
|
+
|
|
1478
|
+
it("reports the depth detail at the level it actually starts dropping", () => {
|
|
1479
|
+
const chain = (levels: number) => {
|
|
1480
|
+
let node: unknown = { $type: "Caption", value: "x" };
|
|
1481
|
+
for (let i = 0; i < levels; i++) {
|
|
1482
|
+
node = { $type: "Card", children: [node] };
|
|
1483
|
+
}
|
|
1484
|
+
return node;
|
|
1485
|
+
};
|
|
1486
|
+
const dropped = (node: unknown) =>
|
|
1487
|
+
toAdaptiveCard(node).warnings.some((warning) =>
|
|
1488
|
+
warning.detail.includes("deeper than"),
|
|
1489
|
+
);
|
|
1490
|
+
expect(dropped(chain(32))).toBe(false);
|
|
1491
|
+
expect(dropped(chain(33))).toBe(true);
|
|
1492
|
+
expect(dropped([chain(31)])).toBe(false);
|
|
1493
|
+
expect(dropped([chain(32)])).toBe(true);
|
|
1494
|
+
});
|
|
1301
1495
|
});
|
|
1302
1496
|
});
|
|
1303
1497
|
|