@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.90

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 (144) 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/CellRendererResolver/index.ts +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +21 -6
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  36. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  37. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  38. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  52. package/Components/MasterCell/dataAdapter.ts +15 -5
  53. package/Components/MasterCell/hooks/index.ts +2 -0
  54. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  55. package/Components/MasterCell/index.tsx +30 -5
  56. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  57. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  58. package/Components/MasterCell/utils/index.ts +85 -46
  59. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  60. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  61. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  62. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  63. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +385 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +728 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  67. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +982 -0
  68. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  71. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  72. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  73. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  74. package/Components/ModalComponent/BottomSheetModalContent.tsx +431 -49
  75. package/Components/ModalComponent/Button/index.tsx +5 -3
  76. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  77. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  78. package/Components/ModalComponent/Header/index.tsx +20 -7
  79. package/Components/ModalComponent/Header/utils.ts +1 -1
  80. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  81. package/Components/ModalComponent/SortableList.tsx +157 -0
  82. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  83. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  84. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  85. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +255 -0
  86. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  87. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  88. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  89. package/Components/ModalComponent/index.tsx +18 -9
  90. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  91. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  92. package/Components/ModalComponent/presets/index.ts +27 -0
  93. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  94. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  95. package/Components/ModalComponent/presets/types.ts +35 -0
  96. package/Components/ModalComponent/showTextInput.ts +43 -0
  97. package/Components/ModalComponent/utils.ts +82 -21
  98. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  99. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  100. package/Components/OfflineHandler/hooks.ts +111 -0
  101. package/Components/OfflineHandler/index.tsx +51 -73
  102. package/Components/OfflineHandler/utils/index.ts +1 -13
  103. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  104. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  105. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  106. package/Components/PreloaderWrapper/index.tsx +1 -1
  107. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -61
  108. package/Components/River/River.tsx +4 -0
  109. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -89
  110. package/Components/River/__tests__/componentsMap.test.js +8 -3
  111. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  112. package/Components/Screen/index.tsx +8 -4
  113. package/Components/Screen/navigationHandler.ts +8 -8
  114. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  115. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  116. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  117. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  118. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  119. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  120. package/Components/TopCutoffOverlay/index.tsx +2 -2
  121. package/Components/Transitioner/Transitioner.tsx +28 -17
  122. package/Components/VideoLive/LiveImageManager.ts +1 -1
  123. package/Components/VideoModal/utils.ts +6 -1
  124. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  125. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  126. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  127. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  128. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  129. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  130. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  131. package/Helpers/index.js +0 -39
  132. package/Hooks/useEntryActionsExpander.ts +63 -0
  133. package/package.json +6 -5
  134. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  135. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  136. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  137. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  138. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  139. package/Components/Screen/utils.ts +0 -16
  140. package/Helpers/Analytics/index.js +0 -95
  141. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  142. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  143. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  144. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,255 @@
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
+ jest.mock("../AudioPlayer/Components", () => ({
86
+ NowPlayingHeaderSection: () => null,
87
+ AudioPlayerButton: ({ data, onPress }: any) => {
88
+ const { Text } = require("react-native");
89
+
90
+ return (
91
+ <Text testID="create-playlist-button" onPress={onPress}>
92
+ {data.title}
93
+ </Text>
94
+ );
95
+ },
96
+ VARIANT_WITH_ICON: {},
97
+ }));
98
+
99
+ const baseProps = {
100
+ items: [{ id: "track-a", title: "Track A", label: "Track A" }],
101
+ onPress: jest.fn(),
102
+ width: 320,
103
+ currentRoute: true,
104
+ dismiss: jest.fn(),
105
+ title: "Sheet Title",
106
+ maxHeight: 400,
107
+ };
108
+
109
+ describe("BottomSheetModalContent role/behavior cells", () => {
110
+ it("renders standard Button rows and default header when no viewModel/role", () => {
111
+ render(<BottomSheetModalContent {...baseProps} />);
112
+
113
+ expect(screen.getByTestId("default-header")).toBeTruthy();
114
+ expect(screen.getByTestId("generic-button")).toBeTruthy();
115
+ expect(screen.queryByTestId("selectable-row")).toBeNull();
116
+ });
117
+
118
+ it("renders selectable rows when viewModel has collection_selector role", () => {
119
+ const viewModel = new ContentViewModel({
120
+ title: "Playlists",
121
+ items: [{ id: "fav-1", title: "Favorites" }],
122
+ role: "collection_selector",
123
+ behavior: { selectMode: "multi", currentSelection: [] },
124
+ });
125
+
126
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
127
+
128
+ expect(screen.getByTestId("default-header")).toBeTruthy();
129
+ expect(screen.getByTestId("selectable-row")).toBeTruthy();
130
+ expect(screen.queryByTestId("generic-button")).toBeNull();
131
+ });
132
+
133
+ it("does not accept styleVariant as a rendering input", () => {
134
+ render(
135
+ <BottomSheetModalContent
136
+ {...baseProps}
137
+ {...({ styleVariant: "audio_player" } as any)}
138
+ />
139
+ );
140
+
141
+ expect(screen.getByTestId("generic-button")).toBeTruthy();
142
+ expect(screen.queryByTestId("selectable-row")).toBeNull();
143
+ });
144
+
145
+ it("renders SortableList when viewModel has dynamic_collection role with reorder operation", () => {
146
+ const moveMock = jest.fn();
147
+ const { BehaviorSubject } = require("rxjs");
148
+
149
+ const viewModel = new ContentViewModel({
150
+ title: "Queue",
151
+ items: [
152
+ { id: "t1", title: "Track 1" },
153
+ { id: "t2", title: "Track 2" },
154
+ ],
155
+ role: "dynamic_collection",
156
+ dynamicCollection: { operations: ["remove", "reorder"] },
157
+ });
158
+
159
+ viewModel.editableCollection = {
160
+ items$: new BehaviorSubject([
161
+ { id: "t1", title: "Track 1" },
162
+ { id: "t2", title: "Track 2" },
163
+ ]),
164
+ operations: ["remove", "reorder"],
165
+ move: moveMock,
166
+ };
167
+
168
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
169
+
170
+ expect(screen.getByTestId("sortable-grid")).toBeTruthy();
171
+ });
172
+
173
+ it("renders create playlist button when operations include add and calls editableCollection.add on press", () => {
174
+ const addMock = jest.fn();
175
+ const { BehaviorSubject } = require("rxjs");
176
+
177
+ const viewModel = new ContentViewModel({
178
+ title: "Playlists",
179
+ items: [{ id: "p1", title: "Playlist 1" }],
180
+ role: "collection_selector",
181
+ dynamicCollection: { operations: ["add"] },
182
+ });
183
+
184
+ viewModel.editableCollection = {
185
+ items$: new BehaviorSubject([{ id: "p1", title: "Playlist 1" }]),
186
+ operations: ["add"],
187
+ add: addMock,
188
+ };
189
+
190
+ render(<BottomSheetModalContent {...baseProps} viewModel={viewModel} />);
191
+
192
+ const createBtn = screen.getByTestId("create-playlist-button");
193
+ expect(createBtn).toBeTruthy();
194
+
195
+ const { fireEvent } = require("@testing-library/react-native");
196
+ fireEvent.press(createBtn);
197
+
198
+ expect(addMock).toHaveBeenCalledWith({
199
+ id: "create_new_playlist",
200
+ title: "Create New Playlist",
201
+ });
202
+ });
203
+
204
+ it("honors disableScrollViewWrap in contentComponentProps to bypass ScrollView wrapping", () => {
205
+ const CustomComponent = () => (
206
+ <React.Fragment>Custom Content</React.Fragment>
207
+ );
208
+
209
+ const { UNSAFE_getByType } = render(
210
+ <BottomSheetModalContent
211
+ {...baseProps}
212
+ contentComponent={CustomComponent}
213
+ contentComponentProps={{ disableScrollViewWrap: true }}
214
+ />
215
+ );
216
+
217
+ const { ScrollView } = require("react-native");
218
+ expect(() => UNSAFE_getByType(ScrollView)).toThrow();
219
+ });
220
+
221
+ it("honors static disableScrollViewWrap property on ContentComponent", () => {
222
+ const CustomSelfScrollingComponent: any = () => (
223
+ <React.Fragment>Self Scrolling</React.Fragment>
224
+ );
225
+
226
+ CustomSelfScrollingComponent.disableScrollViewWrap = true;
227
+
228
+ const { UNSAFE_getByType } = render(
229
+ <BottomSheetModalContent
230
+ {...baseProps}
231
+ contentComponent={CustomSelfScrollingComponent}
232
+ />
233
+ );
234
+
235
+ const { ScrollView } = require("react-native");
236
+ expect(() => UNSAFE_getByType(ScrollView)).toThrow();
237
+ });
238
+
239
+ it("sets keyboardShouldPersistTaps='handled' on ScrollView wrapping contentComponent", () => {
240
+ const CustomComponent = () => (
241
+ <React.Fragment>Custom Content</React.Fragment>
242
+ );
243
+
244
+ const { UNSAFE_getByType } = render(
245
+ <BottomSheetModalContent
246
+ {...baseProps}
247
+ contentComponent={CustomComponent}
248
+ />
249
+ );
250
+
251
+ const { ScrollView } = require("react-native");
252
+ const scrollView = UNSAFE_getByType(ScrollView);
253
+ expect(scrollView.props.keyboardShouldPersistTaps).toBe("handled");
254
+ });
255
+ });
@@ -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,156 @@
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 }: 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
+ </>
35
+ );
36
+ },
37
+ VARIANT_NAME_PLAYLIST_INPUT: {},
38
+ }));
39
+
40
+ jest.mock("../AudioPlayer/Components/Button", () => ({
41
+ AudioPlayerButton: ({ data, onPress }: any) => {
42
+ const { Pressable, Text } = require("react-native");
43
+
44
+ return (
45
+ <Pressable testID="submit-button" onPress={onPress}>
46
+ <Text>{data?.title}</Text>
47
+ </Pressable>
48
+ );
49
+ },
50
+ VARIANT_TEXT_ONLY: {},
51
+ }));
52
+
53
+ import { TextInputContent } from "../TextInputContent";
54
+
55
+ const submitAction = {
56
+ type: "sendCloudEvent",
57
+ options: {
58
+ url: "https://server.com/cloud-events",
59
+ type: "com.applicaster.collection.create.v1",
60
+ subject: "edit_collection",
61
+ data: { collectionId: "playlist-123" },
62
+ },
63
+ };
64
+
65
+ const baseProps = {
66
+ inputLabel: "Name your playlist",
67
+ defaultValue: "Summer Hits",
68
+ buttonLabel: "Update",
69
+ actions: [submitAction],
70
+ };
71
+
72
+ describe("TextInputContent", () => {
73
+ beforeEach(() => {
74
+ jest.clearAllMocks();
75
+ });
76
+
77
+ it("renders default value and button label without a modal header", () => {
78
+ render(<TextInputContent {...baseProps} />);
79
+
80
+ expect(screen.queryByTestId("sheet-header")).toBeNull();
81
+
82
+ expect(screen.getByTestId("playlist-input").props.value).toBe(
83
+ "Summer Hits"
84
+ );
85
+
86
+ expect(screen.getByTestId("submit-button")).toBeTruthy();
87
+ expect(screen.getByText("Update")).toBeTruthy();
88
+ });
89
+
90
+ it("on submit merges name into sendCloudEvent data then dismisses", async () => {
91
+ render(<TextInputContent {...baseProps} />);
92
+
93
+ fireEvent.changeText(screen.getByTestId("playlist-input"), "New Name");
94
+ fireEvent.press(screen.getByTestId("submit-button"));
95
+
96
+ await waitFor(() => {
97
+ expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
98
+ expect(actionExecutor.handleAction).toHaveBeenCalledTimes(1);
99
+ });
100
+
101
+ expect(actionExecutor.handleActions).toHaveBeenCalledWith([
102
+ {
103
+ type: "sendCloudEvent",
104
+ options: {
105
+ url: "https://server.com/cloud-events",
106
+ type: "com.applicaster.collection.create.v1",
107
+ subject: "edit_collection",
108
+ data: {
109
+ collectionId: "playlist-123",
110
+ name: "New Name",
111
+ },
112
+ },
113
+ },
114
+ ]);
115
+
116
+ expect(actionExecutor.handleAction).toHaveBeenCalledWith({
117
+ type: "dismissBottomSheet",
118
+ });
119
+ });
120
+
121
+ it("forwards actionContext into handleActions so rename can refresh the List", async () => {
122
+ const actionContext = {
123
+ component: {
124
+ id: "list-1",
125
+ data: { source: "https://example.com/user/collections/abc" },
126
+ },
127
+ screenData: { id: "playlist-screen" },
128
+ };
129
+
130
+ render(<TextInputContent {...baseProps} actionContext={actionContext} />);
131
+
132
+ fireEvent.press(screen.getByTestId("submit-button"));
133
+
134
+ await waitFor(() => {
135
+ expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ expect(actionExecutor.handleActions).toHaveBeenCalledWith(
139
+ [
140
+ {
141
+ type: "sendCloudEvent",
142
+ options: {
143
+ url: "https://server.com/cloud-events",
144
+ type: "com.applicaster.collection.create.v1",
145
+ subject: "edit_collection",
146
+ data: {
147
+ collectionId: "playlist-123",
148
+ name: "Summer Hits",
149
+ },
150
+ },
151
+ },
152
+ ],
153
+ actionContext
154
+ );
155
+ });
156
+ });
@@ -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
  );
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { Pressable } from "react-native";
3
+ import { fireEvent, render, screen } from "@testing-library/react-native";
4
+ import { getCell } from "../index";
5
+ import { standardCell } from "../standardCell";
6
+ import { selectableCell } from "../selectableCell";
7
+ import { dynamicCollectionCell } from "../dynamicCollectionCell";
8
+ import { mapMenuItemToPlaylistData } from "../../AudioPlayer/Components/Item";
9
+
10
+ describe("getCell", () => {
11
+ it("returns selectable cell for collection_selector", () => {
12
+ expect(getCell("collection_selector")).toBe(selectableCell);
13
+ });
14
+
15
+ it("returns selectable cell for preference_editor", () => {
16
+ expect(getCell("preference_editor")).toBe(selectableCell);
17
+ });
18
+
19
+ it("returns standard cell when role is undefined", () => {
20
+ expect(getCell(undefined)).toBe(standardCell);
21
+ });
22
+
23
+ it("returns standard cell for unknown roles", () => {
24
+ expect(getCell("unknown_role")).toBe(standardCell);
25
+ });
26
+
27
+ it("returns the dynamic collection cell for dynamic_collection", () => {
28
+ expect(getCell("dynamic_collection")).toBe(dynamicCollectionCell);
29
+ });
30
+
31
+ it("does not use behavior to select the cell family", () => {
32
+ expect(getCell(undefined, { selectMode: "multi" })).toBe(standardCell);
33
+
34
+ expect(getCell("collection_selector", { selectMode: "single" })).toBe(
35
+ selectableCell
36
+ );
37
+ });
38
+ });
39
+
40
+ describe("mapMenuItemToPlaylistData", () => {
41
+ const item = {
42
+ title: "Playlist 1",
43
+ summary: "5 items",
44
+ icon: "https://example.com/icon.png",
45
+ };
46
+
47
+ it("defaults to multiSelect trailing button when selectMode is multi or undefined", () => {
48
+ const data1 = mapMenuItemToPlaylistData(item, { selectMode: "multi" });
49
+ expect(data1.trailingButton).toBe("multiSelect");
50
+
51
+ const data2 = mapMenuItemToPlaylistData(item);
52
+ expect(data2.trailingButton).toBe("multiSelect");
53
+ });
54
+
55
+ it("omits trailing button when behavior selectMode is none", () => {
56
+ const data = mapMenuItemToPlaylistData(item, { selectMode: "none" });
57
+ expect(data.trailingButton).toBeUndefined();
58
+ });
59
+
60
+ it("omits trailing button when item trailingButton is explicitly none", () => {
61
+ const itemWithNone = { ...item, trailingButton: "none" };
62
+
63
+ const data = mapMenuItemToPlaylistData(itemWithNone, {
64
+ selectMode: "multi",
65
+ });
66
+
67
+ expect(data.trailingButton).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ describe("selectableCell checkbox press", () => {
72
+ it("invokes onPress with the playlist item when the checkbox is pressed", () => {
73
+ const onPress = jest.fn();
74
+ const item = { id: "queue", title: "Queue" };
75
+
76
+ render(
77
+ selectableCell.renderItem({
78
+ item,
79
+ index: 0,
80
+ onPress,
81
+ context: { behavior: { selectMode: "multi" } } as any,
82
+ }) as React.ReactElement
83
+ );
84
+
85
+ const pressables = screen.UNSAFE_getAllByType(Pressable);
86
+ fireEvent.press(pressables[pressables.length - 1]);
87
+
88
+ expect(onPress).toHaveBeenCalledWith(item);
89
+ });
90
+ });