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

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 (152) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +15 -17
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +28 -8
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/JSONGenerativeUI.server.d.ts +6 -9
  6. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.d.ts +42 -44
  8. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  9. package/dist/a2ui/convert.d.ts +7 -8
  10. package/dist/a2ui/convert.d.ts.map +1 -1
  11. package/dist/a2ui/convert.js +115 -16
  12. package/dist/a2ui/convert.js.map +1 -1
  13. package/dist/a2ui/reducer.d.ts +2 -5
  14. package/dist/a2ui/reducer.d.ts.map +1 -1
  15. package/dist/a2ui/reducer.js +8 -0
  16. package/dist/a2ui/reducer.js.map +1 -1
  17. package/dist/a2ui/snapshot.d.ts +3 -0
  18. package/dist/a2ui/snapshot.d.ts.map +1 -0
  19. package/dist/a2ui/snapshot.js +33 -0
  20. package/dist/a2ui/snapshot.js.map +1 -0
  21. package/dist/a2ui/types.d.ts +67 -58
  22. package/dist/a2ui/types.d.ts.map +1 -1
  23. package/dist/a2ui/types.js.map +1 -1
  24. package/dist/a2ui.d.ts +5 -4
  25. package/dist/a2ui.d.ts.map +1 -0
  26. package/dist/a2ui.js +2 -1
  27. package/dist/actionRegistry.d.ts +17 -20
  28. package/dist/actionRegistry.d.ts.map +1 -1
  29. package/dist/actionRegistry.js.map +1 -1
  30. package/dist/buildPresentParameters.d.ts +3 -6
  31. package/dist/buildPresentParameters.d.ts.map +1 -1
  32. package/dist/buildPresentParameters.js +16 -4
  33. package/dist/buildPresentParameters.js.map +1 -1
  34. package/dist/constants.d.ts +2 -4
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +2 -1
  37. package/dist/constants.js.map +1 -1
  38. package/dist/convert/boundSpec.d.ts +7 -10
  39. package/dist/convert/boundSpec.d.ts.map +1 -1
  40. package/dist/convert/copyBounded.d.ts +3 -6
  41. package/dist/convert/copyBounded.d.ts.map +1 -1
  42. package/dist/convert/isElement.d.ts +2 -5
  43. package/dist/convert/isElement.d.ts.map +1 -1
  44. package/dist/convert/takeRun.d.ts +4 -7
  45. package/dist/convert/takeRun.d.ts.map +1 -1
  46. package/dist/defineGenerativeComponents.d.ts +2 -5
  47. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  48. package/dist/generativeUIToJSX.d.ts +6 -9
  49. package/dist/generativeUIToJSX.d.ts.map +1 -1
  50. package/dist/index.d.ts +3 -13
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.server.d.ts +3 -13
  53. package/dist/index.server.d.ts.map +1 -0
  54. package/dist/index.shared.d.ts +15 -11
  55. package/dist/index.shared.d.ts.map +1 -0
  56. package/dist/ir.d.ts +45 -48
  57. package/dist/ir.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.d.ts +3 -6
  59. package/dist/renderGenerativeUI.d.ts.map +1 -1
  60. package/dist/scopeSchema.d.ts +6 -0
  61. package/dist/scopeSchema.d.ts.map +1 -0
  62. package/dist/scopeSchema.js +56 -0
  63. package/dist/scopeSchema.js.map +1 -0
  64. package/dist/slack/constants.d.ts +43 -45
  65. package/dist/slack/constants.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.d.ts +2 -5
  67. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts +2 -5
  69. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  70. package/dist/slack/toSlackBlocks.d.ts +2 -5
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +1 -1
  73. package/dist/slack/types.d.ts +110 -113
  74. package/dist/slack/types.d.ts.map +1 -1
  75. package/dist/slack.d.ts +5 -5
  76. package/dist/slack.d.ts.map +1 -0
  77. package/dist/teams/constants.d.ts +15 -18
  78. package/dist/teams/constants.d.ts.map +1 -1
  79. package/dist/teams/decodeSubmitData.d.ts +2 -5
  80. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  81. package/dist/teams/toAdaptiveCard.d.ts +10 -13
  82. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  83. package/dist/teams/toTeamsAttachments.d.ts +2 -5
  84. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  85. package/dist/teams/types.d.ts +136 -138
  86. package/dist/teams/types.d.ts.map +1 -1
  87. package/dist/teams.d.ts +5 -5
  88. package/dist/teams.d.ts.map +1 -0
  89. package/dist/types.d.ts +45 -47
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/vocabulary/alert.d.ts +22 -25
  92. package/dist/vocabulary/alert.d.ts.map +1 -1
  93. package/dist/vocabulary/collectFormValues.d.ts +12 -13
  94. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  95. package/dist/vocabulary/collectFormValues.js +3 -2
  96. package/dist/vocabulary/collectFormValues.js.map +1 -1
  97. package/dist/vocabulary/data.d.ts +46 -49
  98. package/dist/vocabulary/data.d.ts.map +1 -1
  99. package/dist/vocabulary/dispatch.d.ts +4 -7
  100. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  101. package/dist/vocabulary/fact.d.ts +9 -12
  102. package/dist/vocabulary/fact.d.ts.map +1 -1
  103. package/dist/vocabulary/form.d.ts +8 -11
  104. package/dist/vocabulary/form.d.ts.map +1 -1
  105. package/dist/vocabulary/icon.d.ts +38 -41
  106. package/dist/vocabulary/icon.d.ts.map +1 -1
  107. package/dist/vocabulary/index.d.ts +2 -5
  108. package/dist/vocabulary/index.d.ts.map +1 -1
  109. package/dist/vocabulary/interactive.d.ts +88 -87
  110. package/dist/vocabulary/interactive.d.ts.map +1 -1
  111. package/dist/vocabulary/interactive.js +6 -4
  112. package/dist/vocabulary/interactive.js.map +1 -1
  113. package/dist/vocabulary/layout.d.ts +76 -79
  114. package/dist/vocabulary/layout.d.ts.map +1 -1
  115. package/dist/vocabulary/list.d.ts +11 -14
  116. package/dist/vocabulary/list.d.ts.map +1 -1
  117. package/dist/vocabulary/media.d.ts +22 -25
  118. package/dist/vocabulary/media.d.ts.map +1 -1
  119. package/dist/vocabulary/text.d.ts +54 -57
  120. package/dist/vocabulary/text.d.ts.map +1 -1
  121. package/dist/vocabulary/toTextContent.d.ts +1 -4
  122. package/dist/vocabulary/toTextContent.d.ts.map +1 -1
  123. package/package.json +12 -10
  124. package/src/JSONGenerativeUI.client.tsx +57 -15
  125. package/src/JSONGenerativeUI.test.tsx +298 -4
  126. package/src/a2ui/convert.test.ts +496 -0
  127. package/src/a2ui/convert.ts +170 -25
  128. package/src/a2ui/reducer.test.ts +18 -0
  129. package/src/a2ui/reducer.ts +16 -0
  130. package/src/a2ui/snapshot.test.ts +52 -0
  131. package/src/a2ui/snapshot.ts +52 -0
  132. package/src/a2ui/types.ts +16 -0
  133. package/src/a2ui.ts +2 -0
  134. package/src/actionRegistry.ts +5 -5
  135. package/src/buildPresentParameters.test.ts +246 -0
  136. package/src/buildPresentParameters.ts +18 -3
  137. package/src/constants.ts +2 -0
  138. package/src/scopeSchema.test.ts +72 -0
  139. package/src/scopeSchema.ts +71 -0
  140. package/src/slack/fromSlackBlocks.test.ts +4 -1
  141. package/src/slack/toSlackBlocks.test.ts +8 -4
  142. package/src/teams/toAdaptiveCard.test.ts +5 -3
  143. package/src/vocabulary/collectFormValues.test.ts +38 -0
  144. package/src/vocabulary/collectFormValues.ts +11 -2
  145. package/src/vocabulary/form.test.tsx +1 -0
  146. package/src/vocabulary/interactive.dispatch.test.tsx +17 -3
  147. package/src/vocabulary/interactive.test.tsx +33 -1
  148. package/src/vocabulary/interactive.tsx +12 -2
  149. package/src/vocabulary/layout.test.tsx +1 -0
  150. package/src/vocabulary/list.test.tsx +5 -4
  151. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +0 -141
  152. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +0 -1
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import type { UIElement } from "../ir";
3
3
  import { convertSurfaceToUISpec } from "./convert";
