@applicaster/zapp-react-native-utils 16.0.0-alpha.1836140445 → 16.0.0-alpha.1852433906

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 (245) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +57 -12
  3. package/actionsExecutor/ActionExecutorContext.tsx +111 -313
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
  6. package/actionsExecutor/__tests__/feedDecorator.test.ts +91 -0
  7. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  8. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  9. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  10. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  11. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  12. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
  13. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +100 -0
  14. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +372 -0
  15. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  16. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  17. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  18. package/actionsExecutor/actions/appRestart.ts +24 -0
  19. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  20. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  21. package/actionsExecutor/actions/goBack.ts +60 -0
  22. package/actionsExecutor/actions/goHome.ts +31 -0
  23. package/actionsExecutor/actions/index.ts +42 -0
  24. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  25. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  26. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  27. package/actionsExecutor/actions/navigateToScreen.ts +121 -0
  28. package/actionsExecutor/actions/openBottomSheet.ts +340 -0
  29. package/actionsExecutor/actions/presentScreen.ts +78 -0
  30. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  31. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  32. package/actionsExecutor/actions/screenToggleFlag.ts +36 -0
  33. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  34. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  35. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  36. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  37. package/actionsExecutor/actions/showAlert.ts +51 -0
  38. package/actionsExecutor/actions/showToast.ts +87 -0
  39. package/actionsExecutor/actions/switchLayout.ts +40 -0
  40. package/actionsExecutor/consts.ts +27 -0
  41. package/actionsExecutor/feedDecorator.ts +12 -6
  42. package/actionsExecutor/types.ts +59 -0
  43. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  44. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  45. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  51. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  52. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  53. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  54. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  55. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  56. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  57. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  58. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  59. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  60. package/analyticsUtils/analyticsMapper.ts +4 -1
  61. package/appUtils/HooksManager/index.ts +9 -0
  62. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +2 -23
  63. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  64. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  65. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -26
  66. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  67. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  68. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  69. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  70. package/appUtils/contextKeysManager/index.ts +29 -130
  71. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  72. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  73. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  74. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  75. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  76. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  77. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  78. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  79. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  80. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  81. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  82. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  83. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  84. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  85. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  86. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  87. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  88. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  89. package/appUtils/playerManager/index.ts +8 -1
  90. package/appUtils/playerManager/player.ts +140 -3
  91. package/appUtils/playerManager/playerFactory.ts +1 -1
  92. package/appUtils/playerManager/playerNative.ts +28 -4
  93. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  94. package/appUtils/playerManager/playerToolbox.ts +196 -0
  95. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  96. package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
  97. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  98. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  99. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  100. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  101. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  102. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  103. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  104. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  105. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  106. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  107. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  108. package/arrayUtils/__tests__/sample.test.ts +61 -0
  109. package/arrayUtils/index.ts +119 -20
  110. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  111. package/bottomSheet/index.ts +17 -0
  112. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  113. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  114. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  115. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  116. package/cellUtils/index.ts +73 -8
  117. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  118. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  119. package/colorUtils/index.ts +50 -0
  120. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  121. package/configurationUtils/__tests__/modalBlocksParser.test.ts +403 -0
  122. package/configurationUtils/manifestKeyParser.ts +50 -10
  123. package/configurationUtils/modalBlocksParser.ts +252 -0
  124. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  125. package/entryActions/aliasPresentation.ts +108 -0
  126. package/entryActions/buildEntryActions.ts +167 -0
  127. package/entryActions/index.ts +15 -0
  128. package/entryActions/types.ts +39 -0
  129. package/focusManager/FocusManager.ts +26 -0
  130. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  131. package/localizationUtils/index.ts +12 -6
  132. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  133. package/logger/xrayToolbox.ts +67 -0
  134. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  135. package/manifestUtils/_internals/index.js +14 -3
  136. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  137. package/manifestUtils/defaultManifestConfigurations/player.js +357 -20
  138. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  139. package/manifestUtils/fieldUtils/index.js +125 -0
  140. package/manifestUtils/index.js +3 -0
  141. package/manifestUtils/keys.js +16 -2
  142. package/manifestUtils/mobileAction/button/index.js +16 -0
  143. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  144. package/manifestUtils/mobileAction/container/index.js +3 -1
  145. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  146. package/manifestUtils/mobileAction/groups/defaults.js +5 -2
  147. package/manifestUtils/modalBlocks.js +304 -0
  148. package/manifestUtils/platformIsTV.js +1 -0
  149. package/modalState/ContentViewModel.ts +113 -0
  150. package/modalState/EditableCollection.ts +17 -0
  151. package/modalState/EditableCollectionRegistry.ts +51 -0
  152. package/modalState/ModalOrchestrator.ts +213 -0
  153. package/modalState/RemoteEditableCollection.ts +235 -0
  154. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  155. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  156. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  157. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +86 -0
  158. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  159. package/modalState/__tests__/index.test.ts +30 -1
  160. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  161. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  162. package/modalState/index.ts +37 -83
  163. package/modalState/store.ts +109 -0
  164. package/modalState/types.ts +173 -0
  165. package/modalState/useBottomSheetContent.ts +109 -0
  166. package/navigationUtils/xrayNavigator.ts +12 -0
  167. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  168. package/numberUtils/__tests__/isZero.test.ts +27 -0
  169. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  170. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  171. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  172. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  173. package/numberUtils/index.ts +27 -8
  174. package/package.json +3 -2
  175. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  176. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  177. package/pipesUtils/index.ts +1 -0
  178. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  179. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  180. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  181. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  182. package/playerUtils/contentDataKeys.ts +134 -0
  183. package/playerUtils/index.ts +37 -16
  184. package/playerUtils/isAudioItem.ts +26 -0
  185. package/playerUtils/resolvePlayerActions.ts +71 -0
  186. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  187. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  188. package/pluginUtils/index.ts +29 -0
  189. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  190. package/quickBrickAssets/index.ts +35 -0
  191. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  192. package/reactHooks/actions/index.ts +124 -3
  193. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  194. package/reactHooks/cell-click/index.ts +24 -17
  195. package/reactHooks/connection/index.tsx +8 -5
  196. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  197. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  198. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  199. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  200. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  201. package/reactHooks/feed/index.ts +5 -1
  202. package/reactHooks/feed/useBatchLoading.ts +9 -1
  203. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  204. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  205. package/reactHooks/feed/useMarkPipesDataStale.ts +97 -0
  206. package/reactHooks/index.ts +2 -0
  207. package/reactHooks/layout/index.ts +1 -1
  208. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  209. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  210. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  211. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  212. package/reactHooks/navigation/index.ts +3 -1
  213. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  214. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  215. package/reactHooks/navigation/useRoute.ts +22 -5
  216. package/reactHooks/state/useComponentScreenState.ts +1 -1
  217. package/reactHooks/usePluginConfiguration.ts +88 -47
  218. package/reactHooks/utils/__tests__/index.test.js +22 -2
  219. package/reactHooks/utils/index.ts +13 -2
  220. package/riverComponetsMeasurementProvider/index.tsx +15 -9
  221. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  222. package/stringUtils/index.ts +2 -2
  223. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  224. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  225. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  226. package/uiActionsRegistrator/index.ts +25 -0
  227. package/uiActionsRegistrator/registry.ts +351 -0
  228. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  229. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  230. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  231. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  232. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  233. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  234. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  235. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  236. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  237. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  238. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  239. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
  240. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  241. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  242. package/playerUtils/getPlayerActionButtons.ts +0 -17
  243. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  244. package/reactHooks/feed/usePipesCacheReset.ts +0 -44
  245. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,71 @@
