@applicaster/zapp-react-native-utils 16.0.0-alpha.9225550512 → 16.0.0-alpha.9338540010

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 (171) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +50 -3
  3. package/actionsExecutor/ActionExecutorContext.tsx +99 -17
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  6. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  7. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  8. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  9. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  10. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
  11. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +100 -0
  12. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +300 -0
  13. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  14. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  15. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  16. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  17. package/actionsExecutor/actions/goBack.ts +60 -0
  18. package/actionsExecutor/actions/goHome.ts +31 -0
  19. package/actionsExecutor/actions/index.ts +12 -0
  20. package/actionsExecutor/actions/navigateToScreen.ts +43 -45
  21. package/actionsExecutor/actions/openBottomSheet.ts +339 -0
  22. package/actionsExecutor/actions/presentScreen.ts +78 -0
  23. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  24. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  25. package/actionsExecutor/actions/showAlert.ts +51 -0
  26. package/actionsExecutor/actions/showToast.ts +64 -8
  27. package/actionsExecutor/consts.ts +27 -0
  28. package/actionsExecutor/feedDecorator.ts +6 -6
  29. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  30. package/appUtils/HooksManager/index.ts +9 -0
  31. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  32. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  33. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  34. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  35. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  36. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  37. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  38. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  39. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  40. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  41. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  42. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  43. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  44. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  45. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  46. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  47. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  48. package/appUtils/playerManager/index.ts +8 -1
  49. package/appUtils/playerManager/player.ts +125 -3
  50. package/appUtils/playerManager/playerFactory.ts +1 -1
  51. package/appUtils/playerManager/playerNative.ts +28 -4
  52. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  53. package/appUtils/playerManager/playerToolbox.ts +157 -0
  54. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  55. package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
  56. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  57. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  58. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  59. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  60. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  61. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  62. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  63. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  64. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  65. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  66. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  67. package/arrayUtils/__tests__/sample.test.ts +61 -0
  68. package/arrayUtils/index.ts +119 -20
  69. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  70. package/bottomSheet/index.ts +17 -0
  71. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  72. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  73. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  74. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  75. package/cellUtils/index.ts +70 -3
  76. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  77. package/configurationUtils/__tests__/modalBlocksParser.test.ts +392 -0
  78. package/configurationUtils/manifestKeyParser.ts +50 -10
  79. package/configurationUtils/modalBlocksParser.ts +252 -0
  80. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  81. package/entryActions/aliasPresentation.ts +108 -0
  82. package/entryActions/buildEntryActions.ts +167 -0
  83. package/entryActions/index.ts +15 -0
  84. package/entryActions/types.ts +39 -0
  85. package/focusManager/FocusManager.ts +26 -0
  86. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  87. package/localizationUtils/index.ts +12 -6
  88. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  89. package/logger/xrayToolbox.ts +67 -0
  90. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  91. package/manifestUtils/_internals/index.js +8 -3
  92. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  93. package/manifestUtils/defaultManifestConfigurations/player.js +342 -5
  94. package/manifestUtils/index.js +3 -0
  95. package/manifestUtils/keys.js +6 -1
  96. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  97. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  98. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  99. package/manifestUtils/modalBlocks.js +304 -0
  100. package/modalState/ContentViewModel.ts +113 -0
  101. package/modalState/EditableCollection.ts +17 -0
  102. package/modalState/EditableCollectionRegistry.ts +51 -0
  103. package/modalState/ModalOrchestrator.ts +212 -0
  104. package/modalState/RemoteEditableCollection.ts +235 -0
  105. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  106. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  107. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  108. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +63 -0
  109. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  110. package/modalState/__tests__/index.test.ts +30 -1
  111. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  112. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  113. package/modalState/index.ts +35 -83
  114. package/modalState/store.ts +109 -0
  115. package/modalState/types.ts +162 -0
  116. package/modalState/useBottomSheetContent.ts +109 -0
  117. package/navigationUtils/xrayNavigator.ts +12 -0
  118. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  119. package/numberUtils/__tests__/isZero.test.ts +27 -0
  120. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  121. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  122. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  123. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  124. package/numberUtils/index.ts +27 -8
  125. package/package.json +3 -2
  126. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  127. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  128. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  129. package/playerUtils/contentDataKeys.ts +134 -0
  130. package/playerUtils/index.ts +37 -16
  131. package/playerUtils/isAudioItem.ts +26 -0
  132. package/playerUtils/resolvePlayerActions.ts +71 -0
  133. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  134. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  135. package/pluginUtils/index.ts +29 -0
  136. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  137. package/quickBrickAssets/index.ts +35 -0
  138. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  139. package/reactHooks/actions/index.ts +67 -2
  140. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  141. package/reactHooks/cell-click/index.ts +21 -14
  142. package/reactHooks/connection/index.tsx +8 -5
  143. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  144. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  145. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  146. package/reactHooks/index.ts +2 -0
  147. package/reactHooks/layout/index.ts +1 -1
  148. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  149. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  150. package/reactHooks/navigation/index.ts +2 -0
  151. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  152. package/reactHooks/navigation/useRoute.ts +22 -5
  153. package/reactHooks/usePluginConfiguration.ts +88 -47
  154. package/reactHooks/utils/__tests__/index.test.js +22 -2
  155. package/reactHooks/utils/index.ts +11 -1
  156. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  157. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  158. package/stringUtils/index.ts +2 -2
  159. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  160. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  161. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  162. package/uiActionsRegistrator/index.ts +25 -203
  163. package/uiActionsRegistrator/registry.ts +351 -0
  164. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  165. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  166. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  167. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  168. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  169. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  170. package/playerUtils/getPlayerActionButtons.ts +0 -17
  171. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,267 @@