4
4
  import { applyA2uiOperations } from "./reducer";
5
+ import { surfaceToOperations } from "./snapshot";
5
6
  import type { A2uiSurfaceState } from "./types";
6
7
 
7
8
  const surfaceFrom = (
@@ -18,6 +19,93 @@ const surfaceFrom = (
18
19
  });
19
20
 
20
21
  describe("convertSurfaceToUISpec", () => {
22
+ it("replays a surface with bindings, templates, and custom components", () => {
23
+ const initial = applyA2uiOperations(new Map(), [
24
+ {
25
+ version: "v0.9",
26
+ createSurface: { surfaceId: "replay", catalogId: "basic" },
27
+ },
28
+ {
29
+ version: "v0.9",
30
+ updateComponents: {
31
+ surfaceId: "replay",
32
+ components: [
33
+ {
34
+ id: "root",
35
+ component: "Column",
36
+ children: ["items", "custom"],
37
+ },
38
+ {
39
+ id: "items",
40
+ component: "List",
41
+ children: {
42
+ template: { componentId: "item", path: "/items" },
43
+ },
44
+ },
45
+ {
46
+ id: "item",
47
+ component: "Text",
48
+ text: { path: "/name" },
49
+ },
50
+ {
51
+ id: "custom",
52
+ component: "StatusPill",
53
+ label: { path: "/status" },
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ {
59
+ version: "v0.9",
60
+ updateDataModel: {
61
+ surfaceId: "replay",
62
+ contents: {
63
+ status: "ready",
64
+ items: [{ name: "one" }, { name: "two" }],
65
+ },
66
+ },
67
+ },
68
+ ]);
69
+ const surface = initial.state.get("replay");
70
+ expect(surface).toBeDefined();
71
+
72
+ const operations = surfaceToOperations(surface!);
73
+ const replayed = applyA2uiOperations(
74
+ new Map(),
75
+ JSON.parse(JSON.stringify(operations)),
76
+ );
77
+ const replayedSurface = replayed.state.get("replay");
78
+
79
+ expect(operations).toEqual([
80
+ {
81
+ version: "v0.9",
82
+ createSurface: { surfaceId: "replay", catalogId: "basic" },
83
+ },
84
+ expect.objectContaining({
85
+ version: "v0.9",
86
+ updateComponents: expect.objectContaining({ surfaceId: "replay" }),
87
+ }),
88
+ {
89
+ version: "v0.9",
90
+ updateDataModel: expect.objectContaining({
91
+ surfaceId: "replay",
92
+ path: "/",
93
+ }),
94
+ },
95
+ ]);
96
+ expect(replayed.warnings).toEqual([]);
97
+ expect(replayedSurface).toMatchObject({
98
+ catalogId: "basic",
99
+ dataModel: surface!.dataModel,
100
+ });
101
+ expect([...replayedSurface!.components.entries()]).toEqual([
102
+ ...surface!.components.entries(),
103
+ ]);
104
+ expect(convertSurfaceToUISpec(replayedSurface!)).toEqual(
105
+ convertSurfaceToUISpec(surface!),
106
+ );
107
+ });
108
+
21
109
  it("converts an operation-built A2UI surface to the exact canonical UISpec", () => {
22
110
  const result = applyA2uiOperations(new Map(), [
23
111
  {
@@ -186,6 +274,149 @@ describe("convertSurfaceToUISpec", () => {
186
274
  });
187
275
  });
188
276
 
277
+ it("maps basic catalog icons, lists, choice pickers, and date inputs", () => {
278
+ const surface = surfaceFrom(
279
+ [
280
+ {
281
+ id: "root",
282
+ component: "Column",
283
+ children: ["icon", "list", "horizontal", "select", "radio", "date"],
284
+ },
285
+ { id: "icon", component: "Icon", name: "calendar", size: "lg" },
286
+ {
287
+ id: "list",
288
+ component: "List",
289
+ children: ["list-item"],
290
+ },
291
+ { id: "list-item", component: "Text", text: "Vertical item" },
292
+ {
293
+ id: "horizontal",
294
+ component: "List",
295
+ direction: "horizontal",
296
+ align: "center",
297
+ children: ["horizontal-item"],
298
+ },
299
+ {
300
+ id: "horizontal-item",
301
+ component: "Text",
302
+ text: "Horizontal item",
303
+ },
304
+ {
305
+ id: "select",
306
+ component: "ChoicePicker",
307
+ label: "Delivery",
308
+ placeholder: "Choose delivery",
309
+ variant: "multipleSelection",
310
+ value: { path: "/form/delivery" },
311
+ options: [
312
+ { label: { path: "/labels/express" }, value: "express" },
313
+ { label: "Standard", value: "standard" },
314
+ ],
315
+ },
316
+ {
317
+ id: "radio",
318
+ component: "ChoicePicker",
319
+ label: "Plan",
320
+ variant: "mutuallyExclusive",
321
+ value: { path: "/form/plan" },
322
+ options: [
323
+ { label: "Free", value: "free" },
324
+ { label: "Pro", value: "pro" },
325
+ ],
326
+ },
327
+ {
328
+ id: "date",
329
+ component: "DateTimeInput",
330
+ label: "Start date",
331
+ value: { path: "/form/startDate" },
332
+ min: "2026-01-01",
333
+ max: "2026-12-31",
334
+ },
335
+ ],
336
+ {
337
+ labels: { express: "Express" },
338
+ form: {
339
+ delivery: ["express"],
340
+ plan: "pro",
341
+ startDate: "2026-06-01",
342
+ },
343
+ },
344
+ );
345
+
346
+ expect(convertSurfaceToUISpec(surface)).toEqual({
347
+ spec: {
348
+ $type: "Col",
349
+ children: [
350
+ { $type: "Icon", name: "calendar", size: "lg" },
351
+ {
352
+ $type: "ListView",
353
+ children: [
354
+ {
355
+ $type: "ListViewItem",
356
+ children: { $type: "Markdown", value: "Vertical item" },
357
+ },
358
+ ],
359
+ },
360
+ {
361
+ $type: "Row",
362
+ align: "center",
363
+ children: [{ $type: "Markdown", value: "Horizontal item" }],
364
+ },
365
+ {
366
+ $type: "Select",
367
+ options: [
368
+ { label: "Express", value: "express" },
369
+ { label: "Standard", value: "standard" },
370
+ ],
371
+ placeholder: "Choose delivery",
372
+ label: "Delivery",
373
+ name: "delivery",
374
+ },
375
+ {
376
+ $type: "RadioGroup",
377
+ options: [
378
+ { label: "Free", value: "free" },
379
+ { label: "Pro", value: "pro" },
380
+ ],
381
+ label: "Plan",
382
+ name: "plan",
383
+ defaultValue: "pro",
384
+ },
385
+ {
386
+ $type: "DatePicker",
387
+ value: "2026-06-01",
388
+ min: "2026-01-01",
389
+ max: "2026-12-31",
390
+ label: "Start date",
391
+ name: "startDate",
392
+ },
393
+ ],
394
+ },
395
+ warnings: [],
396
+ });
397
+ });
398
+
399
+ it("does not pass a ChoicePicker value to Select without an initial-value prop", () => {
400
+ const result = convertSurfaceToUISpec(
401
+ surfaceFrom([
402
+ {
403
+ id: "root",
404
+ component: "ChoicePicker",
405
+ value: "express",
406
+ options: [{ label: "Express", value: "express" }],
407
+ },
408
+ ]),
409
+ );
410
+
411
+ expect(result).toEqual({
412
+ spec: {
413
+ $type: "Select",
414
+ options: [{ label: "Express", value: "express" }],
415
+ },
416
+ warnings: [],
417
+ });
418
+ });
419
+
189
420
  it("drops an unresolvable bound prop without throwing", () => {
190
421
  const surface = surfaceFrom([
191
422
  {
@@ -274,6 +505,89 @@ describe("convertSurfaceToUISpec", () => {
274
505
  });
275
506
  });
276
507
 
508
+ it("preserves a horizontal list container when expanding templates", () => {
509
+ const surface = surfaceFrom(
510
+ [
511
+ {
512
+ id: "root",
513
+ component: "List",
514
+ direction: "horizontal",
515
+ align: "center",
516
+ children: {
517
+ template: { componentId: "item", path: "/items" },
518
+ },
519
+ },
520
+ { id: "item", component: "Text", text: { path: "/label" } },
521
+ ],
522
+ { items: [{ label: "One" }, { label: "Two" }] },
523
+ );
524
+
525
+ expect(convertSurfaceToUISpec(surface)).toEqual({
526
+ spec: {
527
+ $type: "Row",
528
+ align: "center",
529
+ children: [
530
+ { $type: "Markdown", value: "One" },
531
+ { $type: "Markdown", value: "Two" },
532
+ ],
533
+ },
534
+ warnings: [],
535
+ });
536
+ });
537
+
538
+ it("counts only emitted template nodes", () => {
539
+ const components = (root: Record<string, unknown>) => [
540
+ root,
541
+ {
542
+ id: "item",
543
+ component: "Row",
544
+ children: Array.from({ length: 49 }, (_, index) => `divider-${index}`),
545
+ },
546
+ ...Array.from({ length: 49 }, (_, index) => ({
547
+ id: `divider-${index}`,
548
+ component: "Divider",
549
+ })),
550
+ ];
551
+ const dataModel = { items: Array.from({ length: 100 }, () => ({})) };
552
+ const results = [
553
+ convertSurfaceToUISpec(
554
+ surfaceFrom(
555
+ components({
556
+ id: "root",
557
+ component: "List",
558
+ direction: "horizontal",
559
+ children: {
560
+ template: { componentId: "item", path: "/items" },
561
+ },
562
+ }),
563
+ dataModel,
564
+ ),
565
+ ),
566
+ convertSurfaceToUISpec(
567
+ surfaceFrom(
568
+ components({
569
+ id: "root",
570
+ component: "CustomList",
571
+ children: {
572
+ template: { componentId: "item", path: "/items" },
573
+ },
574
+ }),
575
+ dataModel,
576
+ ),
577
+ { keepUnknownComponents: true },
578
+ ),
579
+ ];
580
+
581
+ for (const result of results) {
582
+ const children = (result.spec as UIElement).children as UIElement[];
583
+ expect(children).toHaveLength(100);
584
+ expect(children.at(-1)?.children as UIElement[]).toHaveLength(48);
585
+ expect(result.warnings).toEqual([
586
+ "A2UI node budget of 5000 was reached.",
587
+ ]);
588
+ }
589
+ });
590
+
277
591
  it("caps template expansion at 100 items", () => {
278
592
  const surface = surfaceFrom(
279
593
  [
@@ -335,6 +649,28 @@ describe("convertSurfaceToUISpec", () => {
335
649
  expect(result.warnings).toContain("A2UI depth cap of 32 was reached.");
336
650
  });
337
651
 
652
+ it("applies the depth cap to kept unknown components", () => {
653
+ const components = Array.from({ length: 34 }, (_, index) => ({
654
+ id: index === 0 ? "root" : `node-${index}`,
655
+ component: "CustomContainer",
656
+ ...(index < 33
657
+ ? { children: [index === 0 ? "node-1" : `node-${index + 1}`] }
658
+ : {}),
659
+ }));
660
+ const result = convertSurfaceToUISpec(surfaceFrom(components), {
661
+ keepUnknownComponents: true,
662
+ });
663
+ let node = result.spec as UIElement;
664
+ let count = 1;
665
+ while (Array.isArray(node.children) && node.children.length > 0) {
666
+ node = node.children[0] as UIElement;
667
+ count++;
668
+ }
669
+
670
+ expect(count).toBe(32);
671
+ expect(result.warnings).toContain("A2UI depth cap of 32 was reached.");
672
+ });
673
+
338
674
  it("enforces the total emitted node budget", () => {
339
675
  const childCount = 5001;
340
676
  const components = [
@@ -368,6 +704,166 @@ describe("convertSurfaceToUISpec", () => {
368
704
  ]);
369
705
  });
370
706
 
707
+ it("skips a supported component that cannot be mapped", () => {
708
+ const surface = surfaceFrom([
709
+ { id: "root", component: "Icon", name: "not-in-the-vocabulary" },
710
+ ]);
711
+ const expected = {
712
+ spec: null,
713
+ warnings: ['A2UI component "Icon" could not be mapped and was skipped.'],
714
+ };
715
+
716
+ expect(convertSurfaceToUISpec(surface)).toEqual(expected);
717
+ expect(
718
+ convertSurfaceToUISpec(surface, { keepUnknownComponents: true }),
719
+ ).toEqual(expected);
720
+ });
721
+
722
+ it("optionally keeps unknown components with resolved props and children", () => {
723
+ const surface = surfaceFrom(
724
+ [
725
+ {
726
+ id: "root",
727
+ component: "StatusPill",
728
+ title: { path: "/status/title" },
729
+ details: { level: { path: "/status/level" } },
730
+ children: ["text"],
731
+ },
732
+ { id: "text", component: "Text", text: { path: "/status/body" } },
733
+ ],
734
+ {
735
+ status: {
736
+ title: "Deployment",
737
+ level: "success",
738
+ body: "Ready",
739
+ },
740
+ },
741
+ );
742
+
743
+ expect(convertSurfaceToUISpec(surface)).toEqual({
744
+ spec: null,
745
+ warnings: ['Unknown A2UI component "StatusPill" was skipped.'],
746
+ });
747
+ expect(
748
+ convertSurfaceToUISpec(surface, { keepUnknownComponents: true }),
749
+ ).toEqual({
750
+ spec: {
751
+ $type: "StatusPill",
752
+ title: "Deployment",
753
+ details: { level: "success" },
754
+ children: [{ $type: "Markdown", value: "Ready" }],
755
+ },
756
+ warnings: [],
757
+ });
758
+ });
759
+
760
+ it("drops framework props from kept unknown components", () => {
761
+ const staticSurface = surfaceFrom([
762
+ {
763
+ id: "root",
764
+ component: "StatusPill",
765
+ label: "Ready",
766
+ $action: { type: "injected" },
767
+ $key: "injected",
768
+ $status: "injected",
769
+ },
770
+ ]);
771
+ const templateSurface = surfaceFrom(
772
+ [
773
+ {
774
+ id: "root",
775
+ component: "CustomList",
776
+ title: "Tasks",
777
+ $action: { type: "injected" },
778
+ $key: "injected",
779
+ $status: "injected",
780
+ children: {
781
+ template: { componentId: "item", path: "/items" },
782
+ },
783
+ },
784
+ { id: "item", component: "Text", text: { path: "/label" } },
785
+ ],
786
+ { items: [{ label: "One" }] },
787
+ );
788
+
789
+ expect(
790
+ convertSurfaceToUISpec(staticSurface, { keepUnknownComponents: true }),
791
+ ).toEqual({
792
+ spec: { $type: "StatusPill", label: "Ready" },
793
+ warnings: [],
794
+ });
795
+ expect(
796
+ convertSurfaceToUISpec(templateSurface, { keepUnknownComponents: true }),
797
+ ).toEqual({
798
+ spec: {
799
+ $type: "CustomList",
800
+ title: "Tasks",
801
+ children: [{ $type: "Markdown", value: "One" }],
802
+ },
803
+ warnings: [],
804
+ });
805
+ });
806
+
807
+ it("expands an unknown component with template children into a list by default", () => {
808
+ const surface = surfaceFrom(
809
+ [
810
+ {
811
+ id: "root",
812
+ component: "CustomList",
813
+ children: {
814
+ template: { componentId: "item", path: "/items" },
815
+ },
816
+ },
817
+ { id: "item", component: "Text", text: { path: "/label" } },
818
+ ],
819
+ { items: [{ label: "One" }] },
820
+ );
821
+
822
+ expect(convertSurfaceToUISpec(surface)).toEqual({
823
+ spec: {
824
+ $type: "ListView",
825
+ children: [
826
+ {
827
+ $type: "ListViewItem",
828
+ children: { $type: "Markdown", value: "One" },
829
+ },
830
+ ],
831
+ },
832
+ warnings: [],
833
+ });
834
+ });
835
+
836
+ it("keeps unknown template components as resolved nodes", () => {
837
+ const surface = surfaceFrom(
838
+ [
839
+ {
840
+ id: "root",
841
+ component: "CustomList",
842
+ title: { path: "/title" },
843
+ children: {
844
+ template: { componentId: "item", path: "/items" },
845
+ },
846
+ },
847
+ { id: "item", component: "Text", text: { path: "/label" } },
848
+ ],
849
+ { title: "Tasks", items: [{ label: "One" }, { label: "Two" }] },
850
+ );
851
+
852
+ expect(
853
+ convertSurfaceToUISpec(surface, { keepUnknownComponents: true }),
854
+ ).toEqual({
855
+ spec: {
856
+ $type: "CustomList",
857
+ title: "Tasks",
858
+ children: [
859
+ { $type: "Markdown", value: "One" },
860
+ { $type: "Markdown", value: "Two" },
861
+ ],
862
+ },
863
+ warnings: [],
864
+ });
865
+ });
866
+
371
867
  it("returns null with a warning when root is missing", () => {
372
868
  expect(
373
869
  convertSurfaceToUISpec(