@applicaster/zapp-react-native-utils 16.0.0-alpha.6861239588 → 16.0.0-alpha.6918106967

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 (136) hide show
  1. package/actionsExecutor/ActionExecutor.ts +40 -2
  2. package/actionsExecutor/ActionExecutorContext.tsx +96 -17
  3. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  4. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  5. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  6. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  7. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  8. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
  9. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  10. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +212 -0
  11. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  12. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  13. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  14. package/actionsExecutor/actions/goBack.ts +60 -0
  15. package/actionsExecutor/actions/goHome.ts +31 -0
  16. package/actionsExecutor/actions/index.ts +10 -0
  17. package/actionsExecutor/actions/navigateToScreen.ts +18 -4
  18. package/actionsExecutor/actions/openBottomSheet.ts +325 -0
  19. package/actionsExecutor/actions/presentScreen.ts +78 -0
  20. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  21. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  22. package/actionsExecutor/actions/showToast.ts +64 -8
  23. package/actionsExecutor/consts.ts +26 -0
  24. package/actionsExecutor/feedDecorator.ts +6 -6
  25. package/actionsExecutor/types.ts +1 -1
  26. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  27. package/appUtils/HooksManager/index.ts +9 -0
  28. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +143 -53
  29. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  30. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +49 -0
  31. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  32. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  33. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  34. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  35. package/appUtils/playerManager/OverlayObserver/utils.ts +54 -20
  36. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  37. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  38. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  39. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  40. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  41. package/appUtils/playerManager/index.ts +1 -1
  42. package/appUtils/playerManager/player.ts +115 -3
  43. package/appUtils/playerManager/playerFactory.ts +1 -1
  44. package/appUtils/playerManager/playerNative.ts +6 -4
  45. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  46. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  47. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  48. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  49. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  50. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  51. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  52. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  53. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  54. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  55. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  56. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  57. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  58. package/arrayUtils/__tests__/sample.test.ts +61 -0
  59. package/arrayUtils/index.ts +136 -20
  60. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  61. package/bottomSheet/index.ts +17 -0
  62. package/cellUtils/index.ts +30 -3
  63. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  64. package/configurationUtils/__tests__/modalBlocksParser.test.ts +260 -0
  65. package/configurationUtils/manifestKeyParser.ts +50 -10
  66. package/configurationUtils/modalBlocksParser.ts +296 -0
  67. package/entryActions/__tests__/buildEntryActions.test.ts +156 -0
  68. package/entryActions/aliasPresentation.ts +124 -0
  69. package/entryActions/buildEntryActions.ts +166 -0
  70. package/entryActions/index.ts +17 -0
  71. package/entryActions/types.ts +39 -0
  72. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  73. package/manifestUtils/_internals/index.js +8 -3
  74. package/manifestUtils/defaultManifestConfigurations/player.js +349 -20
  75. package/manifestUtils/index.js +3 -0
  76. package/manifestUtils/modalBlocks.js +304 -0
  77. package/modalState/ContentViewModel.ts +113 -0
  78. package/modalState/EditableCollection.ts +17 -0
  79. package/modalState/EditableCollectionRegistry.ts +51 -0
  80. package/modalState/ModalOrchestrator.ts +211 -0
  81. package/modalState/RemoteEditableCollection.ts +227 -0
  82. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  83. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  84. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  85. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  86. package/modalState/__tests__/RemoteEditableCollection.test.ts +326 -0
  87. package/modalState/__tests__/index.test.ts +30 -1
  88. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  89. package/modalState/index.ts +35 -83
  90. package/modalState/store.ts +102 -0
  91. package/modalState/types.ts +159 -0
  92. package/modalState/useBottomSheetContent.ts +104 -0
  93. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  94. package/numberUtils/__tests__/isZero.test.ts +27 -0
  95. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  96. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  97. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  98. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  99. package/numberUtils/index.ts +27 -8
  100. package/package.json +2 -2
  101. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  102. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  103. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  104. package/playerUtils/contentDataKeys.ts +134 -0
  105. package/playerUtils/index.ts +37 -16
  106. package/playerUtils/isAudioItem.ts +26 -0
  107. package/playerUtils/resolvePlayerActions.ts +71 -0
  108. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  109. package/reactHooks/actions/index.ts +67 -2
  110. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  111. package/reactHooks/index.ts +2 -0
  112. package/reactHooks/layout/index.ts +1 -1
  113. package/reactHooks/navigation/__tests__/index.test.tsx +48 -0
  114. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  115. package/reactHooks/navigation/index.ts +2 -0
  116. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  117. package/reactHooks/navigation/useRoute.ts +14 -1
  118. package/reactHooks/state/useComponentScreenState.ts +1 -1
  119. package/reactHooks/usePluginConfiguration.ts +4 -1
  120. package/reactHooks/utils/__tests__/index.test.js +22 -2
  121. package/reactHooks/utils/index.ts +11 -1
  122. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  123. package/stringUtils/index.ts +2 -2
  124. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  125. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  126. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  127. package/uiActionsRegistrator/index.ts +25 -203
  128. package/uiActionsRegistrator/registry.ts +335 -0
  129. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  130. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  131. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  132. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  133. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  134. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  135. package/playerUtils/getPlayerActionButtons.ts +0 -17
  136. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -12,7 +12,7 @@ const { log_error, log_debug, log_info } = createLogger({
12
12
  category: "General",
13
13
  });
