@roku-sdk/components 0.0.0-stage → 2.18.0

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 (211) hide show
  1. package/BREAKING_CHANGES.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -2
  4. package/core-jsx.d.ts +53 -0
  5. package/eslint.config.mjs +3 -0
  6. package/jsx.d.ts +273 -0
  7. package/package.json +84 -3
  8. package/src/__tests__/container.test.tsx +150 -0
  9. package/src/__tests__/glyphs.test.ts +202 -0
  10. package/src/__tests__/modal-boundary-export.test.ts +12 -0
  11. package/src/__tests__/native-modal.test.tsx +224 -0
  12. package/src/__tests__/tween-release.test.tsx +125 -0
  13. package/src/__tests__/tween-stops-shape.test.tsx +165 -0
  14. package/src/__tests__/tween-without-compat.test.tsx +64 -0
  15. package/src/__tests__/tween.test.tsx +195 -0
  16. package/src/blended-busy-spinner.props.ts +37 -0
  17. package/src/blended-busy-spinner.tsx +13 -0
  18. package/src/bounding-tracker.props.ts +18 -0
  19. package/src/bounding-tracker.tsx +13 -0
  20. package/src/button-row.tsx +126 -0
  21. package/src/channel-store.props.ts +425 -0
  22. package/src/channel-store.tsx +76 -0
  23. package/src/container.tsx +130 -0
  24. package/src/core/jsx-runtime.ts +39 -0
  25. package/src/focusable-group.props.ts +8 -0
  26. package/src/focusable-group.tsx +17 -0
  27. package/src/focusable.props.ts +14 -0
  28. package/src/focusable.tsx +17 -0
  29. package/src/glyphs.ts +116 -0
  30. package/src/helpers/index.tsx +4 -0
  31. package/src/helpers/resolution-context.tsx +68 -0
  32. package/src/helpers/scale-for-device-resolution.ts +31 -0
  33. package/src/index.tsx +70 -0
  34. package/src/jsx-props.ts +87 -0
  35. package/src/kdfs.d.ts +14 -0
  36. package/src/kdfs.ts +15 -0
  37. package/src/key-handler.props.ts +33 -0
  38. package/src/key-handler.tsx +24 -0
  39. package/src/modal-dialog.props.ts +38 -0
  40. package/src/modal-dialog.tsx +41 -0
  41. package/src/retain-asset.ts +24 -0
  42. package/src/sg-intrinsics.json +1153 -0
  43. package/src/sg-nodes/__tests__/__snapshots__/font.test.tsx.snap +13 -0
  44. package/src/sg-nodes/__tests__/__snapshots__/prop-forwarding.test.tsx.snap +4409 -0
  45. package/src/sg-nodes/__tests__/core-props.test.ts +301 -0
  46. package/src/sg-nodes/__tests__/font.test.tsx +141 -0
  47. package/src/sg-nodes/__tests__/imperative-methods.test.ts +268 -0
  48. package/src/sg-nodes/__tests__/prop-forwarding.test.tsx +1049 -0
  49. package/src/sg-nodes/__tests__/solid-integration.test.tsx +719 -0
  50. package/src/sg-nodes/animated-image.props.ts +64 -0
  51. package/src/sg-nodes/animated-image.tsx +23 -0
  52. package/src/sg-nodes/animation.props.ts +292 -0
  53. package/src/sg-nodes/animation.tsx +190 -0
  54. package/src/sg-nodes/array-grid.ts +602 -0
  55. package/src/sg-nodes/audio.props.ts +302 -0
  56. package/src/sg-nodes/audio.tsx +57 -0
  57. package/src/sg-nodes/busy-spinner.props.ts +59 -0
  58. package/src/sg-nodes/busy-spinner.tsx +84 -0
  59. package/src/sg-nodes/button-group.props.ts +141 -0
  60. package/src/sg-nodes/button-group.tsx +30 -0
  61. package/src/sg-nodes/button.props.ts +119 -0
  62. package/src/sg-nodes/button.tsx +31 -0
  63. package/src/sg-nodes/check-list.props.ts +71 -0
  64. package/src/sg-nodes/check-list.tsx +55 -0
  65. package/src/sg-nodes/content-node.props.ts +1316 -0
  66. package/src/sg-nodes/content-node.tsx +153 -0
  67. package/src/sg-nodes/core-props.ts +197 -0
  68. package/src/sg-nodes/custom-keyboard.props.ts +108 -0
  69. package/src/sg-nodes/custom-keyboard.tsx +48 -0
  70. package/src/sg-nodes/dialog.props.ts +159 -0
  71. package/src/sg-nodes/dialog.tsx +36 -0
  72. package/src/sg-nodes/dynamic-key-grid.props.ts +22 -0
  73. package/src/sg-nodes/dynamic-key-grid.tsx +16 -0
  74. package/src/sg-nodes/dynamic-keyboard.props.ts +30 -0
  75. package/src/sg-nodes/dynamic-keyboard.tsx +18 -0
  76. package/src/sg-nodes/dynamic-mini-keyboard.props.ts +22 -0
  77. package/src/sg-nodes/dynamic-mini-keyboard.tsx +18 -0
  78. package/src/sg-nodes/dynamic-pinpad.props.ts +57 -0
  79. package/src/sg-nodes/dynamic-pinpad.tsx +18 -0
  80. package/src/sg-nodes/effect.props.ts +116 -0
  81. package/src/sg-nodes/effect.tsx +39 -0
  82. package/src/sg-nodes/font.props.ts +45 -0
  83. package/src/sg-nodes/font.tsx +80 -0
  84. package/src/sg-nodes/group.props.ts +105 -0
  85. package/src/sg-nodes/group.tsx +30 -0
  86. package/src/sg-nodes/label-list.props.ts +86 -0
  87. package/src/sg-nodes/label-list.tsx +64 -0
  88. package/src/sg-nodes/label.props.ts +207 -0
  89. package/src/sg-nodes/label.tsx +56 -0
  90. package/src/sg-nodes/layout-group.props.ts +69 -0
  91. package/src/sg-nodes/layout-group.tsx +75 -0
  92. package/src/sg-nodes/list-panel.props.ts +104 -0
  93. package/src/sg-nodes/list-panel.tsx +49 -0
  94. package/src/sg-nodes/markup-grid.props.ts +78 -0
  95. package/src/sg-nodes/markup-grid.tsx +38 -0
  96. package/src/sg-nodes/markup-list.props.ts +68 -0
  97. package/src/sg-nodes/markup-list.tsx +38 -0
  98. package/src/sg-nodes/mask-group.props.ts +78 -0
  99. package/src/sg-nodes/mask-group.tsx +59 -0
  100. package/src/sg-nodes/media-playback-methods.ts +20 -0
  101. package/src/sg-nodes/node.ts +77 -0
  102. package/src/sg-nodes/overhang.props.ts +169 -0
  103. package/src/sg-nodes/overhang.tsx +29 -0
  104. package/src/sg-nodes/panel-set.props.ts +71 -0
  105. package/src/sg-nodes/panel-set.tsx +53 -0
  106. package/src/sg-nodes/panel.props.ts +176 -0
  107. package/src/sg-nodes/panel.tsx +56 -0
  108. package/src/sg-nodes/poster-grid.props.ts +277 -0
  109. package/src/sg-nodes/poster-grid.tsx +45 -0
  110. package/src/sg-nodes/poster.props.ts +125 -0
  111. package/src/sg-nodes/poster.tsx +44 -0
  112. package/src/sg-nodes/radio-button-list.props.ts +59 -0
  113. package/src/sg-nodes/radio-button-list.tsx +68 -0
  114. package/src/sg-nodes/rectangle.props.ts +29 -0
  115. package/src/sg-nodes/rectangle.tsx +27 -0
  116. package/src/sg-nodes/row-list.props.ts +288 -0
  117. package/src/sg-nodes/row-list.tsx +47 -0
  118. package/src/sg-nodes/scene.props.ts +65 -0
  119. package/src/sg-nodes/scene.tsx +15 -0
  120. package/src/sg-nodes/scrollable-text.props.ts +133 -0
  121. package/src/sg-nodes/scrollable-text.tsx +64 -0
  122. package/src/sg-nodes/scrolling-label.props.ts +106 -0
  123. package/src/sg-nodes/scrolling-label.tsx +42 -0
  124. package/src/sg-nodes/simple-label.props.ts +156 -0
  125. package/src/sg-nodes/simple-label.tsx +59 -0
  126. package/src/sg-nodes/sound-effect.props.ts +130 -0
  127. package/src/sg-nodes/sound-effect.tsx +65 -0
  128. package/src/sg-nodes/standard-dialog.props.ts +520 -0
  129. package/src/sg-nodes/standard-dialog.tsx +428 -0
  130. package/src/sg-nodes/standard-keyboard-dialog.props.ts +84 -0
  131. package/src/sg-nodes/standard-keyboard-dialog.tsx +59 -0
  132. package/src/sg-nodes/standard-message-dialog.props.ts +56 -0
  133. package/src/sg-nodes/standard-message-dialog.tsx +48 -0
  134. package/src/sg-nodes/standard-pinpad-dialog.props.ts +64 -0
  135. package/src/sg-nodes/standard-pinpad-dialog.tsx +46 -0
  136. package/src/sg-nodes/standard-progress-dialog.props.ts +30 -0
  137. package/src/sg-nodes/standard-progress-dialog.tsx +43 -0
  138. package/src/sg-nodes/target-group.props.ts +278 -0
  139. package/src/sg-nodes/target-group.tsx +63 -0
  140. package/src/sg-nodes/target-list.props.ts +128 -0
  141. package/src/sg-nodes/target-list.tsx +66 -0
  142. package/src/sg-nodes/target-set.props.ts +63 -0
  143. package/src/sg-nodes/target-set.tsx +42 -0
  144. package/src/sg-nodes/time-grid.props.ts +646 -0
  145. package/src/sg-nodes/time-grid.tsx +87 -0
  146. package/src/sg-nodes/tween.props.ts +115 -0
  147. package/src/sg-nodes/types.ts +150 -0
  148. package/src/sg-nodes/video.props.ts +573 -0
  149. package/src/sg-nodes/video.tsx +73 -0
  150. package/src/sg-nodes/voice-text-edit-box.props.ts +91 -0
  151. package/src/sg-nodes/voice-text-edit-box.tsx +15 -0
  152. package/src/sg-nodes/zoom-row-list.props.ts +416 -0
  153. package/src/sg-nodes/zoom-row-list.tsx +42 -0
  154. package/src/standard-dialog-next/__tests__/standard-dialog-next.test.tsx +186 -0
  155. package/src/standard-dialog-next/index.ts +5 -0
  156. package/src/standard-dialog-next/standard-dialog-next.props.ts +80 -0
  157. package/src/standard-dialog-next/standard-dialog-next.tsx +326 -0
  158. package/src/test-helpers/tween-test-setup.ts +78 -0
  159. package/src/tween.tsx +173 -0
  160. package/src/types.ts +8 -0
  161. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.brs +13 -0
  162. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml +10 -0
  163. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml.d.ts +42 -0
  164. package/src/xml-components/bounding-tracker/BoundingTracker.brs +26 -0
  165. package/src/xml-components/bounding-tracker/BoundingTracker.xml +14 -0
  166. package/src/xml-components/bounding-tracker/BoundingTracker.xml.d.ts +42 -0
  167. package/src/xml-components/channel-store/channelStoreExt.brs +159 -0
  168. package/src/xml-components/channel-store/channelStoreExt.xml +29 -0
  169. package/src/xml-components/channel-store/channelStoreExt.xml.d.ts +57 -0
  170. package/src/xml-components/focus/focusable-group.brs +251 -0
  171. package/src/xml-components/focus/focusable-group.xml +15 -0
  172. package/src/xml-components/focus/focusable-group.xml.d.ts +42 -0
  173. package/src/xml-components/focus/focusable.xml +18 -0
  174. package/src/xml-components/focus/focusable.xml.d.ts +48 -0
  175. package/src/xml-components/keyboards/CustomAddressKDF.keyboard.json +547 -0
  176. package/src/xml-components/keyboards/CustomKeyboard.brs +322 -0
  177. package/src/xml-components/keyboards/CustomKeyboard.xml +45 -0
  178. package/src/xml-components/keyboards/CustomKeyboard.xml.d.ts +73 -0
  179. package/src/xml-components/keyboards/KeyGridExt.xml +4 -0
  180. package/src/xml-components/keyboards/KeyGridExt.xml.d.ts +37 -0
  181. package/src/xml-components/keyboards/KeyboardExt.xml +4 -0
  182. package/src/xml-components/keyboards/KeyboardExt.xml.d.ts +37 -0
  183. package/src/xml-components/keyboards/MOPPinPadCustomKDF.keyboard.json +151 -0
  184. package/src/xml-components/keyboards/MiniKeyboardExt.xml +4 -0
  185. package/src/xml-components/keyboards/MiniKeyboardExt.xml.d.ts +37 -0
  186. package/src/xml-components/keyboards/PhonePadCustomKDF.keyboard.json +42 -0
  187. package/src/xml-components/keyboards/PinPadExt.brs +102 -0
  188. package/src/xml-components/keyboards/PinPadExt.xml +17 -0
  189. package/src/xml-components/keyboards/PinPadExt.xml.d.ts +49 -0
  190. package/src/xml-components/keyboards/WifiCustomKDF.keyboard.json +1242 -0
  191. package/src/xml-components/keyhandler/KeyHandler.brs +30 -0
  192. package/src/xml-components/keyhandler/KeyHandler.xml +16 -0
  193. package/src/xml-components/keyhandler/KeyHandler.xml.d.ts +45 -0
  194. package/src/xml-components/managed-list-panel/ManagedListPanel.brs +137 -0
  195. package/src/xml-components/managed-list-panel/ManagedListPanel.xml +10 -0
  196. package/src/xml-components/managed-list-panel/ManagedListPanel.xml.d.ts +37 -0
  197. package/src/xml-components/modal/Modal.brs +105 -0
  198. package/src/xml-components/modal/Modal.xml +22 -0
  199. package/src/xml-components/modal/Modal.xml.d.ts +54 -0
  200. package/src/xml-components/modal/StandardDialogExt.xml +10 -0
  201. package/src/xml-components/modal/StandardDialogExt.xml.d.ts +37 -0
  202. package/src/xml-components/modal/StandardKeyboardDialogExt.xml +10 -0
  203. package/src/xml-components/modal/StandardKeyboardDialogExt.xml.d.ts +37 -0
  204. package/src/xml-components/modal/StandardMessageDialogExt.xml +10 -0
  205. package/src/xml-components/modal/StandardMessageDialogExt.xml.d.ts +37 -0
  206. package/src/xml-components/modal/StandardPinPadDialogExt.xml +10 -0
  207. package/src/xml-components/modal/StandardPinPadDialogExt.xml.d.ts +37 -0
  208. package/src/xml-components/modal/StandardProgressDialogExt.xml +10 -0
  209. package/src/xml-components/modal/StandardProgressDialogExt.xml.d.ts +37 -0
  210. package/src/xml-components/modal/tests/Modal.test.brs +195 -0
  211. package/tsconfig.json +10 -0
