@applicaster/zapp-react-native-utils 16.0.0-alpha.8716141508 → 16.0.0-alpha.9024462102

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 (122) hide show
  1. package/actionsExecutor/ActionExecutor.ts +50 -3
  2. package/actionsExecutor/ActionExecutorContext.tsx +99 -17
  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 +300 -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/dismissBottomSheet.ts +23 -0
  16. package/actionsExecutor/actions/goBack.ts +60 -0
  17. package/actionsExecutor/actions/goHome.ts +31 -0
  18. package/actionsExecutor/actions/index.ts +12 -0
  19. package/actionsExecutor/actions/navigateToScreen.ts +18 -4
  20. package/actionsExecutor/actions/openBottomSheet.ts +338 -0
  21. package/actionsExecutor/actions/presentScreen.ts +78 -0
  22. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  23. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  24. package/actionsExecutor/actions/showAlert.ts +51 -0
  25. package/actionsExecutor/actions/showToast.ts +64 -8
  26. package/actionsExecutor/consts.ts +27 -0
  27. package/actionsExecutor/feedDecorator.ts +6 -6
  28. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  29. package/appUtils/HooksManager/index.ts +9 -0
  30. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +131 -54
  31. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  32. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  33. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  34. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  35. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  36. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  37. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  38. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  39. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  40. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  41. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  42. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  43. package/appUtils/playerManager/{conts.ts → const.ts} +14 -0
  44. package/appUtils/playerManager/index.ts +1 -1
  45. package/appUtils/playerManager/player.ts +115 -3
  46. package/appUtils/playerManager/playerFactory.ts +1 -1
  47. package/appUtils/playerManager/playerNative.ts +6 -4
  48. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  49. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  50. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  51. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  52. package/arrayUtils/index.ts +62 -0
  53. package/cellUtils/index.ts +30 -3
  54. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  55. package/configurationUtils/__tests__/modalBlocksParser.test.ts +269 -0
  56. package/configurationUtils/manifestKeyParser.ts +50 -10
  57. package/configurationUtils/modalBlocksParser.ts +214 -0
  58. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  59. package/entryActions/aliasPresentation.ts +108 -0
  60. package/entryActions/buildEntryActions.ts +167 -0
  61. package/entryActions/index.ts +15 -0
  62. package/entryActions/types.ts +39 -0
  63. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  64. package/manifestUtils/_internals/index.js +8 -3
  65. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  66. package/manifestUtils/defaultManifestConfigurations/player.js +330 -1
  67. package/manifestUtils/index.js +3 -0
  68. package/manifestUtils/modalBlocks.js +304 -0
  69. package/modalState/ContentViewModel.ts +113 -0
  70. package/modalState/EditableCollection.ts +17 -0
  71. package/modalState/EditableCollectionRegistry.ts +51 -0
  72. package/modalState/ModalOrchestrator.ts +212 -0
  73. package/modalState/RemoteEditableCollection.ts +235 -0
  74. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  75. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  76. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  77. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +63 -0
  78. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  79. package/modalState/__tests__/index.test.ts +30 -1
  80. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  81. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  82. package/modalState/index.ts +35 -83
  83. package/modalState/store.ts +109 -0
  84. package/modalState/types.ts +162 -0
  85. package/modalState/useBottomSheetContent.ts +109 -0
  86. package/package.json +2 -2
  87. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  88. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  89. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  90. package/playerUtils/contentDataKeys.ts +134 -0
  91. package/playerUtils/index.ts +37 -16
  92. package/playerUtils/isAudioItem.ts +26 -0
  93. package/playerUtils/resolvePlayerActions.ts +71 -0
  94. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  95. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  96. package/pluginUtils/index.ts +29 -0
  97. package/reactHooks/actions/index.ts +67 -2
  98. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  99. package/reactHooks/cell-click/index.ts +21 -14
  100. package/reactHooks/connection/__tests__/index.test.js +154 -2
  101. package/reactHooks/connection/index.tsx +71 -7
  102. package/reactHooks/index.ts +2 -0
  103. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  104. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  105. package/reactHooks/navigation/index.ts +2 -0
  106. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  107. package/reactHooks/navigation/useRoute.ts +22 -5
  108. package/reactHooks/usePluginConfiguration.ts +4 -1
  109. package/reactHooks/utils/__tests__/index.test.js +22 -2
  110. package/reactHooks/utils/index.ts +11 -1
  111. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  112. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  113. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  114. package/uiActionsRegistrator/index.ts +25 -203
  115. package/uiActionsRegistrator/registry.ts +335 -0
  116. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  117. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  118. package/zappFrameworkUtils/localStorageHelper.ts +10 -2
  119. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  120. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  121. package/playerUtils/getPlayerActionButtons.ts +0 -17
  122. /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