14
14
 
15
- class ActionExecutor {
15
+ export class ActionExecutor {
16
16
  private registeredActions: Record<string, ActionHandler> = {};
17
17
 
18
18
  // TODO: Remove context
@@ -101,7 +101,8 @@ class ActionExecutor {
101
101
 
102
102
  registerAction = <TOptions = Record<string, any>>(
103
103
  type: string,
104
- handler: ActionHandler<TOptions>
104
+ handler: ActionHandler<TOptions>,
105
+ stateResolver?: (entry?: ZappEntry) => any
105
106
  ) => {
106
107
  if (this.registeredActions[type]) {
107
108
  log_error(`registerAction: action type: ${type} already registered`);
@@ -112,11 +113,48 @@ class ActionExecutor {
112
113
  this.registeredActions[type] = handler;
113
114
  log_debug(`registerAction: action type: ${type} registered`);
114
115
 
116
+ let unregisterState = () => {};
117
+
118
+ if (stateResolver) {
119
+ unregisterState = this.registerActionState(type, stateResolver);
120
+ }
121
+
115
122
  return () => {
116
123
  this.unregisterAction(type);
124
+ unregisterState();
117
125
  log_debug(`registerAction: action type: ${type} un-registered`);
118
126
  };
119
127
  };
128
+
129
+ private actionStates: Record<string, (entry?: ZappEntry) => any> = {};
130
+
131
+ registerActionState = (
132
+ type: string,
133
+ stateResolver: (entry?: ZappEntry) => any
134
+ ) => {
135
+ if (this.actionStates[type]) {
136
+ log_error(
137
+ `registerActionState: action type state: ${type} already registered`
138
+ );
139
+
140
+ return () => {};
141
+ }
142
+
143
+ this.actionStates[type] = stateResolver;
144
+ log_debug(`registerActionState: action type state: ${type} registered`);
145
+
146
+ return () => {
147
+ delete this.actionStates[type];
148
+
149
+ log_debug(
150
+ `registerActionState: action type state: ${type} un-registered`
151
+ );
152
+ };
153
+ };
154
+
155
+ getActionState = (type: string, entry?: ZappEntry) => {
156
+ return this.actionStates[type]?.(entry);
157
+ };
120
158
  }
121
159
 
122
160
  export const actionExecutor = new ActionExecutor();
@@ -9,7 +9,7 @@ import { ActionExecutionContext, ActionHandler } from "./types";
9
9
  import { batchRemoveAllFromNamespaceForStorage } from "../zappFrameworkUtils/localStorageHelper";
10
10
  import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
11
11
 
12
- import { useNavigation, useRivers } from "../reactHooks";
12
+ import { useNavigation, usePresentScreen, useRivers } from "../reactHooks";
13
13
  import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
14
14
  import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
15
15
  import { APP_EVENTS } from "../appUtils/events";
@@ -18,10 +18,15 @@ import { createLogger } from "../logger";
18
18
  import {
19
19
  appRestartAction,
20
20
  confirmDialogAction,
21
+ createGoBackAction,
22
+ createGoHomeAction,
21
23
  createNavigateToScreenAction,
24
+ dismissBottomSheetAction,
22
25
  localStorageRemoveAction,
23
26
  localStorageSetAction,
24
27
  localStorageToggleFlagAction,
28
+ createPresentScreenAction,
29
+ openBottomSheetAction,
25
30
  refreshComponentAction,
26
31
  screenSetVariableAction,
27
32
  screenToggleFlagAction,
@@ -33,6 +38,8 @@ import {
33
38
  showToastAction,
34
39
  } from "./actions";
35
40
 
41
+ import { ACTION_TYPES } from "./consts";
42
+
36
43
  export const { log_error, log_info, log_debug } = createLogger({
37
44
  subsystem: "ActionExecutorContext",
38
45
  category: "General",
@@ -41,9 +48,15 @@ export const { log_error, log_info, log_debug } = createLogger({
41
48
  export type ActionExecutorContextType = {
42
49
  registerAction: <TOptions = Record<string, any>>(
43
50
  type: string,
44
- handler: ActionHandler<TOptions>
51
+ handler: ActionHandler<TOptions>,
52
+ stateResolver?: (entry?: ZappEntry) => any
45
53
  ) => void;
46
54
  unregisterAction: (type: string) => void;
55
+ registerActionState: (
56
+ type: string,
57
+ stateResolver: (entry?: ZappEntry) => any
58
+ ) => () => void;
59
+ getActionState: (type: string, entry?: ZappEntry) => any;
47
60
  handleAction: (
48
61
  action: ActionType,
49
62
  context?: ActionExecutionContext
@@ -63,34 +76,76 @@ type Props = {
63
76
  };
64
77
 
65
78
  const prepareDefaultActions = (actionExecutor) => {
66
- actionExecutor.registerAction("localStorageSet", localStorageSetAction);
67
- actionExecutor.registerAction("sessionStorageSet", sessionStorageSetAction);
68
- actionExecutor.registerAction("localStorageRemove", localStorageRemoveAction);
79
+ actionExecutor.registerAction(
80
+ ACTION_TYPES.LOCAL_STORAGE_SET,
81
+ localStorageSetAction
82
+ );
69
83
 
70
84
  actionExecutor.registerAction(
71
- "sessionStorageRemove",
85
+ ACTION_TYPES.SESSION_STORAGE_SET,
86
+ sessionStorageSetAction
87
+ );
88
+
89
+ actionExecutor.registerAction(
90
+ ACTION_TYPES.LOCAL_STORAGE_REMOVE,
91
+ localStorageRemoveAction
92
+ );
93
+
94
+ actionExecutor.registerAction(
95
+ ACTION_TYPES.SESSION_STORAGE_REMOVE,
72
96
  sessionStorageRemoveAction
73
97
  );
74
98
 
75
- actionExecutor.registerAction("refreshComponent", refreshComponentAction);
76
- actionExecutor.registerAction("switchLayout", switchLayoutAction);
77
- actionExecutor.registerAction("appRestart", appRestartAction);
78
- actionExecutor.registerAction("confirmDialog", confirmDialogAction);
79
- actionExecutor.registerAction("sendCloudEvent", sendCloudEventAction);
99
+ actionExecutor.registerAction(
100
+ ACTION_TYPES.REFRESH_COMPONENT,
101
+ refreshComponentAction
102
+ );
103
+
104
+ actionExecutor.registerAction(ACTION_TYPES.SWITCH_LAYOUT, switchLayoutAction);
105
+
106
+ actionExecutor.registerAction(ACTION_TYPES.APP_RESTART, appRestartAction);
107
+
108
+ actionExecutor.registerAction(
109
+ ACTION_TYPES.CONFIRM_DIALOG,
110
+ confirmDialogAction
111
+ );
80
112
 
81
113
  actionExecutor.registerAction(
82
- "sessionStorageToggleFlag",
114
+ ACTION_TYPES.SEND_CLOUD_EVENT,
115
+ sendCloudEventAction
116
+ );
117
+
118
+ actionExecutor.registerAction(
119
+ ACTION_TYPES.SESSION_STORAGE_TOGGLE_FLAG,
83
120
  sessionStorageToggleFlagAction
84
121
  );
85
122
 
86
123
  actionExecutor.registerAction(
87
- "localStorageToggleFlag",
124
+ ACTION_TYPES.LOCAL_STORAGE_TOGGLE_FLAG,
88
125
  localStorageToggleFlagAction
89
126
  );
90
127
 
91
- actionExecutor.registerAction("screenSetVariable", screenSetVariableAction);
92
- actionExecutor.registerAction("screenToggleFlag", screenToggleFlagAction);
93
- actionExecutor.registerAction("showToast", showToastAction);
128
+ actionExecutor.registerAction(
129
+ ACTION_TYPES.SCREEN_SET_VARIABLE,
130
+ screenSetVariableAction
131
+ );
132
+
133
+ actionExecutor.registerAction(
134
+ ACTION_TYPES.SCREEN_TOGGLE_FLAG,
135
+ screenToggleFlagAction
136
+ );
137
+
138
+ actionExecutor.registerAction(
139
+ ACTION_TYPES.OPEN_BOTTOM_SHEET,
140
+ openBottomSheetAction
141
+ );
142
+
143
+ actionExecutor.registerAction(
144
+ ACTION_TYPES.DISMISS_BOTTOM_SHEET,
145
+ dismissBottomSheetAction
146
+ );
147
+
148
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_TOAST, showToastAction);
94
149
  };
95
150
 
96
151
  export const ActionExecutorContext =
@@ -103,11 +158,14 @@ export function withActionExecutor(Component) {
103
158
  const navigator = useNavigation();
104
159
  const rivers = useRivers();
105
160
  const contentTypes = useContentTypes();
161
+ const presentScreen = usePresentScreen();
106
162
 
107
163
  const handlers = useMemo(() => {
108
164
  return {
109
165
  unregisterAction: _actionExecutor.unregisterAction,
110
166
  registerAction: _actionExecutor.registerAction,
167
+ registerActionState: _actionExecutor.registerActionState,
168
+ getActionState: _actionExecutor.getActionState,
111
169
  handleAction: _actionExecutor.handleAction,
112
170
  handleActions: _actionExecutor.handleActions,
113
171
  handleEntryActions: _actionExecutor.handleEntryActions,
@@ -130,11 +188,32 @@ export function withActionExecutor(Component) {
130
188
 
131
189
  useEffect(() => {
132
190
  return _actionExecutor.registerAction(
133
- "navigateToScreen",
191
+ ACTION_TYPES.NAVIGATE_TO_SCREEN,
134
192
  createNavigateToScreenAction(navigator, rivers, contentTypes)
135
193
  );
136
194
  }, [navigator, rivers, contentTypes]);
137
195
 
196
+ useEffect(() => {
197
+ return _actionExecutor.registerAction(
198
+ ACTION_TYPES.PRESENT_SCREEN,
199
+ createPresentScreenAction(presentScreen)
200
+ );
201
+ }, [presentScreen]);
202
+
203
+ useEffect(() => {
204
+ return _actionExecutor.registerAction(
205
+ ACTION_TYPES.GO_BACK,
206
+ createGoBackAction(navigator)
207
+ );
208
+ }, [navigator]);
209
+
210
+ useEffect(() => {
211
+ return _actionExecutor.registerAction(
212
+ ACTION_TYPES.GO_HOME,
213
+ createGoHomeAction(navigator)
214
+ );
215
+ }, [navigator]);
216
+
138
217
  return (
139
218
  <ActionExecutorContext.Provider value={handlers}>
140
219
  <Component {...props} />
@@ -0,0 +1,61 @@
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
+ });
@@ -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
+ });
@@ -0,0 +1,133 @@
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
+ // Entries cannot go through replace(), which only accepts a river, so the
87
+ // non-push path falls back to replaceTop().
88
+ it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
89
+ const navigator = createNavigator();
90
+ const entry = { id: "entry-id" } as unknown as ZappEntry;
91
+
92
+ const action = {
93
+ type: "navigateToScreen",
94
+ options: {
95
+ typeMapping: "parentLock",
96
+ entry,
97
+ navigationOptions: { pinFlow: "change-pin" },
98
+ },
99
+ };
100
+
101
+ await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
102
+
103
+ expect(navigator.replaceTop).toHaveBeenCalledWith(
104
+ { id: "entry-id", type: { value: "parentLock" } },
105
+ { pinFlow: "change-pin" }
106
+ );
107
+
108
+ expect(navigator.replace).not.toHaveBeenCalled();
109
+ });
110
+
111
+ // Regression guard: the no-options path must keep working unchanged.
112
+ it("still navigates when no navigationOptions are configured", async () => {
113
+ const navigator = createNavigator();
114
+
115
+ const action = {
116
+ type: "navigateToScreen",
117
+ options: {
118
+ typeMapping: "parentLock",
119
+ navigationAction: "push",
120
+ },
121
+ };
122
+
123
+ const result = await createNavigateToScreenAction(
124
+ navigator,
125
+ rivers,
126
+ contentTypes
127
+ )(action);
128
+
129
+ expect(navigator.push).toHaveBeenCalledTimes(1);
130
+ expect((navigator.push as jest.Mock).mock.calls[0][0]).toBe(river);
131
+ expect(result).toBe(ActionResult.Success);
132
+ });
133
+ });