@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,719 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
5
+ import { registerInternStringApi } from "@roku-sdk/common/string-interner";
6
+ import { transactionManager } from "@roku-sdk/common/transaction";
7
+ import { Button, ContentNode, Label, LabelList } from "@roku-sdk/components";
8
+ import { AnchorNode, DomNode } from "@roku-sdk/interfaces";
9
+ import { InternStringApi, StructuredDataApi, TreeManipulationApi } from "@roku-sdk/interfaces";
10
+ import { cleanUp, createEffect, createSignal, For, makeSolidRenderer, Renderer, Show } from "@roku-sdk/solid";
11
+
12
+ describe("Solid", () => {
13
+ const anchorNodeRef = { id: 0 };
14
+ const anchorNameRef = { id: 0 };
15
+
16
+ let render: Renderer<DomNode>["render"];
17
+ const internMap = new Map<string, object>();
18
+ let mockTmApi: TreeManipulationApi;
19
+ let mockStructuredDataApi: StructuredDataApi;
20
+ const commitEarly = transactionManager.commitEarly;
21
+
22
+ beforeEach(() => {
23
+ let nodeRef = 0;
24
+ let internRef = 0;
25
+ let arrayRef = 0;
26
+ let objectRef = 0;
27
+ mockTmApi = {
28
+ createNode: jest.fn().mockImplementation((): object => {
29
+ return { id: ++nodeRef };
30
+ }),
31
+ appendNode: jest.fn(),
32
+ insertNodeBefore: jest.fn(),
33
+ removeNode: jest.fn(),
34
+ setNodePropNull: jest.fn(),
35
+ setNodePropBool: jest.fn(),
36
+ setNodePropInt: jest.fn(),
37
+ setNodePropFloat: jest.fn(),
38
+ setNodePropString: jest.fn(),
39
+ setNodePropObject: jest.fn(),
40
+ setNodePropArray: jest.fn(),
41
+ setNodePropNodeRef: jest.fn(),
42
+ registerFieldChangeHandler: jest.fn(),
43
+ unregisterFieldHandler: jest.fn(),
44
+ setFocus: jest.fn()
45
+ };
46
+
47
+ const mockIntern: InternStringApi = {
48
+ internString: (str: string): object => {
49
+ const interned = internMap.get(str);
50
+ if (!interned) {
51
+ const ref = { id: ++internRef };
52
+ internMap.set(str, ref);
53
+ return ref;
54
+ }
55
+ return interned;
56
+ }
57
+ };
58
+ registerInternStringApi(mockIntern);
59
+
60
+ mockStructuredDataApi = {
61
+ createArray: jest.fn().mockImplementation(() => ({ id: ++arrayRef })),
62
+ appendInt: jest.fn(),
63
+ createObject: jest.fn().mockImplementation(() => ({ id: ++objectRef })),
64
+ insertInt: jest.fn(),
65
+ insertArray: jest.fn()
66
+ // Add other methods as needed
67
+ };
68
+
69
+ const solidRenderer = makeSolidRenderer(mockTmApi, mockStructuredDataApi, mockStructuredDataApi);
70
+ ({ render } = solidRenderer.renderer);
71
+ });
72
+
73
+ test("should render a component", async () => {
74
+ const App = () => <Label text="Hello World!"></Label>;
75
+
76
+ const anchorNode = { r: anchorNodeRef, c: [], n: anchorNameRef };
77
+ const dispose = render(() => <App />, anchorNode);
78
+ commitEarly();
79
+ // Tests the Tree
80
+ expect(anchorNode).toEqual({ r: anchorNodeRef, c: [{ r: { id: 1 } }], n: anchorNameRef });
81
+
82
+ // Tests the TM API. Use this to test if props are set correctly
83
+ expect(mockTmApi.createNode).toHaveBeenCalledTimes(1);
84
+ expect(mockTmApi.createNode).toHaveBeenCalledWith({}, { id: 1 });
85
+ expect(mockTmApi.appendNode).toHaveBeenCalledTimes(1);
86
+ expect(mockTmApi.appendNode).toHaveBeenCalledWith({}, anchorNodeRef, { id: 1 });
87
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
88
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Hello World!");
89
+ dispose();
90
+ });
91
+
92
+ test("should work with a functional component", async () => {
93
+ const Nested = () => <Label text="Nested"></Label>;
94
+ const App = () => {
95
+ return (
96
+ <>
97
+ <Label text="This is a Header"></Label>
98
+ <Nested />
99
+ </>
100
+ );
101
+ };
102
+
103
+ const anchorNode = {};
104
+ render(() => <App />, anchorNode);
105
+ commitEarly();
106
+
107
+ expect(anchorNode).toEqual({
108
+ c: [{ r: { id: 1 } }, { r: { id: 2 } }]
109
+ });
110
+ });
111
+
112
+ test("should work with signals", async () => {
113
+ const [count, setCount] = createSignal(0);
114
+ const Counter = () => {
115
+ return (
116
+ <>
117
+ <Label text={`Count: ${count()}`}></Label>
118
+ </>
119
+ );
120
+ };
121
+
122
+ const anchorNode = {};
123
+ render(() => <Counter />, anchorNode);
124
+ commitEarly();
125
+ expect(anchorNode).toEqual({ c: [{ r: { id: 1 } }] });
126
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
127
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Count: 0");
128
+
129
+ setCount(count() + 1);
130
+ commitEarly();
131
+ expect(anchorNode).toEqual({ c: [{ r: { id: 1 } }] });
132
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(2);
133
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Count: 1");
134
+ });
135
+
136
+ test("should work with effects", async () => {
137
+ const [count, setCount] = createSignal(0);
138
+ let lastEffect;
139
+ const Counter = () => {
140
+ createEffect(() => {
141
+ lastEffect = count();
142
+ // function gets called each time count is updated
143
+ });
144
+
145
+ return <Label text={`Count: ${count()}`}></Label>;
146
+ };
147
+ const anchorNode = { r: anchorNodeRef };
148
+ render(() => <Counter />, anchorNode);
149
+ commitEarly();
150
+
151
+ setCount(10);
152
+ commitEarly();
153
+ expect(lastEffect).toBe(10);
154
+ });
155
+
156
+ test("signals should update props - also derived signals", async () => {
157
+ const [count, setCount] = createSignal(0);
158
+ const Counter = () => {
159
+ const doubleCount = () => count() * 2;
160
+
161
+ return <Label text={`Count: ${doubleCount()}`}></Label>;
162
+ };
163
+ const anchorNode = { r: anchorNodeRef };
164
+ render(() => <Counter />, anchorNode);
165
+ commitEarly();
166
+ expect(anchorNode).toEqual({ r: anchorNodeRef, c: [{ r: { id: 1 } }] });
167
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
168
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Count: 0");
169
+
170
+ setCount(10);
171
+ commitEarly();
172
+ expect(anchorNode).toEqual({ r: anchorNodeRef, c: [{ r: { id: 1 } }] });
173
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(2);
174
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Count: 20");
175
+ });
176
+
177
+ test("should work with Show", async () => {
178
+ const [loggedIn, setLoggedIn] = createSignal(false);
179
+ const App = () => (
180
+ <>
181
+ <Show when={loggedIn()} fallback={<Label text="Logged Out"></Label>}>
182
+ <Label text="Logged In"></Label>
183
+ </Show>
184
+ </>
185
+ );
186
+ const anchorNode = { r: 0 };
187
+ render(() => <App />, anchorNode);
188
+ commitEarly();
189
+ expect(anchorNode).toEqual({ r: 0, c: [{ r: { id: 1 } }] });
190
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
191
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "Logged Out");
192
+
193
+ setLoggedIn(true);
194
+ commitEarly();
195
+ expect(anchorNode).toEqual({ r: 0, c: [{ r: { id: 2 } }] });
196
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(2);
197
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 2 }, { id: 2 }, "Logged In");
198
+ });
199
+
200
+ test("should work with Show without fallback (sole child)", async () => {
201
+ const [visible, setVisible] = createSignal(false);
202
+ const App = () => (
203
+ <>
204
+ <Show when={visible()}>
205
+ <Label text="Visible"></Label>
206
+ </Show>
207
+ </>
208
+ );
209
+ const anchorNode = { r: 0 } as DomNode;
210
+ render(() => <App />, anchorNode);
211
+ commitEarly();
212
+
213
+ // When Show is the sole child with when=false and no fallback, parent has no children
214
+ // (the universal renderer's cleanChildren with marker=undefined removes all children)
215
+ expect(anchorNode.c?.length ?? 0).toBe(0);
216
+
217
+ setVisible(true);
218
+ commitEarly();
219
+ expect(anchorNode.c!.length).toBe(1);
220
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, anchorNode.c![0].r, expect.anything(), "Visible");
221
+
222
+ setVisible(false);
223
+ commitEarly();
224
+ // Should not throw when toggling back to false
225
+ expect(anchorNode.c?.length ?? 0).toBe(0);
226
+ });
227
+
228
+ test("should work with Show without fallback alongside siblings", async () => {
229
+ // Show must be inside a real element (not Fragment) with siblings to trigger
230
+ // the insert(parent, show, marker) code path where createTextNode is called.
231
+ // Fragments compile to arrays which use a different (marker-free) path.
232
+ const [visible, setVisible] = createSignal(false);
233
+ const App = () => (
234
+ <group>
235
+ <label text="Before" />
236
+ <Show when={visible()}>
237
+ <label text="Dynamic" />
238
+ </Show>
239
+ <label text="After" />
240
+ </group>
241
+ );
242
+ const anchorNode = { r: 0 } as DomNode;
243
+ render(() => <App />, anchorNode);
244
+ commitEarly();
245
+
246
+ // group is child of anchor; its children include Before, placeholder, After
247
+ const groupNode = anchorNode.c![0];
248
+ expect(groupNode.c!.length).toBeGreaterThanOrEqual(2);
249
+
250
+ setVisible(true);
251
+ commitEarly();
252
+ const dynamicSet = mockTmApi.setNodePropString.mock.calls.find((call: unknown[]) => call[3] === "Dynamic");
253
+ expect(dynamicSet).toBeDefined();
254
+
255
+ setVisible(false);
256
+ commitEarly();
257
+ // Should not throw — placeholder replaces Dynamic label
258
+ expect(groupNode.c!.length).toBeGreaterThanOrEqual(2);
259
+ });
260
+
261
+ test("can re-order elements without re-creating", async () => {
262
+ const [cats, setCats] = createSignal([
263
+ { id: "J---aiyznGQ", name: "Keyboard Cat" },
264
+ { id: "z_AbfPXTKms", name: "Maru" },
265
+ { id: "OUtn3pvWmpg", name: "Henri The Existential Cat" }
266
+ ]);
267
+ const App = () => {
268
+ return (
269
+ <>
270
+ <For each={cats()}>
271
+ {(cat, i) => <Label text={`idx: ${i()}, id: ${cat.id}, name: ${cat.name}`}></Label>}
272
+ </For>
273
+ </>
274
+ );
275
+ };
276
+ const anchorNode = { r: { id: 0 } };
277
+ render(() => <App />, anchorNode);
278
+ commitEarly();
279
+ expect(anchorNode).toEqual({
280
+ r: { id: 0 },
281
+ c: [{ r: { id: 1 } }, { r: { id: 2 } }, { r: { id: 3 } }]
282
+ });
283
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(3);
284
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
285
+ {},
286
+ { id: 1 },
287
+ { id: 2 },
288
+ "idx: 0, id: J---aiyznGQ, name: Keyboard Cat"
289
+ );
290
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
291
+ {},
292
+ { id: 2 },
293
+ { id: 2 },
294
+ "idx: 1, id: z_AbfPXTKms, name: Maru"
295
+ );
296
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
297
+ {},
298
+ { id: 3 },
299
+ { id: 2 },
300
+ "idx: 2, id: OUtn3pvWmpg, name: Henri The Existential Cat"
301
+ );
302
+
303
+ const currentCats = cats();
304
+ setCats([currentCats[1], currentCats[0], currentCats[2]]);
305
+ commitEarly();
306
+ expect(anchorNode).toEqual({
307
+ r: { id: 0 },
308
+ c: [{ r: { id: 2 } }, { r: { id: 1 } }, { r: { id: 3 } }]
309
+ });
310
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(5);
311
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
312
+ {},
313
+ { id: 2 },
314
+ { id: 2 },
315
+ "idx: 0, id: z_AbfPXTKms, name: Maru"
316
+ );
317
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
318
+ {},
319
+ { id: 1 },
320
+ { id: 2 },
321
+ "idx: 1, id: J---aiyznGQ, name: Keyboard Cat"
322
+ );
323
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith(
324
+ {},
325
+ { id: 3 },
326
+ { id: 2 },
327
+ "idx: 2, id: OUtn3pvWmpg, name: Henri The Existential Cat"
328
+ );
329
+ expect(mockTmApi.insertNodeBefore).toHaveBeenCalledTimes(1);
330
+ expect(mockTmApi.insertNodeBefore).toHaveBeenCalledWith({}, { id: 0 }, { id: 1 }, { id: 3 });
331
+ expect(mockTmApi.removeNode).toHaveBeenCalledTimes(0);
332
+ });
333
+
334
+ test("Changing visibility should removeNode and create new ones", async () => {
335
+ const [visibility, setVisibility] = createSignal(true);
336
+ const App = () => {
337
+ return (
338
+ <>
339
+ <Show when={visibility()}>
340
+ <Label text="Toy Story"></Label>
341
+ </Show>
342
+ <Label text="Finding Nemo"></Label>
343
+ <Show when={!visibility()}>
344
+ <Label text="Cars"></Label>
345
+ </Show>
346
+ <Label text="Up"></Label>
347
+ </>
348
+ );
349
+ };
350
+
351
+ const anchorNode = { r: { id: 0 } };
352
+ render(() => <App />, anchorNode);
353
+ commitEarly();
354
+ expect(anchorNode).toEqual({
355
+ r: { id: 0 },
356
+ c: [{ r: { id: 1 } }, { r: { id: 2 } }, { r: { id: 3 } }]
357
+ });
358
+ setVisibility(false);
359
+ commitEarly();
360
+ expect(anchorNode).toEqual({
361
+ r: { id: 0 },
362
+ c: [{ r: { id: 2 } }, { r: { id: 4 } }, { r: { id: 3 } }]
363
+ });
364
+
365
+ expect(mockTmApi.removeNode).toHaveBeenCalledTimes(1);
366
+ setVisibility(true);
367
+ commitEarly();
368
+ expect(anchorNode).toEqual({
369
+ r: { id: 0 },
370
+ c: [{ r: { id: 5 } }, { r: { id: 2 } }, { r: { id: 3 } }]
371
+ });
372
+
373
+ expect(mockTmApi.removeNode).toHaveBeenCalledTimes(2);
374
+ });
375
+
376
+ test("can add elements without re-creating", async () => {
377
+ const [cats, setCats] = createSignal(["Keyboard Cat", "Maru", "Henri The Existential Cat"]);
378
+ const App = () => {
379
+ return (
380
+ <>
381
+ <For each={cats()}>{(cat, i) => <Label text={`idx: ${i()}, ${cat}`}></Label>}</For>
382
+ </>
383
+ );
384
+ };
385
+ const anchorNode = { r: { id: 0 } };
386
+ render(() => <App />, anchorNode);
387
+ commitEarly();
388
+ expect(anchorNode).toEqual({
389
+ r: { id: 0 },
390
+ c: [{ r: { id: 1 } }, { r: { id: 2 } }, { r: { id: 3 } }]
391
+ });
392
+
393
+ await new Promise(resolve => setTimeout(resolve, 0));
394
+ setCats(cats => cats.concat("New Cat"));
395
+ commitEarly();
396
+ console.log(anchorNode);
397
+ expect(anchorNode).toEqual({
398
+ r: { id: 0 },
399
+ c: [{ r: { id: 1 } }, { r: { id: 2 } }, { r: { id: 3 } }, { r: { id: 4 } }]
400
+ });
401
+ });
402
+
403
+ test("can add items to contentNode", async () => {
404
+ const [items, setItems] = createSignal(["Drama", "Action", "Horror", "Comedy"]);
405
+ const App = () => {
406
+ return (
407
+ <>
408
+ <LabelList id="moviemenu" translation={[160, 92]} itemSize={[440, 48]}>
409
+ <ContentNode role="content">
410
+ <For each={items()}>{(item, i) => <ContentNode title={`idx: ${i()} ${item}`} />}</For>
411
+ </ContentNode>
412
+ </LabelList>
413
+ </>
414
+ );
415
+ };
416
+ const anchorNode = { r: 0 };
417
+ render(() => <App />, anchorNode);
418
+ commitEarly();
419
+ console.log(JSON.stringify(anchorNode));
420
+ expect(anchorNode).toEqual({
421
+ r: 0,
422
+ c: [
423
+ {
424
+ r: { id: 1 },
425
+ c: [
426
+ {
427
+ r: { id: 2 },
428
+ c: [{ r: { id: 3 } }, { r: { id: 4 } }, { r: { id: 5 } }, { r: { id: 6 } }]
429
+ }
430
+ ]
431
+ }
432
+ ]
433
+ });
434
+ expect(mockTmApi.setNodePropArray).toHaveBeenCalledTimes(2);
435
+ expect(mockTmApi.setNodePropArray).toHaveBeenCalledWith({}, { id: 1 }, { id: 6 }, { id: 1 });
436
+ expect(mockTmApi.setNodePropArray).toHaveBeenCalledWith({}, { id: 1 }, { id: 7 }, { id: 2 });
437
+
438
+ await new Promise(resolve => setTimeout(resolve, 0));
439
+ console.log("heree...1");
440
+ setItems(items => items.concat("New Cat"));
441
+ commitEarly();
442
+ console.log(anchorNode);
443
+ expect(anchorNode).toEqual({
444
+ r: 0,
445
+ c: [
446
+ {
447
+ r: { id: 1 },
448
+ c: [
449
+ {
450
+ r: { id: 2 },
451
+ c: [
452
+ { r: { id: 3 } },
453
+ { r: { id: 4 } },
454
+ { r: { id: 5 } },
455
+ { r: { id: 6 } },
456
+ { r: { id: 7 } }
457
+ ]
458
+ }
459
+ ]
460
+ }
461
+ ]
462
+ });
463
+ });
464
+
465
+ test("should respond to external signals", async () => {
466
+ const [signal, setSignal] = createSignal(0);
467
+
468
+ const App = (props: { i: number }) => <Label text={`${props.i}`}></Label>;
469
+
470
+ const anchorNode = { r: 0 };
471
+ const props = {} as { i: number };
472
+ Object.defineProperty(props, "i", {
473
+ get: signal,
474
+ enumerable: true
475
+ });
476
+ const dispose = render(() => <App {...props} />, anchorNode);
477
+ commitEarly();
478
+
479
+ // Initial render check
480
+ await new Promise(resolve => setTimeout(resolve, 0));
481
+ expect(anchorNode).toEqual({ r: 0, c: [{ r: { id: 1 } }] });
482
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
483
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "0");
484
+
485
+ // Update signal and check if the component updates
486
+ setSignal(101);
487
+ commitEarly();
488
+ await new Promise(resolve => setTimeout(resolve, 0));
489
+ expect(anchorNode).toEqual({ r: 0, c: [{ r: { id: 1 } }] });
490
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(2);
491
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 }, "101");
492
+
493
+ dispose();
494
+ });
495
+
496
+ test("should render multiple sub trees", async () => {
497
+ // Use fake timers for this specific test to prevent runaway intervals
498
+ jest.useFakeTimers();
499
+
500
+ const [count1, setCount1] = createSignal(1);
501
+
502
+ const App1 = () => {
503
+ return (
504
+ <>
505
+ <ContentNode title={`Hello World ${count1()}`}></ContentNode>
506
+ </>
507
+ );
508
+ };
509
+
510
+ const [count2, setCount2] = createSignal(2);
511
+ const App2 = () => {
512
+ return (
513
+ <>
514
+ <Label text={`Hello World ${count2()}`}></Label>
515
+ </>
516
+ );
517
+ };
518
+
519
+ const anchorNode1 = { r: 0, c: [] };
520
+ const dispose1 = render(() => <App1 />, anchorNode1);
521
+ commitEarly();
522
+ expect(anchorNode1).toEqual({
523
+ r: 0,
524
+ c: [{ r: { id: 1 } }]
525
+ });
526
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(1);
527
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 3 }, "Hello World 1");
528
+
529
+ const anchorNode2 = { r: 1, c: [] };
530
+ const dispose2 = render(() => <App2 />, anchorNode2);
531
+ commitEarly();
532
+ expect(anchorNode2).toEqual({
533
+ r: 1,
534
+ c: [{ r: { id: 2 } }]
535
+ });
536
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(2);
537
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 2 }, { id: 2 }, "Hello World 2");
538
+
539
+ function updateCount1() {
540
+ setCount1(odd => odd + 2);
541
+ commitEarly();
542
+ }
543
+
544
+ function updateCount2() {
545
+ setCount2(even => even + 2);
546
+ commitEarly();
547
+ }
548
+
549
+ updateCount1();
550
+ updateCount2();
551
+ // TODO: Check if the props are updated
552
+ expect(anchorNode1).toEqual({
553
+ r: 0,
554
+ c: [{ r: { id: 1 } }]
555
+ });
556
+ expect(anchorNode2).toEqual({
557
+ r: 1,
558
+ c: [{ r: { id: 2 } }]
559
+ });
560
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledTimes(4);
561
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 1 }, { id: 3 }, "Hello World 3");
562
+ expect(mockTmApi.setNodePropString).toHaveBeenCalledWith({}, { id: 2 }, { id: 2 }, "Hello World 4");
563
+
564
+ let endTest;
565
+ const promise = new Promise(resolve => (endTest = resolve));
566
+ const clearCount1 = setInterval(() => {
567
+ if (count1() < 10) {
568
+ updateCount1();
569
+ } else {
570
+ clearInterval(clearCount1);
571
+ }
572
+ }, 1);
573
+ const clearCount2 = setInterval(() => {
574
+ if (count2() < 10) {
575
+ updateCount2();
576
+ } else {
577
+ clearInterval(clearCount2);
578
+ }
579
+ }, 1);
580
+
581
+ let count = 0;
582
+ const clearCombined = setInterval(() => {
583
+ if (count < 5) {
584
+ updateCount1();
585
+ updateCount2();
586
+ } else if (count === 5) {
587
+ dispose1();
588
+ updateCount1();
589
+ updateCount2();
590
+ } else if (count === 6) {
591
+ dispose2();
592
+ updateCount1();
593
+ updateCount2();
594
+ } else if (count > 6) {
595
+ clearInterval(clearCombined);
596
+ endTest();
597
+ }
598
+ ++count;
599
+ }, 10);
600
+
601
+ // Fast forward timers to complete the test quickly
602
+ jest.advanceTimersByTime(1000); // Advance by 1 second
603
+
604
+ // Restore real timers before awaiting the promise
605
+ jest.useRealTimers();
606
+
607
+ return promise;
608
+ });
609
+ test("should warn if orphan instances are present", () => {
610
+ const App = () => {
611
+ return (
612
+ <>
613
+ <Label text="This is a Header"></Label>
614
+ </>
615
+ );
616
+ };
617
+
618
+ const anchorNode = { r: 0 };
619
+ render(() => <App />, anchorNode);
620
+
621
+ // Should not throw an exception, just log a warning
622
+ expect(() => commitEarly()).not.toThrow();
623
+
624
+ cleanUp();
625
+ });
626
+
627
+ test("setFocus be called with true", async () => {
628
+ const App = () => {
629
+ return (
630
+ <LabelList gainFocus={true} id="moviemenu" translation={{ fhd: [160, 92], hd: [110, 62] }}>
631
+ <ContentNode>
632
+ <ContentNode title="Backdrops preferences" />
633
+ <ContentNode title="Connect" />
634
+ <ContentNode title="Help" />
635
+ </ContentNode>
636
+ </LabelList>
637
+ );
638
+ };
639
+
640
+ const anchorNode = { r: { id: 0 }, n: { id: 0 } };
641
+ render(() => <App />, anchorNode);
642
+ commitEarly();
643
+
644
+ expect(mockTmApi.setFocus).toHaveBeenCalledTimes(1);
645
+ expect(mockTmApi.setFocus).toHaveBeenCalledWith({}, { id: 1 }, true);
646
+ });
647
+
648
+ test("setFocus be called with false", async () => {
649
+ const [focus, setFocus] = createSignal(true);
650
+ const App = () => {
651
+ return (
652
+ <LabelList gainFocus={focus()} id="moviemenu" translation={{ fhd: [160, 92], hd: [110, 62] }}>
653
+ <ContentNode>
654
+ <ContentNode title="Backdrops preferences" />
655
+ <ContentNode title="Connect" />
656
+ <ContentNode title="Help" />
657
+ </ContentNode>
658
+ </LabelList>
659
+ );
660
+ };
661
+
662
+ // canReceiveFocus=true simulates an anchor node that extends FocusBoundaryBase,
663
+ // which is the only case where the re-anchor fires.
664
+ // Use r:{} so the anchor node ref stays an empty object, matching the setFocus assertions below.
665
+ const anchorNode: AnchorNode = { r: {}, n: anchorNameRef, canReceiveFocus: true };
666
+ render(() => <App />, anchorNode);
667
+ commitEarly();
668
+ expect(mockTmApi.setFocus).toHaveBeenCalledTimes(1);
669
+ expect(mockTmApi.setFocus).toHaveBeenCalledWith({}, { id: 1 }, true);
670
+
671
+ setFocus(false);
672
+ commitEarly();
673
+ expect(mockTmApi.setFocus).toHaveBeenCalledTimes(3);
674
+ expect(mockTmApi.setFocus).toHaveBeenCalledWith({}, { id: 1 }, false);
675
+ // SG focus is re-anchored to the node's owning anchor node when gainFocus=false
676
+ // fires with no new SG target in the same transaction.
677
+ expect(mockTmApi.setFocus).toHaveBeenCalledWith({}, {}, true);
678
+ });
679
+
680
+ test("Register field change handler", async () => {
681
+ const App = () => {
682
+ return <Button onButtonSelected={() => {}} text="Hello World!" opacity={1} />;
683
+ };
684
+ const anchorNode = { r: 0 };
685
+ render(() => <App />, anchorNode);
686
+ commitEarly();
687
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledTimes(1);
688
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 });
689
+ });
690
+
691
+ test("Change handler based on signal", async () => {
692
+ const [isSelected, setIsSelected] = createSignal(0);
693
+ const App = () => {
694
+ return (
695
+ <Button onButtonSelected={[null, () => {}, undefined][isSelected()]} text="Hello World!" opacity={1} />
696
+ );
697
+ };
698
+ const anchorNode = { r: 0 };
699
+ render(() => <App />, anchorNode);
700
+ commitEarly();
701
+ expect(mockTmApi.unregisterFieldHandler).toHaveBeenCalledTimes(0);
702
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledTimes(0);
703
+
704
+ setIsSelected(1);
705
+ commitEarly();
706
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledTimes(1);
707
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledWith({}, { id: 1 }, { id: 2 });
708
+ expect(mockTmApi.unregisterFieldHandler).toHaveBeenCalledTimes(0);
709
+
710
+ setIsSelected(2);
711
+ commitEarly();
712
+ expect(mockTmApi.unregisterFieldHandler).toHaveBeenCalledTimes(1);
713
+ expect(mockTmApi.registerFieldChangeHandler).toHaveBeenCalledTimes(1);
714
+ });
715
+
716
+ // TODO: These are cases are possible but not implemented yet
717
+ test("Assign node ref as child and also as prop", async () => {});
718
+ test("Remove node ref as child and assign as prop and viceversa", async () => {});
719
+ });