1
+ import { ActionExecutor, ActionResult } from "../ActionExecutor";
2
+
3
+ jest.mock("../../logger", () => ({
4
+ createLogger: () => ({
5
+ log_error: jest.fn(),
6
+ log_debug: jest.fn(),
7
+ log_info: jest.fn(),
8
+ }),
9
+ }));
10
+
11
+ describe("handleActions", () => {
12
+ const executor = () => {
13
+ const ran: string[] = [];
14
+ const actionExecutor = new ActionExecutor();
15
+
16
+ const register = (type: string, result?: ActionResult) =>
17
+ actionExecutor.registerAction(type, async () => {
18
+ ran.push(type);
19
+
20
+ return result as ActionResult;
21
+ });
22
+
23
+ register("ok", ActionResult.Success);
24
+ register("fails", ActionResult.Error);
25
+ register("cancels", ActionResult.Cancel);
26
+ register("recover", ActionResult.Success);
27
+ register("after", ActionResult.Success);
28
+
29
+ return { actionExecutor, ran };
30
+ };
31
+
32
+ // Everything after a failed step was written for a state that never
33
+ // happened: the event was refused, the screen never opened.
34
+ it("stops when an action fails", async () => {
35
+ const { actionExecutor, ran } = executor();
36
+
37
+ const result = await actionExecutor.handleActions([
38
+ { type: "fails" },
39
+ { type: "after" },
40
+ ]);
41
+
42
+ expect(ran).toEqual(["fails"]);
43
+ expect(result).toBe(ActionResult.Error);
44
+ });
45
+
46
+ it("still stops on a cancel", async () => {
47
+ const { actionExecutor, ran } = executor();
48
+
49
+ const result = await actionExecutor.handleActions([
50
+ { type: "cancels" },
51
+ { type: "after" },
52
+ ]);
53
+
54
+ expect(ran).toEqual(["cancels"]);
55
+ expect(result).toBe(ActionResult.Cancel);
56
+ });
57
+
58
+ // An action nobody registered never ran, which is a misconfigured feed
59
+ // rather than a step that failed. It is stepped over, as it always was.
60
+ it("steps over an action with no handler", async () => {
61
+ const { actionExecutor, ran } = executor();
62
+
63
+ const result = await actionExecutor.handleActions([
64
+ { type: "nobody-registered-this" },
65
+ { type: "after" },
66
+ ]);
67
+
68
+ expect(ran).toEqual(["after"]);
69
+ expect(result).toBe(ActionResult.Success);
70
+ });
71
+ });
@@ -0,0 +1,98 @@
1
+ import { ActionResult } from "../ActionExecutor";
2
+ import { localStorageToggleFlag } from "../StorageActions";
3
+ import { TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT } from "../consts";
4
+ import { postEvent } from "../../reactHooks/useSubscriberFor";
5
+
6
+ jest.mock("../ActionExecutorContext", () => ({
7
+ log_error: jest.fn(),
8
+ log_info: jest.fn(),
9
+ }));
10
+
11
+ jest.mock("../../reactHooks/useSubscriberFor", () => ({
12
+ postEvent: jest.fn(),
13
+ }));
14
+
15
+ // In-memory stand-in for the storage-backed provider; the limit check under
16
+ // test lives in storageToggleFlag, not in the provider.
17
+ const mockStore: Record<string, string[]> = {};
18
+
19
+ jest.mock("../../storage/StorageMultiSelectProvider", () => ({
20
+ StorageMultiSelectProvider: {
21
+ getProvider: (keyNamespace: string) => ({
22
+ getSelectedAsync: async () => [...(mockStore[keyNamespace] ?? [])],
23
+ addItem: async (item: string) => {
24
+ mockStore[keyNamespace] = [...(mockStore[keyNamespace] ?? []), item];
25
+ },
26
+ removeItem: async (item: string) => {
27
+ mockStore[keyNamespace] = (mockStore[keyNamespace] ?? []).filter(
28
+ (it) => it !== item
29
+ );
30
+ },
31
+ }),
32
+ },
33
+ }));
34
+
35
+ const KEY = "user_preferences.teams";
36
+
37
+ // Entries of a team selection feed with explicit tap actions
38
+ // (localStorageToggleFlag with key and selector).
39
+ const teamEntry = (n: number) =>
40
+ ({
41
+ id: `team-${n}`,
42
+ title: `Team ${n}`,
43
+ type: { value: "action" },
44
+ extensions: { tag: `TEAM ${n}` },
45
+ }) as unknown as ZappEntry;
46
+
47
+ const tapAction = (extraOptions = {}) =>
48
+ ({
49
+ type: "localStorageToggleFlag",
50
+ options: { key: KEY, selector: "extensions.tag", ...extraOptions },
51
+ }) as unknown as ActionType;
52
+
53
+ const tapTeams = async (count: number, extraOptions = {}) => {
54
+ const results: ActionResult[] = [];
55
+
56
+ for (let n = 1; n <= count; n++) {
57
+ results.push(
58
+ await localStorageToggleFlag(
59
+ { entry: teamEntry(n) },
60
+ tapAction(extraOptions)
61
+ )
62
+ );
63
+ }
64
+
65
+ return results;
66
+ };
67
+
68
+ describe("localStorageToggleFlag max_items", () => {
69
+ beforeEach(() => {
70
+ delete mockStore[KEY];
71
+ jest.clearAllMocks();
72
+ });
73
+
74
+ it("blocks the 11th team when max_items is in the action options", async () => {
75
+ const results = await tapTeams(11, { max_items: "10" });
76
+
77
+ expect(results.slice(0, 10)).toEqual(Array(10).fill(ActionResult.Success));
78
+ expect(results[10]).toBe(ActionResult.Cancel);
79
+ expect(mockStore[KEY]).toHaveLength(10);
80
+
81
+ expect(postEvent).toHaveBeenCalledWith(
82
+ TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT,
83
+ [expect.objectContaining({ maxItems: "10", tag: "TEAM 11" })]
84
+ );
85
+ });
86
+
87
+ it("still lets a selected team be removed at the limit", async () => {
88
+ await tapTeams(10, { max_items: "10" });
89
+
90
+ const result = await localStorageToggleFlag(
91
+ { entry: teamEntry(1) },
92
+ tapAction({ max_items: "10" })
93
+ );
94
+
95
+ expect(result).toBe(ActionResult.Success);
96
+ expect(mockStore[KEY]).toHaveLength(9);
97
+ });
98
+ });
@@ -0,0 +1,91 @@
1
+ import { decorateFeed } from "../feedDecorator";
2
+
3
+ const preferenceFeed = (selectMode: string) =>
4
+ ({
5
+ id: "agree-component",
6
+ title: "I agree to the terms",
7
+ type: { value: "feed" },
8
+ extensions: {
9
+ role: "preference_editor",
10
+ preference_editor_options: {
11
+ key: "agree",
12
+ scope: "screen",
13
+ select_mode: selectMode,
14
+ },
15
+ },
16
+ entry: [
17
+ {
18
+ id: "I agree to the terms-agree",
19
+ title: "I agree to the terms",
20
+ type: { value: "action" },
21
+ extensions: { tag: "true" },
22
+ },
23
+ ],
24
+ }) as unknown as ZappFeed;
25
+
26
+ describe("decorateFeed", () => {
27
+ it("decorates single select feeds with screenSetVariable", () => {
28
+ const decorated = decorateFeed(preferenceFeed("single"));
29
+
30
+ expect(decorated.extensions.behavior).toEqual(
31
+ expect.objectContaining({
32
+ select_mode: "single",
33
+ current_selection: "@{screen/agree}",
34
+ })
35
+ );
36
+
37
+ expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
38
+ "screenSetVariable"
39
+ );
40
+ });
41
+
42
+ it("decorates multi select feeds with screenToggleFlag", () => {
43
+ const decorated = decorateFeed(preferenceFeed("multi"));
44
+
45
+ expect(decorated.extensions.behavior.select_mode).toBe("multi");
46
+
47
+ expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
48
+ "screenToggleFlag"
49
+ );
50
+ });
51
+
52
+ it("leaves a feed without the preference_editor role untouched", () => {
53
+ const feed = preferenceFeed("single");
54
+ feed.extensions.role = undefined;
55
+
56
+ const decorated = decorateFeed(feed);
57
+
58
+ expect(decorated.extensions.behavior).toBeUndefined();
59
+ expect(decorated.entry[0].extensions.tap_actions).toBeUndefined();
60
+ });
61
+ });
62
+
63
+ describe("decorateFeed max_items", () => {
64
+ const toggleAction = (feed: ZappFeed) =>
65
+ decorateFeed(feed).entry[0].extensions.tap_actions.actions[0];
66
+
67
+ it.each([
68
+ ["screen", "screenToggleFlag"],
69
+ ["session", "sessionStorageToggleFlag"],
70
+ ["ctx", "localStorageToggleFlag"],
71
+ ])("passes max_items to the %s toggle action", (scope, type) => {
72
+ const feed = preferenceFeed("multi");
73
+ feed.extensions.preference_editor_options.scope = scope;
74
+ feed.extensions.preference_editor_options.max_items = "10";
75
+
76
+ const toggle = toggleAction(feed);
77
+
78
+ expect(toggle.type).toBe(type);
79
+ expect(toggle.options).toEqual({ key: "agree", max_items: 10 });
80
+ });
81
+
82
+ it.each([undefined, 0, "0", "ten", -1])(
83
+ "does not set a limit for max_items %p",
84
+ (maxItems) => {
85
+ const feed = preferenceFeed("multi");
86
+ feed.extensions.preference_editor_options.max_items = maxItems;
87
+
88
+ expect(toggleAction(feed).options).toEqual({ key: "agree" });
89
+ }
90
+ );
91
+ });
@@ -0,0 +1,90 @@
1
+ import { ActionExecutor, ActionResult } from "../ActionExecutor";
2
+ import { screenToggleFlagAction } from "../actions/screenToggleFlag";
3
+
4
+ jest.mock("../ActionExecutorContext", () => ({
5
+ log_error: jest.fn(),
6
+ log_info: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../StorageActions", () => ({
10
+ onMaxTagsReached: jest.fn(),
11
+ }));
12
+
13
+ // not used here, but it pulls the native bridge logger in at import time
14
+ jest.mock("../../storage/ScreenSingleValueProvider", () => ({}));
15
+
16
+ jest.mock("../../logger", () => ({
17
+ createLogger: () => ({
18
+ log_error: jest.fn(),
19
+ log_debug: jest.fn(),
20
+ log_info: jest.fn(),
21
+ }),
22
+ }));
23
+
24
+ const mockSelected: string[] = [];
25
+
26
+ jest.mock("../../storage/ScreenStateMultiSelectProvider", () => ({
27
+ ScreenMultiSelectProvider: {
28
+ getProvider: () => ({
29
+ getSelectedAsync: async () => [...mockSelected],
30
+ addItem: async (item: string) => {
31
+ mockSelected.push(item);
32
+ },
33
+ removeItem: async (item: string) => {
34
+ mockSelected.splice(mockSelected.indexOf(item), 1);
35
+ },
36
+ }),
37
+ },
38
+ }));
39
+
40
+ const toggleAction = (options = {}) =>
41
+ ({
42
+ type: "screenToggleFlag",
43
+ options: { key: "teams", selector: "extensions.tag", ...options },
44
+ }) as unknown as ActionType;
45
+
46
+ const context = (tag: string) => ({
47
+ screenRoute: "/screen",
48
+ screenStateStore: {},
49
+ entry: { id: tag, extensions: { tag } },
50
+ });
51
+
52
+ describe("screenToggleFlagAction at the max_items limit", () => {
53
+ beforeEach(() => {
54
+ mockSelected.length = 0;
55
+ });
56
+
57
+ it("returns the cancellation so the chained actions do not run", async () => {
58
+ const afterToggle = jest.fn();
59
+ const executor = new ActionExecutor();
60
+
61
+ executor.registerAction("screenToggleFlag", screenToggleFlagAction);
62
+
63
+ executor.registerAction("sendCloudEvent", async () => {
64
+ afterToggle();
65
+
66
+ return ActionResult.Success;
67
+ });
68
+
69
+ mockSelected.push("A", "B");
70
+
71
+ const result = await executor.handleActions(
72
+ [toggleAction({ max_items: 2 }), { type: "sendCloudEvent" }],
73
+ context("C") as never
74
+ );
75
+
76
+ expect(result).toBe(ActionResult.Cancel);
77
+ expect(afterToggle).not.toHaveBeenCalled();
78
+ expect(mockSelected).toEqual(["A", "B"]);
79
+ });
80
+
81
+ it("lets the chain continue below the limit", async () => {
82
+ const result = await screenToggleFlagAction(
83
+ toggleAction({ max_items: 2 }),
84
+ context("A") as never
85
+ );
86
+
87
+ expect(result).toBe(ActionResult.Success);
88
+ expect(mockSelected).toEqual(["A"]);
89
+ });
90
+ });
@@ -0,0 +1,25 @@
1
+ import { dismissBottomSheetAction } from "../dismissBottomSheet";
2
+ import { dismissModal } from "../../../modalState";
3
+ import { ActionResult } from "../../ActionExecutor";
4
+
5
+ jest.mock("../../../modalState", () => ({
6
+ dismissModal: jest.fn(),
7
+ modalOrchestrator: {
8
+ close: jest.fn(),
9
+ },
10
+ }));
11
+
12
+ describe("dismissBottomSheetAction", () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it("calls dismissModal and returns ActionResult.Success", async () => {
18
+ const result = await dismissBottomSheetAction({
19
+ type: "dismissBottomSheet",
20
+ });
21
+
22
+ expect(dismissModal).toHaveBeenCalledTimes(1);
23
+ expect(result).toBe(ActionResult.Success);
24
+ });
25
+ });
@@ -0,0 +1,62 @@
1
+ import { createGoBackAction } from "../goBack";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+
4
+ function createNavigator(canGoBack = true) {
5
+ return {
6
+ goBack: jest.fn(),
7
+ canGoBack: jest.fn().mockReturnValue(canGoBack),
8
+ } as unknown as QuickBrickAppNavigator;
9
+ }
10
+
11
+ describe("goBack", () => {
12
+ it("goes back with fallbackToHome enabled when no options are provided", async () => {
13
+ const navigator = createNavigator();
14
+
15
+ const result = await createGoBackAction(navigator)({ type: "goBack" });
16
+
17
+ expect(navigator.goBack).toHaveBeenCalledWith(true, false, false);
18
+ expect(result).toBe(ActionResult.Success);
19
+ });
20
+
21
+ it("forwards backToTop", async () => {
22
+ const navigator = createNavigator();
23
+
24
+ const action = {
25
+ type: "goBack",
26
+ options: { backToTop: true },
27
+ };
28
+
29
+ const result = await createGoBackAction(navigator)(action);
30
+
31
+ expect(navigator.goBack).toHaveBeenCalledWith(true, false, true);
32
+ expect(result).toBe(ActionResult.Success);
33
+ });
34
+
35
+ it("goes back without falling back to home when the stack allows it", async () => {
36
+ const navigator = createNavigator(true);
37
+
38
+ const action = {
39
+ type: "goBack",
40
+ options: { fallbackToHome: false },
41
+ };
42
+
43
+ const result = await createGoBackAction(navigator)(action);
44
+
45
+ expect(navigator.goBack).toHaveBeenCalledWith(false, false, false);
46
+ expect(result).toBe(ActionResult.Success);
47
+ });
48
+
49
+ it("does nothing when there is nothing to go back to and fallbackToHome is off", async () => {
50
+ const navigator = createNavigator(false);
51
+
52
+ const action = {
53
+ type: "goBack",
54
+ options: { fallbackToHome: false },
55
+ };
56
+
57
+ const result = await createGoBackAction(navigator)(action);
58
+
59
+ expect(navigator.goBack).not.toHaveBeenCalled();
60
+ expect(result).toBe(ActionResult.Error);
61
+ });
62
+ });
@@ -0,0 +1,19 @@
1
+ import { createGoHomeAction } from "../goHome";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+
4
+ function createNavigator() {
5
+ return {
6
+ goHome: jest.fn(),
7
+ } as unknown as QuickBrickAppNavigator;
8
+ }
9
+
10
+ describe("goHome", () => {
11
+ it("navigates to the home screen", async () => {
12
+ const navigator = createNavigator();
13
+
14
+ const result = await createGoHomeAction(navigator)({ type: "goHome" });
15
+
16
+ expect(navigator.goHome).toHaveBeenCalledWith();
17
+ expect(result).toBe(ActionResult.Success);
18
+ });
19
+ });
@@ -0,0 +1,88 @@
1
+ import { loadItemsFromSource } from "../openBottomSheet";
2
+
3
+ const mockCall = jest.fn();
4
+
5
+ jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
6
+ RequestBuilder: jest.fn().mockImplementation(() => ({
7
+ setEntryContext: jest.fn().mockReturnThis(),
8
+ setScreenContext: jest.fn().mockReturnThis(),
9
+ setUrl: jest.fn().mockReturnThis(),
10
+ buildAxiosRequest: jest.fn().mockResolvedValue(undefined),
11
+ call: (...args: any[]) => mockCall(...args),
12
+ })),
13
+ PipesClientResponseHelper: jest.fn().mockImplementation(() => ({
14
+ error: null,
15
+ })),
16
+ }));
17
+
18
+ describe("loadItemsFromSource", () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ it("returns items plus role and behavior from feed.extensions", async () => {
24
+ mockCall.mockResolvedValueOnce({
25
+ response: {
26
+ entry: [{ id: "a", title: "Playlist A", media_group: [] }],
27
+ extensions: {
28
+ role: "collection_selector",
29
+ behavior: {
30
+ select_mode: "multi",
31
+ current_selection: ["a"],
32
+ selector: "extensions.tag",
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ const result = await loadItemsFromSource("https://example.com/feed");
39
+
40
+ expect(result.role).toBe("collection_selector");
41
+
42
+ expect(result.behavior).toEqual({
43
+ selectMode: "multi",
44
+ currentSelection: ["a"],
45
+ selector: "extensions.tag",
46
+ });
47
+
48
+ expect(result.items).toHaveLength(1);
49
+ expect(result.items[0].isSelected).toBe(true);
50
+ expect(result.items[0].title).toBe("Playlist A");
51
+ });
52
+
53
+ it("returns undefined role/behavior when feed has no extensions", async () => {
54
+ mockCall.mockResolvedValueOnce({
55
+ response: {
56
+ entry: [{ id: "1", title: "Plain", media_group: [] }],
57
+ },
58
+ });
59
+
60
+ const result = await loadItemsFromSource("https://example.com/plain");
61
+
62
+ expect(result.role).toBeUndefined();
63
+ expect(result.behavior).toBeUndefined();
64
+ expect(result.items).toHaveLength(1);
65
+ });
66
+
67
+ it("preserves events in dynamic_collection_options", async () => {
68
+ mockCall.mockResolvedValueOnce({
69
+ response: {
70
+ entry: [],
71
+ extensions: {
72
+ dynamic_collection_options: {
73
+ operations: "add,remove",
74
+ events: {
75
+ add: [{ type: "showTextInput", options: {} }],
76
+ },
77
+ },
78
+ },
79
+ },
80
+ });
81
+
82
+ const result = await loadItemsFromSource("https://example.com/feed");
83
+
84
+ expect(result.dynamicCollection?.events).toEqual({
85
+ add: [{ type: "showTextInput", options: {} }],
86
+ });
87
+ });
88
+ });