@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.
- package/BREAKING_CHANGES.md +84 -0
- package/LICENSE +21 -0
- package/README.md +19 -2
- package/core-jsx.d.ts +53 -0
- package/eslint.config.mjs +3 -0
- package/jsx.d.ts +273 -0
- package/package.json +84 -3
- package/src/__tests__/container.test.tsx +150 -0
- package/src/__tests__/glyphs.test.ts +202 -0
- package/src/__tests__/modal-boundary-export.test.ts +12 -0
- package/src/__tests__/native-modal.test.tsx +224 -0
- package/src/__tests__/tween-release.test.tsx +125 -0
- package/src/__tests__/tween-stops-shape.test.tsx +165 -0
- package/src/__tests__/tween-without-compat.test.tsx +64 -0
- package/src/__tests__/tween.test.tsx +195 -0
- package/src/blended-busy-spinner.props.ts +37 -0
- package/src/blended-busy-spinner.tsx +13 -0
- package/src/bounding-tracker.props.ts +18 -0
- package/src/bounding-tracker.tsx +13 -0
- package/src/button-row.tsx +126 -0
- package/src/channel-store.props.ts +425 -0
- package/src/channel-store.tsx +76 -0
- package/src/container.tsx +130 -0
- package/src/core/jsx-runtime.ts +39 -0
- package/src/focusable-group.props.ts +8 -0
- package/src/focusable-group.tsx +17 -0
- package/src/focusable.props.ts +14 -0
- package/src/focusable.tsx +17 -0
- package/src/glyphs.ts +116 -0
- package/src/helpers/index.tsx +4 -0
- package/src/helpers/resolution-context.tsx +68 -0
- package/src/helpers/scale-for-device-resolution.ts +31 -0
- package/src/index.tsx +70 -0
- package/src/jsx-props.ts +87 -0
- package/src/kdfs.d.ts +14 -0
- package/src/kdfs.ts +15 -0
- package/src/key-handler.props.ts +33 -0
- package/src/key-handler.tsx +24 -0
- package/src/modal-dialog.props.ts +38 -0
- package/src/modal-dialog.tsx +41 -0
- package/src/retain-asset.ts +24 -0
- package/src/sg-intrinsics.json +1153 -0
- package/src/sg-nodes/__tests__/__snapshots__/font.test.tsx.snap +13 -0
- package/src/sg-nodes/__tests__/__snapshots__/prop-forwarding.test.tsx.snap +4409 -0
- package/src/sg-nodes/__tests__/core-props.test.ts +301 -0
- package/src/sg-nodes/__tests__/font.test.tsx +141 -0
- package/src/sg-nodes/__tests__/imperative-methods.test.ts +268 -0
- package/src/sg-nodes/__tests__/prop-forwarding.test.tsx +1049 -0
- package/src/sg-nodes/__tests__/solid-integration.test.tsx +719 -0
- package/src/sg-nodes/animated-image.props.ts +64 -0
- package/src/sg-nodes/animated-image.tsx +23 -0
- package/src/sg-nodes/animation.props.ts +292 -0
- package/src/sg-nodes/animation.tsx +190 -0
- package/src/sg-nodes/array-grid.ts +602 -0
- package/src/sg-nodes/audio.props.ts +302 -0
- package/src/sg-nodes/audio.tsx +57 -0
- package/src/sg-nodes/busy-spinner.props.ts +59 -0
- package/src/sg-nodes/busy-spinner.tsx +84 -0
- package/src/sg-nodes/button-group.props.ts +141 -0
- package/src/sg-nodes/button-group.tsx +30 -0
- package/src/sg-nodes/button.props.ts +119 -0
- package/src/sg-nodes/button.tsx +31 -0
- package/src/sg-nodes/check-list.props.ts +71 -0
- package/src/sg-nodes/check-list.tsx +55 -0
- package/src/sg-nodes/content-node.props.ts +1316 -0
- package/src/sg-nodes/content-node.tsx +153 -0
- package/src/sg-nodes/core-props.ts +197 -0
- package/src/sg-nodes/custom-keyboard.props.ts +108 -0
- package/src/sg-nodes/custom-keyboard.tsx +48 -0
- package/src/sg-nodes/dialog.props.ts +159 -0
- package/src/sg-nodes/dialog.tsx +36 -0
- package/src/sg-nodes/dynamic-key-grid.props.ts +22 -0
- package/src/sg-nodes/dynamic-key-grid.tsx +16 -0
- package/src/sg-nodes/dynamic-keyboard.props.ts +30 -0
- package/src/sg-nodes/dynamic-keyboard.tsx +18 -0
- package/src/sg-nodes/dynamic-mini-keyboard.props.ts +22 -0
- package/src/sg-nodes/dynamic-mini-keyboard.tsx +18 -0
- package/src/sg-nodes/dynamic-pinpad.props.ts +57 -0
- package/src/sg-nodes/dynamic-pinpad.tsx +18 -0
- package/src/sg-nodes/effect.props.ts +116 -0
- package/src/sg-nodes/effect.tsx +39 -0
- package/src/sg-nodes/font.props.ts +45 -0
- package/src/sg-nodes/font.tsx +80 -0
- package/src/sg-nodes/group.props.ts +105 -0
- package/src/sg-nodes/group.tsx +30 -0
- package/src/sg-nodes/label-list.props.ts +86 -0
- package/src/sg-nodes/label-list.tsx +64 -0
- package/src/sg-nodes/label.props.ts +207 -0
- package/src/sg-nodes/label.tsx +56 -0
- package/src/sg-nodes/layout-group.props.ts +69 -0
- package/src/sg-nodes/layout-group.tsx +75 -0
- package/src/sg-nodes/list-panel.props.ts +104 -0
- package/src/sg-nodes/list-panel.tsx +49 -0
- package/src/sg-nodes/markup-grid.props.ts +78 -0
- package/src/sg-nodes/markup-grid.tsx +38 -0
- package/src/sg-nodes/markup-list.props.ts +68 -0
- package/src/sg-nodes/markup-list.tsx +38 -0
- package/src/sg-nodes/mask-group.props.ts +78 -0
- package/src/sg-nodes/mask-group.tsx +59 -0
- package/src/sg-nodes/media-playback-methods.ts +20 -0
- package/src/sg-nodes/node.ts +77 -0
- package/src/sg-nodes/overhang.props.ts +169 -0
- package/src/sg-nodes/overhang.tsx +29 -0
- package/src/sg-nodes/panel-set.props.ts +71 -0
- package/src/sg-nodes/panel-set.tsx +53 -0
- package/src/sg-nodes/panel.props.ts +176 -0
- package/src/sg-nodes/panel.tsx +56 -0
- package/src/sg-nodes/poster-grid.props.ts +277 -0
- package/src/sg-nodes/poster-grid.tsx +45 -0
- package/src/sg-nodes/poster.props.ts +125 -0
- package/src/sg-nodes/poster.tsx +44 -0
- package/src/sg-nodes/radio-button-list.props.ts +59 -0
- package/src/sg-nodes/radio-button-list.tsx +68 -0
- package/src/sg-nodes/rectangle.props.ts +29 -0
- package/src/sg-nodes/rectangle.tsx +27 -0
- package/src/sg-nodes/row-list.props.ts +288 -0
- package/src/sg-nodes/row-list.tsx +47 -0
- package/src/sg-nodes/scene.props.ts +65 -0
- package/src/sg-nodes/scene.tsx +15 -0
- package/src/sg-nodes/scrollable-text.props.ts +133 -0
- package/src/sg-nodes/scrollable-text.tsx +64 -0
- package/src/sg-nodes/scrolling-label.props.ts +106 -0
- package/src/sg-nodes/scrolling-label.tsx +42 -0
- package/src/sg-nodes/simple-label.props.ts +156 -0
- package/src/sg-nodes/simple-label.tsx +59 -0
- package/src/sg-nodes/sound-effect.props.ts +130 -0
- package/src/sg-nodes/sound-effect.tsx +65 -0
- package/src/sg-nodes/standard-dialog.props.ts +520 -0
- package/src/sg-nodes/standard-dialog.tsx +428 -0
- package/src/sg-nodes/standard-keyboard-dialog.props.ts +84 -0
- package/src/sg-nodes/standard-keyboard-dialog.tsx +59 -0
- package/src/sg-nodes/standard-message-dialog.props.ts +56 -0
- package/src/sg-nodes/standard-message-dialog.tsx +48 -0
- package/src/sg-nodes/standard-pinpad-dialog.props.ts +64 -0
- package/src/sg-nodes/standard-pinpad-dialog.tsx +46 -0
- package/src/sg-nodes/standard-progress-dialog.props.ts +30 -0
- package/src/sg-nodes/standard-progress-dialog.tsx +43 -0
- package/src/sg-nodes/target-group.props.ts +278 -0
- package/src/sg-nodes/target-group.tsx +63 -0
- package/src/sg-nodes/target-list.props.ts +128 -0
- package/src/sg-nodes/target-list.tsx +66 -0
- package/src/sg-nodes/target-set.props.ts +63 -0
- package/src/sg-nodes/target-set.tsx +42 -0
- package/src/sg-nodes/time-grid.props.ts +646 -0
- package/src/sg-nodes/time-grid.tsx +87 -0
- package/src/sg-nodes/tween.props.ts +115 -0
- package/src/sg-nodes/types.ts +150 -0
- package/src/sg-nodes/video.props.ts +573 -0
- package/src/sg-nodes/video.tsx +73 -0
- package/src/sg-nodes/voice-text-edit-box.props.ts +91 -0
- package/src/sg-nodes/voice-text-edit-box.tsx +15 -0
- package/src/sg-nodes/zoom-row-list.props.ts +416 -0
- package/src/sg-nodes/zoom-row-list.tsx +42 -0
- package/src/standard-dialog-next/__tests__/standard-dialog-next.test.tsx +186 -0
- package/src/standard-dialog-next/index.ts +5 -0
- package/src/standard-dialog-next/standard-dialog-next.props.ts +80 -0
- package/src/standard-dialog-next/standard-dialog-next.tsx +326 -0
- package/src/test-helpers/tween-test-setup.ts +78 -0
- package/src/tween.tsx +173 -0
- package/src/types.ts +8 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.brs +13 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml +10 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml.d.ts +42 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.brs +26 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.xml +14 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.xml.d.ts +42 -0
- package/src/xml-components/channel-store/channelStoreExt.brs +159 -0
- package/src/xml-components/channel-store/channelStoreExt.xml +29 -0
- package/src/xml-components/channel-store/channelStoreExt.xml.d.ts +57 -0
- package/src/xml-components/focus/focusable-group.brs +251 -0
- package/src/xml-components/focus/focusable-group.xml +15 -0
- package/src/xml-components/focus/focusable-group.xml.d.ts +42 -0
- package/src/xml-components/focus/focusable.xml +18 -0
- package/src/xml-components/focus/focusable.xml.d.ts +48 -0
- package/src/xml-components/keyboards/CustomAddressKDF.keyboard.json +547 -0
- package/src/xml-components/keyboards/CustomKeyboard.brs +322 -0
- package/src/xml-components/keyboards/CustomKeyboard.xml +45 -0
- package/src/xml-components/keyboards/CustomKeyboard.xml.d.ts +73 -0
- package/src/xml-components/keyboards/KeyGridExt.xml +4 -0
- package/src/xml-components/keyboards/KeyGridExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/KeyboardExt.xml +4 -0
- package/src/xml-components/keyboards/KeyboardExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/MOPPinPadCustomKDF.keyboard.json +151 -0
- package/src/xml-components/keyboards/MiniKeyboardExt.xml +4 -0
- package/src/xml-components/keyboards/MiniKeyboardExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/PhonePadCustomKDF.keyboard.json +42 -0
- package/src/xml-components/keyboards/PinPadExt.brs +102 -0
- package/src/xml-components/keyboards/PinPadExt.xml +17 -0
- package/src/xml-components/keyboards/PinPadExt.xml.d.ts +49 -0
- package/src/xml-components/keyboards/WifiCustomKDF.keyboard.json +1242 -0
- package/src/xml-components/keyhandler/KeyHandler.brs +30 -0
- package/src/xml-components/keyhandler/KeyHandler.xml +16 -0
- package/src/xml-components/keyhandler/KeyHandler.xml.d.ts +45 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.brs +137 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.xml +10 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.xml.d.ts +37 -0
- package/src/xml-components/modal/Modal.brs +105 -0
- package/src/xml-components/modal/Modal.xml +22 -0
- package/src/xml-components/modal/Modal.xml.d.ts +54 -0
- package/src/xml-components/modal/StandardDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardKeyboardDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardKeyboardDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardMessageDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardMessageDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardPinPadDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardPinPadDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardProgressDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardProgressDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/tests/Modal.test.brs +195 -0
- 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,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
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
4
|
+
import { createSignal, Show, tmTransaction } from "@roku-sdk/solid";
|
|
5
|
+
import { render } from "@roku-sdk/testing-library";
|
|
6
|
+
|
|
7
|
+
import { Modal } from "../modal-dialog";
|
|
8
|
+
import { StandardDialog } from "../sg-nodes/standard-dialog";
|
|
9
|
+
import { StandardKeyboardDialog } from "../sg-nodes/standard-keyboard-dialog";
|
|
10
|
+
import { StandardMessageDialog } from "../sg-nodes/standard-message-dialog";
|
|
11
|
+
import { StandardPinPadDialog } from "../sg-nodes/standard-pinpad-dialog";
|
|
12
|
+
import { StandardProgressDialog } from "../sg-nodes/standard-progress-dialog";
|
|
13
|
+
|
|
14
|
+
describe("native modal wrappers", () => {
|
|
15
|
+
it("retires handlers on cached dialog and fallback nodes when the modal unmounts", async () => {
|
|
16
|
+
const [visible, setVisible] = createSignal(true);
|
|
17
|
+
const selected = jest.fn();
|
|
18
|
+
const closed = jest.fn();
|
|
19
|
+
const view = await render(() => (
|
|
20
|
+
<Show when={visible()}>
|
|
21
|
+
<StandardDialog
|
|
22
|
+
id="content"
|
|
23
|
+
onButtonSelected={selected}
|
|
24
|
+
onWasClosed={closed}
|
|
25
|
+
fallback={<standardDialog id="fallback" onButtonSelected={selected} />}
|
|
26
|
+
/>
|
|
27
|
+
</Show>
|
|
28
|
+
));
|
|
29
|
+
try {
|
|
30
|
+
const modal = view.getByTag("modal");
|
|
31
|
+
const content = view.getByProp("id", "content");
|
|
32
|
+
const fallback = view.getByProp("id", "fallback");
|
|
33
|
+
await view.fireEvent(content, "onButtonSelected", 0);
|
|
34
|
+
expect(selected).toHaveBeenCalledTimes(1);
|
|
35
|
+
setVisible(false);
|
|
36
|
+
await view.transaction();
|
|
37
|
+
expect(view.queryByProp("id", "content")).toBeFalsy();
|
|
38
|
+
await view.fireEvent(content, "onButtonSelected", 0);
|
|
39
|
+
await view.fireEvent(fallback, "onButtonSelected", 0);
|
|
40
|
+
expect(selected).toHaveBeenCalledTimes(1);
|
|
41
|
+
await expect(view.fireEvent(modal, "onWasClosed", true)).rejects.toThrow("not registered");
|
|
42
|
+
expect(closed).not.toHaveBeenCalled();
|
|
43
|
+
} finally {
|
|
44
|
+
view.dispose();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prepares content and fallback in one modal and releases it on disposal", async () => {
|
|
49
|
+
const [readiness, setReadiness] = createSignal(0);
|
|
50
|
+
const view = await render(() => (
|
|
51
|
+
<StandardDialog
|
|
52
|
+
ready={readiness() > 0}
|
|
53
|
+
replaceExisting={false}
|
|
54
|
+
fallback={
|
|
55
|
+
<standardDialog id="loading">
|
|
56
|
+
<stdDlgContentArea>
|
|
57
|
+
<stdDlgProgressItem text="Loading" />
|
|
58
|
+
</stdDlgContentArea>
|
|
59
|
+
</standardDialog>
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
<stdDlgContentArea>
|
|
63
|
+
<stdDlgTextItem text="Prepared content" />
|
|
64
|
+
</stdDlgContentArea>
|
|
65
|
+
</StandardDialog>
|
|
66
|
+
));
|
|
67
|
+
const modal = view.getByTag("modal");
|
|
68
|
+
try {
|
|
69
|
+
expect(view.getAllByTag("modal")).toHaveLength(1);
|
|
70
|
+
expect(modal.props).toMatchObject({ mounted: true, ready: false });
|
|
71
|
+
const dialog = view.getByTag("standardDialogExt");
|
|
72
|
+
const fallback = view.getByTag("standardDialog");
|
|
73
|
+
setReadiness(1);
|
|
74
|
+
await view.transaction();
|
|
75
|
+
expect(modal.props).toMatchObject({ ready: true });
|
|
76
|
+
expect(view.getByTag("standardDialogExt")).toBe(dialog);
|
|
77
|
+
expect(view.getByTag("standardDialog")).toBe(fallback);
|
|
78
|
+
setReadiness(2);
|
|
79
|
+
await view.transaction();
|
|
80
|
+
expect(view.getByTag("standardDialogExt")).toBe(dialog);
|
|
81
|
+
expect(view.getByTag("standardDialog")).toBe(fallback);
|
|
82
|
+
} finally {
|
|
83
|
+
view.dispose();
|
|
84
|
+
}
|
|
85
|
+
await view.transaction();
|
|
86
|
+
expect(modal.props).toMatchObject({ mounted: false });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("updates existing content and still allows an intentional dialog replacement", async () => {
|
|
90
|
+
const [title, setTitle] = createSignal("First");
|
|
91
|
+
const [replacement, setReplacement] = createSignal(false);
|
|
92
|
+
const selected = jest.fn();
|
|
93
|
+
const view = await render(() => (
|
|
94
|
+
<Modal
|
|
95
|
+
dialog={
|
|
96
|
+
replacement() ? (
|
|
97
|
+
<standardDialog id="replacement" onButtonSelected={selected} />
|
|
98
|
+
) : (
|
|
99
|
+
<standardDialog id="original" title={title()} onButtonSelected={selected} />
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
));
|
|
104
|
+
try {
|
|
105
|
+
const original = view.getByProp("id", "original");
|
|
106
|
+
setTitle("Updated");
|
|
107
|
+
await view.transaction();
|
|
108
|
+
expect(view.getByProp("id", "original")).toBe(original);
|
|
109
|
+
expect(original.props).toMatchObject({ title: "Updated" });
|
|
110
|
+
setReplacement(true);
|
|
111
|
+
await view.transaction();
|
|
112
|
+
expect(view.queryByProp("id", "original")).toBeFalsy();
|
|
113
|
+
expect(view.getByProp("id", "replacement")).not.toBe(original);
|
|
114
|
+
await view.fireEvent(original, "onButtonSelected", 0);
|
|
115
|
+
expect(selected).not.toHaveBeenCalled();
|
|
116
|
+
await view.fireEvent(view.getByProp("id", "replacement"), "onButtonSelected", 0);
|
|
117
|
+
expect(selected).toHaveBeenCalledTimes(1);
|
|
118
|
+
} finally {
|
|
119
|
+
view.dispose();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("replaces the fallback without recreating content or retaining old handlers", async () => {
|
|
124
|
+
const [replacement, setReplacement] = createSignal(false);
|
|
125
|
+
const selected = jest.fn();
|
|
126
|
+
const view = await render(() => (
|
|
127
|
+
<Modal
|
|
128
|
+
dialog={<standardDialog id="content" />}
|
|
129
|
+
fallback={
|
|
130
|
+
replacement() ? (
|
|
131
|
+
<standardDialog id="new-fallback" />
|
|
132
|
+
) : (
|
|
133
|
+
<standardDialog id="old-fallback" onButtonSelected={selected} />
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
));
|
|
138
|
+
try {
|
|
139
|
+
const content = view.getByProp("id", "content");
|
|
140
|
+
const fallback = view.getByProp("id", "old-fallback");
|
|
141
|
+
setReplacement(true);
|
|
142
|
+
await view.transaction();
|
|
143
|
+
expect(view.getByProp("id", "content")).toBe(content);
|
|
144
|
+
expect(view.queryByProp("id", "old-fallback")).toBeFalsy();
|
|
145
|
+
expect(view.getByProp("id", "new-fallback")).not.toBe(fallback);
|
|
146
|
+
await view.fireEvent(fallback, "onButtonSelected", 0);
|
|
147
|
+
expect(selected).not.toHaveBeenCalled();
|
|
148
|
+
} finally {
|
|
149
|
+
view.dispose();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it.each([
|
|
154
|
+
["standardDialogExt", StandardDialog],
|
|
155
|
+
["standardMessageDialogExt", StandardMessageDialog],
|
|
156
|
+
["standardKeyboardDialogExt", StandardKeyboardDialog],
|
|
157
|
+
["standardPinPadDialogExt", StandardPinPadDialog],
|
|
158
|
+
["standardProgressDialogExt", StandardProgressDialog]
|
|
159
|
+
] as const)("routes lifecycle props to Modal for %s", async (tag, Dialog) => {
|
|
160
|
+
const [open, setOpen] = createSignal(false);
|
|
161
|
+
const [close, setClose] = createSignal(false, { equals: false });
|
|
162
|
+
const opened = jest.fn();
|
|
163
|
+
const closed = jest.fn();
|
|
164
|
+
const blocked = jest.fn();
|
|
165
|
+
const view = await render(() => (
|
|
166
|
+
<Dialog
|
|
167
|
+
id="dialog"
|
|
168
|
+
title="Native title"
|
|
169
|
+
width={1154}
|
|
170
|
+
optionsDialog
|
|
171
|
+
open={open()}
|
|
172
|
+
close={close()}
|
|
173
|
+
replaceExisting={false}
|
|
174
|
+
initialFocusId="cancel"
|
|
175
|
+
onOpened={opened}
|
|
176
|
+
onWasClosed={closed}
|
|
177
|
+
onBlocked={blocked}
|
|
178
|
+
/>
|
|
179
|
+
));
|
|
180
|
+
const writes = jest.spyOn(tmTransaction, "setProperty");
|
|
181
|
+
try {
|
|
182
|
+
const modal = view.getByTag("modal");
|
|
183
|
+
const dialog = view.getByTag(tag);
|
|
184
|
+
expect(modal.props).toMatchObject({
|
|
185
|
+
mounted: true,
|
|
186
|
+
open: false,
|
|
187
|
+
replaceExisting: false,
|
|
188
|
+
initialFocusId: "cancel"
|
|
189
|
+
});
|
|
190
|
+
expect(dialog.props).toMatchObject({
|
|
191
|
+
title: "Native title",
|
|
192
|
+
width: 1154,
|
|
193
|
+
optionsDialog: true,
|
|
194
|
+
close: false
|
|
195
|
+
});
|
|
196
|
+
expect(modal.props).not.toHaveProperty("close");
|
|
197
|
+
for (const name of ["open", "replaceExisting", "initialFocusId", "onOpened", "onWasClosed", "onBlocked"])
|
|
198
|
+
expect(dialog.props).not.toHaveProperty(name);
|
|
199
|
+
expect(opened).not.toHaveBeenCalled();
|
|
200
|
+
setOpen(true);
|
|
201
|
+
await view.transaction();
|
|
202
|
+
expect(modal.props).toMatchObject({ open: true });
|
|
203
|
+
expect(view.getByTag(tag)).toBe(dialog);
|
|
204
|
+
writes.mockClear();
|
|
205
|
+
setClose(false);
|
|
206
|
+
await view.transaction();
|
|
207
|
+
expect(writes.mock.calls.filter(([, name]) => name === "close")).toEqual([]);
|
|
208
|
+
setClose(true);
|
|
209
|
+
await view.transaction();
|
|
210
|
+
expect(dialog.props).toMatchObject({ close: true });
|
|
211
|
+
expect(view.getByTag(tag)).toBe(dialog);
|
|
212
|
+
expect(modal.props).not.toHaveProperty("close");
|
|
213
|
+
await view.fireEvent(modal, "onOpened", true);
|
|
214
|
+
await view.fireEvent(modal, "onWasClosed", true);
|
|
215
|
+
await view.fireEvent(modal, "onBlocked", true);
|
|
216
|
+
expect(opened).toHaveBeenCalledTimes(1);
|
|
217
|
+
expect(closed).toHaveBeenCalledTimes(1);
|
|
218
|
+
expect(blocked).toHaveBeenCalledTimes(1);
|
|
219
|
+
} finally {
|
|
220
|
+
writes.mockRestore();
|
|
221
|
+
view.dispose();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|