@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1453660071 → 16.0.0-alpha.1454930193

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 (124) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  7. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  8. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
  9. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  10. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
  11. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  12. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  15. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  16. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  17. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  18. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  19. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  20. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  21. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  22. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  23. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  24. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  25. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  26. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  27. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  28. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  29. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  30. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  31. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  32. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  33. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  34. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  35. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  38. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  39. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  40. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  41. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  42. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  43. package/Components/MasterCell/hooks/index.ts +2 -0
  44. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  45. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  46. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  47. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  48. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  49. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  50. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  51. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +772 -0
  52. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +728 -0
  53. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +1007 -0
  54. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  55. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  56. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  57. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  58. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +356 -0
  59. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +521 -0
  60. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  61. package/Components/ModalComponent/BottomSheetModalContent.tsx +505 -50
  62. package/Components/ModalComponent/Button/index.tsx +5 -3
  63. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  64. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  65. package/Components/ModalComponent/Header/index.tsx +20 -7
  66. package/Components/ModalComponent/Header/utils.ts +1 -1
  67. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  68. package/Components/ModalComponent/SortableList.tsx +156 -0
  69. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  70. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  71. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  72. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  73. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  74. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  75. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  76. package/Components/ModalComponent/index.tsx +18 -9
  77. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  78. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  79. package/Components/ModalComponent/presets/index.ts +27 -0
  80. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  81. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  82. package/Components/ModalComponent/presets/types.ts +35 -0
  83. package/Components/ModalComponent/showTextInput.ts +52 -0
  84. package/Components/ModalComponent/utils.ts +82 -21
  85. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  86. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  87. package/Components/OfflineHandler/hooks.ts +111 -0
  88. package/Components/OfflineHandler/index.tsx +51 -73
  89. package/Components/OfflineHandler/utils/index.ts +1 -13
  90. package/Components/PlayerContainer/PlayerContainer.tsx +11 -7
  91. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  92. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  93. package/Components/PreloaderWrapper/index.tsx +1 -1
  94. package/Components/River/ComponentsMap/ComponentsMap.tsx +85 -59
  95. package/Components/River/River.tsx +4 -0
  96. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -75
  97. package/Components/River/__tests__/componentsMap.test.js +8 -3
  98. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  99. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  100. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  101. package/Components/Screen/index.tsx +5 -0
  102. package/Components/Screen/navigationHandler.ts +8 -8
  103. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  104. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  105. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  106. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  107. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  109. package/Components/TopCutoffOverlay/index.tsx +2 -2
  110. package/Components/Transitioner/Transitioner.tsx +30 -20
  111. package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
  112. package/Components/VideoLive/LiveImageManager.ts +9 -3
  113. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  114. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  115. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  116. package/Hooks/useEntryActionsExpander.ts +63 -0
  117. package/assets/empty-Info.png +0 -0
  118. package/package.json +6 -5
  119. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  120. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  121. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  122. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  123. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  124. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,343 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react-native";