1
+ import { createNavigateToScreenAction } from "../navigateToScreen";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+
4
+ const river = { id: "screen-id", type: "parent-lock" } as unknown as ZappRiver;
5
+
6
+ const rivers = { "screen-id": river } as Record<string, ZappRiver>;
7
+
8
+ const contentTypes = {
9
+ parentLock: { screen_id: "screen-id" },
10
+ } as unknown as ZappContentTypes;
11
+
12
+ function createNavigator() {
13
+ return {
14
+ push: jest.fn(),
15
+ replace: jest.fn(),
16
+ replaceTop: jest.fn(),
17
+ } as unknown as QuickBrickAppNavigator;
18
+ }
19
+
20
+ describe("navigateToScreen navigationOptions", () => {
21
+ it("forwards navigationOptions to push when navigating to a screen", async () => {
22
+ const navigator = createNavigator();
23
+
24
+ const action = {
25
+ type: "navigateToScreen",
26
+ options: {
27
+ typeMapping: "parentLock",
28
+ navigationAction: "push",
29
+ navigationOptions: { pinFlow: "set-pin" },
30
+ },
31
+ };
32
+
33
+ const result = await createNavigateToScreenAction(
34
+ navigator,
35
+ rivers,
36
+ contentTypes
37
+ )(action);
38
+
39
+ expect(navigator.push).toHaveBeenCalledWith(river, {
40
+ pinFlow: "set-pin",
41
+ });
42
+
43
+ expect(result).toBe(ActionResult.Success);
44
+ });
45
+
46
+ it("forwards navigationOptions to replace when navigating to a screen", async () => {
47
+ const navigator = createNavigator();
48
+
49
+ const action = {
50
+ type: "navigateToScreen",
51
+ options: {
52
+ typeMapping: "parentLock",
53
+ navigationOptions: { pinFlow: "change-pin" },
54
+ },
55
+ };
56
+
57
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
58
+
59
+ expect(navigator.replace).toHaveBeenCalledWith(river, {
60
+ pinFlow: "change-pin",
61
+ });
62
+ });
63
+
64
+ it("forwards navigationOptions to push when navigating with an entry", async () => {
65
+ const navigator = createNavigator();
66
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
67
+
68
+ const action = {
69
+ type: "navigateToScreen",
70
+ options: {
71
+ typeMapping: "parentLock",
72
+ navigationAction: "push",
73
+ entry,
74
+ navigationOptions: { pinFlow: "set-pin" },
75
+ },
76
+ };
77
+
78
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
79
+
80
+ expect(navigator.push).toHaveBeenCalledWith(
81
+ { id: "entry-id", type: { value: "parentLock" } },
82
+ { pinFlow: "set-pin" }
83
+ );
84
+ });
85
+
86
+ it("forwards navigationOptions to replace when navigating with an entry", async () => {
87
+ const navigator = createNavigator();
88
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
89
+
90
+ const action = {
91
+ type: "navigateToScreen",
92
+ options: {
93
+ typeMapping: "parentLock",
94
+ entry,
95
+ navigationOptions: { pinFlow: "change-pin" },
96
+ },
97
+ };
98
+
99
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
100
+
101
+ expect(navigator.replace).toHaveBeenCalledWith(
102
+ { id: "entry-id", type: { value: "parentLock" } },
103
+ { pinFlow: "change-pin" }
104
+ );
105
+
106
+ expect(navigator.replaceTop).not.toHaveBeenCalled();
107
+ });
108
+
109
+ // Regression guard: the no-options path must keep working unchanged.
110
+ it("still navigates when no navigationOptions are configured", async () => {
111
+ const navigator = createNavigator();
112
+
113
+ const action = {
114
+ type: "navigateToScreen",
115
+ options: {
116
+ typeMapping: "parentLock",
117
+ navigationAction: "push",
118
+ },
119
+ };
120
+
121
+ const result = await createNavigateToScreenAction(
122
+ navigator,
123
+ rivers,
124
+ contentTypes
125
+ )(action);
126
+
127
+ expect(navigator.push).toHaveBeenCalledTimes(1);
128
+ expect((navigator.push as jest.Mock).mock.calls[0][0]).toBe(river);
129
+ expect(result).toBe(ActionResult.Success);
130
+ });
131
+ });
132
+
133
+ describe("navigateToScreen target resolution", () => {
134
+ function run(options, context?) {
135
+ const navigator = createNavigator();
136
+
137
+ const result = createNavigateToScreenAction(
138
+ navigator,
139
+ rivers,
140
+ contentTypes
141
+ )({ type: "navigateToScreen", options }, context);
142
+
143
+ return { navigator, result };
144
+ }
145
+
146
+ it("overrides entry.type with typeMapping when both are provided", async () => {
147
+ const entry = {
148
+ id: "entry-id",
149
+ type: { value: "video" },
150
+ } as unknown as ZappEntry;
151
+
152
+ const { navigator, result } = run({
153
+ typeMapping: "parentLock",
154
+ navigationAction: "push",
155
+ entry,
156
+ });
157
+
158
+ expect(await result).toBe(ActionResult.Success);
159
+
160
+ expect(navigator.push).toHaveBeenCalledWith(
161
+ { id: "entry-id", type: { value: "parentLock" } },
162
+ undefined
163
+ );
164
+
165
+ expect(entry.type).toEqual({ value: "video" });
166
+ });
167
+
168
+ it("navigates with the entry's own type when typeMapping is not provided", async () => {
169
+ const entry = {
170
+ id: "entry-id",
171
+ type: { value: "video" },
172
+ } as unknown as ZappEntry;
173
+
174
+ const { navigator, result } = run({ navigationAction: "push", entry });
175
+
176
+ expect(await result).toBe(ActionResult.Success);
177
+
178
+ expect(navigator.push).toHaveBeenCalledWith(
179
+ { id: "entry-id", type: { value: "video" } },
180
+ undefined
181
+ );
182
+ });
183
+
184
+ it("resolves the context entry when entry is @{entry/} and typeMapping is not provided", async () => {
185
+ const entry = {
186
+ id: "context-entry-id",
187
+ type: { value: "video" },
188
+ } as unknown as ZappEntry;
189
+
190
+ const { navigator, result } = run({ entry: "@{entry/}" }, { entry });
191
+
192
+ expect(await result).toBe(ActionResult.Success);
193
+
194
+ expect(navigator.replace).toHaveBeenCalledWith(
195
+ { id: "context-entry-id", type: { value: "video" } },
196
+ undefined
197
+ );
198
+ });
199
+
200
+ it("returns an error when neither typeMapping nor entry type is provided", async () => {
201
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
202
+
203
+ const { navigator, result } = run({ navigationAction: "push", entry });
204
+
205
+ expect(await result).toBe(ActionResult.Error);
206
+ expect(navigator.push).not.toHaveBeenCalled();
207
+ expect(navigator.replace).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("returns an error when neither typeMapping nor entry is provided", async () => {
211
+ const { navigator, result } = run({ navigationAction: "push" });
212
+
213
+ expect(await result).toBe(ActionResult.Error);
214
+ expect(navigator.push).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it("returns an error when typeMapping has no screen_id mapping", async () => {
218
+ const callback = jest.fn();
219
+
220
+ const { navigator, result } = run(
221
+ { typeMapping: "unknownType", navigationAction: "push" },
222
+ { callback }
223
+ );
224
+
225
+ expect(await result).toBe(ActionResult.Error);
226
+ expect(navigator.push).not.toHaveBeenCalled();
227
+ expect(callback).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it("returns an error when the mapped screen_id has no river", async () => {
231
+ const navigator = createNavigator();
232
+ const callback = jest.fn();
233
+
234
+ const result = await createNavigateToScreenAction(navigator, rivers, {
235
+ orphan: { screen_id: "missing-screen-id" },
236
+ } as unknown as ZappContentTypes)(
237
+ {
238
+ type: "navigateToScreen",
239
+ options: { typeMapping: "orphan", navigationAction: "push" },
240
+ },
241
+ { callback }
242
+ );
243
+
244
+ expect(result).toBe(ActionResult.Error);
245
+ expect(navigator.push).not.toHaveBeenCalled();
246
+ expect(callback).not.toHaveBeenCalled();
247
+ });
248
+
249
+ it("aborts the action pipeline before navigating to a resolved river", async () => {
250
+ const callback = jest.fn();
251
+
252
+ const { navigator, result } = run(
253
+ { typeMapping: "parentLock" },
254
+ { callback }
255
+ );
256
+
257
+ expect(await result).toBe(ActionResult.Success);
258
+
259
+ expect(callback).toHaveBeenCalledWith({
260
+ success: false,
261
+ error: null,
262
+ abort: true,
263
+ });
264
+
265
+ expect(navigator.replace).toHaveBeenCalledWith(river, undefined);
266
+ });
267
+ });
@@ -0,0 +1,100 @@
1
+ import { openBottomSheetAction } from "../openBottomSheet";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+ import { openBottomSheetModal } from "../../../modalState/store";
4
+ import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
5
+
6
+ jest.mock("../../../modalState/store", () => ({
7
+ openBottomSheetModal: jest.fn(),
8
+ }));
9
+
10
+ jest.mock("../../../modalState/ModalOrchestrator", () => ({
11
+ modalOrchestrator: {
12
+ open: jest.fn(),
13
+ },
14
+ isPresentSubMenuAction: jest.requireActual("../../../modalState/types")
15
+ .isPresentSubMenuAction,
16
+ }));
17
+
18
+ describe("openBottomSheetAction custom content", () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ it("opens default sheet with contentComponent body props", async () => {
24
+ const CustomContent = function CustomContent() {
25
+ return null;
26
+ };
27
+
28
+ const result = await openBottomSheetAction({
29
+ type: "openBottomSheet",
30
+ options: {
31
+ header: { title: "Create Playlist" },
32
+ content: {
33
+ component: CustomContent,
34
+ props: {
35
+ inputLabel: "Name your playlist",
36
+ buttonLabel: "Create",
37
+ defaultValue: "",
38
+ submitAction: { type: "sendCloudEvent", options: {} },
39
+ },
40
+ },
41
+ },
42
+ } as any);
43
+
44
+ expect(result).toBe(ActionResult.Success);
45
+ expect(openBottomSheetModal).toHaveBeenCalledTimes(1);
46
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
47
+
48
+ const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
49
+ // Default ModalComponent provides the standard header
50
+ expect(args.ModalBottomSheetContent).toBeUndefined();
51
+
52
+ expect(args.modalBottomSheetContentProps).toMatchObject({
53
+ title: "Create Playlist",
54
+ contentComponent: CustomContent,
55
+ contentComponentProps: {
56
+ inputLabel: "Name your playlist",
57
+ buttonLabel: "Create",
58
+ defaultValue: "",
59
+ },
60
+ });
61
+ });
62
+
63
+ it("passes themePluginId through to the name sheet", async () => {
64
+ const CustomContent = function CustomContent() {
65
+ return null;
66
+ };
67
+
68
+ await openBottomSheetAction({
69
+ type: "openBottomSheet",
70
+ options: {
71
+ header: { title: "Create New Playlist" },
72
+ themePluginId: "playlist-experience",
73
+ content: {
74
+ component: CustomContent,
75
+ props: { buttonLabel: "Create" },
76
+ },
77
+ },
78
+ } as any);
79
+
80
+ const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
81
+
82
+ expect(args.modalBottomSheetContentProps.themePluginId).toBe(
83
+ "playlist-experience"
84
+ );
85
+ });
86
+
87
+ it("returns Error when neither list data nor content.component is provided", async () => {
88
+ const result = await openBottomSheetAction({
89
+ type: "openBottomSheet",
90
+ options: {
91
+ header: { title: "Empty" },
92
+ content: {},
93
+ },
94
+ } as any);
95
+
96
+ expect(result).toBe(ActionResult.Error);
97
+ expect(openBottomSheetModal).not.toHaveBeenCalled();
98
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
99
+ });
100
+ });
@@ -0,0 +1,300 @@
1
+ import { openBottomSheetAction } from "../openBottomSheet";
2
+ import { actionExecutor, ActionResult } from "../../ActionExecutor";
3
+ import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
4
+
5
+ jest.mock("../../../modalState/store", () => ({
6
+ openBottomSheetModal: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../../../modalState/ModalOrchestrator", () => ({
10
+ modalOrchestrator: {
11
+ open: jest.fn(),
12
+ replace: jest.fn(),
13
+ stackSize: 0,
14
+ },
15
+ isPresentSubMenuAction: jest.requireActual("../../../modalState/types")
16
+ .isPresentSubMenuAction,
17
+ }));
18
+
19
+ describe("openBottomSheetAction inline items", () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ (modalOrchestrator as any).stackSize = 0;
23
+ });
24
+
25
+ const speedSheetAction = {
26
+ type: "openBottomSheet",
27
+ options: {
28
+ header: { title: "Playback Speed" },
29
+ content: {
30
+ role: "collection_selector",
31
+ behavior: {
32
+ selectMode: "single",
33
+ currentSelection: ["1"],
34
+ },
35
+ items: [
36
+ {
37
+ id: "0.8",
38
+ title: "0.8x",
39
+ extensions: {
40
+ tap_actions: {
41
+ actions: [
42
+ { type: "setPlaybackSpeed", options: { value: 0.8 } },
43
+ ],
44
+ },
45
+ },
46
+ },
47
+ {
48
+ id: "1",
49
+ title: "1x",
50
+ extensions: {
51
+ tap_actions: {
52
+ actions: [{ type: "setPlaybackSpeed", options: { value: 1 } }],
53
+ },
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ },
59
+ } as any;
60
+
61
+ it("opens orchestrator with inline items and marks current selection", async () => {
62
+ const result = await openBottomSheetAction(speedSheetAction);
63
+
64
+ expect(result).toBe(ActionResult.Success);
65
+ expect(modalOrchestrator.open).toHaveBeenCalledTimes(1);
66
+ expect(modalOrchestrator.replace).not.toHaveBeenCalled();
67
+
68
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
69
+
70
+ expect(menu.content.items[0].isSelected).toBe(false);
71
+
72
+ expect(menu.content.items[1]).toMatchObject({
73
+ id: "1",
74
+ title: "1x",
75
+ isSelected: true,
76
+ action: { type: "setPlaybackSpeed", options: { value: 1 } },
77
+ });
78
+ });
79
+
80
+ it("opens an empty inline collection when dynamic collection options are present", async () => {
81
+ const result = await openBottomSheetAction({
82
+ type: "openBottomSheet",
83
+ options: {
84
+ header: { title: "Queue" },
85
+ content: {
86
+ items: [],
87
+ emptyMessage: "Queued items will be added here",
88
+ dynamicCollection: {
89
+ operations: ["remove"],
90
+ provider: "@{provider/queue}",
91
+ },
92
+ },
93
+ },
94
+ } as any);
95
+
96
+ expect(result).toBe(ActionResult.Success);
97
+
98
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
99
+
100
+ expect(menu.content.items).toEqual([]);
101
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
102
+
103
+ expect(menu.content.dynamicCollection).toMatchObject({
104
+ operations: ["remove"],
105
+ provider: "@{provider/queue}",
106
+ });
107
+ });
108
+
109
+ it("accepts empty inline items when dynamic_collection_options live on extensions", async () => {
110
+ const result = await openBottomSheetAction({
111
+ type: "openBottomSheet",
112
+ options: {
113
+ content: {
114
+ items: [],
115
+ extensions: {
116
+ dynamic_collection_options: {
117
+ operations: "remove,reorder",
118
+ provider: "@{provider/offline}",
119
+ },
120
+ },
121
+ },
122
+ },
123
+ } as any);
124
+
125
+ expect(result).toBe(ActionResult.Success);
126
+
127
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
128
+
129
+ expect(menu.content.items).toEqual([]);
130
+
131
+ expect(menu.content.dynamicCollection).toMatchObject({
132
+ operations: ["remove", "reorder"],
133
+ provider: "@{provider/offline}",
134
+ });
135
+ });
136
+
137
+ it("returns an error for an empty items array with no source", async () => {
138
+ const result = await openBottomSheetAction({
139
+ type: "openBottomSheet",
140
+ options: {
141
+ content: { items: [] },
142
+ },
143
+ } as any);
144
+
145
+ expect(result).toBe(ActionResult.Error);
146
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("forwards content.emptyMessage onto the menu", async () => {
150
+ const result = await openBottomSheetAction({
151
+ type: "openBottomSheet",
152
+ options: {
153
+ header: { title: "Queue" },
154
+ content: {
155
+ items: [{ id: "t1", title: "Track 1" }],
156
+ emptyMessage: "Queued items will be added here",
157
+ },
158
+ },
159
+ } as any);
160
+
161
+ expect(result).toBe(ActionResult.Success);
162
+
163
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
164
+
165
+ expect(menu.content.emptyMessage).toBe("Queued items will be added here");
166
+ });
167
+
168
+ it("pushes a new sheet by default even when another sheet is already open", async () => {
169
+ (modalOrchestrator as any).stackSize = 1;
170
+
171
+ await openBottomSheetAction(speedSheetAction);
172
+
173
+ expect(modalOrchestrator.open).toHaveBeenCalledTimes(1);
174
+ expect(modalOrchestrator.replace).not.toHaveBeenCalled();
175
+ });
176
+
177
+ it("replaces the current sheet when options.replace is true", async () => {
178
+ (modalOrchestrator as any).stackSize = 1;
179
+
180
+ await openBottomSheetAction({
181
+ ...speedSheetAction,
182
+ options: { ...speedSheetAction.options, replace: true },
183
+ });
184
+
185
+ expect(modalOrchestrator.replace).toHaveBeenCalledTimes(1);
186
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it("executes all actions in tap_actions when onItemPress is called", async () => {
190
+ const multiActionSheet = {
191
+ type: "openBottomSheet",
192
+ options: {
193
+ header: { title: "Target Playlists" },
194
+ content: {
195
+ role: "collection_selector",
196
+ behavior: { selectMode: "none", currentSelection: [] },
197
+ items: [
198
+ {
199
+ id: "playlist-1",
200
+ title: "My Playlist",
201
+ extensions: {
202
+ tap_actions: {
203
+ actions: [
204
+ {
205
+ type: "sendCloudEvent",
206
+ options: { subject: "add_collection_to_collection" },
207
+ },
208
+ {
209
+ type: "showToast",
210
+ options: { message: "Added to playlist" },
211
+ },
212
+ { type: "dismissBottomSheet", options: {} },
213
+ ],
214
+ },
215
+ },
216
+ },
217
+ ],
218
+ },
219
+ },
220
+ } as any;
221
+
222
+ await openBottomSheetAction(multiActionSheet);
223
+
224
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
225
+ .calls[0];
226
+
227
+ expect(menu.content.items[0].actions).toHaveLength(3);
228
+
229
+ const handleActionsSpy = jest
230
+ .spyOn(actionExecutor, "handleActions")
231
+ .mockResolvedValue(ActionResult.Success);
232
+
233
+ await onItemPress(menu.content.items[0]);
234
+
235
+ expect(handleActionsSpy).toHaveBeenCalledWith(
236
+ [
237
+ {
238
+ type: "sendCloudEvent",
239
+ options: { subject: "add_collection_to_collection" },
240
+ },
241
+ {
242
+ type: "showToast",
243
+ options: { message: "Added to playlist" },
244
+ },
245
+ { type: "dismissBottomSheet", options: {} },
246
+ ],
247
+ expect.objectContaining({
248
+ entry: menu.content.items[0],
249
+ })
250
+ );
251
+
252
+ handleActionsSpy.mockRestore();
253
+ });
254
+
255
+ it("executes preceding actions and excludes presentSubMenu from actionExecutor", async () => {
256
+ const subMenuSheet = {
257
+ type: "openBottomSheet",
258
+ options: {
259
+ header: { title: "Menu" },
260
+ content: {
261
+ items: [
262
+ {
263
+ id: "item-with-submenu",
264
+ title: "More Options",
265
+ extensions: {
266
+ tap_actions: {
267
+ actions: [
268
+ { type: "sendAnalytics", options: { event: "click_more" } },
269
+ {
270
+ type: "presentSubMenu",
271
+ menu: { content: { title: "Sub Options", items: [] } },
272
+ },
273
+ ],
274
+ },
275
+ },
276
+ },
277
+ ],
278
+ },
279
+ },
280
+ } as any;
281
+
282
+ await openBottomSheetAction(subMenuSheet);
283
+
284
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
285
+ .calls[0];
286
+
287
+ const handleActionsSpy = jest
288
+ .spyOn(actionExecutor, "handleActions")
289
+ .mockResolvedValue(ActionResult.Success);
290
+
291
+ await onItemPress(menu.content.items[0]);
292
+
293
+ expect(handleActionsSpy).toHaveBeenCalledWith(
294
+ [{ type: "sendAnalytics", options: { event: "click_more" } }],
295
+ expect.objectContaining({ entry: menu.content.items[0] })
296
+ );
297
+
298
+ handleActionsSpy.mockRestore();
299
+ });
300
+ });