@@ -56,7 +56,6 @@ class ActionExecutor {
56
56
  context?: ActionExecutionContext
57
57
  ): Promise<ActionResult> => {
58
58
  for (const action of actionTypes) {
59
- // TODO: Handle action result error
60
59
  const result = await this.handleAction(action, context);
61
60
 
62
61
  if (result === ActionResult.Cancel) {
@@ -64,6 +63,16 @@ class ActionExecutor {
64
63
 
65
64
  return result;
66
65
  }
66
+
67
+ if (result === ActionResult.Error) {
68
+ // Everything after a failed step was written for a state that never
69
+ // happened — the event was refused, the screen never opened — so
70
+ // running it is how a chain ends up telling the user their mail was
71
+ // sent when it was not.
72
+ log_error(`handleActions: action: ${action.type} failed`, { action });
73
+
74
+ return result;
75
+ }
67
76
  }
68
77
 
69
78
  return ActionResult.Success;
@@ -101,7 +110,8 @@ class ActionExecutor {
101
110
 
102
111
  registerAction = <TOptions = Record<string, any>>(
103
112
  type: string,
104
- handler: ActionHandler<TOptions>
113
+ handler: ActionHandler<TOptions>,
114
+ stateResolver?: (entry?: ZappEntry) => any
105
115
  ) => {
106
116
  if (this.registeredActions[type]) {
107
117
  log_error(`registerAction: action type: ${type} already registered`);
@@ -112,11 +122,48 @@ class ActionExecutor {
112
122
  this.registeredActions[type] = handler;
113
123
  log_debug(`registerAction: action type: ${type} registered`);
114
124
 
125
+ let unregisterState = () => {};
126
+
127
+ if (stateResolver) {
128
+ unregisterState = this.registerActionState(type, stateResolver);
129
+ }
130
+
115
131
  return () => {
116
132
  this.unregisterAction(type);
133
+ unregisterState();
117
134
  log_debug(`registerAction: action type: ${type} un-registered`);
118
135
  };
119
136
  };
137
+
138
+ private actionStates: Record<string, (entry?: ZappEntry) => any> = {};
139
+
140
+ registerActionState = (
141
+ type: string,
142
+ stateResolver: (entry?: ZappEntry) => any
143
+ ) => {
144
+ if (this.actionStates[type]) {
145
+ log_error(
146
+ `registerActionState: action type state: ${type} already registered`
147
+ );
148
+
149
+ return () => {};
150
+ }
151
+
152
+ this.actionStates[type] = stateResolver;
153
+ log_debug(`registerActionState: action type state: ${type} registered`);
154
+
155
+ return () => {
156
+ delete this.actionStates[type];
157
+
158
+ log_debug(
159
+ `registerActionState: action type state: ${type} un-registered`
160
+ );
161
+ };
162
+ };
163
+
164
+ getActionState = (type: string, entry?: ZappEntry) => {
165
+ return this.actionStates[type]?.(entry);
166
+ };
120
167
  }
121
168
 
122
169
  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,16 @@ import { createLogger } from "../logger";
18
18
  import {
19
19
  appRestartAction,
20
20
  confirmDialogAction,
21
+ showAlertAction,
22
+ createGoBackAction,
23
+ createGoHomeAction,
21
24
  createNavigateToScreenAction,
25
+ dismissBottomSheetAction,
22
26
  localStorageRemoveAction,
23
27
  localStorageSetAction,
24
28
  localStorageToggleFlagAction,
29
+ createPresentScreenAction,
30
+ openBottomSheetAction,
25
31
  refreshComponentAction,
26
32
  screenSetVariableAction,
27
33
  screenToggleFlagAction,
@@ -33,6 +39,8 @@ import {
33
39
  showToastAction,
34
40
  } from "./actions";
35
41
 
42
+ import { ACTION_TYPES } from "./consts";
43
+
36
44
  export const { log_error, log_info, log_debug } = createLogger({
37
45
  subsystem: "ActionExecutorContext",
38
46
  category: "General",
@@ -41,9 +49,15 @@ export const { log_error, log_info, log_debug } = createLogger({
41
49
  export type ActionExecutorContextType = {
42
50
  registerAction: <TOptions = Record<string, any>>(
43
51
  type: string,
44
- handler: ActionHandler<TOptions>
52
+ handler: ActionHandler<TOptions>,
53
+ stateResolver?: (entry?: ZappEntry) => any
45
54
  ) => void;
46
55
  unregisterAction: (type: string) => void;
56
+ registerActionState: (
57
+ type: string,
58
+ stateResolver: (entry?: ZappEntry) => any
59
+ ) => () => void;
60
+ getActionState: (type: string, entry?: ZappEntry) => any;
47
61
  handleAction: (
48
62
  action: ActionType,
49
63
  context?: ActionExecutionContext
@@ -63,34 +77,78 @@ type Props = {
63
77
  };
64
78
 
65
79
  const prepareDefaultActions = (actionExecutor) => {
66
- actionExecutor.registerAction("localStorageSet", localStorageSetAction);
67
- actionExecutor.registerAction("sessionStorageSet", sessionStorageSetAction);
68
- actionExecutor.registerAction("localStorageRemove", localStorageRemoveAction);
80
+ actionExecutor.registerAction(
81
+ ACTION_TYPES.LOCAL_STORAGE_SET,
82
+ localStorageSetAction
83
+ );
84
+
85
+ actionExecutor.registerAction(
86
+ ACTION_TYPES.SESSION_STORAGE_SET,
87
+ sessionStorageSetAction
88
+ );
89
+
90
+ actionExecutor.registerAction(
91
+ ACTION_TYPES.LOCAL_STORAGE_REMOVE,
92
+ localStorageRemoveAction
93
+ );
69
94
 
70
95
  actionExecutor.registerAction(
71
- "sessionStorageRemove",
96
+ ACTION_TYPES.SESSION_STORAGE_REMOVE,
72
97
  sessionStorageRemoveAction
73
98
  );
74
99
 
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);
100
+ actionExecutor.registerAction(
101
+ ACTION_TYPES.REFRESH_COMPONENT,
102
+ refreshComponentAction
103
+ );
104
+
105
+ actionExecutor.registerAction(ACTION_TYPES.SWITCH_LAYOUT, switchLayoutAction);
106
+
107
+ actionExecutor.registerAction(ACTION_TYPES.APP_RESTART, appRestartAction);
108
+
109
+ actionExecutor.registerAction(
110
+ ACTION_TYPES.CONFIRM_DIALOG,
111
+ confirmDialogAction
112
+ );
113
+
114
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
80
115
 
81
116
  actionExecutor.registerAction(
82
- "sessionStorageToggleFlag",
117
+ ACTION_TYPES.SEND_CLOUD_EVENT,
118
+ sendCloudEventAction
119
+ );
120
+
121
+ actionExecutor.registerAction(
122
+ ACTION_TYPES.SESSION_STORAGE_TOGGLE_FLAG,
83
123
  sessionStorageToggleFlagAction
84
124
  );
85
125
 
86
126
  actionExecutor.registerAction(
87
- "localStorageToggleFlag",
127
+ ACTION_TYPES.LOCAL_STORAGE_TOGGLE_FLAG,
88
128
  localStorageToggleFlagAction
89
129
  );
90
130
 
91
- actionExecutor.registerAction("screenSetVariable", screenSetVariableAction);
92
- actionExecutor.registerAction("screenToggleFlag", screenToggleFlagAction);
93
- actionExecutor.registerAction("showToast", showToastAction);
131
+ actionExecutor.registerAction(
132
+ ACTION_TYPES.SCREEN_SET_VARIABLE,
133
+ screenSetVariableAction
134
+ );
135
+
136
+ actionExecutor.registerAction(
137
+ ACTION_TYPES.SCREEN_TOGGLE_FLAG,
138
+ screenToggleFlagAction
139
+ );
140
+
141
+ actionExecutor.registerAction(
142
+ ACTION_TYPES.OPEN_BOTTOM_SHEET,
143
+ openBottomSheetAction
144
+ );
145
+
146
+ actionExecutor.registerAction(
147
+ ACTION_TYPES.DISMISS_BOTTOM_SHEET,
148
+ dismissBottomSheetAction
149
+ );
150
+
151
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_TOAST, showToastAction);
94
152
  };
95
153
 
96
154
  export const ActionExecutorContext =
@@ -103,11 +161,14 @@ export function withActionExecutor(Component) {
103
161
  const navigator = useNavigation();
104
162
  const rivers = useRivers();
105
163
  const contentTypes = useContentTypes();
164
+ const presentScreen = usePresentScreen();
106
165
 
107
166
  const handlers = useMemo(() => {
108
167
  return {
109
168
  unregisterAction: _actionExecutor.unregisterAction,
110
169
  registerAction: _actionExecutor.registerAction,
170
+ registerActionState: _actionExecutor.registerActionState,
171
+ getActionState: _actionExecutor.getActionState,
111
172
  handleAction: _actionExecutor.handleAction,
112
173
  handleActions: _actionExecutor.handleActions,
113
174
  handleEntryActions: _actionExecutor.handleEntryActions,
@@ -130,11 +191,32 @@ export function withActionExecutor(Component) {
130
191
 
131
192
  useEffect(() => {
132
193
  return _actionExecutor.registerAction(
133
- "navigateToScreen",
194
+ ACTION_TYPES.NAVIGATE_TO_SCREEN,
134
195
  createNavigateToScreenAction(navigator, rivers, contentTypes)
135
196
  );
136
197
  }, [navigator, rivers, contentTypes]);
137
198
 
199
+ useEffect(() => {
200
+ return _actionExecutor.registerAction(
201
+ ACTION_TYPES.PRESENT_SCREEN,
202
+ createPresentScreenAction(presentScreen)
203
+ );
204
+ }, [presentScreen]);
205
+
206
+ useEffect(() => {
207
+ return _actionExecutor.registerAction(
208
+ ACTION_TYPES.GO_BACK,
209
+ createGoBackAction(navigator)
210
+ );
211
+ }, [navigator]);
212
+
213
+ useEffect(() => {
214
+ return _actionExecutor.registerAction(
215
+ ACTION_TYPES.GO_HOME,
216
+ createGoHomeAction(navigator)
217
+ );
218
+ }, [navigator]);
219
+
138
220
  return (
139
221
  <ActionExecutorContext.Provider value={handlers}>
140
222
  <Component {...props} />
@@ -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,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
+ });