@roku-sdk/components 0.0.0-stage → 2.19.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 (212) 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 +85 -3
  8. package/src/__tests__/container.test.tsx +150 -0
  9. package/src/__tests__/effect-gradient-validation.test.tsx +133 -0
  10. package/src/__tests__/glyphs.test.ts +202 -0
  11. package/src/__tests__/modal-boundary-export.test.ts +12 -0
  12. package/src/__tests__/native-modal.test.tsx +224 -0
  13. package/src/__tests__/tween-release.test.tsx +125 -0
  14. package/src/__tests__/tween-stops-shape.test.tsx +165 -0
  15. package/src/__tests__/tween-without-compat.test.tsx +64 -0
  16. package/src/__tests__/tween.test.tsx +195 -0
  17. package/src/blended-busy-spinner.props.ts +37 -0
  18. package/src/blended-busy-spinner.tsx +13 -0
  19. package/src/bounding-tracker.props.ts +18 -0
  20. package/src/bounding-tracker.tsx +13 -0
  21. package/src/button-row.tsx +126 -0
  22. package/src/channel-store.props.ts +425 -0
  23. package/src/channel-store.tsx +76 -0
  24. package/src/container.tsx +130 -0
  25. package/src/core/jsx-runtime.ts +39 -0
  26. package/src/focusable-group.props.ts +8 -0
  27. package/src/focusable-group.tsx +17 -0
  28. package/src/focusable.props.ts +14 -0
  29. package/src/focusable.tsx +17 -0
  30. package/src/glyphs.ts +116 -0
  31. package/src/helpers/index.tsx +4 -0
  32. package/src/helpers/resolution-context.tsx +68 -0
  33. package/src/helpers/scale-for-device-resolution.ts +31 -0
  34. package/src/index.tsx +70 -0
  35. package/src/jsx-props.ts +87 -0
  36. package/src/kdfs.d.ts +14 -0
  37. package/src/kdfs.ts +15 -0
  38. package/src/key-handler.props.ts +33 -0
  39. package/src/key-handler.tsx +24 -0
  40. package/src/modal-dialog.props.ts +38 -0
  41. package/src/modal-dialog.tsx +41 -0
  42. package/src/retain-asset.ts +24 -0
  43. package/src/sg-intrinsics.json +1153 -0
  44. package/src/sg-nodes/__tests__/__snapshots__/font.test.tsx.snap +13 -0
  45. package/src/sg-nodes/__tests__/__snapshots__/prop-forwarding.test.tsx.snap +4409 -0
  46. package/src/sg-nodes/__tests__/core-props.test.ts +301 -0
  47. package/src/sg-nodes/__tests__/font.test.tsx +141 -0
  48. package/src/sg-nodes/__tests__/imperative-methods.test.ts +276 -0
  49. package/src/sg-nodes/__tests__/prop-forwarding.test.tsx +1049 -0
  50. package/src/sg-nodes/__tests__/solid-integration.test.tsx +719 -0
  51. package/src/sg-nodes/animated-image.props.ts +64 -0
  52. package/src/sg-nodes/animated-image.tsx +23 -0
  53. package/src/sg-nodes/animation.props.ts +292 -0
  54. package/src/sg-nodes/animation.tsx +190 -0
  55. package/src/sg-nodes/array-grid.ts +602 -0
  56. package/src/sg-nodes/audio.props.ts +302 -0
  57. package/src/sg-nodes/audio.tsx +57 -0
  58. package/src/sg-nodes/busy-spinner.props.ts +59 -0
  59. package/src/sg-nodes/busy-spinner.tsx +84 -0
  60. package/src/sg-nodes/button-group.props.ts +141 -0
  61. package/src/sg-nodes/button-group.tsx +30 -0
  62. package/src/sg-nodes/button.props.ts +119 -0
  63. package/src/sg-nodes/button.tsx +31 -0
  64. package/src/sg-nodes/check-list.props.ts +71 -0
  65. package/src/sg-nodes/check-list.tsx +55 -0
  66. package/src/sg-nodes/content-node.props.ts +1316 -0
  67. package/src/sg-nodes/content-node.tsx +153 -0
  68. package/src/sg-nodes/core-props.ts +197 -0
  69. package/src/sg-nodes/custom-keyboard.props.ts +108 -0
  70. package/src/sg-nodes/custom-keyboard.tsx +48 -0
  71. package/src/sg-nodes/dialog.props.ts +159 -0
  72. package/src/sg-nodes/dialog.tsx +36 -0
  73. package/src/sg-nodes/dynamic-key-grid.props.ts +22 -0
  74. package/src/sg-nodes/dynamic-key-grid.tsx +16 -0
  75. package/src/sg-nodes/dynamic-keyboard.props.ts +30 -0
  76. package/src/sg-nodes/dynamic-keyboard.tsx +18 -0
  77. package/src/sg-nodes/dynamic-mini-keyboard.props.ts +22 -0
  78. package/src/sg-nodes/dynamic-mini-keyboard.tsx +18 -0
  79. package/src/sg-nodes/dynamic-pinpad.props.ts +57 -0
  80. package/src/sg-nodes/dynamic-pinpad.tsx +18 -0
  81. package/src/sg-nodes/effect.props.ts +119 -0
  82. package/src/sg-nodes/effect.tsx +167 -0
  83. package/src/sg-nodes/font.props.ts +45 -0
  84. package/src/sg-nodes/font.tsx +80 -0
  85. package/src/sg-nodes/group.props.ts +105 -0
  86. package/src/sg-nodes/group.tsx +30 -0
  87. package/src/sg-nodes/label-list.props.ts +86 -0
  88. package/src/sg-nodes/label-list.tsx +64 -0
  89. package/src/sg-nodes/label.props.ts +207 -0
  90. package/src/sg-nodes/label.tsx +56 -0
  91. package/src/sg-nodes/layout-group.props.ts +69 -0
  92. package/src/sg-nodes/layout-group.tsx +75 -0
  93. package/src/sg-nodes/list-panel.props.ts +103 -0
  94. package/src/sg-nodes/list-panel.tsx +49 -0
  95. package/src/sg-nodes/markup-grid.props.ts +78 -0
  96. package/src/sg-nodes/markup-grid.tsx +38 -0
  97. package/src/sg-nodes/markup-list.props.ts +66 -0
  98. package/src/sg-nodes/markup-list.tsx +38 -0
  99. package/src/sg-nodes/mask-group.props.ts +78 -0
  100. package/src/sg-nodes/mask-group.tsx +59 -0
  101. package/src/sg-nodes/media-playback-methods.ts +20 -0
  102. package/src/sg-nodes/node.ts +77 -0
  103. package/src/sg-nodes/overhang.props.ts +169 -0
  104. package/src/sg-nodes/overhang.tsx +29 -0
  105. package/src/sg-nodes/panel-set.props.ts +71 -0
  106. package/src/sg-nodes/panel-set.tsx +53 -0
  107. package/src/sg-nodes/panel.props.ts +176 -0
  108. package/src/sg-nodes/panel.tsx +56 -0
  109. package/src/sg-nodes/poster-grid.props.ts +277 -0
  110. package/src/sg-nodes/poster-grid.tsx +45 -0
  111. package/src/sg-nodes/poster.props.ts +125 -0
  112. package/src/sg-nodes/poster.tsx +44 -0
  113. package/src/sg-nodes/radio-button-list.props.ts +59 -0
  114. package/src/sg-nodes/radio-button-list.tsx +68 -0
  115. package/src/sg-nodes/rectangle.props.ts +29 -0
  116. package/src/sg-nodes/rectangle.tsx +27 -0
  117. package/src/sg-nodes/row-list.props.ts +288 -0
  118. package/src/sg-nodes/row-list.tsx +47 -0
  119. package/src/sg-nodes/scene.props.ts +65 -0
  120. package/src/sg-nodes/scene.tsx +15 -0
  121. package/src/sg-nodes/scrollable-text.props.ts +133 -0
  122. package/src/sg-nodes/scrollable-text.tsx +64 -0
  123. package/src/sg-nodes/scrolling-label.props.ts +106 -0
  124. package/src/sg-nodes/scrolling-label.tsx +42 -0
  125. package/src/sg-nodes/simple-label.props.ts +156 -0
  126. package/src/sg-nodes/simple-label.tsx +59 -0
  127. package/src/sg-nodes/sound-effect.props.ts +130 -0
  128. package/src/sg-nodes/sound-effect.tsx +65 -0
  129. package/src/sg-nodes/standard-dialog.props.ts +520 -0
  130. package/src/sg-nodes/standard-dialog.tsx +428 -0
  131. package/src/sg-nodes/standard-keyboard-dialog.props.ts +84 -0
  132. package/src/sg-nodes/standard-keyboard-dialog.tsx +58 -0
  133. package/src/sg-nodes/standard-message-dialog.props.ts +56 -0
  134. package/src/sg-nodes/standard-message-dialog.tsx +47 -0
  135. package/src/sg-nodes/standard-pinpad-dialog.props.ts +64 -0
  136. package/src/sg-nodes/standard-pinpad-dialog.tsx +44 -0
  137. package/src/sg-nodes/standard-progress-dialog.props.ts +30 -0
  138. package/src/sg-nodes/standard-progress-dialog.tsx +40 -0
  139. package/src/sg-nodes/target-group.props.ts +278 -0
  140. package/src/sg-nodes/target-group.tsx +63 -0
  141. package/src/sg-nodes/target-list.props.ts +128 -0
  142. package/src/sg-nodes/target-list.tsx +66 -0
  143. package/src/sg-nodes/target-set.props.ts +63 -0
  144. package/src/sg-nodes/target-set.tsx +42 -0
  145. package/src/sg-nodes/time-grid.props.ts +646 -0
  146. package/src/sg-nodes/time-grid.tsx +87 -0
  147. package/src/sg-nodes/tween.props.ts +115 -0
  148. package/src/sg-nodes/types.ts +150 -0
  149. package/src/sg-nodes/video.props.ts +642 -0
  150. package/src/sg-nodes/video.tsx +75 -0
  151. package/src/sg-nodes/voice-text-edit-box.props.ts +91 -0
  152. package/src/sg-nodes/voice-text-edit-box.tsx +15 -0
  153. package/src/sg-nodes/zoom-row-list.props.ts +416 -0
  154. package/src/sg-nodes/zoom-row-list.tsx +42 -0
  155. package/src/standard-dialog-next/__tests__/standard-dialog-next.test.tsx +186 -0
  156. package/src/standard-dialog-next/index.ts +5 -0
  157. package/src/standard-dialog-next/standard-dialog-next.props.ts +80 -0
  158. package/src/standard-dialog-next/standard-dialog-next.tsx +326 -0
  159. package/src/test-helpers/tween-test-setup.ts +78 -0
  160. package/src/tween.tsx +173 -0
  161. package/src/types.ts +8 -0
  162. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.brs +13 -0
  163. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml +10 -0
  164. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml.d.ts +42 -0
  165. package/src/xml-components/bounding-tracker/BoundingTracker.brs +26 -0
  166. package/src/xml-components/bounding-tracker/BoundingTracker.xml +14 -0
  167. package/src/xml-components/bounding-tracker/BoundingTracker.xml.d.ts +42 -0
  168. package/src/xml-components/channel-store/channelStoreExt.brs +159 -0
  169. package/src/xml-components/channel-store/channelStoreExt.xml +29 -0
  170. package/src/xml-components/channel-store/channelStoreExt.xml.d.ts +57 -0
  171. package/src/xml-components/focus/focusable-group.brs +251 -0
  172. package/src/xml-components/focus/focusable-group.xml +15 -0
  173. package/src/xml-components/focus/focusable-group.xml.d.ts +42 -0
  174. package/src/xml-components/focus/focusable.xml +18 -0
  175. package/src/xml-components/focus/focusable.xml.d.ts +48 -0
  176. package/src/xml-components/keyboards/CustomAddressKDF.keyboard.json +547 -0
  177. package/src/xml-components/keyboards/CustomKeyboard.brs +322 -0
  178. package/src/xml-components/keyboards/CustomKeyboard.xml +45 -0
  179. package/src/xml-components/keyboards/CustomKeyboard.xml.d.ts +73 -0
  180. package/src/xml-components/keyboards/KeyGridExt.xml +4 -0
  181. package/src/xml-components/keyboards/KeyGridExt.xml.d.ts +37 -0
  182. package/src/xml-components/keyboards/KeyboardExt.xml +4 -0
  183. package/src/xml-components/keyboards/KeyboardExt.xml.d.ts +37 -0
  184. package/src/xml-components/keyboards/MOPPinPadCustomKDF.keyboard.json +151 -0
  185. package/src/xml-components/keyboards/MiniKeyboardExt.xml +4 -0
  186. package/src/xml-components/keyboards/MiniKeyboardExt.xml.d.ts +37 -0
  187. package/src/xml-components/keyboards/PhonePadCustomKDF.keyboard.json +42 -0
  188. package/src/xml-components/keyboards/PinPadExt.brs +102 -0
  189. package/src/xml-components/keyboards/PinPadExt.xml +17 -0
  190. package/src/xml-components/keyboards/PinPadExt.xml.d.ts +49 -0
  191. package/src/xml-components/keyboards/WifiCustomKDF.keyboard.json +1242 -0
  192. package/src/xml-components/keyhandler/KeyHandler.brs +30 -0
  193. package/src/xml-components/keyhandler/KeyHandler.xml +16 -0
  194. package/src/xml-components/keyhandler/KeyHandler.xml.d.ts +45 -0
  195. package/src/xml-components/managed-list-panel/ManagedListPanel.brs +137 -0
  196. package/src/xml-components/managed-list-panel/ManagedListPanel.xml +10 -0
  197. package/src/xml-components/managed-list-panel/ManagedListPanel.xml.d.ts +37 -0
  198. package/src/xml-components/modal/Modal.brs +105 -0
  199. package/src/xml-components/modal/Modal.xml +22 -0
  200. package/src/xml-components/modal/Modal.xml.d.ts +54 -0
  201. package/src/xml-components/modal/StandardDialogExt.xml +10 -0
  202. package/src/xml-components/modal/StandardDialogExt.xml.d.ts +37 -0
  203. package/src/xml-components/modal/StandardKeyboardDialogExt.xml +10 -0
  204. package/src/xml-components/modal/StandardKeyboardDialogExt.xml.d.ts +37 -0
  205. package/src/xml-components/modal/StandardMessageDialogExt.xml +10 -0
  206. package/src/xml-components/modal/StandardMessageDialogExt.xml.d.ts +37 -0
  207. package/src/xml-components/modal/StandardPinPadDialogExt.xml +10 -0
  208. package/src/xml-components/modal/StandardPinPadDialogExt.xml.d.ts +37 -0
  209. package/src/xml-components/modal/StandardProgressDialogExt.xml +10 -0
  210. package/src/xml-components/modal/StandardProgressDialogExt.xml.d.ts +37 -0
  211. package/src/xml-components/modal/tests/Modal.test.brs +195 -0
  212. package/tsconfig.json +10 -0
