@applicaster/zapp-react-native-utils 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 (201) hide show
  1. package/actionsExecutor/ActionExecutor.ts +57 -12
  2. package/actionsExecutor/ActionExecutorContext.tsx +113 -314
  3. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  4. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  5. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  6. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  7. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  8. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  9. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
  10. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  11. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +212 -0
  12. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  13. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  14. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  15. package/actionsExecutor/actions/appRestart.ts +24 -0
  16. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  17. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  18. package/actionsExecutor/actions/goBack.ts +60 -0
  19. package/actionsExecutor/actions/goHome.ts +31 -0
  20. package/actionsExecutor/actions/index.ts +42 -0
  21. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  22. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  23. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  24. package/actionsExecutor/actions/navigateToScreen.ts +137 -0
  25. package/actionsExecutor/actions/openBottomSheet.ts +329 -0
  26. package/actionsExecutor/actions/presentScreen.ts +78 -0
  27. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  28. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  29. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  30. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  31. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  32. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  33. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  34. package/actionsExecutor/actions/showAlert.ts +51 -0
  35. package/actionsExecutor/actions/showToast.ts +87 -0
  36. package/actionsExecutor/actions/switchLayout.ts +40 -0
  37. package/actionsExecutor/consts.ts +27 -0
  38. package/actionsExecutor/feedDecorator.ts +6 -6
  39. package/actionsExecutor/types.ts +59 -0
  40. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  41. package/analyticsUtils/PlayerAnalyticsManager.ts +12 -2
  42. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  43. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  44. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  45. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  51. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  52. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  53. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  54. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  55. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  56. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  57. package/analyticsUtils/analyticsMapper.ts +4 -1
  58. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  59. package/appUtils/HooksManager/index.ts +12 -8
  60. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  61. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  62. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  63. package/appUtils/contextKeysManager/index.ts +1 -1
  64. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  65. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +143 -53
  66. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  67. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +49 -0
  68. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  69. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  70. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  71. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  72. package/appUtils/playerManager/OverlayObserver/utils.ts +54 -20
  73. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  74. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  75. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  76. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  77. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  78. package/appUtils/playerManager/index.ts +1 -1
  79. package/appUtils/playerManager/player.ts +115 -3
  80. package/appUtils/playerManager/playerFactory.ts +1 -1
  81. package/appUtils/playerManager/playerNative.ts +6 -4
  82. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  83. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  84. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  85. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  86. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  87. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  88. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  89. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  90. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  91. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  92. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  93. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  94. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  95. package/arrayUtils/__tests__/sample.test.ts +61 -0
  96. package/arrayUtils/index.ts +136 -20
  97. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  98. package/bottomSheet/index.ts +17 -0
  99. package/cellUtils/index.ts +33 -8
  100. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  101. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  102. package/colorUtils/index.ts +50 -0
  103. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  104. package/configurationUtils/__tests__/modalBlocksParser.test.ts +260 -0
  105. package/configurationUtils/manifestKeyParser.ts +50 -10
  106. package/configurationUtils/modalBlocksParser.ts +296 -0
  107. package/entryActions/__tests__/buildEntryActions.test.ts +195 -0
  108. package/entryActions/aliasPresentation.ts +124 -0
  109. package/entryActions/buildEntryActions.ts +167 -0
  110. package/entryActions/index.ts +17 -0
  111. package/entryActions/types.ts +39 -0
  112. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  113. package/manifestUtils/_internals/index.js +14 -3
  114. package/manifestUtils/defaultManifestConfigurations/generalContent.js +41 -0
  115. package/manifestUtils/defaultManifestConfigurations/player.js +349 -20
  116. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  117. package/manifestUtils/fieldUtils/index.js +125 -0
  118. package/manifestUtils/index.js +3 -0
  119. package/manifestUtils/keys.js +9 -0
  120. package/manifestUtils/mobileAction/button/index.js +16 -0
  121. package/manifestUtils/mobileAction/container/index.js +3 -1
  122. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  123. package/manifestUtils/modalBlocks.js +304 -0
  124. package/manifestUtils/platformIsTV.js +1 -0
  125. package/modalState/ContentViewModel.ts +113 -0
  126. package/modalState/EditableCollection.ts +17 -0
  127. package/modalState/EditableCollectionRegistry.ts +51 -0
  128. package/modalState/ModalOrchestrator.ts +211 -0
  129. package/modalState/RemoteEditableCollection.ts +235 -0
  130. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  131. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  132. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  133. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  134. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  135. package/modalState/__tests__/index.test.ts +30 -1
  136. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  137. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  138. package/modalState/index.ts +35 -83
  139. package/modalState/store.ts +109 -0
  140. package/modalState/types.ts +160 -0
  141. package/modalState/useBottomSheetContent.ts +109 -0
  142. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  143. package/numberUtils/__tests__/isZero.test.ts +27 -0
  144. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  145. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  146. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  147. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  148. package/numberUtils/index.ts +27 -8
  149. package/package.json +2 -2
  150. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  151. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  152. package/pipesUtils/index.ts +1 -0
  153. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  154. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  155. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  156. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  157. package/playerUtils/contentDataKeys.ts +134 -0
  158. package/playerUtils/index.ts +41 -18
  159. package/playerUtils/isAudioItem.ts +26 -0
  160. package/playerUtils/resolvePlayerActions.ts +71 -0
  161. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  162. package/reactHooks/actions/index.ts +117 -2
  163. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  164. package/reactHooks/cell-click/index.ts +39 -24
  165. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  166. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  167. package/reactHooks/feed/index.ts +5 -1
  168. package/reactHooks/feed/useBatchLoading.ts +9 -1
  169. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  170. package/reactHooks/index.ts +2 -0
  171. package/reactHooks/layout/index.ts +1 -1
  172. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  173. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  174. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  175. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  176. package/reactHooks/navigation/index.ts +3 -1
  177. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  178. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  179. package/reactHooks/navigation/useRoute.ts +22 -5
  180. package/reactHooks/state/useComponentScreenState.ts +1 -1
  181. package/reactHooks/usePluginConfiguration.ts +4 -1
  182. package/reactHooks/utils/__tests__/index.test.js +22 -2
  183. package/reactHooks/utils/index.ts +13 -2
  184. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  185. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  186. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  187. package/stringUtils/index.ts +2 -2
  188. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  189. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  190. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  191. package/uiActionsRegistrator/index.ts +25 -0
  192. package/uiActionsRegistrator/registry.ts +335 -0
  193. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  194. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  195. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  196. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  197. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  198. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  199. package/playerUtils/getPlayerActionButtons.ts +0 -17
  200. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  201. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,415 @@
