@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,301 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * CoreProps membership and subset checks.
6
+ *
7
+ * Two things are checked, and the important one is checked by the compiler rather than at runtime:
8
+ *
9
+ * 1. **Assignability (compile-time).** Every `I*CoreProps` must be a genuine subset of its source
10
+ * `I*Props` — a value satisfying the full interface must always satisfy the core one. If a
11
+ * core member ever stops matching the real prop's type, this file fails to compile. That is
12
+ * the actual gate; the runtime assertions below exist so the suite reports something meaningful
13
+ * when it runs.
14
+ *
15
+ * 2. **Membership (runtime).** The frozen key list per primitive, asserted explicitly so widening
16
+ * or narrowing a core interface is a visible, reviewable diff rather than a silent change.
17
+ * Narrowing is a breaking change (see `@roku-sdk/interfaces` FREEZE.md).
18
+ */
19
+
20
+ import { readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ import { describe, expect, test } from "@jest/globals";
24
+
25
+ import type {
26
+ CorePropsByTag,
27
+ IEffectCoreProps,
28
+ IGroupCoreProps,
29
+ ILabelCoreProps,
30
+ IPosterCoreProps,
31
+ IRectangleCoreProps,
32
+ ITweenCoreProps
33
+ } from "../core-props";
34
+ import type { IEffectProps } from "../effect.props";
35
+ import type { IGroupProps } from "../group.props";
36
+ import type { ILabelProps } from "../label.props";
37
+ import type { IPosterProps } from "../poster.props";
38
+ import type { IRectangleProps } from "../rectangle.props";
39
+ import type { ITweenProps } from "../tween.props";
40
+
41
+ /** Compile-time assertion that `T` is assignable to `U`. */
42
+ type AssertAssignable<T extends U, U> = T;
43
+
44
+ /** The tags the core intrinsic map exposes, in `CorePropsByTag` order. */
45
+ const CORE_TAGS = ["group", "rectangle", "label", "poster", "effect", "tween"] as const;
46
+
47
+ /**
48
+ * The shipped `core-jsx.d.ts` with its comment lines removed.
49
+ *
50
+ * Comments are stripped because that file's header quotes the very construct the assertions below
51
+ * forbid, in order to explain why it is forbidden. Matching on the raw text would find the warning
52
+ * rather than a real declaration.
53
+ */
54
+ function readCoreMapCode(): string {
55
+ return readFileSync(join(__dirname, "..", "..", "..", "core-jsx.d.ts"), "utf-8")
56
+ .split("\n")
57
+ .filter(line => !line.trim().startsWith("//"))
58
+ .join("\n");
59
+ }
60
+
61
+ /*
62
+ * If any CoreProps drifts from its source interface, these aliases stop compiling. They are
63
+ * intentionally never referenced — instantiating the generic IS the assertion, so being "unused"
64
+ * is the normal state of a passing check rather than dead code.
65
+ */
66
+ /* eslint-disable @typescript-eslint/no-unused-vars -- type-level assertions; instantiation is the check */
67
+ type _GroupIsSubset = AssertAssignable<IGroupProps, IGroupCoreProps>;
68
+ type _RectangleIsSubset = AssertAssignable<IRectangleProps, IRectangleCoreProps>;
69
+ type _LabelIsSubset = AssertAssignable<ILabelProps, ILabelCoreProps>;
70
+ type _PosterIsSubset = AssertAssignable<IPosterProps, IPosterCoreProps>;
71
+ type _EffectIsSubset = AssertAssignable<IEffectProps, IEffectCoreProps>;
72
+ type _TweenIsSubset = AssertAssignable<ITweenProps, ITweenCoreProps>;
73
+
74
+ /*
75
+ * `CorePropsByTag` is the authoritative tag-to-interface table for the shipped intrinsic map in
76
+ * `core-jsx.d.ts`, which an app reaches with `jsxImportSource: "@roku-sdk/components/core"`. Adding
77
+ * a key there is how a tag becomes writable; a tag with no entry is a `Property '<tag>' does not
78
+ * exist` error.
79
+ *
80
+ * The map cannot `extends CorePropsByTag` — it has to spell each tag out as a direct member, or
81
+ * solid-js's inherited DOM `label` wins over the core one (see the comment in `core-jsx.d.ts`).
82
+ * That hand-written list is what "the core map exposes exactly the frozen tags" below re-derives
83
+ * from the shipped file, so the two cannot drift apart silently.
84
+ *
85
+ * Assigning in both directions pins the key set exactly rather than merely as a subset.
86
+ */
87
+ type _TagsCoverTheMap = AssertAssignable<keyof CorePropsByTag, (typeof CORE_TAGS)[number]>;
88
+ type _MapCoversTheTags = AssertAssignable<(typeof CORE_TAGS)[number], keyof CorePropsByTag>;
89
+
90
+ /* Each tag resolves to its own core surface, not merely to something core-shaped. */
91
+ type _GroupTag = AssertAssignable<CorePropsByTag["group"], IGroupCoreProps>;
92
+ type _RectangleTag = AssertAssignable<CorePropsByTag["rectangle"], IRectangleCoreProps>;
93
+ type _LabelTag = AssertAssignable<CorePropsByTag["label"], ILabelCoreProps>;
94
+ type _PosterTag = AssertAssignable<CorePropsByTag["poster"], IPosterCoreProps>;
95
+ type _EffectTag = AssertAssignable<CorePropsByTag["effect"], IEffectCoreProps>;
96
+ type _TweenTag = AssertAssignable<CorePropsByTag["tween"], ITweenCoreProps>;
97
+ /* eslint-enable @typescript-eslint/no-unused-vars */
98
+
99
+ /**
100
+ * The frozen membership, mirrored as runtime values. Kept in the same order as `core-props.ts`
101
+ * so the two read as one list.
102
+ */
103
+ const FROZEN_MEMBERSHIP = {
104
+ group: ["translation", "rotation", "scale", "scaleRotateCenter", "clippingRect", "opacity", "visible"],
105
+ rectangle: ["color", "width", "height", "blendingEnabled", "effect"],
106
+ label: ["text", "font", "color", "width", "height", "horizAlign", "vertAlign", "maxLines", "wrap"],
107
+ poster: [
108
+ "uri",
109
+ "width",
110
+ "height",
111
+ "blendColor",
112
+ "loadDisplayMode",
113
+ "loadingBitmapUri",
114
+ "loadingBitmapOpacity",
115
+ "failedBitmapUri",
116
+ "failedBitmapOpacity",
117
+ "effect"
118
+ ],
119
+ tween: [
120
+ "targetNode",
121
+ "field",
122
+ "targetValue",
123
+ "targetVector",
124
+ "duration",
125
+ "easeFunction",
126
+ "axis",
127
+ "preserveOther",
128
+ "proposedStops",
129
+ "control",
130
+ "onAnimationStart",
131
+ "onAnimationStop",
132
+ "onAnimationState"
133
+ ]
134
+ } as const;
135
+
136
+ describe("CoreProps", () => {
137
+ test("a value satisfying the full interface satisfies CoreProps (compile-time)", () => {
138
+ // The real assertion is the `AssertAssignable` aliases above; this exercises one concrete
139
+ // value per primitive so the relationship is also demonstrated at runtime.
140
+ const rect: IRectangleProps = { color: "0x1A2A3AFF", width: 100, height: 40 };
141
+ const asCore: IRectangleCoreProps = rect;
142
+ expect(asCore.color).toBe("0x1A2A3AFF");
143
+ expect(asCore.width).toBe(100);
144
+ expect(asCore.height).toBe(40);
145
+ });
146
+
147
+ test("group core membership is frozen", () => {
148
+ // `rotation` completes the transform triple with `scale` and `scaleRotateCenter`; the
149
+ // latter names the centre of the rotation, so freezing it without `rotation` is incoherent.
150
+ expect([...FROZEN_MEMBERSHIP.group]).toEqual([
151
+ "translation",
152
+ "rotation",
153
+ "scale",
154
+ "scaleRotateCenter",
155
+ "clippingRect",
156
+ "opacity",
157
+ "visible"
158
+ ]);
159
+ });
160
+
161
+ test("rectangle core membership is frozen", () => {
162
+ // cowboy-tv's landscape-card is the worked example: <rectangle color width height />.
163
+ // `blendingEnabled` is almost always written valueless (<rectangle blendingEnabled />),
164
+ // the exact shape a regex usage-scan misses; it is caught by the TS-parser-based scanner.
165
+ expect([...FROZEN_MEMBERSHIP.rectangle]).toEqual(["color", "width", "height", "blendingEnabled", "effect"]);
166
+ });
167
+
168
+ test("label core membership is frozen", () => {
169
+ expect([...FROZEN_MEMBERSHIP.label]).toEqual([
170
+ "text",
171
+ "font",
172
+ "color",
173
+ "width",
174
+ "height",
175
+ "horizAlign",
176
+ "vertAlign",
177
+ "maxLines",
178
+ "wrap"
179
+ ]);
180
+ });
181
+
182
+ test("poster blendColor is explicitly included in the shipped CoreProps Pick", () => {
183
+ const code = readFileSync(join(__dirname, "../core-props.ts"), "utf8");
184
+ const poster = code.slice(
185
+ code.indexOf("export type IPosterCoreProps"),
186
+ code.indexOf("export type IEffectCoreProps")
187
+ );
188
+ expect(poster).toMatch(/\| "blendColor"/);
189
+ });
190
+
191
+ test("poster core membership is frozen, including the placeholder bitmap family", () => {
192
+ expect([...FROZEN_MEMBERSHIP.poster]).toEqual([
193
+ "uri",
194
+ "width",
195
+ "height",
196
+ "blendColor",
197
+ "loadDisplayMode",
198
+ "loadingBitmapUri",
199
+ "loadingBitmapOpacity",
200
+ "failedBitmapUri",
201
+ "failedBitmapOpacity",
202
+ "effect"
203
+ ]);
204
+ });
205
+
206
+ test("the styled-surface `effect` child is in the core for both surfaces that accept one", () => {
207
+ // Promoted deliberately rather than from usage data: no in-scope app uses it yet, but it
208
+ // carries rounded corners / borders / gradients and degrades silently where unsupported,
209
+ // so portable code can write it without a capability branch.
210
+ expect([...FROZEN_MEMBERSHIP.rectangle]).toContain("effect");
211
+ expect([...FROZEN_MEMBERSHIP.poster]).toContain("effect");
212
+ });
213
+
214
+ test("poster core excludes the extended load and 9-patch properties", () => {
215
+ const excluded = [
216
+ "loadSync",
217
+ "loadWidth",
218
+ "loadHeight",
219
+ "onBitmapMargins",
220
+ "audioGuideText",
221
+ "shaderCompatible"
222
+ ];
223
+ for (const prop of excluded) {
224
+ expect([...FROZEN_MEMBERSHIP.poster]).not.toContain(prop);
225
+ }
226
+ });
227
+
228
+ test("tween core membership is frozen", () => {
229
+ // `tween` is the sixth committed primitive, the unified declarative animation node.
230
+ // Unlike the visual primitives its membership is the node's specified field contract
231
+ // rather than usage-derived, because it is still being brought up and does not appear in
232
+ // any in-scope app's source yet — the FlexGrid apps reach it through the `<Tween>`
233
+ // wrapper. The freeze applies once that contract is final.
234
+ expect([...FROZEN_MEMBERSHIP.tween]).toEqual([
235
+ "targetNode",
236
+ "field",
237
+ "targetValue",
238
+ "targetVector",
239
+ "duration",
240
+ "easeFunction",
241
+ "axis",
242
+ "preserveOther",
243
+ "proposedStops",
244
+ "control",
245
+ "onAnimationStart",
246
+ "onAnimationStop",
247
+ "onAnimationState"
248
+ ]);
249
+ });
250
+
251
+ test("the core intrinsic map exposes exactly the six committed primitives", () => {
252
+ // The compile-time assertions above are the real gate; this makes the shipped tag list
253
+ // reviewable as a plain list. A primitive absent from it has no mapping at all, which is
254
+ // the element-level half of the contract.
255
+ expect([...CORE_TAGS]).toEqual(["group", "rectangle", "label", "poster", "effect", "tween"]);
256
+ expect([...CORE_TAGS]).not.toContain("rowList");
257
+ expect([...CORE_TAGS]).not.toContain("contentNode");
258
+ });
259
+
260
+ test("the shipped core map declares exactly the frozen tags, each on its own core interface", () => {
261
+ // `core-jsx.d.ts` has to list its tags by hand rather than extending `CorePropsByTag`, so
262
+ // nothing in the type system ties the two together any more. Re-deriving the map from the
263
+ // shipped file is what replaces that: adding a tag to `CorePropsByTag` without adding it to
264
+ // the map, or pointing a tag at the wrong interface, fails here.
265
+ //
266
+ // Read as text on purpose. The package has no `tsc` step in CI, so a compile-time assertion
267
+ // would never actually run; and importing the augmented `JSX.IntrinsicElements` would drag
268
+ // in solid-js's inherited DOM tags, which makes an exact key-set comparison meaningless.
269
+ const body = /interface IntrinsicElements \{([^}]*)\}/.exec(readCoreMapCode())?.[1];
270
+ expect(body).toBeDefined();
271
+
272
+ const declared = [...(body as string).matchAll(/^\s*(\w+):\s*(\w+);/gm)].map(([, tag, type]) => [tag, type]);
273
+
274
+ expect(declared).toEqual([
275
+ ["group", "IGroupCoreProps"],
276
+ ["rectangle", "IRectangleCoreProps"],
277
+ ["label", "ILabelCoreProps"],
278
+ ["poster", "IPosterCoreProps"],
279
+ ["effect", "IEffectCoreProps"],
280
+ ["tween", "ITweenCoreProps"]
281
+ ]);
282
+ expect(declared.map(([tag]) => tag)).toEqual([...CORE_TAGS]);
283
+ });
284
+
285
+ test("the core map declares `label` directly, not through an extends clause", () => {
286
+ // The regression that motivated the hand-written list. Under
287
+ // `interface IntrinsicElements extends CorePropsByTag {}` the tag resolves to solid-js's
288
+ // `LabelHTMLAttributes<HTMLLabelElement>`, because an inherited member cannot displace
289
+ // another inherited member — every core `<label>` in an app then fails to typecheck.
290
+ const code = readCoreMapCode();
291
+
292
+ expect(code).not.toMatch(/interface IntrinsicElements\s+extends/);
293
+ expect(code).toMatch(/^\s*label: ILabelCoreProps;$/m);
294
+ });
295
+
296
+ test("cowboy-tv's landscape-card props are all within CoreProps", () => {
297
+ // Regression guard for the migration target: these are the exact props the card uses.
298
+ const card: IRectangleCoreProps = { color: "0x6E1EFFFF", width: 320, height: 4 };
299
+ expect(card).toEqual({ color: "0x6E1EFFFF", width: 320, height: 4 });
300
+ });
301
+ });
@@ -0,0 +1,141 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /// <reference types="jest" />
5
+
6
+ import { describe, expect, test } from "@jest/globals";
7
+ import { render } from "@roku-sdk/testing-library";
8
+
9
+ import { Font } from "../font";
10
+ import type { IFontProps } from "../font.props";
11
+ import { Label } from "../label";
12
+ import { FontValue, SystemFont } from "../types";
13
+
14
+ /**
15
+ * Tests for the Font component and FontValue type.
16
+ *
17
+ * The Font component provides custom TrueType/OpenType font support for Label and other text nodes.
18
+ * FontValue is a union type that restricts font prop values to:
19
+ * - SystemFont enum values (e.g., SystemFont.Large)
20
+ * - Font string literals (e.g., "font:MediumBoldSystemFont")
21
+ * - JSX.Element (e.g., <Font uri="..." size={24} />)
22
+ *
23
+ * Raw file path strings like "pkg:/fonts/foo.ttf" are NOT valid FontValue types.
24
+ */
25
+ describe("Font Component", () => {
26
+ describe("rendering", () => {
27
+ test("renders Font with uri and size", async () => {
28
+ const { toString, dispose } = await render(() => <Font uri="pkg:/fonts/test-font.ttf" size={24} />);
29
+
30
+ expect(toString()).toMatchSnapshot();
31
+ await dispose();
32
+ });
33
+
34
+ test("renders Font with all props", async () => {
35
+ const { toString, dispose } = await render(() => (
36
+ <Font uri="pkg:/fonts/test-font.ttf" size={32} fallbackGlyph="u0020" />
37
+ ));
38
+
39
+ expect(toString()).toMatchSnapshot();
40
+ await dispose();
41
+ });
42
+
43
+ test("renders Font with default size when only uri provided", async () => {
44
+ const { toString, dispose } = await render(() => <Font uri="pkg:/fonts/custom.ttf" />);
45
+
46
+ expect(toString()).toMatchSnapshot();
47
+ await dispose();
48
+ });
49
+ });
50
+
51
+ describe("Label with font prop", () => {
52
+ test("renders Label with inline Font component", async () => {
53
+ const { toString, getByProp, dispose } = await render(() => (
54
+ <Label text="Hello World" font={<Font uri="pkg:/fonts/test-font.ttf" size={24} />} />
55
+ ));
56
+
57
+ expect(toString()).toMatchSnapshot();
58
+ // Verify label renders with correct text
59
+ getByProp("text", "Hello World");
60
+ await dispose();
61
+ });
62
+
63
+ test("renders Label with SystemFont enum", async () => {
64
+ const { toString, dispose } = await render(() => <Label text="System Font Text" font={SystemFont.Large} />);
65
+
66
+ expect(toString()).toMatchSnapshot();
67
+ await dispose();
68
+ });
69
+
70
+ test("renders Label with reusable Font definition", async () => {
71
+ const customFont = <Font uri="pkg:/fonts/custom.ttf" size={28} />;
72
+
73
+ const { toString, dispose } = await render(() => <Label text="Reusable Font" font={customFont} />);
74
+
75
+ expect(toString()).toMatchSnapshot();
76
+ await dispose();
77
+ });
78
+ });
79
+ });
80
+
81
+ /**
82
+ * Type-level tests for FontValue.
83
+ *
84
+ * These tests verify compile-time type safety for the font prop.
85
+ * The FontValue type should accept:
86
+ * - SystemFont enum values
87
+ * - Font string literals matching `font:${string}` pattern
88
+ * - JSX.Element (Font components)
89
+ *
90
+ * Note: JSX.Element in SolidJS is broadly typed, so @ts-expect-error based
91
+ * rejection tests don't work as expected. The type guards are primarily
92
+ * enforced through the SystemFont enum and `font:${string}` template literal.
93
+ */
94
+ describe("FontValue type safety", () => {
95
+ test("accepts SystemFont enum values (compile-time verification)", () => {
96
+ const fontValues: FontValue[] = [
97
+ SystemFont.Large,
98
+ SystemFont.Small,
99
+ SystemFont.MediumBold,
100
+ SystemFont.Smallest,
101
+ SystemFont.Medium
102
+ ];
103
+
104
+ expect(fontValues).toHaveLength(5);
105
+ });
106
+
107
+ test("accepts font: prefixed string literals (compile-time verification)", () => {
108
+ const fontValues: FontValue[] = ["font:MediumSystemFont", "font:LargeBoldSystemFont", "font:CustomFont"];
109
+
110
+ expect(fontValues).toHaveLength(3);
111
+ });
112
+
113
+ test("accepts Font JSX component (compile-time verification)", () => {
114
+ const fontValues: FontValue[] = [<Font uri="pkg:/fonts/test.ttf" size={24} />];
115
+
116
+ expect(fontValues).toHaveLength(1);
117
+ });
118
+
119
+ test("Font component props are correctly typed", () => {
120
+ // Verify IFontProps interface accepts all documented properties
121
+ const fontProps: IFontProps = {
122
+ uri: "pkg:/fonts/test.ttf",
123
+ size: 24,
124
+ fallbackGlyph: "u0020"
125
+ };
126
+
127
+ expect(fontProps.uri).toBe("pkg:/fonts/test.ttf");
128
+ expect(fontProps.size).toBe(24);
129
+ expect(fontProps.fallbackGlyph).toBe("u0020");
130
+ });
131
+
132
+ test("IFontProps allows partial props (all optional except inherited)", () => {
133
+ // Font can be created with just uri
134
+ const minimalProps: IFontProps = { uri: "pkg:/fonts/minimal.ttf" };
135
+ expect(minimalProps.uri).toBeDefined();
136
+
137
+ // Or with uri and size
138
+ const withSize: IFontProps = { uri: "pkg:/fonts/sized.ttf", size: 32 };
139
+ expect(withSize.size).toBe(32);
140
+ });
141
+ });