@@ -0,0 +1,150 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Transaction semantics of the imperative `ContainerRef` API.
5
+ *
6
+ * `insertNode` / `removeNode` are called from outside the Solid renderer, so
7
+ * unlike every renderer-driven mutation they have to open the transaction
8
+ * themselves. The mocks below record native tree-manipulation calls and
9
+ * transaction boundaries into one ordered log so a call that escapes its
10
+ * transaction is visible.
11
+ *
12
+ * Imports use relative module paths (not the @roku-sdk/components barrel) so
13
+ * Jest only transforms this feature's dependency subtree, per the monorepo's
14
+ * OOM-avoidance guidance.
15
+ */
16
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
17
+ import { registerInternStringApi } from "@roku-sdk/common/string-interner";
18
+ import { registerTransactionApi, transactionManager } from "@roku-sdk/common/transaction";
19
+ import { DomNode, InternStringApi, StructuredDataApi, TmNodeRef, TreeManipulationApi } from "@roku-sdk/interfaces";
20
+ import { makeSolidRenderer, Renderer } from "@roku-sdk/solid";
21
+
22
+ import { Container, ContainerRef } from "../container";
23
+
24
+ describe("Container imperative API", () => {
25
+ const commitEarly = transactionManager.commitEarly;
26
+
27
+ let render: Renderer<DomNode>["render"];
28
+ let mockTmApi: TreeManipulationApi;
29
+ /** Ordered log of transaction boundaries and native tree mutations. */
30
+ let calls: string[];
31
+
32
+ beforeEach(() => {
33
+ calls = [];
34
+ let nodeRef = 0;
35
+ let internRef = 0;
36
+
37
+ mockTmApi = {
38
+ createNode: jest.fn().mockImplementation((): object => ({ id: ++nodeRef })),
39
+ appendNode: jest.fn().mockImplementation(() => {
40
+ calls.push("appendNode");
41
+ }),
42
+ insertNodeBefore: jest.fn(),
43
+ removeNode: jest.fn().mockImplementation(() => {
44
+ calls.push("removeNode");
45
+ }),
46
+ setNodePropNull: jest.fn(),
47
+ setNodePropBool: jest.fn(),
48
+ setNodePropInt: jest.fn(),
49
+ setNodePropFloat: jest.fn(),
50
+ setNodePropString: jest.fn(),
51
+ setNodePropObject: jest.fn(),
52
+ setNodePropArray: jest.fn(),
53
+ setNodePropNodeRef: jest.fn(),
54
+ registerFieldChangeHandler: jest.fn(),
55
+ unregisterFieldHandler: jest.fn(),
56
+ setFocus: jest.fn()
57
+ } as unknown as TreeManipulationApi;
58
+
59
+ const internMap = new Map<string, object>();
60
+ const mockIntern: InternStringApi = {
61
+ internString: (str: string): object => {
62
+ const interned = internMap.get(str);
63
+ if (interned) {
64
+ return interned;
65
+ }
66
+ const ref = { id: ++internRef };
67
+ internMap.set(str, ref);
68
+ return ref;
69
+ }
70
+ };
71
+ registerInternStringApi(mockIntern);
72
+
73
+ registerTransactionApi({
74
+ start: () => calls.push("tx:start"),
75
+ end: () => calls.push("tx:end")
76
+ });
77
+
78
+ const mockStructuredDataApi = {
79
+ createArray: jest.fn().mockImplementation(() => ({})),
80
+ appendInt: jest.fn(),
81
+ createObject: jest.fn().mockImplementation(() => ({})),
82
+ insertInt: jest.fn(),
83
+ insertArray: jest.fn()
84
+ } as unknown as StructuredDataApi;
85
+
86
+ const solidRenderer = makeSolidRenderer(mockTmApi, mockStructuredDataApi, mockStructuredDataApi);
87
+ ({ render } = solidRenderer.renderer);
88
+ });
89
+
90
+ /** Renders a Container holding one child and returns handles to both. */
91
+ function renderContainer(): { containerRef: ContainerRef; child: DomNode; dispose: () => void } {
92
+ let containerRef!: ContainerRef;
93
+ let child!: DomNode;
94
+
95
+ const dispose = render(
96
+ () => (
97
+ <Container
98
+ ref={(el: ContainerRef) => {
99
+ containerRef = el;
100
+ }}
101
+ >
102
+ <group
103
+ ref={(el: DomNode) => {
104
+ child = el;
105
+ }}
106
+ />
107
+ </Container>
108
+ ),
109
+ { r: {} as TmNodeRef, c: [] }
110
+ );
111
+ commitEarly();
112
+ calls.length = 0;
113
+
114
+ return { containerRef, child, dispose };
115
+ }
116
+
117
+ test("removeNode detaches the child inside a transaction", () => {
118
+ const { containerRef, child, dispose } = renderContainer();
119
+
120
+ containerRef.removeNode(child);
121
+ // The detach is queued, not applied — it lands in the pre-commit hook,
122
+ // which only runs because removeNode opened a transaction.
123
+ expect(calls).toEqual(["tx:start"]);
124
+
125
+ commitEarly();
126
+ expect(calls).toEqual(["tx:start", "removeNode", "tx:end"]);
127
+ expect(child.parent).toBeUndefined();
128
+
129
+ dispose();
130
+ });
131
+
132
+ test("insertNode re-attaches the child inside a transaction", () => {
133
+ const { containerRef, child, dispose } = renderContainer();
134
+
135
+ containerRef.removeNode(child);
136
+ commitEarly();
137
+ calls.length = 0;
138
+
139
+ containerRef.insertNode(child);
140
+ // appendNode reaches native synchronously, so the transaction has to be
141
+ // open before it — on firmware 15.2.x an append outside a transaction
142
+ // raises a render-thread exception and the child is never attached.
143
+ expect(calls).toEqual(["tx:start", "appendNode"]);
144
+
145
+ commitEarly();
146
+ expect(calls).toEqual(["tx:start", "appendNode", "tx:end"]);
147
+
148
+ dispose();
149
+ });
150
+ });
@@ -0,0 +1,133 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ // ---------------------------------------------------------------------------
4
+ // Effect warns in __DEV__ when gradientColors / gradientStops violate the firmware
5
+ // constraints (2–8 colors, stops length and [0, 1] range) while gradientStyle is not
6
+ // "none". Quiet for valid gradients and for style "none" with empty arrays (effects-lab
7
+ // edit-demo). Each distinct problem is reported once per node; nothing without __DEV__.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import "@roku-sdk/compat";
11
+
12
+ import { afterEach, beforeEach, describe, expect, jest, test } from "@jest/globals";
13
+ import { createSignal, type JSX } from "@roku-sdk/solid";
14
+
15
+ import { Effect, installEffectGradientDevValidation } from "../sg-nodes/effect";
16
+ import { commitEarly, createAnchorNode, installSolidTestRenderer } from "../test-helpers/tween-test-setup";
17
+
18
+ async function flushMicrotasks(): Promise<void> {
19
+ await Promise.resolve();
20
+ await Promise.resolve();
21
+ }
22
+
23
+ describe("Effect gradient prop warning (dev only)", () => {
24
+ let render: ReturnType<typeof installSolidTestRenderer>;
25
+ let warn: jest.SpiedFunction<typeof console.warn>;
26
+ let realDev: boolean | undefined;
27
+ let dispose: (() => void) | undefined;
28
+
29
+ beforeEach(() => {
30
+ realDev = (globalThis as { __DEV__?: boolean }).__DEV__;
31
+ (globalThis as { __DEV__?: boolean }).__DEV__ = true;
32
+ installEffectGradientDevValidation();
33
+ render = installSolidTestRenderer();
34
+ warn = jest.spyOn(console, "warn").mockImplementation(() => {});
35
+ });
36
+
37
+ afterEach(() => {
38
+ dispose?.();
39
+ dispose = undefined;
40
+ (globalThis as { __DEV__?: boolean }).__DEV__ = realDev;
41
+ warn.mockRestore();
42
+ });
43
+
44
+ const mount = (element: JSX.Element): void => {
45
+ dispose = render(() => <group>{element}</group>, createAnchorNode());
46
+ commitEarly();
47
+ };
48
+
49
+ const effectWarnings = (): string[] =>
50
+ warn.mock.calls.map(c => String(c[0])).filter(message => message.startsWith("Effect"));
51
+
52
+ test("warns when gradientColors has fewer than 2 entries", async () => {
53
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
54
+ await flushMicrotasks();
55
+ expect(effectWarnings()).toEqual([
56
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
57
+ ]);
58
+ });
59
+
60
+ test("warns when gradientColors has more than 8 entries", async () => {
61
+ const colors = Array.from({ length: 9 }, (_, i) => `0xFF00000${i}`);
62
+ mount(<effect gradientStyle="radial" gradientColors={colors} />);
63
+ await flushMicrotasks();
64
+ expect(effectWarnings()).toEqual([
65
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "radial" (got 9).'
66
+ ]);
67
+ });
68
+
69
+ test("warns when gradientStops length does not match gradientColors", async () => {
70
+ mount(
71
+ <effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 0.5, 1]} />
72
+ );
73
+ await flushMicrotasks();
74
+ expect(effectWarnings()).toEqual(["Effect: gradientStops length (3) must equal gradientColors length (2)."]);
75
+ });
76
+
77
+ test("warns when a gradientStops entry is outside [0, 1]", async () => {
78
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 1.5]} />);
79
+ await flushMicrotasks();
80
+ expect(effectWarnings()).toEqual(["Effect: gradientStops entries must be in [0, 1] (got 1.5 at index 1)."]);
81
+ });
82
+
83
+ test("includes id in the warning when the effect has one", async () => {
84
+ mount(<effect id="hero-fill" gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
85
+ await flushMicrotasks();
86
+ expect(effectWarnings()).toEqual([
87
+ 'Effect[id=hero-fill]: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
88
+ ]);
89
+ });
90
+
91
+ test("warns through the PascalCase Effect wrapper", async () => {
92
+ mount(<Effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
93
+ await flushMicrotasks();
94
+ expect(effectWarnings()).toEqual([
95
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
96
+ ]);
97
+ });
98
+
99
+ test("is silent for a valid gradient", async () => {
100
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 1]} />);
101
+ await flushMicrotasks();
102
+ expect(effectWarnings()).toEqual([]);
103
+ });
104
+
105
+ test("is silent for gradientStyle none with empty colors and stops (effects-lab edit-demo)", async () => {
106
+ mount(<effect gradientStyle="none" gradientColors={[]} gradientStops={[]} />);
107
+ await flushMicrotasks();
108
+ expect(effectWarnings()).toEqual([]);
109
+ });
110
+
111
+ test("reports each distinct problem once per node across updates", async () => {
112
+ const [colors, setColors] = createSignal<string[]>(["0xFF0000FF"]);
113
+ mount(<effect gradientStyle="linear" gradientColors={colors()} />);
114
+ await flushMicrotasks();
115
+ setColors(["0xFF0000FF"]);
116
+ commitEarly();
117
+ await flushMicrotasks();
118
+ setColors(Array.from({ length: 9 }, (_, i) => `0xFF00000${i}`));
119
+ commitEarly();
120
+ await flushMicrotasks();
121
+ // Same key "colors:count" — still one warning even though the count changed.
122
+ expect(effectWarnings()).toEqual([
123
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
124
+ ]);
125
+ });
126
+
127
+ test("is silent without __DEV__", async () => {
128
+ (globalThis as { __DEV__?: boolean }).__DEV__ = false;
129
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
130
+ await flushMicrotasks();
131
+ expect(effectWarnings()).toEqual([]);
132
+ });
133
+ });
@@ -0,0 +1,202 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import { LEFT_BUTTON_GLYPH as SubpathGlyphSmoke } from "@roku-sdk/components/glyphs";
6
+
7
+ import {
8
+ BACK_BUTTON_GLYPH,
9
+ BALLOT_X,
10
+ BLACK_CIRCLE,
11
+ BLACK_STAR,
12
+ BULLET,
13
+ CHECK_MARK,
14
+ CLOSED_CAPTIONS_GLYPH,
15
+ DOT_GLYPH,
16
+ DOWN_ARROW,
17
+ DOWN_BUTTON_GLYPH,
18
+ ELLIPSIS,
19
+ EM_DASH,
20
+ EN_DASH,
21
+ FAST_FORWARD_GLYPH,
22
+ GENERAL_APP_GLYPH,
23
+ HOME_BUTTON_GLYPH,
24
+ HOME_GLYPH,
25
+ KIDS_PROFILE_GLYPH,
26
+ LEFT_ARROW,
27
+ LEFT_BUTTON_GLYPH,
28
+ LIVE_TV_GLYPH,
29
+ MICROPHONE_BUTTON_GLYPH,
30
+ MICROPHONE_GLYPH,
31
+ OK_BUTTON_GLYPH,
32
+ OPTIONS_BUTTON_GLYPH,
33
+ OPTIONS_GLYPH,
34
+ PLAY_PAUSE_BUTTON_GLYPH,
35
+ PLAY_PAUSE_GLYPH,
36
+ POWER_BUTTON_GLYPH,
37
+ QUICK_LAUNCH_BUTTON_GLYPH,
38
+ REMOTE_GLYPH,
39
+ REPLAY_GLYPH,
40
+ REWIND_GLYPH,
41
+ RIGHT_ARROW,
42
+ RIGHT_BUTTON_GLYPH,
43
+ SEARCH_BUTTON_GLYPH,
44
+ SEARCH_GLYPH,
45
+ SHORTCUT_ONE_BUTTON_GLYPH,
46
+ SHORTCUT_TWO_BUTTON_GLYPH,
47
+ SLEEP_TIMER_GLYPH,
48
+ THUMBS_DOWN_GLYPH,
49
+ THUMBS_UP_GLYPH,
50
+ UP_ARROW,
51
+ UP_BUTTON_GLYPH,
52
+ WHITE_STAR
53
+ } from "../glyphs";
54
+
55
+ describe("RokuFont01 glyph constants", () => {
56
+ test("exports expected codepoints", () => {
57
+ const expectedGlyphs: Record<string, string> = {
58
+ OPTIONS_GLYPH: "\uE000",
59
+ OK_BUTTON_GLYPH: "\uE001",
60
+ OPTIONS_BUTTON_GLYPH: "\uE002",
61
+ MICROPHONE_GLYPH: "\uE003",
62
+ PLAY_PAUSE_BUTTON_GLYPH: "\uE004",
63
+ PLAY_PAUSE_GLYPH: "\uE005",
64
+ DOT_GLYPH: "\uE006",
65
+ HOME_GLYPH: "\uE007",
66
+ SEARCH_GLYPH: "\uE008",
67
+ REMOTE_GLYPH: "\uE009",
68
+ REPLAY_GLYPH: "\uE010",
69
+ MICROPHONE_BUTTON_GLYPH: "\uE011",
70
+ SEARCH_BUTTON_GLYPH: "\uE012",
71
+ FAST_FORWARD_GLYPH: "\uE013",
72
+ REWIND_GLYPH: "\uE014",
73
+ SHORTCUT_ONE_BUTTON_GLYPH: "\uE015",
74
+ SHORTCUT_TWO_BUTTON_GLYPH: "\uE016",
75
+ CLOSED_CAPTIONS_GLYPH: "\uE017",
76
+ LIVE_TV_GLYPH: "\uE018",
77
+ SLEEP_TIMER_GLYPH: "\uE019",
78
+ GENERAL_APP_GLYPH: "\uE020",
79
+ BACK_BUTTON_GLYPH: "\uE021",
80
+ HOME_BUTTON_GLYPH: "\uE022",
81
+ QUICK_LAUNCH_BUTTON_GLYPH: "\uE023",
82
+ DOWN_BUTTON_GLYPH: "\uE024",
83
+ LEFT_BUTTON_GLYPH: "\uE025",
84
+ RIGHT_BUTTON_GLYPH: "\uE026",
85
+ KIDS_PROFILE_GLYPH: "\uE027",
86
+ UP_BUTTON_GLYPH: "\uE028",
87
+ POWER_BUTTON_GLYPH: "\uE029",
88
+ THUMBS_UP_GLYPH: "\uE030",
89
+ THUMBS_DOWN_GLYPH: "\uE031"
90
+ };
91
+
92
+ const actualGlyphs: Record<string, string> = {
93
+ OPTIONS_GLYPH,
94
+ OK_BUTTON_GLYPH,
95
+ OPTIONS_BUTTON_GLYPH,
96
+ MICROPHONE_GLYPH,
97
+ PLAY_PAUSE_BUTTON_GLYPH,
98
+ PLAY_PAUSE_GLYPH,
99
+ DOT_GLYPH,
100
+ HOME_GLYPH,
101
+ SEARCH_GLYPH,
102
+ REMOTE_GLYPH,
103
+ REPLAY_GLYPH,
104
+ MICROPHONE_BUTTON_GLYPH,
105
+ SEARCH_BUTTON_GLYPH,
106
+ FAST_FORWARD_GLYPH,
107
+ REWIND_GLYPH,
108
+ SHORTCUT_ONE_BUTTON_GLYPH,
109
+ SHORTCUT_TWO_BUTTON_GLYPH,
110
+ CLOSED_CAPTIONS_GLYPH,
111
+ LIVE_TV_GLYPH,
112
+ SLEEP_TIMER_GLYPH,
113
+ GENERAL_APP_GLYPH,
114
+ BACK_BUTTON_GLYPH,
115
+ HOME_BUTTON_GLYPH,
116
+ QUICK_LAUNCH_BUTTON_GLYPH,
117
+ DOWN_BUTTON_GLYPH,
118
+ LEFT_BUTTON_GLYPH,
119
+ RIGHT_BUTTON_GLYPH,
120
+ KIDS_PROFILE_GLYPH,
121
+ UP_BUTTON_GLYPH,
122
+ POWER_BUTTON_GLYPH,
123
+ THUMBS_UP_GLYPH,
124
+ THUMBS_DOWN_GLYPH
125
+ };
126
+
127
+ expect(actualGlyphs).toEqual(expectedGlyphs);
128
+ });
129
+
130
+ test("supports importing a single constant from subpath", () => {
131
+ expect(SubpathGlyphSmoke).toBe("\uE025");
132
+ });
133
+ });
134
+
135
+ describe("RokuFont01 standard-Unicode symbol constants", () => {
136
+ test("exports expected codepoints", () => {
137
+ expect({
138
+ BLACK_CIRCLE,
139
+ BLACK_STAR,
140
+ WHITE_STAR
141
+ }).toEqual({
142
+ BLACK_CIRCLE: "\u25CF",
143
+ BLACK_STAR: "\u2605",
144
+ WHITE_STAR: "\u2606"
145
+ });
146
+ });
147
+
148
+ test("DOT_GLYPH and BLACK_CIRCLE are distinct alignment variants of the same dot", () => {
149
+ expect(DOT_GLYPH).not.toBe(BLACK_CIRCLE);
150
+ });
151
+ });
152
+
153
+ describe("RokuUI-Regular symbol constants", () => {
154
+ test("exports expected codepoints", () => {
155
+ expect({
156
+ BULLET,
157
+ ELLIPSIS,
158
+ EN_DASH,
159
+ EM_DASH,
160
+ LEFT_ARROW,
161
+ UP_ARROW,
162
+ RIGHT_ARROW,
163
+ DOWN_ARROW,
164
+ CHECK_MARK,
165
+ BALLOT_X
166
+ }).toEqual({
167
+ BULLET: "\u2022",
168
+ ELLIPSIS: "\u2026",
169
+ EN_DASH: "\u2013",
170
+ EM_DASH: "\u2014",
171
+ LEFT_ARROW: "\u2190",
172
+ UP_ARROW: "\u2191",
173
+ RIGHT_ARROW: "\u2192",
174
+ DOWN_ARROW: "\u2193",
175
+ CHECK_MARK: "\u2713",
176
+ BALLOT_X: "\u2717"
177
+ });
178
+ });
179
+
180
+ test("every exported constant is a single character", () => {
181
+ const all = [
182
+ BULLET,
183
+ ELLIPSIS,
184
+ EN_DASH,
185
+ EM_DASH,
186
+ LEFT_ARROW,
187
+ UP_ARROW,
188
+ RIGHT_ARROW,
189
+ DOWN_ARROW,
190
+ CHECK_MARK,
191
+ BALLOT_X,
192
+ BLACK_CIRCLE,
193
+ BLACK_STAR,
194
+ WHITE_STAR,
195
+ DOT_GLYPH
196
+ ];
197
+ for (const c of all) {
198
+ expect(c).toHaveLength(1);
199
+ }
200
+ expect(new Set(all).size).toBe(all.length);
201
+ });
202
+ });
@@ -0,0 +1,12 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import { ModalBoundary, useModal } from "@roku-sdk/components";
6
+
7
+ describe("ModalBoundary partner export", () => {
8
+ test("re-exports ModalBoundary and useModal from @roku-sdk/components", () => {
9
+ expect(typeof ModalBoundary).toBe("function");
10
+ expect(typeof useModal).toBe("function");
11
+ });
12
+ });