1
+ import { RemoteEditableCollection } from "../RemoteEditableCollection";
2
+ import { actionExecutor } from "../../actionsExecutor/ActionExecutor";
3
+ import type { Content, MenuItem } from "../types";
4
+
5
+ jest.mock("../../actionsExecutor/ActionExecutor", () => ({
6
+ actionExecutor: { handleAction: jest.fn() },
7
+ ActionResult: { Success: "success", Error: "error" },
8
+ }));
9
+
10
+ const handleAction = actionExecutor.handleAction as jest.Mock;
11
+
12
+ /**
13
+ * BehaviorSubject emits its current value synchronously on subscribe, so this
14
+ * returns the latest items snapshot.
15
+ */
16
+ function currentItems(col: RemoteEditableCollection): MenuItem[] {
17
+ let items: MenuItem[] = [];
18
+ const sub = col.items$.subscribe((i) => (items = i));
19
+ sub.unsubscribe();
20
+
21
+ return items;
22
+ }
23
+
24
+ const removeAction = { type: "sendCloudEvent", options: { foo: "bar" } };
25
+
26
+ const itemWithRemove: MenuItem = {
27
+ id: "1",
28
+ title: "A",
29
+ entryActions: [{ button: { alias: "remove_item" }, actions: [removeAction] }],
30
+ };
31
+
32
+ const itemPlain: MenuItem = { id: "2", title: "B" };
33
+
34
+ function makeCollection(
35
+ items: MenuItem[],
36
+ refetch = jest.fn().mockResolvedValue(undefined),
37
+ operations = ["remove", "reorder"],
38
+ context?: Record<string, unknown>
39
+ ) {
40
+ const content = {
41
+ items,
42
+ context,
43
+ dynamicCollection: { operations, postUrl: "https://x/cloud-events" },
44
+ } as unknown as Content;
45
+
46
+ return { col: new RemoteEditableCollection(content, refetch), refetch };
47
+ }
48
+
49
+ const openingContext = {
50
+ component: {
51
+ id: "list-1",
52
+ data: { source: "https://example.com/user/collections/abc" },
53
+ },
54
+ screenData: { id: "playlist-screen" },
55
+ };
56
+
57
+ describe("RemoteEditableCollection", () => {
58
+ beforeEach(() => jest.clearAllMocks());
59
+
60
+ it("exposes the declared operations and initial items", () => {
61
+ const { col } = makeCollection([itemWithRemove, itemPlain]);
62
+
63
+ expect(col.operations).toEqual(["remove", "reorder"]);
64
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
65
+ });
66
+
67
+ describe("remove", () => {
68
+ it("optimistically removes, dispatches the aliased action, then refetches", async () => {
69
+ handleAction.mockResolvedValue("success");
70
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
71
+
72
+ await col.remove(0);
73
+
74
+ expect(handleAction).toHaveBeenCalledWith(removeAction, {
75
+ entry: itemWithRemove,
76
+ });
77
+
78
+ expect(refetch).toHaveBeenCalledTimes(1);
79
+ expect(currentItems(col)).toEqual([itemPlain]);
80
+ });
81
+
82
+ it("rolls back to the snapshot when the action dispatch fails", async () => {
83
+ handleAction.mockRejectedValue(new Error("network"));
84
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
85
+
86
+ await col.remove(0);
87
+
88
+ // optimistic removal reverted
89
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
90
+ // refetch never reached on failure
91
+ expect(refetch).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it("lets the refetched server result replace the optimistic list", async () => {
95
+ handleAction.mockResolvedValue("success");
96
+ const serverItems: MenuItem[] = [{ id: "99", title: "server truth" }];
97
+
98
+ const refetch = jest.fn();
99
+ const { col } = makeCollection([itemWithRemove, itemPlain], refetch);
100
+
101
+ // Emulate ContentViewModel.refetch pushing authoritative items via sync()
102
+ refetch.mockImplementation(async () => {
103
+ col.sync(serverItems);
104
+ });
105
+
106
+ await col.remove(0);
107
+
108
+ expect(currentItems(col)).toEqual(serverItems);
109
+ });
110
+
111
+ it("is a no-op for an out-of-range index", async () => {
112
+ const { col, refetch } = makeCollection([itemWithRemove]);
113
+
114
+ await col.remove(5);
115
+
116
+ expect(handleAction).not.toHaveBeenCalled();
117
+ expect(refetch).not.toHaveBeenCalled();
118
+ expect(currentItems(col)).toEqual([itemWithRemove]);
119
+ });
120
+
121
+ it("skips dispatch (no throw) when no matching alias exists", async () => {
122
+ const { col, refetch } = makeCollection([itemPlain, itemWithRemove]);
123
+
124
+ await col.remove(0); // itemPlain has no entryActions
125
+
126
+ expect(handleAction).not.toHaveBeenCalled();
127
+ // still optimistic-removes and refetches (server is source of truth)
128
+ expect(refetch).toHaveBeenCalledTimes(1);
129
+ expect(currentItems(col)).toEqual([itemWithRemove]);
130
+ });
131
+
132
+ it("forwards stored sheet context plus the removed item as entry", async () => {
133
+ handleAction.mockResolvedValue("success");
134
+
135
+ const { col } = makeCollection(
136
+ [itemWithRemove, itemPlain],
137
+ jest.fn().mockResolvedValue(undefined),
138
+ ["remove", "reorder"],
139
+ openingContext
140
+ );
141
+
142
+ await col.remove(0);
143
+
144
+ expect(handleAction).toHaveBeenCalledWith(removeAction, {
145
+ ...openingContext,
146
+ entry: itemWithRemove,
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("move", () => {
152
+ it("optimistically reorders and dispatches reorder_item with itemIds", async () => {
153
+ handleAction.mockResolvedValue("success");
154
+
155
+ const itemA: MenuItem = {
156
+ id: "2",
157
+ title: "B",
158
+ entryActions: [
159
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
160
+ ],
161
+ };
162
+
163
+ const moved: MenuItem = {
164
+ id: "3",
165
+ title: "C",
166
+ entryActions: [
167
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
168
+ ],
169
+ };
170
+
171
+ const { col, refetch } = makeCollection([moved, itemA]);
172
+
173
+ await col.move(["2", "3"]);
174
+
175
+ expect(currentItems(col)).toEqual([itemA, moved]);
176
+
177
+ expect(handleAction).toHaveBeenCalledWith(
178
+ {
179
+ ...removeAction,
180
+ options: {
181
+ ...removeAction.options,
182
+ itemIds: ["2", "3"],
183
+ data: {
184
+ ...removeAction.options?.data,
185
+ itemIds: ["2", "3"],
186
+ },
187
+ },
188
+ },
189
+ { entry: itemA }
190
+ );
191
+
192
+ expect(refetch).toHaveBeenCalledTimes(1);
193
+ });
194
+
195
+ it("forwards stored sheet context plus the moved item as entry", async () => {
196
+ handleAction.mockResolvedValue("success");
197
+
198
+ const itemA: MenuItem = {
199
+ id: "2",
200
+ title: "B",
201
+ entryActions: [
202
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
203
+ ],
204
+ };
205
+
206
+ const { col } = makeCollection(
207
+ [itemA],
208
+ jest.fn().mockResolvedValue(undefined),
209
+ ["remove", "reorder"],
210
+ openingContext
211
+ );
212
+
213
+ await col.move(["2"]);
214
+
215
+ expect(handleAction).toHaveBeenCalledWith(
216
+ expect.objectContaining({ type: "sendCloudEvent" }),
217
+ {
218
+ ...openingContext,
219
+ entry: itemA,
220
+ }
221
+ );
222
+ });
223
+
224
+ it("rolls back on failure", async () => {
225
+ handleAction.mockRejectedValue(new Error("nope"));
226
+
227
+ const itemA: MenuItem = {
228
+ id: "2",
229
+ title: "B",
230
+ entryActions: [
231
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
232
+ ],
233
+ };
234
+
235
+ const moved: MenuItem = {
236
+ id: "3",
237
+ title: "C",
238
+ entryActions: [
239
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
240
+ ],
241
+ };
242
+
243
+ const { col } = makeCollection([moved, itemA]);
244
+
245
+ await col.move(["2", "3"]);
246
+
247
+ expect(currentItems(col)).toEqual([moved, itemA]);
248
+ });
249
+
250
+ it("is a no-op for empty or non-existent IDs", async () => {
251
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
252
+
253
+ await col.move([]);
254
+
255
+ expect(refetch).not.toHaveBeenCalled();
256
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
257
+ });
258
+ });
259
+
260
+ describe("sync", () => {
261
+ it("replaces the current items with the provided list", () => {
262
+ const { col } = makeCollection([itemWithRemove]);
263
+ const next: MenuItem[] = [{ id: "7", title: "next" }];
264
+
265
+ col.sync(next);
266
+
267
+ expect(currentItems(col)).toEqual(next);
268
+ });
269
+
270
+ it("suppresses sync updates while a local mutation is in-flight", async () => {
271
+ let resolveAction: () => void = () => {};
272
+
273
+ handleAction.mockImplementation(
274
+ () =>
275
+ new Promise<void>((resolve) => {
276
+ resolveAction = resolve;
277
+ })
278
+ );
279
+
280
+ const { col } = makeCollection([itemWithRemove, itemPlain]);
281
+ const staleServerItems: MenuItem[] = [{ id: "stale", title: "Stale" }];
282
+
283
+ // Initiate optimistic removal (mutation in-flight)
284
+ const removePromise = col.remove(0);
285
+
286
+ // Verify optimistic state applied locally
287
+ expect(currentItems(col)).toEqual([itemPlain]);
288
+
289
+ // Attempt sync while mutation is in-flight
290
+ col.sync(staleServerItems);
291
+
292
+ // Verify sync was suppressed and optimistic state remained
293
+ expect(currentItems(col)).toEqual([itemPlain]);
294
+
295
+ // Resolve the in-flight mutation
296
+ resolveAction();
297
+ await removePromise;
298
+
299
+ // After mutation completes, sync applies normally
300
+ const freshServerItems: MenuItem[] = [{ id: "fresh", title: "Fresh" }];
301
+ col.sync(freshServerItems);
302
+
303
+ expect(currentItems(col)).toEqual(freshServerItems);
304
+ });
305
+ });
306
+
307
+ describe("deleteCollection", () => {
308
+ it("optimistically clears items and calls refetch", async () => {
309
+ handleAction.mockResolvedValue("success");
310
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
311
+
312
+ await col.deleteCollection();
313
+
314
+ expect(currentItems(col)).toEqual([]);
315
+ expect(refetch).toHaveBeenCalledTimes(1);
316
+ });
317
+
318
+ it("dispatches aliased action when first item contains delete_collection alias", async () => {
319
+ handleAction.mockResolvedValue("success");
320
+
321
+ const deleteAction = {
322
+ type: "sendCloudEvent",
323
+ options: { subject: "delete_collection" },
324
+ };
325
+
326
+ const itemWithDelete: MenuItem = {
327
+ id: "100",
328
+ title: "Collection Item",
329
+ entryActions: [
330
+ {
331
+ button: { alias: "delete_collection" },
332
+ actions: [deleteAction],
333
+ },
334
+ ],
335
+ };
336
+
337
+ const { col, refetch } = makeCollection([itemWithDelete]);
338
+
339
+ await col.deleteCollection();
340
+
341
+ expect(handleAction).toHaveBeenCalledWith(deleteAction, {
342
+ entry: itemWithDelete,
343
+ });
344
+
345
+ expect(refetch).toHaveBeenCalledTimes(1);
346
+ expect(currentItems(col)).toEqual([]);
347
+ });
348
+ });
349
+
350
+ describe("add", () => {
351
+ it("dispatches actions from dynamicCollection.events.add and refetches", async () => {
352
+ handleAction.mockResolvedValue("success");
353
+
354
+ const addAction = {
355
+ type: "sendCloudEvent",
356
+ options: {
357
+ url: "https://x/cloud-events",
358
+ type: "com.applicaster.collection.create.v1",
359
+ subject: "create_collection",
360
+ },
361
+ };
362
+
363
+ const content = {
364
+ items: [],
365
+ dynamicCollection: {
366
+ operations: ["add"],
367
+ postUrl: "https://x/cloud-events",
368
+ events: {
369
+ add: [addAction],
370
+ },
371
+ },
372
+ } as any;
373
+
374
+ const refetch = jest.fn().mockResolvedValue(undefined);
375
+ const col = new RemoteEditableCollection(content, refetch);
376
+
377
+ await col.add();
378
+
379
+ expect(handleAction).toHaveBeenCalledWith(addAction);
380
+ expect(refetch).toHaveBeenCalledTimes(1);
381
+ });
382
+
383
+ it("forwards stored sheet context into events.add actions", async () => {
384
+ handleAction.mockResolvedValue("success");
385
+
386
+ const addAction = {
387
+ type: "showTextInput",
388
+ options: {
389
+ headerTitle: "Create Playlist",
390
+ buttonLabel: "Create",
391
+ actions: [{ type: "sendCloudEvent" }],
392
+ },
393
+ };
394
+
395
+ const content = {
396
+ items: [],
397
+ context: openingContext,
398
+ dynamicCollection: {
399
+ operations: ["add"],
400
+ postUrl: "https://x/cloud-events",
401
+ events: {
402
+ add: [addAction],
403
+ },
404
+ },
405
+ } as any;
406
+
407
+ const refetch = jest.fn().mockResolvedValue(undefined);
408
+ const col = new RemoteEditableCollection(content, refetch);
409
+
410
+ await col.add();
411
+
412
+ expect(handleAction).toHaveBeenCalledWith(addAction, openingContext);
413
+ });
414
+ });
415
+ });
@@ -1,4 +1,4 @@
1
- import { modalStore } from "../";
1
+ import { modalStore, getMenuItemActions } from "../";
2
2
 
3
3
  describe("Modal State", () => {
4
4
  it("should have initial state", () => {
@@ -48,4 +48,33 @@ describe("Modal State", () => {
48
48
  });
49
49
  });
50
50
  });
51
+
52
+ describe("getMenuItemActions", () => {
53
+ it("returns actions array when actions is provided", () => {
54
+ const actions = [{ type: "action1" }, { type: "action2" }];
55
+ const item = { id: "1", title: "Item", actions };
56
+
57
+ expect(getMenuItemActions(item)).toEqual(actions);
58
+ });
59
+
60
+ it("returns [action] when single action is provided", () => {
61
+ const action = { type: "action1" };
62
+ const item = { id: "1", title: "Item", action };
63
+
64
+ expect(getMenuItemActions(item)).toEqual([action]);
65
+ });
66
+
67
+ it("returns action when action is passed as an array", () => {
68
+ const actions = [{ type: "action1" }];
69
+ const item = { id: "1", title: "Item", action: actions as any };
70
+
71
+ expect(getMenuItemActions(item)).toEqual(actions);
72
+ });
73
+
74
+ it("returns empty array when no action or actions are provided", () => {
75
+ expect(getMenuItemActions({ id: "1", title: "Item" })).toEqual([]);
76
+ expect(getMenuItemActions(null)).toEqual([]);
77
+ expect(getMenuItemActions(undefined)).toEqual([]);
78
+ });
79
+ });
51
80
  });