3
+ import { BottomSheetModalContent } from "../BottomSheetModalContent";
4
+ import { ContentViewModel } from "@applicaster/zapp-react-native-utils/modalState";
5
+
6
+ jest.mock("react-native-sortables", () => {
7
+ const { View } = require("react-native");
8
+
9
+ const Grid = ({ data, renderItem }: any) => (
10
+ <View testID="sortable-grid">
11
+ {(data || []).map((item: any, index: number) =>
12
+ renderItem({
13
+ item,
14
+ index,
15
+ renderHandle: (children: any) => (
16
+ <View testID={`sortable-handle-${index}`}>{children}</View>
17
+ ),
18
+ })
19
+ )}
20
+ </View>
21
+ );
22
+
23
+ const Handle = ({ children }: any) => (
24
+ <View testID="sortable-handle-wrapper">{children}</View>
25
+ );
26
+
27
+ return {
28
+ __esModule: true,
29
+ default: {
30
+ Grid,
31
+ Handle,
32
+ },
33
+ Grid,
34
+ Handle,
35
+ };
36
+ });
37
+
38
+ jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
39
+ useTheme: () => ({
40
+ modal_bottom_sheet_padding_top: 0,
41
+ modal_bottom_sheet_padding_bottom: 0,
42
+ modal_bottom_sheet_background_color: "#000",
43
+ modal_bottom_sheet_item_selected_icon: null,
44
+ }),
45
+ }));
46
+
47
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
48
+ usePlugins: () => [],
49
+ }));
50
+
51
+ jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
52
+ useLocalizedStrings: () => ({}),
53
+ }));
54
+
55
+ jest.mock("react-native-safe-area-context", () => ({
56
+ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
57
+ }));
58
+
59
+ jest.mock("../Header", () => ({
60
+ ModalHeader: ({ title }: any) => {
61
+ const { Text } = require("react-native");
62
+
63
+ return <Text testID="default-header">{title}</Text>;
64
+ },
65
+ }));
66
+
67
+ jest.mock("../Button", () => ({
68
+ Button: ({ label }: any) => {
69
+ const { Text } = require("react-native");
70
+
71
+ return <Text testID="generic-button">{label}</Text>;
72
+ },
73
+ }));
74
+
75
+ jest.mock("../AudioPlayer/Components/Item", () => ({
76
+ AudioPlayerTrackItem: ({ data }: any) => {
77
+ const { Text } = require("react-native");
78
+
79
+ return <Text testID="selectable-row">{data.textLabel1}</Text>;
80
+ },
81
+ mapMenuItemToPlaylistData: (item: any) => ({ textLabel1: item.title }),
82
+ VARIANT_PLAYLIST_ITEM: {},
83
+ }));
84
+
85
+ const mockAudioPlayerButtonProps: Array<any> = [];
86
+ const mockNowPlayingHeaderProps: Array<any> = [];
87
+
88
+ jest.mock("../AudioPlayer/Components", () => ({
89
+ NowPlayingHeaderSection: (props: any) => {
90
+ mockNowPlayingHeaderProps.push(props);
91
+
92
+ return null;
93
+ },
94
+ AudioPlayerButton: (props: any) => {
95
+ const { Text } = require("react-native");
96
+
97
+ mockAudioPlayerButtonProps.push(props);
98
+
99
+ return (
100
+ <Text testID="create-playlist-button" onPress={props.onPress}>
101
+ {props.data?.title}
102
+ </Text>
103
+ );
104
+ },
105
+ }));
106
+
107
+ const baseProps = {
108
+ items: [{ id: "track-a", title: "Track A", label: "Track A" }],
109
+ onPress: jest.fn(),
110
+ width: 320,
111
+ currentRoute: true,
112
+ dismiss: jest.fn(),
113
+ title: "Sheet Title",
114
+ maxHeight: 400,
115
+ };
116
+
117
+ describe("BottomSheetModalContent role/behavior cells", () => {
118
+ it("renders standard Button rows and default header when no viewModel/role", () => {
119
+ render(<BottomSheetModalContent {...baseProps} />);
120
+
121
+ expect(screen.getByTestId("default-header")).toBeTruthy();
122
+ expect(screen.getByTestId("generic-button")).toBeTruthy();
123
+ expect(screen.queryByTestId("selectable-row")).toBeNull();
124
+ });
125
+
126
+ it("renders selectable rows when viewModel has collection_selector role", () => {
127
+ const viewModel = new ContentViewModel({
128
+ title: "Playlists",
129
+ items: [{ id: "fav-1", title: "Favorites" }],
130
+ role: "collection_selector",
131
+ behavior: { selectMode: "multi", currentSelection: [] },
132
+ });
133
+
134
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
135
+
136
+ expect(screen.getByTestId("default-header")).toBeTruthy();
137
+ expect(screen.getByTestId("selectable-row")).toBeTruthy();
138
+ expect(screen.queryByTestId("generic-button")).toBeNull();
139
+ });
140
+
141
+ it("does not accept styleVariant as a rendering input", () => {
142
+ render(
143
+ <BottomSheetModalContent
144
+ {...baseProps}
145
+ {...({ styleVariant: "audio_player" } as any)}
146
+ />
147
+ );
148
+
149
+ expect(screen.getByTestId("generic-button")).toBeTruthy();
150
+ expect(screen.queryByTestId("selectable-row")).toBeNull();
151
+ });
152
+
153
+ it("renders SortableList when viewModel has dynamic_collection role with reorder operation", () => {
154
+ const moveMock = jest.fn();
155
+ const { BehaviorSubject } = require("rxjs");
156
+
157
+ const viewModel = new ContentViewModel({
158
+ title: "Queue",
159
+ items: [
160
+ { id: "t1", title: "Track 1" },
161
+ { id: "t2", title: "Track 2" },
162
+ ],
163
+ role: "dynamic_collection",
164
+ dynamicCollection: { operations: ["remove", "reorder"] },
165
+ });
166
+
167
+ viewModel.editableCollection = {
168
+ items$: new BehaviorSubject([
169
+ { id: "t1", title: "Track 1" },
170
+ { id: "t2", title: "Track 2" },
171
+ ]),
172
+ operations: ["remove", "reorder"],
173
+ move: moveMock,
174
+ };
175
+
176
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
177
+
178
+ expect(screen.getByTestId("sortable-grid")).toBeTruthy();
179
+ });
180
+
181
+ it("renders create playlist button when operations include add and calls editableCollection.add on press", () => {
182
+ mockAudioPlayerButtonProps.length = 0;
183
+
184
+ const addMock = jest.fn();
185
+ const { BehaviorSubject } = require("rxjs");
186
+
187
+ const viewModel = new ContentViewModel({
188
+ title: "Playlists",
189
+ items: [{ id: "p1", title: "Playlist 1" }],
190
+ role: "collection_selector",
191
+ dynamicCollection: { operations: ["add"] },
192
+ });
193
+
194
+ viewModel.editableCollection = {
195
+ items$: new BehaviorSubject([{ id: "p1", title: "Playlist 1" }]),
196
+ operations: ["add"],
197
+ add: addMock,
198
+ };
199
+
200
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
201
+
202
+ const createBtn = screen.getByTestId("create-playlist-button");
203
+ expect(createBtn).toBeTruthy();
204
+
205
+ // The button is rendered bare: styling comes from the modal blocks theme
206
+ // and the title from the button block, so no configuration/data is passed.
207
+ expect(mockAudioPlayerButtonProps.length).toBeGreaterThan(0);
208
+
209
+ for (const props of mockAudioPlayerButtonProps) {
210
+ expect(props.configuration).toBeUndefined();
211
+ expect(props.data).toBeUndefined();
212
+ expect(typeof props.onPress).toBe("function");
213
+ }
214
+
215
+ const { fireEvent } = require("@testing-library/react-native");
216
+ fireEvent.press(createBtn);
217
+
218
+ expect(addMock).toHaveBeenCalledWith({
219
+ id: "create_new_playlist",
220
+ title: "unknown title",
221
+ });
222
+ });
223
+
224
+ it("shows the empty Next Up message only when now playing is on and the list is empty", () => {
225
+ mockNowPlayingHeaderProps.length = 0;
226
+ const { BehaviorSubject } = require("rxjs");
227
+
228
+ const emptyQueue = new ContentViewModel({
229
+ title: "Queue",
230
+ items: [],
231
+ role: "dynamic_collection",
232
+ dynamicCollection: {
233
+ operations: ["nowPlaying", "remove", "reorder", "clearAll"],
234
+ },
235
+ });
236
+
237
+ emptyQueue.editableCollection = {
238
+ items$: new BehaviorSubject([]),
239
+ operations: ["nowPlaying", "remove", "reorder", "clearAll"],
240
+ move: jest.fn(),
241
+ };
242
+
243
+ const { rerender } = render(
244
+ <BottomSheetModalContent
245
+ {...baseProps}
246
+ items={[]}
247
+ emptyMessage="Queued items will be added here"
248
+ viewModel={emptyQueue}
249
+ />
250
+ );
251
+
252
+ expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
253
+ isEmpty: true,
254
+ emptyMessage: "Queued items will be added here",
255
+ });
256
+
257
+ const withItem = new ContentViewModel({
258
+ title: "Queue",
259
+ items: [{ id: "t1", title: "Track 1" }],
260
+ role: "dynamic_collection",
261
+ dynamicCollection: {
262
+ operations: ["nowPlaying", "remove", "reorder", "clearAll"],
263
+ },
264
+ });
265
+
266
+ withItem.editableCollection = {
267
+ items$: new BehaviorSubject([{ id: "t1", title: "Track 1" }]),
268
+ operations: ["nowPlaying", "remove", "reorder", "clearAll"],
269
+ move: jest.fn(),
270
+ };
271
+
272
+ rerender(
273
+ <BottomSheetModalContent
274
+ {...baseProps}
275
+ items={[{ id: "t1", title: "Track 1", label: "Track 1" }]}
276
+ emptyMessage="Queued items will be added here"
277
+ viewModel={withItem}
278
+ />
279
+ );
280
+
281
+ expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
282
+ isEmpty: false,
283
+ });
284
+
285
+ mockNowPlayingHeaderProps.length = 0;
286
+
287
+ render(<BottomSheetModalContent {...baseProps} />);
288
+
289
+ expect(mockNowPlayingHeaderProps).toHaveLength(0);
290
+ });
291
+
292
+ it("honors disableScrollViewWrap in contentComponentProps to bypass ScrollView wrapping", () => {
293
+ const CustomComponent = () => (
294
+ <React.Fragment>Custom Content</React.Fragment>
295
+ );
296
+
297
+ const { UNSAFE_getByType } = render(
298
+ <BottomSheetModalContent
299
+ {...baseProps}
300
+ contentComponent={CustomComponent}
301
+ contentComponentProps={{ disableScrollViewWrap: true }}
302
+ />
303
+ );
304
+
305
+ const { ScrollView } = require("react-native");
306
+ expect(() => UNSAFE_getByType(ScrollView)).toThrow();
307
+ });
308
+
309
+ it("honors static disableScrollViewWrap property on ContentComponent", () => {
310
+ const CustomSelfScrollingComponent: any = () => (
311
+ <React.Fragment>Self Scrolling</React.Fragment>
312
+ );
313
+
314
+ CustomSelfScrollingComponent.disableScrollViewWrap = true;
315
+
316
+ const { UNSAFE_getByType } = render(
317
+ <BottomSheetModalContent
318
+ {...baseProps}
319
+ contentComponent={CustomSelfScrollingComponent}
320
+ />
321
+ );
322
+
323
+ const { ScrollView } = require("react-native");
324
+ expect(() => UNSAFE_getByType(ScrollView)).toThrow();
325
+ });
326
+
327
+ it("sets keyboardShouldPersistTaps='handled' on ScrollView wrapping contentComponent", () => {
328
+ const CustomComponent = () => (
329
+ <React.Fragment>Custom Content</React.Fragment>
330
+ );
331
+
332
+ const { UNSAFE_getByType } = render(
333
+ <BottomSheetModalContent
334
+ {...baseProps}
335
+ contentComponent={CustomComponent}
336
+ />
337
+ );
338
+
339
+ const { ScrollView } = require("react-native");
340
+ const scrollView = UNSAFE_getByType(ScrollView);
341
+ expect(scrollView.props.keyboardShouldPersistTaps).toBe("handled");
342
+ });
343
+ });
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { SortableList } from "../SortableList.web";
4
+
5
+ describe("SortableList.web", () => {
6
+ it("renders nothing", () => {
7
+ const { toJSON } = render(
8
+ <SortableList
9
+ renderItem={() => null}
10
+ sortableData={[{ id: "1", title: "Track 1" }]}
11
+ />
12
+ );
13
+
14
+ expect(toJSON()).toBeNull();
15
+ });
16
+ });
@@ -0,0 +1,169 @@
1
+ import React from "react";
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@testing-library/react-native";
8
+ import { actionExecutor } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
9
+
10
+ jest.mock(
11
+ "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor",
12
+ () => ({
13
+ actionExecutor: {
14
+ handleAction: jest.fn().mockResolvedValue(undefined),
15
+ handleActions: jest.fn().mockResolvedValue(undefined),
16
+ },
17
+ ActionResult: { Success: "success", Error: "error" },
18
+ })
19
+ );
20
+
21
+ jest.mock("../AudioPlayer/Components/Input", () => ({
22
+ AudioPlayerInput: ({ value, onChangeText, data, validationMessage }: any) => {
23
+ const { TextInput, Text } = require("react-native");
24
+
25
+ return (
26
+ <>
27
+ <Text testID="input-label">{data?.label}</Text>
28
+ <TextInput
29
+ testID="playlist-input"
30
+ value={value}
31
+ onChangeText={onChangeText}
32
+ placeholder={data?.placeholder}
33
+ />
34
+ {validationMessage ? (
35
+ <Text testID="input-validation-message">{validationMessage}</Text>
36
+ ) : null}
37
+ </>
38
+ );
39
+ },
40
+ VARIANT_NAME_PLAYLIST_INPUT: {},
41
+ }));
42
+
43
+ jest.mock("../AudioPlayer/Components/Button", () => ({
44
+ AudioPlayerButton: ({ data, onPress }: any) => {
45
+ const { Pressable, Text } = require("react-native");
46
+
47
+ return (
48
+ <Pressable testID="submit-button" onPress={onPress}>
49
+ <Text>{data?.title}</Text>
50
+ </Pressable>
51
+ );
52
+ },
53
+ VARIANT_TEXT_ONLY: {},
54
+ }));
55
+
56
+ import { TextInputContent } from "../TextInputContent";
57
+
58
+ const submitAction = {
59
+ type: "sendCloudEvent",
60
+ options: {
61
+ url: "https://server.com/cloud-events",
62
+ type: "com.applicaster.collection.create.v1",
63
+ subject: "edit_collection",
64
+ data: { collectionId: "playlist-123" },
65
+ },
66
+ };
67
+
68
+ const baseProps = {
69
+ inputLabel: "Name your playlist",
70
+ defaultValue: "Summer Hits",
71
+ buttonLabel: "Update",
72
+ actions: [submitAction],
73
+ };
74
+
75
+ describe("TextInputContent", () => {
76
+ beforeEach(() => {
77
+ jest.clearAllMocks();
78
+ });
79
+
80
+ it("renders default value and button label without a modal header", () => {
81
+ render(<TextInputContent {...baseProps} />);
82
+
83
+ expect(screen.queryByTestId("sheet-header")).toBeNull();
84
+
85
+ expect(screen.getByTestId("playlist-input").props.value).toBe(
86
+ "Summer Hits"
87
+ );
88
+
89
+ expect(screen.getByTestId("submit-button")).toBeTruthy();
90
+ expect(screen.getByText("Update")).toBeTruthy();
91
+ });
92
+
93
+ it("shows validation below the input when its value is too long", () => {
94
+ render(<TextInputContent maxLength={100} {...baseProps} />);
95
+
96
+ fireEvent.changeText(screen.getByTestId("playlist-input"), "x".repeat(101));
97
+
98
+ expect(screen.getByTestId("input-validation-message")).toHaveTextContent(
99
+ "Too many characters"
100
+ );
101
+ });
102
+
103
+ it("on submit merges name into sendCloudEvent data then dismisses", async () => {
104
+ render(<TextInputContent {...baseProps} />);
105
+
106
+ fireEvent.changeText(screen.getByTestId("playlist-input"), "New Name");
107
+ fireEvent.press(screen.getByTestId("submit-button"));
108
+
109
+ await waitFor(() => {
110
+ expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
111
+ expect(actionExecutor.handleAction).toHaveBeenCalledTimes(1);
112
+ });
113
+
114
+ expect(actionExecutor.handleActions).toHaveBeenCalledWith([
115
+ {
116
+ type: "sendCloudEvent",
117
+ options: {
118
+ url: "https://server.com/cloud-events",
119
+ type: "com.applicaster.collection.create.v1",
120
+ subject: "edit_collection",
121
+ data: {
122
+ collectionId: "playlist-123",
123
+ name: "New Name",
124
+ },
125
+ },
126
+ },
127
+ ]);
128
+
129
+ expect(actionExecutor.handleAction).toHaveBeenCalledWith({
130
+ type: "dismissBottomSheet",
131
+ });
132
+ });
133
+
134
+ it("forwards actionContext into handleActions so rename can refresh the List", async () => {
135
+ const actionContext = {
136
+ component: {
137
+ id: "list-1",
138
+ data: { source: "https://example.com/user/collections/abc" },
139
+ },
140
+ screenData: { id: "playlist-screen" },
141
+ };
142
+
143
+ render(<TextInputContent {...baseProps} actionContext={actionContext} />);
144
+
145
+ fireEvent.press(screen.getByTestId("submit-button"));
146
+
147
+ await waitFor(() => {
148
+ expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ expect(actionExecutor.handleActions).toHaveBeenCalledWith(
152
+ [
153
+ {
154
+ type: "sendCloudEvent",
155
+ options: {
156
+ url: "https://server.com/cloud-events",
157
+ type: "com.applicaster.collection.create.v1",
158
+ subject: "edit_collection",
159
+ data: {
160
+ collectionId: "playlist-123",
161
+ name: "Summer Hits",
162
+ },
163
+ },
164
+ },
165
+ ],
166
+ actionContext
167
+ );
168
+ });
169
+ });
@@ -0,0 +1,95 @@
1
+ import {
2
+ actionExecutor,
3
+ ActionResult,
4
+ } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
5
+ import { TextInputContent } from "../TextInputContent";
6
+ import { showTextInputAction } from "../showTextInput";
7
+
8
+ jest.mock(
9
+ "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor",
10
+ () => ({
11
+ actionExecutor: {
12
+ handleAction: jest.fn().mockResolvedValue("success"),
13
+ },
14
+ ActionResult: { Success: "success", Error: "error" },
15
+ })
16
+ );
17
+
18
+ jest.mock("../TextInputContent", () => ({
19
+ TextInputContent: function MockTextInputContent() {
20
+ return null;
21
+ },
22
+ }));
23
+
24
+ describe("showTextInputAction", () => {
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ it("delegates to openBottomSheet with TextInputContent as body-only component", async () => {
30
+ const action = {
31
+ type: "showTextInput",
32
+ options: {
33
+ headerTitle: "Create Playlist",
34
+ inputLabel: "Name your playlist",
35
+ defaultValue: "",
36
+ buttonLabel: "Create",
37
+ actions: [
38
+ {
39
+ type: "sendCloudEvent",
40
+ options: {
41
+ url: "https://example.com/events",
42
+ type: "com.applicaster.collection.create.v1",
43
+ data: {},
44
+ },
45
+ },
46
+ {
47
+ type: "showToast",
48
+ options: {
49
+ message: "Playlist created.",
50
+ },
51
+ },
52
+ ],
53
+ },
54
+ };
55
+
56
+ const result = await showTextInputAction(action as any);
57
+
58
+ expect(result).toBe(ActionResult.Success);
59
+ expect(actionExecutor.handleAction).toHaveBeenCalledTimes(1);
60
+
61
+ expect(actionExecutor.handleAction).toHaveBeenCalledWith(
62
+ {
63
+ type: "openBottomSheet",
64
+ options: {
65
+ header: {
66
+ title: "Create Playlist",
67
+ },
68
+ content: {
69
+ component: TextInputContent,
70
+ props: {
71
+ inputLabel: "Name your playlist",
72
+ defaultValue: "",
73
+ buttonLabel: "Create",
74
+ actions: action.options.actions,
75
+ },
76
+ },
77
+ },
78
+ },
79
+ undefined
80
+ );
81
+ });
82
+
83
+ it("returns Error when required options are missing", async () => {
84
+ const action = {
85
+ type: "showTextInput",
86
+ options: {
87
+ headerTitle: "Create Playlist",
88
+ },
89
+ };
90
+
91
+ const result = await showTextInputAction(action as any);
92
+ expect(result).toBe(ActionResult.Error);
93
+ expect(actionExecutor.handleAction).not.toHaveBeenCalled();
94
+ });
95
+ });
@@ -1,7 +1,18 @@
1
1
  import React, { useCallback, useState } from "react";
2
+ import { actionExecutor } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
2
3
 
3
4
  import { BasicMeasurementsPortal as MeasurementsPortal } from "@applicaster/zapp-react-native-ui-components/Components/MeasurmentsPortal";
4
5
  import { BottomSheetModalContent } from "./BottomSheetModalContent";
6
+ import { showTextInputAction } from "./showTextInput";
7
+
8
+ actionExecutor.registerAction("showTextInput", showTextInputAction);
9
+
10
+ export { SortableList, type SortableListProps } from "./SortableList";
11
+
12
+ export {
13
+ TextInputContent,
14
+ type TextInputContentProps,
15
+ } from "./TextInputContent";
5
16
 
6
17
  export type PluginConfiguration = {
7
18
  [key: string]: any;
@@ -15,20 +26,18 @@ type Props = {
15
26
  export function ModalComponent(props: ModalBottomSheetContentProps & Props) {
16
27
  const [height, setHeight] = useState(0);
17
28
 
18
- const onLayout = useCallback(
19
- ({ nativeEvent }) => {
20
- if (height === 0) {
21
- setHeight(nativeEvent.layout.height);
22
- }
23
- },
24
- [height]
25
- );
29
+ const onLayout = useCallback(({ nativeEvent }) => {
30
+ setHeight((value) => (value === 0 ? nativeEvent.layout.height : value));
31
+ }, []);
26
32
 
27
33
  if (height === 0) {
28
34
  return (
29
35
  <MeasurementsPortal
30
36
  Component={BottomSheetModalContent}
31
- componentProps={props}
37
+ componentProps={{
38
+ ...props,
39
+ measurementPhase: true,
40
+ }}
32
41
  onLayout={onLayout}
33
42
  />
34
43
  );