@@ -0,0 +1,1049 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import { registerInternStringApi } from "@roku-sdk/common/string-interner";
6
+ import { transactionManager } from "@roku-sdk/common/transaction";
7
+ import {
8
+ AnimatedImage,
9
+ Animation,
10
+ Audio,
11
+ BifDisplay,
12
+ BusySpinner,
13
+ Button,
14
+ ButtonGroup,
15
+ CheckList,
16
+ ColorFieldInterpolator,
17
+ ContentNode,
18
+ FloatFieldInterpolator,
19
+ Font,
20
+ Group,
21
+ Label,
22
+ LabelList,
23
+ LayoutGroup,
24
+ MarkupGrid,
25
+ MarkupList,
26
+ MaskGroup,
27
+ MultiStyleLabel,
28
+ Overhang,
29
+ Panel,
30
+ PanelSet,
31
+ Poster,
32
+ PosterGrid,
33
+ ProgressBar,
34
+ RadioButtonList,
35
+ Rectangle,
36
+ RowList,
37
+ Scene,
38
+ ScrollableText,
39
+ ScrollingLabel,
40
+ SequentialAnimation,
41
+ SimpleLabel,
42
+ SoundEffect,
43
+ TargetGroup,
44
+ TargetList,
45
+ TargetSet,
46
+ TimeGrid,
47
+ TrickPlayBar,
48
+ Vector2DFieldInterpolator,
49
+ Video,
50
+ VideoContentNode,
51
+ ZoomRowList
52
+ } from "@roku-sdk/components";
53
+ import type { Component, JSX } from "@roku-sdk/components/types";
54
+ import { DomNode, InternStringApi, StructuredDataApi, TreeManipulationApi } from "@roku-sdk/interfaces";
55
+ import { makeSolidRenderer } from "@roku-sdk/solid";
56
+
57
+ /**
58
+ * Captured, human-readable record of everything the SolidJS renderer forwards
59
+ * to the SceneGraph TreeManipulationApi for a single wrapper render.
60
+ *
61
+ * Keys are prop names (the user-visible names on the component), values
62
+ * describe which TM setter was invoked and with what value.
63
+ */
64
+ type Captured = Record<string, CapturedEntry>;
65
+
66
+ type CapturedEntry =
67
+ | { kind: "bool"; value: boolean }
68
+ | { kind: "int"; value: number }
69
+ | { kind: "float"; value: number }
70
+ | { kind: "string"; value: string }
71
+ | { kind: "array"; value: unknown[] }
72
+ | { kind: "object"; value: Record<string, unknown> }
73
+ | { kind: "nodeRef"; value: string }
74
+ | { kind: "null" }
75
+ | { kind: "eventHandler" }
76
+ | { kind: "focus"; value: boolean };
77
+
78
+ interface CapturedForwardedProps {
79
+ captured: Captured;
80
+ order: string[];
81
+ }
82
+
83
+ const commitEarly = transactionManager.commitEarly;
84
+
85
+ /**
86
+ * Replicates the `isEventHandler` classification used by the SolidJS renderer:
87
+ * any prop whose key matches the shape `on[A-Z]...`.
88
+ *
89
+ * Kept in sync with `packages/solid/src/solid.ts`.
90
+ */
91
+ function isEventHandler(key: string): boolean {
92
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] >= "A" && key[2] <= "Z";
93
+ }
94
+
95
+ /**
96
+ * Replicates the renderer's on-prop -> event-name transformation.
97
+ * `onFocus` -> `focus`, `onLoadStatus` -> `loadStatus`.
98
+ *
99
+ * Kept in sync with `packages/solid/src/solid.ts`.
100
+ */
101
+ function transformEventName(eventName: string): string {
102
+ return eventName.slice(2).charAt(0).toLowerCase() + eventName.slice(3);
103
+ }
104
+
105
+ /**
106
+ * Render a wrapper component with a given sample-prop object and return a
107
+ * deterministic record of every TM API call that represents prop forwarding.
108
+ *
109
+ * The returned record is suitable for direct `toMatchSnapshot()` comparison -
110
+ * inserting or dropping any prop anywhere in the thin wrapper pipeline will
111
+ * surface as a diff in the snapshot file.
112
+ *
113
+ * `buildProps` is a factory so that JSX.Element values (which require the
114
+ * SolidJS renderer to be initialized) are evaluated after the mock renderer
115
+ * is wired up.
116
+ */
117
+ function captureForwardedProps<P extends object>(
118
+ Component: Component<P>,
119
+ buildProps: () => P & { children?: JSX.Element }
120
+ ): CapturedForwardedProps {
121
+ // --- Intern string API (id -> string reverse map) ---
122
+ const internForward = new Map<string, object>();
123
+ const internReverse = new Map<object, string>();
124
+ let internRef = 0;
125
+ const mockIntern: InternStringApi = {
126
+ internString: (str: string): object => {
127
+ const existing = internForward.get(str);
128
+ if (existing) return existing;
129
+ const ref = { id: ++internRef };
130
+ internForward.set(str, ref);
131
+ internReverse.set(ref, str);
132
+ return ref;
133
+ }
134
+ };
135
+ registerInternStringApi(mockIntern);
136
+
137
+ const resolveInterned = (ref: unknown): string => {
138
+ if (ref && typeof ref === "object") {
139
+ const s = internReverse.get(ref as object);
140
+ if (s !== undefined) return s;
141
+ }
142
+ return String(ref);
143
+ };
144
+
145
+ // --- Node refs: { id } + side-channel tag map so JSX.Element props can be labelled. ---
146
+ const nodeTag = new WeakMap<object, string>();
147
+ let nodeCounter = 0;
148
+
149
+ const nodeRefToLabel = (ref: unknown): string => {
150
+ if (ref && typeof ref === "object") {
151
+ const tag = nodeTag.get(ref as object);
152
+ if (tag) return `<ref:${tag}>`;
153
+ }
154
+ return "<ref>";
155
+ };
156
+
157
+ // --- Structured data API: refs ARE the JS values so snapshots capture
158
+ // full array/object contents. ---
159
+ const mockSd: StructuredDataApi = {
160
+ createObject: () => ({}),
161
+ createArray: () => [],
162
+ insertBool: (o, p, v) => {
163
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
164
+ },
165
+ insertNull: (o, p) => {
166
+ (o as Record<string, unknown>)[resolveInterned(p)] = null;
167
+ },
168
+ insertInt: (o, p, v) => {
169
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
170
+ },
171
+ insertFloat: (o, p, v) => {
172
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
173
+ },
174
+ insertString: (o, p, v) => {
175
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
176
+ },
177
+ insertObject: (o, p, v) => {
178
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
179
+ },
180
+ insertArray: (o, p, v) => {
181
+ (o as Record<string, unknown>)[resolveInterned(p)] = v;
182
+ },
183
+ insertNodeRef: (o, p, v) => {
184
+ (o as Record<string, unknown>)[resolveInterned(p)] = nodeRefToLabel(v);
185
+ },
186
+ appendBool: (a, v) => {
187
+ (a as unknown[]).push(v);
188
+ },
189
+ appendNull: a => {
190
+ (a as unknown[]).push(null);
191
+ },
192
+ appendInt: (a, v) => {
193
+ (a as unknown[]).push(v);
194
+ },
195
+ appendFloat: (a, v) => {
196
+ (a as unknown[]).push(v);
197
+ },
198
+ appendString: (a, v) => {
199
+ (a as unknown[]).push(typeof v === "string" ? v : resolveInterned(v));
200
+ },
201
+ appendObject: (a, v) => {
202
+ (a as unknown[]).push(v);
203
+ },
204
+ appendArray: (a, v) => {
205
+ (a as unknown[]).push(v);
206
+ },
207
+ appendNodeRef: (a, v) => {
208
+ (a as unknown[]).push(nodeRefToLabel(v));
209
+ },
210
+ getArray: () => undefined,
211
+ getObject: () => undefined
212
+ };
213
+
214
+ // Record every call along with the target node so we can filter down
215
+ // to the wrapper's root element AFTER rendering is complete.
216
+ type Entry = { nodeRef: object; propName: string; entry: CapturedEntry };
217
+ const allCalls: Entry[] = [];
218
+ const focusCalls: { nodeRef: object; value: boolean }[] = [];
219
+
220
+ const push = (nodeRef: unknown, propRef: unknown, entry: CapturedEntry) => {
221
+ if (!nodeRef || typeof nodeRef !== "object") return;
222
+ allCalls.push({ nodeRef: nodeRef as object, propName: resolveInterned(propRef), entry });
223
+ };
224
+
225
+ const mockTm: TreeManipulationApi = {
226
+ createNode: (_anchor, tagRef) => {
227
+ const ref: object = { id: ++nodeCounter };
228
+ const tagName = resolveInterned(tagRef);
229
+ nodeTag.set(ref, tagName);
230
+ return ref;
231
+ },
232
+ appendNode: () => undefined,
233
+ insertNodeBefore: () => undefined,
234
+ removeNode: () => undefined,
235
+ setNodePropNull: (_a, n, p) => push(n, p, { kind: "null" }),
236
+ setNodePropBool: (_a, n, p, v) => push(n, p, { kind: "bool", value: v }),
237
+ setNodePropInt: (_a, n, p, v) => push(n, p, { kind: "int", value: v }),
238
+ setNodePropFloat: (_a, n, p, v) => push(n, p, { kind: "float", value: v }),
239
+ setNodePropString: (_a, n, p, v) => push(n, p, { kind: "string", value: v }),
240
+ setNodePropObject: (_a, n, p, v) => push(n, p, { kind: "object", value: v as Record<string, unknown> }),
241
+ setNodePropArray: (_a, n, p, v) => push(n, p, { kind: "array", value: v as unknown[] }),
242
+ setNodePropNodeRef: (_a, n, p, v) => push(n, p, { kind: "nodeRef", value: nodeRefToLabel(v) }),
243
+ removeNodeProp: () => undefined,
244
+ updateNodeProps: () => undefined,
245
+ removeNodeProps: () => undefined,
246
+ registerFieldChangeHandler: (_a, n, p) => push(n, p, { kind: "eventHandler" }),
247
+ unregisterFieldHandler: () => undefined,
248
+ setFocus: (_a, n, v) => {
249
+ if (n && typeof n === "object") focusCalls.push({ nodeRef: n as object, value: v });
250
+ }
251
+ };
252
+
253
+ const { renderer } = makeSolidRenderer(mockTm, mockSd, mockSd);
254
+ const anchorName = { id: 0 };
255
+
256
+ const anchorNode: DomNode = { r: {}, n: anchorName } as DomNode;
257
+ const props = buildProps();
258
+ const dispose = renderer.render((() => <Component {...props} />) as unknown as () => DomNode, anchorNode);
259
+ commitEarly();
260
+
261
+ // The wrapper's root DomNode is the first direct child of the anchor.
262
+ const rootDom = anchorNode.c && anchorNode.c[0];
263
+ const rootNodeRef = rootDom?.r as object | undefined;
264
+
265
+ dispose();
266
+
267
+ const captured: Captured = {};
268
+ if (rootNodeRef) {
269
+ for (const { nodeRef, propName, entry } of allCalls) {
270
+ if (nodeRef === rootNodeRef) captured[propName] = entry;
271
+ }
272
+ for (const { nodeRef, value } of focusCalls) {
273
+ if (nodeRef === rootNodeRef) captured["gainFocus"] = { kind: "focus", value };
274
+ }
275
+ }
276
+
277
+ return {
278
+ captured: sortKeys(captured),
279
+ order: allCalls.filter(({ nodeRef }) => nodeRef === rootNodeRef).map(({ propName }) => propName)
280
+ };
281
+ }
282
+
283
+ function sortKeys(obj: Captured): Captured {
284
+ const out: Captured = {};
285
+ for (const k of Object.keys(obj).sort()) {
286
+ out[k] = obj[k];
287
+ }
288
+ return out;
289
+ }
290
+
291
+ // ─────────────────────────────────────────────────────────────────────────
292
+ // Canonical sample values - reused across wrappers to keep snapshots stable
293
+ // ─────────────────────────────────────────────────────────────────────────
294
+
295
+ const V2: [number, number] = [10, 20];
296
+ const V2B: [number, number] = [30, 40];
297
+ const RECT: [number, number, number, number] = [0, 0, 100, 200];
298
+ const URI_RES = { hd: "pkg:/a-hd.png", fhd: "pkg:/a-fhd.png" };
299
+
300
+ // Common properties every IGroupProps component forwards.
301
+ const buildGroupProps = () => ({
302
+ id: "node-id",
303
+ focusable: true,
304
+ visible: true,
305
+ translation: V2,
306
+ opacity: 0.5,
307
+ rotation: 1.57,
308
+ scale: V2,
309
+ scaleRotateCenter: V2,
310
+ childRenderOrder: "renderLast" as const,
311
+ inheritParentTransform: true,
312
+ inheritParentOpacity: true,
313
+ clippingRect: RECT,
314
+ renderPass: 1,
315
+ muteAudioGuide: false,
316
+ enableRenderTracking: true,
317
+ onRenderTracking: () => {},
318
+ onFocusedChild: () => {}
319
+ });
320
+
321
+ // ─────────────────────────────────────────────────────────────────────────
322
+ // Thin-wrapper table - each row = one snapshot row under `describe.each`.
323
+ // Keep entries alphabetical by `name` for readability.
324
+ // ─────────────────────────────────────────────────────────────────────────
325
+
326
+ type WrapperRow = {
327
+ name: string;
328
+ Component: Component<Record<string, unknown>>;
329
+ // Props are built lazily because JSX.Element values must be created
330
+ // AFTER makeSolidRenderer() has run.
331
+ props: () => Record<string, unknown>;
332
+ };
333
+
334
+ const WRAPPERS: WrapperRow[] = [
335
+ {
336
+ name: "Animation",
337
+ Component: Animation as Component<Record<string, unknown>>,
338
+ props: () => ({
339
+ id: "anim",
340
+ control: "start",
341
+ duration: 0.5,
342
+ easeFunction: "linear",
343
+ repeat: true,
344
+ delay: 0.1,
345
+ optional: false,
346
+ onState: () => {}
347
+ })
348
+ },
349
+ {
350
+ name: "Audio",
351
+ Component: Audio as Component<Record<string, unknown>>,
352
+ props: () => ({
353
+ ...buildGroupProps(),
354
+ id: "audio",
355
+ contentIsPlaylist: false,
356
+ loop: true,
357
+ control: "play",
358
+ notificationInterval: 0.5,
359
+ seek: 5.5,
360
+ content: <ContentNode title="song" />,
361
+ onState: () => {},
362
+ onPosition: () => {},
363
+ onDuration: () => {},
364
+ onAudioFormat: () => {},
365
+ onBufferingStatus: () => {},
366
+ onErrorCode: () => {}
367
+ })
368
+ },
369
+ {
370
+ name: "BifDisplay",
371
+ Component: BifDisplay as Component<Record<string, unknown>>,
372
+ props: () => ({
373
+ ...buildGroupProps(),
374
+ id: "bif",
375
+ width: 240,
376
+ height: 135
377
+ })
378
+ },
379
+ {
380
+ name: "BusySpinner",
381
+ Component: BusySpinner as Component<Record<string, unknown>>,
382
+ props: () => ({
383
+ ...buildGroupProps(),
384
+ id: "spinner",
385
+ uri: "pkg:/images/spinner.png",
386
+ control: "start",
387
+ clockwise: true,
388
+ spinInterval: 1.0
389
+ })
390
+ },
391
+ {
392
+ name: "Button",
393
+ Component: Button as Component<Record<string, unknown>>,
394
+ props: () => ({
395
+ ...buildGroupProps(),
396
+ id: "btn",
397
+ text: "Click me",
398
+ textColor: "#FFFFFFFF",
399
+ focusedTextColor: "#000000FF",
400
+ iconUri: "pkg:/images/icon.png",
401
+ focusBitmapUri: "pkg:/images/focus.9.png",
402
+ onButtonSelected: () => {}
403
+ })
404
+ },
405
+ {
406
+ name: "ButtonGroup",
407
+ Component: ButtonGroup as Component<Record<string, unknown>>,
408
+ props: () => ({
409
+ ...buildGroupProps(),
410
+ id: "btn-group",
411
+ textColor: "#FFFFFFFF",
412
+ focusedTextColor: "#000000FF",
413
+ iconUri: "pkg:/icons/a.png",
414
+ height: 64,
415
+ minWidth: 200,
416
+ maxWidth: 600,
417
+ focusButton: 1,
418
+ buttons: ["One", "Two", "Three"],
419
+ layoutDirection: "vert",
420
+ horizAlignment: "left",
421
+ vertAlignment: "top",
422
+ itemSpacings: [8, 8],
423
+ onButtonSelected: () => {}
424
+ })
425
+ },
426
+ {
427
+ name: "CheckList",
428
+ Component: CheckList as Component<Record<string, unknown>>,
429
+ props: () => ({
430
+ ...buildGroupProps(),
431
+ id: "checklist",
432
+ itemSize: V2,
433
+ checkedState: [true, false, true],
434
+ checkOnSelect: true,
435
+ checkedIconUri: "pkg:/check.png",
436
+ uncheckedIconUri: "pkg:/uncheck.png",
437
+ checkedFocusIconUri: "pkg:/check-focus.png",
438
+ uncheckedFocusIconUri: "pkg:/uncheck-focus.png"
439
+ })
440
+ },
441
+ {
442
+ name: "ColorFieldInterpolator",
443
+ Component: ColorFieldInterpolator as Component<Record<string, unknown>>,
444
+ props: () => ({
445
+ id: "color-interp",
446
+ key: [0.0, 1.0],
447
+ keyValue: ["#FF0000FF", "#00FF00FF"],
448
+ fieldToInterp: "color"
449
+ })
450
+ },
451
+ {
452
+ name: "ContentNode",
453
+ Component: ContentNode as Component<Record<string, unknown>>,
454
+ props: () => ({
455
+ id: "content",
456
+ title: "My Title",
457
+ description: "Description text",
458
+ contentType: "episode",
459
+ hdPosterUrl: "pkg:/poster-hd.jpg",
460
+ actors: ["Alice", "Bob"],
461
+ categories: ["drama", "sci-fi"],
462
+ length: 3600,
463
+ starRating: 80,
464
+ live: false,
465
+ streamBitrates: [500, 1500, 3000]
466
+ })
467
+ },
468
+ {
469
+ name: "FloatFieldInterpolator",
470
+ Component: FloatFieldInterpolator as Component<Record<string, unknown>>,
471
+ props: () => ({
472
+ id: "float-interp",
473
+ key: [0.0, 1.0],
474
+ keyValue: [0.0, 1.0],
475
+ fieldToInterp: "opacity"
476
+ })
477
+ },
478
+ {
479
+ name: "Font",
480
+ Component: Font as Component<Record<string, unknown>>,
481
+ props: () => ({
482
+ id: "font",
483
+ uri: "pkg:/fonts/Roboto-Bold.ttf",
484
+ size: 28,
485
+ fallbackGlyph: "?"
486
+ })
487
+ },
488
+ {
489
+ name: "Group",
490
+ Component: Group as Component<Record<string, unknown>>,
491
+ props: () => ({ ...buildGroupProps() })
492
+ },
493
+ {
494
+ name: "Group (ResType values)",
495
+ Component: Group as Component<Record<string, unknown>>,
496
+ props: () => ({
497
+ ...buildGroupProps(),
498
+ id: "res",
499
+ translation: { hd: V2, fhd: V2B },
500
+ scaleRotateCenter: { hd: V2, fhd: V2B },
501
+ clippingRect: { hd: RECT, fhd: [10, 10, 50, 50] as [number, number, number, number] }
502
+ })
503
+ },
504
+ {
505
+ name: "Label",
506
+ Component: Label as Component<Record<string, unknown>>,
507
+ props: () => ({
508
+ ...buildGroupProps(),
509
+ id: "label",
510
+ text: "Hello",
511
+ color: "#FFFFFFFF",
512
+ width: 300,
513
+ height: 80,
514
+ font: "font:MediumSystemFont",
515
+ horizAlign: "center",
516
+ vertAlign: "center",
517
+ monospacedDigits: true,
518
+ numLines: 1,
519
+ maxLines: 3,
520
+ wrap: true,
521
+ displayPartialLines: false,
522
+ ellipsizeOnBoundary: true,
523
+ wordBreakChars: "-",
524
+ ellipsisText: "…",
525
+ lineSpacing: 4,
526
+ truncateOnDelimiter: ",",
527
+ leadingEllipsis: false,
528
+ onIsTextEllipsized: () => {},
529
+ onWrappedLines: () => {}
530
+ })
531
+ },
532
+ {
533
+ name: "LabelList",
534
+ Component: LabelList as Component<Record<string, unknown>>,
535
+ props: () => ({
536
+ ...buildGroupProps(),
537
+ id: "ll",
538
+ itemSize: V2,
539
+ numRows: 8,
540
+ drawFocusFeedback: true,
541
+ textHorizAlign: "left",
542
+ color: "#CCCCCCFF",
543
+ focusedColor: "#FFFFFFFF",
544
+ font: <Font uri="pkg:/font.ttf" size={24} />,
545
+ focusedFont: <Font uri="pkg:/font-bold.ttf" size={24} />,
546
+ onItemSelected: () => {},
547
+ onItemFocused: () => {}
548
+ })
549
+ },
550
+ {
551
+ name: "LabelList (gainFocus)",
552
+ Component: LabelList as Component<Record<string, unknown>>,
553
+ props: () => ({
554
+ id: "ll-focus",
555
+ itemSize: V2,
556
+ gainFocus: true
557
+ })
558
+ },
559
+ {
560
+ name: "LayoutGroup",
561
+ Component: LayoutGroup as Component<Record<string, unknown>>,
562
+ props: () => ({
563
+ ...buildGroupProps(),
564
+ id: "layout",
565
+ layoutDirection: "horiz",
566
+ horizAlignment: "center",
567
+ vertAlignment: "top",
568
+ itemSpacings: [8, 16],
569
+ addItemSpacingAfterChild: true
570
+ })
571
+ },
572
+ {
573
+ name: "MarkupGrid",
574
+ Component: MarkupGrid as unknown as Component<Record<string, unknown>>,
575
+ props: () => ({
576
+ ...buildGroupProps(),
577
+ id: "markup-grid",
578
+ itemComponentName: "MyItem",
579
+ itemSize: V2,
580
+ numRows: 3,
581
+ numColumns: 4,
582
+ content: <ContentNode />,
583
+ imageWellBitmapUri: "pkg:/well.png"
584
+ })
585
+ },
586
+ {
587
+ name: "MarkupList",
588
+ Component: MarkupList as unknown as Component<Record<string, unknown>>,
589
+ props: () => ({
590
+ ...buildGroupProps(),
591
+ id: "markup-list",
592
+ itemComponentName: "MyItem",
593
+ itemSize: V2,
594
+ content: <ContentNode />
595
+ })
596
+ },
597
+ {
598
+ name: "MaskGroup",
599
+ Component: MaskGroup as Component<Record<string, unknown>>,
600
+ props: () => ({
601
+ ...buildGroupProps(),
602
+ id: "mask",
603
+ maskUri: "pkg:/mask.png",
604
+ maskSize: V2,
605
+ maskOffset: V2B,
606
+ onMaskBitmapWidth: () => {},
607
+ onMaskBitmapHeight: () => {}
608
+ })
609
+ },
610
+ {
611
+ name: "MultiStyleLabel",
612
+ Component: MultiStyleLabel as Component<Record<string, unknown>>,
613
+ props: () => ({
614
+ ...buildGroupProps(),
615
+ id: "msl",
616
+ text: "Hello <b>world</b>",
617
+ color: "#FFFFFFFF",
618
+ width: 400,
619
+ wrap: true,
620
+ drawingStyles: {
621
+ bold: { fontSize: 24, fontUri: "font:MediumBoldSystemFont", color: "#FFFFFFFF" }
622
+ }
623
+ })
624
+ },
625
+ {
626
+ name: "Overhang",
627
+ Component: Overhang as Component<Record<string, unknown>>,
628
+ props: () => ({
629
+ ...buildGroupProps(),
630
+ id: "overhang",
631
+ color: "#00FF00FF",
632
+ backgroundUri: "pkg:/bg.png",
633
+ logoUri: "pkg:/logo.png",
634
+ title: "App Title",
635
+ showClock: true,
636
+ optionsAvailable: false,
637
+ dividerBitmapUri: "pkg:/div.png"
638
+ })
639
+ },
640
+ {
641
+ name: "Panel",
642
+ Component: Panel as Component<Record<string, unknown>>,
643
+ props: () => ({
644
+ ...buildGroupProps(),
645
+ id: "panel",
646
+ panelSize: "full",
647
+ width: 1920,
648
+ height: 1080,
649
+ leftPosition: 0,
650
+ leftOnly: false,
651
+ hasNextPanel: true,
652
+ isFullScreen: false,
653
+ goBackCount: 1,
654
+ overhangTitle: "Title",
655
+ onIsOffscreenLeft: () => {}
656
+ })
657
+ },
658
+ {
659
+ name: "PanelSet",
660
+ Component: PanelSet as Component<Record<string, unknown>>,
661
+ props: () => ({
662
+ ...buildGroupProps(),
663
+ id: "panel-set",
664
+ width: 1920,
665
+ height: 1080,
666
+ slideDuration: 0.25,
667
+ onIsGoingBack: () => {},
668
+ onNumPanels: () => {}
669
+ })
670
+ },
671
+ {
672
+ name: "AnimatedImage",
673
+ Component: AnimatedImage as Component<Record<string, unknown>>,
674
+ props: () => ({
675
+ ...buildGroupProps(),
676
+ id: "animated-image",
677
+ width: 200,
678
+ height: 300,
679
+ loadWidth: 200,
680
+ loadHeight: 300,
681
+ loadDisplayMode: "scaleToFit",
682
+ mimeType: "video/mp4",
683
+ control: "loop",
684
+ loadingBitmapUri: "pkg:/loading.png",
685
+ loadingBitmapOpacity: 0.5,
686
+ failedBitmapUri: "pkg:/failed.png",
687
+ failedBitmapOpacity: 1.0,
688
+ audioGuideText: "Animated image",
689
+ onState: () => {},
690
+ onError: () => {},
691
+ onMediaWidth: () => {},
692
+ onMediaHeight: () => {},
693
+ uri: URI_RES
694
+ })
695
+ },
696
+ {
697
+ name: "Poster",
698
+ Component: Poster as Component<Record<string, unknown>>,
699
+ props: () => ({
700
+ ...buildGroupProps(),
701
+ id: "poster",
702
+ width: 200,
703
+ height: 300,
704
+ loadSync: false,
705
+ loadWidth: 200,
706
+ loadHeight: 300,
707
+ loadDisplayMode: "scaleToFit",
708
+ blendColor: "#FFFFFFFF",
709
+ loadingBitmapUri: "pkg:/loading.png",
710
+ loadingBitmapOpacity: 0.5,
711
+ failedBitmapUri: "pkg:/failed.png",
712
+ failedBitmapOpacity: 1.0,
713
+ audioGuideText: "Poster image",
714
+ onLoadStatus: () => {},
715
+ onBitmapWidth: () => {},
716
+ onBitmapHeight: () => {},
717
+ onBitmapMargins: () => {},
718
+ uri: URI_RES
719
+ })
720
+ },
721
+ {
722
+ name: "PosterGrid",
723
+ Component: PosterGrid as unknown as Component<Record<string, unknown>>,
724
+ props: () => ({
725
+ ...buildGroupProps(),
726
+ id: "poster-grid",
727
+ content: <ContentNode />,
728
+ basePosterSize: V2,
729
+ useAtlas: false,
730
+ posterDisplayMode: "scaleToFit",
731
+ loadingBitmapUri: "pkg:/loading.png",
732
+ failedBitmapUri: "pkg:/failed.png",
733
+ caption1NumLines: 1
734
+ })
735
+ },
736
+ {
737
+ name: "ProgressBar",
738
+ Component: ProgressBar as Component<Record<string, unknown>>,
739
+ props: () => ({
740
+ ...buildGroupProps(),
741
+ id: "progress-bar",
742
+ width: 800,
743
+ height: 8,
744
+ percentage: 0.5
745
+ })
746
+ },
747
+ {
748
+ name: "RadioButtonList",
749
+ Component: RadioButtonList as unknown as Component<Record<string, unknown>>,
750
+ props: () => ({
751
+ ...buildGroupProps(),
752
+ id: "radio",
753
+ itemSize: V2,
754
+ checkedItem: 0,
755
+ checkOnSelect: true,
756
+ checkedIconUri: "pkg:/r-check.png",
757
+ uncheckedIconUri: "pkg:/r-uncheck.png",
758
+ checkedFocusIconUri: "pkg:/r-check-focus.png",
759
+ uncheckedFocusIconUri: "pkg:/r-uncheck-focus.png"
760
+ })
761
+ },
762
+ {
763
+ name: "Rectangle",
764
+ Component: Rectangle as Component<Record<string, unknown>>,
765
+ props: () => ({
766
+ ...buildGroupProps(),
767
+ id: "rect",
768
+ width: 100,
769
+ height: 50,
770
+ color: "#FF0000FF",
771
+ blendingEnabled: true
772
+ })
773
+ },
774
+ {
775
+ name: "RowList",
776
+ Component: RowList as unknown as Component<Record<string, unknown>>,
777
+ props: () => ({
778
+ ...buildGroupProps(),
779
+ id: "row-list",
780
+ content: <ContentNode />,
781
+ itemComponentName: "Item",
782
+ rowTitleComponentName: "Title",
783
+ rowItemSize: [V2, V2B],
784
+ rowItemSpacing: [V2, V2B],
785
+ showRowLabel: [true, false],
786
+ focusXOffset: [0, 10],
787
+ jumpToRowItem: [0, 0]
788
+ })
789
+ },
790
+ {
791
+ name: "Scene",
792
+ Component: Scene as Component<Record<string, unknown>>,
793
+ props: () => ({
794
+ ...buildGroupProps(),
795
+ id: "scene",
796
+ backgroundURI: "pkg:/bg.jpg",
797
+ backgroundColor: "#000000FF",
798
+ backExitsScene: false,
799
+ dialog: <ContentNode />,
800
+ palette: <ContentNode />,
801
+ onCurrentDesignResolution: () => {}
802
+ })
803
+ },
804
+ {
805
+ name: "ScrollableText",
806
+ Component: ScrollableText as Component<Record<string, unknown>>,
807
+ props: () => ({
808
+ ...buildGroupProps(),
809
+ id: "scrollable",
810
+ text: "Long text",
811
+ color: "#FFFFFFFF",
812
+ font: "font:SmallSystemFont",
813
+ width: 400,
814
+ height: 200,
815
+ lineSpacing: 4,
816
+ horizAlign: "left",
817
+ vertAlign: "top",
818
+ scrollbarTrackBitmapUri: "pkg:/track.png",
819
+ scrollbarThumbBitmapUri: "pkg:/thumb.png"
820
+ })
821
+ },
822
+ {
823
+ name: "ScrollingLabel",
824
+ Component: ScrollingLabel as Component<Record<string, unknown>>,
825
+ props: () => ({
826
+ ...buildGroupProps(),
827
+ id: "scrolling",
828
+ text: "Ticker",
829
+ color: "#FFFFFFFF",
830
+ font: "font:MediumSystemFont",
831
+ maxWidth: 400,
832
+ height: 32,
833
+ scrollSpeed: 1.0,
834
+ repeatCount: 3,
835
+ horizAlign: "left",
836
+ vertAlign: "center"
837
+ })
838
+ },
839
+ {
840
+ name: "SequentialAnimation",
841
+ Component: SequentialAnimation as Component<Record<string, unknown>>,
842
+ props: () => ({
843
+ id: "seq",
844
+ control: "start",
845
+ repeat: false,
846
+ delay: 0.2,
847
+ optional: false
848
+ })
849
+ },
850
+ {
851
+ name: "SimpleLabel",
852
+ Component: SimpleLabel as Component<Record<string, unknown>>,
853
+ props: () => ({
854
+ ...buildGroupProps(),
855
+ id: "simple",
856
+ text: "Hi",
857
+ color: "#FFFFFFFF",
858
+ fontUri: "font:MediumSystemFont",
859
+ fontSize: 24,
860
+ horizOrigin: "left",
861
+ vertOrigin: "center"
862
+ })
863
+ },
864
+ {
865
+ name: "SoundEffect",
866
+ Component: SoundEffect as Component<Record<string, unknown>>,
867
+ props: () => ({
868
+ id: "sfx",
869
+ uri: "pkg:/sfx/select.wav",
870
+ control: "play",
871
+ volume: 0.75,
872
+ onState: () => {},
873
+ onLoadStatus: () => {}
874
+ })
875
+ },
876
+ {
877
+ name: "TargetGroup",
878
+ Component: TargetGroup as Component<Record<string, unknown>>,
879
+ props: () => ({
880
+ ...buildGroupProps(),
881
+ id: "tg",
882
+ itemComponentName: "MyItem",
883
+ content: <ContentNode />,
884
+ targetSet: <TargetSet targetRects={[RECT]} />,
885
+ defaultTargetSetFocusIndex: 0,
886
+ wrap: false,
887
+ duration: 0.25,
888
+ easeFunction: "outQuad",
889
+ showTargetRects: false,
890
+ jumpToItem: 0,
891
+ animateToItem: 0,
892
+ animateToTargetSet: 0
893
+ })
894
+ },
895
+ {
896
+ name: "TargetList",
897
+ Component: TargetList as Component<Record<string, unknown>>,
898
+ props: () => ({
899
+ ...buildGroupProps(),
900
+ id: "tl",
901
+ itemComponentName: "MyItem",
902
+ content: <ContentNode />,
903
+ unfocusedTargetSet: <TargetSet targetRects={[RECT]} />,
904
+ advanceKey: "right",
905
+ reverseKey: "left"
906
+ })
907
+ },
908
+ {
909
+ name: "TargetSet",
910
+ Component: TargetSet as Component<Record<string, unknown>>,
911
+ props: () => ({
912
+ id: "ts",
913
+ targetRects: [RECT, [10, 10, 50, 50]],
914
+ focusIndex: 1,
915
+ color: "#FF0000FF"
916
+ })
917
+ },
918
+ {
919
+ name: "TimeGrid",
920
+ Component: TimeGrid as Component<Record<string, unknown>>,
921
+ props: () => ({
922
+ ...buildGroupProps(),
923
+ id: "timegrid",
924
+ content: <ContentNode />,
925
+ contentStartTime: 1700000000,
926
+ duration: 7200,
927
+ leftEdgeTargetTime: 1700000000,
928
+ channelInfoComponentName: "ChannelInfo",
929
+ itemComponentName: "Program",
930
+ numRows: 6,
931
+ rowHeight: 96
932
+ })
933
+ },
934
+ {
935
+ name: "TrickPlayBar",
936
+ Component: TrickPlayBar as Component<Record<string, unknown>>,
937
+ props: () => ({
938
+ ...buildGroupProps(),
939
+ id: "trickplay",
940
+ trackBlendColor: "#FFFFFFFF",
941
+ filledBarBlendColor: "#00FF00FF",
942
+ thumbBlendColor: "#FF0000FF"
943
+ })
944
+ },
945
+ {
946
+ name: "Vector2DFieldInterpolator",
947
+ Component: Vector2DFieldInterpolator as Component<Record<string, unknown>>,
948
+ props: () => ({
949
+ id: "v2-interp",
950
+ key: [0.0, 1.0],
951
+ keyValue: [V2, V2B],
952
+ fieldToInterp: "translation"
953
+ })
954
+ },
955
+ {
956
+ name: "Video",
957
+ Component: Video as Component<Record<string, unknown>>,
958
+ props: () => ({
959
+ ...buildGroupProps(),
960
+ id: "video",
961
+ content: <ContentNode url="http://example.com/video.mp4" />,
962
+ control: "play",
963
+ loop: false,
964
+ seek: 0,
965
+ width: 1920,
966
+ height: 1080,
967
+ enableUI: true,
968
+ globalCaptionMode: "On",
969
+ mute: false
970
+ })
971
+ },
972
+ {
973
+ name: "VideoContentNode",
974
+ Component: VideoContentNode as Component<Record<string, unknown>>,
975
+ props: () => ({
976
+ id: "video-content",
977
+ title: "Movie",
978
+ url: "http://example.com/movie.mp4",
979
+ streamFormat: "mp4",
980
+ live: false
981
+ })
982
+ },
983
+ {
984
+ name: "ZoomRowList",
985
+ Component: ZoomRowList as unknown as Component<Record<string, unknown>>,
986
+ props: () => ({
987
+ ...buildGroupProps(),
988
+ id: "zoom-row",
989
+ content: <ContentNode />,
990
+ itemComponentName: "Item",
991
+ rowDecorationComponentName: "Deco",
992
+ rowWidth: 1600,
993
+ rowHeight: 180,
994
+ rowZoomHeight: 240,
995
+ rowItemAspectRatio: 1.78
996
+ })
997
+ }
998
+ ];
999
+
1000
+ // ─────────────────────────────────────────────────────────────────────────
1001
+
1002
+ describe("sg-nodes thin wrappers forward every prop to the TM API", () => {
1003
+ describe.each(WRAPPERS)("$name", ({ Component, props }) => {
1004
+ test("forwards props to TreeManipulationApi (snapshot)", () => {
1005
+ const { captured } = captureForwardedProps(Component, props);
1006
+ expect(captured).toMatchSnapshot();
1007
+ });
1008
+
1009
+ // Verifies every `on*` handler prop reaches the TM layer via
1010
+ // `registerFieldChangeHandler`. Separate from the snapshot so a
1011
+ // missed handler surfaces as a direct, readable diff rather than
1012
+ // requiring a reviewer to decode the snapshot.
1013
+ test("forwards every on* handler prop as a registered event handler", () => {
1014
+ const builtProps = props() as Record<string, unknown>;
1015
+ const expectedEvents = new Set(
1016
+ Object.keys(builtProps)
1017
+ .filter(k => isEventHandler(k) && typeof builtProps[k] === "function")
1018
+ .map(transformEventName)
1019
+ );
1020
+
1021
+ const { captured } = captureForwardedProps(Component, props);
1022
+ const actualEvents = new Set(
1023
+ Object.entries(captured)
1024
+ .filter(([, v]) => v.kind === "eventHandler")
1025
+ .map(([k]) => k)
1026
+ );
1027
+
1028
+ expect(actualEvents).toEqual(expectedEvents);
1029
+ });
1030
+ });
1031
+
1032
+ test("AnimatedImage installs configuration and handlers before its URI", () => {
1033
+ const animatedImage = WRAPPERS.find(({ name }) => name === "AnimatedImage");
1034
+ if (!animatedImage) throw new Error("AnimatedImage forwarding fixture is missing");
1035
+
1036
+ const { order } = captureForwardedProps(animatedImage.Component, () => ({
1037
+ uri: URI_RES,
1038
+ ...animatedImage.props()
1039
+ }));
1040
+ const uriIndex = order.indexOf("uri");
1041
+
1042
+ expect(uriIndex).toBeGreaterThan(-1);
1043
+ // TM uses native field names; transformEventName maps onState to state, etc.
1044
+ for (const prop of ["loadDisplayMode", "state", "error", "mediaWidth", "mediaHeight"]) {
1045
+ expect(order.indexOf(prop)).toBeGreaterThan(-1);
1046
+ expect(order.indexOf(prop)).toBeLessThan(uriIndex);
1047
+ }
1048
+ });
1049
+ });