@applicaster/zapp-react-native-utils 16.0.0-alpha.3795714551 → 16.0.0-alpha.4004661200

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 (102) hide show
  1. package/actionsExecutor/ActionExecutor.ts +40 -2
  2. package/actionsExecutor/ActionExecutorContext.tsx +86 -16
  3. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  4. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +22 -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 +100 -0
  11. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  12. package/actionsExecutor/actions/dismissBottomSheet.ts +22 -0
  13. package/actionsExecutor/actions/goBack.ts +60 -0
  14. package/actionsExecutor/actions/goHome.ts +31 -0
  15. package/actionsExecutor/actions/index.ts +8 -0
  16. package/actionsExecutor/actions/navigateToScreen.ts +18 -4
  17. package/actionsExecutor/actions/openBottomSheet.ts +320 -0
  18. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  19. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  20. package/actionsExecutor/actions/showToast.ts +64 -8
  21. package/actionsExecutor/consts.ts +25 -0
  22. package/actionsExecutor/feedDecorator.ts +6 -6
  23. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  24. package/appUtils/HooksManager/index.ts +9 -0
  25. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +143 -53
  26. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  27. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +49 -0
  28. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  29. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  30. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  31. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  32. package/appUtils/playerManager/OverlayObserver/utils.ts +54 -20
  33. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  34. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  35. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  36. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  37. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  38. package/appUtils/playerManager/index.ts +1 -1
  39. package/appUtils/playerManager/player.ts +115 -3
  40. package/appUtils/playerManager/playerFactory.ts +1 -1
  41. package/appUtils/playerManager/playerNative.ts +6 -4
  42. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  43. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  44. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  45. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  46. package/arrayUtils/index.ts +79 -0
  47. package/cellUtils/index.ts +30 -3
  48. package/configurationUtils/__tests__/modalBlocksParser.test.ts +107 -0
  49. package/configurationUtils/manifestKeyParser.ts +38 -9
  50. package/configurationUtils/modalBlocksParser.ts +178 -0
  51. package/entryActions/__tests__/buildEntryActions.test.ts +156 -0
  52. package/entryActions/aliasPresentation.ts +124 -0
  53. package/entryActions/buildEntryActions.ts +166 -0
  54. package/entryActions/index.ts +17 -0
  55. package/entryActions/types.ts +39 -0
  56. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  57. package/manifestUtils/_internals/index.js +8 -3
  58. package/manifestUtils/defaultManifestConfigurations/player.js +330 -1
  59. package/manifestUtils/index.js +3 -0
  60. package/manifestUtils/modalBlocks.js +304 -0
  61. package/modalState/ContentViewModel.ts +113 -0
  62. package/modalState/EditableCollection.ts +17 -0
  63. package/modalState/EditableCollectionRegistry.ts +51 -0
  64. package/modalState/ModalOrchestrator.ts +199 -0
  65. package/modalState/RemoteEditableCollection.ts +227 -0
  66. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  67. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  68. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  69. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  70. package/modalState/__tests__/RemoteEditableCollection.test.ts +326 -0
  71. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  72. package/modalState/index.ts +30 -83
  73. package/modalState/store.ts +102 -0
  74. package/modalState/types.ts +133 -0
  75. package/modalState/useBottomSheetContent.ts +102 -0
  76. package/package.json +2 -2
  77. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  78. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  79. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  80. package/playerUtils/contentDataKeys.ts +134 -0
  81. package/playerUtils/index.ts +37 -16
  82. package/playerUtils/isAudioItem.ts +26 -0
  83. package/playerUtils/resolvePlayerActions.ts +71 -0
  84. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  85. package/reactHooks/actions/index.ts +67 -2
  86. package/reactHooks/index.ts +2 -0
  87. package/reactHooks/layout/index.ts +1 -1
  88. package/reactHooks/usePluginConfiguration.ts +4 -1
  89. package/reactHooks/utils/__tests__/index.test.js +22 -2
  90. package/reactHooks/utils/index.ts +11 -1
  91. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  92. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  93. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  94. package/uiActionsRegistrator/index.ts +25 -203
  95. package/uiActionsRegistrator/registry.ts +335 -0
  96. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  97. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  98. package/zappFrameworkUtils/localStorageHelper.ts +10 -2
  99. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  100. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  101. package/playerUtils/getPlayerActionButtons.ts +0 -17
  102. /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();
@@ -18,10 +18,14 @@ 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
+ openBottomSheetAction,
25
29
  refreshComponentAction,
26
30
  screenSetVariableAction,
27
31
  screenToggleFlagAction,
@@ -33,6 +37,8 @@ import {
33
37
  showToastAction,
34
38
  } from "./actions";
35
39
 
40
+ import { ACTION_TYPES } from "./consts";
41
+
36
42
  export const { log_error, log_info, log_debug } = createLogger({
37
43
  subsystem: "ActionExecutorContext",
38
44
  category: "General",
@@ -41,9 +47,15 @@ export const { log_error, log_info, log_debug } = createLogger({
41
47
  export type ActionExecutorContextType = {
42
48
  registerAction: <TOptions = Record<string, any>>(
43
49
  type: string,
44
- handler: ActionHandler<TOptions>
50
+ handler: ActionHandler<TOptions>,
51
+ stateResolver?: (entry?: ZappEntry) => any
45
52
  ) => void;
46
53
  unregisterAction: (type: string) => void;
54
+ registerActionState: (
55
+ type: string,
56
+ stateResolver: (entry?: ZappEntry) => any
57
+ ) => () => void;
58
+ getActionState: (type: string, entry?: ZappEntry) => any;
47
59
  handleAction: (
48
60
  action: ActionType,
49
61
  context?: ActionExecutionContext
@@ -63,34 +75,76 @@ type Props = {
63
75
  };
64
76
 
65
77
  const prepareDefaultActions = (actionExecutor) => {
66
- actionExecutor.registerAction("localStorageSet", localStorageSetAction);
67
- actionExecutor.registerAction("sessionStorageSet", sessionStorageSetAction);
68
- actionExecutor.registerAction("localStorageRemove", localStorageRemoveAction);
78
+ actionExecutor.registerAction(
79
+ ACTION_TYPES.LOCAL_STORAGE_SET,
80
+ localStorageSetAction
81
+ );
82
+
83
+ actionExecutor.registerAction(
84
+ ACTION_TYPES.SESSION_STORAGE_SET,
85
+ sessionStorageSetAction
86
+ );
87
+
88
+ actionExecutor.registerAction(
89
+ ACTION_TYPES.LOCAL_STORAGE_REMOVE,
90
+ localStorageRemoveAction
91
+ );
69
92
 
70
93
  actionExecutor.registerAction(
71
- "sessionStorageRemove",
94
+ ACTION_TYPES.SESSION_STORAGE_REMOVE,
72
95
  sessionStorageRemoveAction
73
96
  );
74
97
 
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);
98
+ actionExecutor.registerAction(
99
+ ACTION_TYPES.REFRESH_COMPONENT,
100
+ refreshComponentAction
101
+ );
102
+
103
+ actionExecutor.registerAction(ACTION_TYPES.SWITCH_LAYOUT, switchLayoutAction);
104
+
105
+ actionExecutor.registerAction(ACTION_TYPES.APP_RESTART, appRestartAction);
106
+
107
+ actionExecutor.registerAction(
108
+ ACTION_TYPES.CONFIRM_DIALOG,
109
+ confirmDialogAction
110
+ );
111
+
112
+ actionExecutor.registerAction(
113
+ ACTION_TYPES.SEND_CLOUD_EVENT,
114
+ sendCloudEventAction
115
+ );
80
116
 
81
117
  actionExecutor.registerAction(
82
- "sessionStorageToggleFlag",
118
+ ACTION_TYPES.SESSION_STORAGE_TOGGLE_FLAG,
83
119
  sessionStorageToggleFlagAction
84
120
  );
85
121
 
86
122
  actionExecutor.registerAction(
87
- "localStorageToggleFlag",
123
+ ACTION_TYPES.LOCAL_STORAGE_TOGGLE_FLAG,
88
124
  localStorageToggleFlagAction
89
125
  );
90
126
 
91
- actionExecutor.registerAction("screenSetVariable", screenSetVariableAction);
92
- actionExecutor.registerAction("screenToggleFlag", screenToggleFlagAction);
93
- actionExecutor.registerAction("showToast", showToastAction);
127
+ actionExecutor.registerAction(
128
+ ACTION_TYPES.SCREEN_SET_VARIABLE,
129
+ screenSetVariableAction
130
+ );
131
+
132
+ actionExecutor.registerAction(
133
+ ACTION_TYPES.SCREEN_TOGGLE_FLAG,
134
+ screenToggleFlagAction
135
+ );
136
+
137
+ actionExecutor.registerAction(
138
+ ACTION_TYPES.OPEN_BOTTOM_SHEET,
139
+ openBottomSheetAction
140
+ );
141
+
142
+ actionExecutor.registerAction(
143
+ ACTION_TYPES.DISMISS_BOTTOM_SHEET,
144
+ dismissBottomSheetAction
145
+ );
146
+
147
+ actionExecutor.registerAction(ACTION_TYPES.SHOW_TOAST, showToastAction);
94
148
  };
95
149
 
96
150
  export const ActionExecutorContext =
@@ -108,6 +162,8 @@ export function withActionExecutor(Component) {
108
162
  return {
109
163
  unregisterAction: _actionExecutor.unregisterAction,
110
164
  registerAction: _actionExecutor.registerAction,
165
+ registerActionState: _actionExecutor.registerActionState,
166
+ getActionState: _actionExecutor.getActionState,
111
167
  handleAction: _actionExecutor.handleAction,
112
168
  handleActions: _actionExecutor.handleActions,
113
169
  handleEntryActions: _actionExecutor.handleEntryActions,
@@ -130,11 +186,25 @@ export function withActionExecutor(Component) {
130
186
 
131
187
  useEffect(() => {
132
188
  return _actionExecutor.registerAction(
133
- "navigateToScreen",
189
+ ACTION_TYPES.NAVIGATE_TO_SCREEN,
134
190
  createNavigateToScreenAction(navigator, rivers, contentTypes)
135
191
  );
136
192
  }, [navigator, rivers, contentTypes]);
137
193
 
194
+ useEffect(() => {
195
+ return _actionExecutor.registerAction(
196
+ ACTION_TYPES.GO_BACK,
197
+ createGoBackAction(navigator)
198
+ );
199
+ }, [navigator]);
200
+
201
+ useEffect(() => {
202
+ return _actionExecutor.registerAction(
203
+ ACTION_TYPES.GO_HOME,
204
+ createGoHomeAction(navigator)
205
+ );
206
+ }, [navigator]);
207
+
138
208
  return (
139
209
  <ActionExecutorContext.Provider value={handlers}>
140
210
  <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,22 @@
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
+ }));
8
+
9
+ describe("dismissBottomSheetAction", () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it("calls dismissModal and returns ActionResult.Success", async () => {
15
+ const result = await dismissBottomSheetAction({
16
+ type: "dismissBottomSheet",
17
+ });
18
+
19
+ expect(dismissModal).toHaveBeenCalledTimes(1);
20
+ expect(result).toBe(ActionResult.Success);
21
+ });
22
+ });
@@ -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
+ });
@@ -0,0 +1,76 @@
1
+ import { openBottomSheetAction } from "../openBottomSheet";
2
+ import { ActionResult } from "../../ActionExecutor";
3
+ import { openBottomSheetModal } from "../../../modalState/store";
4
+ import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
5
+
6
+ jest.mock("../../../modalState/store", () => ({
7
+ openBottomSheetModal: jest.fn(),
8
+ }));
9
+
10
+ jest.mock("../../../modalState/ModalOrchestrator", () => ({
11
+ modalOrchestrator: {
12
+ open: jest.fn(),
13
+ },
14
+ isPresentSubMenuAction: jest.requireActual("../../../modalState/types")
15
+ .isPresentSubMenuAction,
16
+ }));
17
+
18
+ describe("openBottomSheetAction custom content", () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ it("opens default sheet with contentComponent body props", async () => {
24
+ const CustomContent = function CustomContent() {
25
+ return null;
26
+ };
27
+
28
+ const result = await openBottomSheetAction({
29
+ type: "openBottomSheet",
30
+ options: {
31
+ header: { title: "Create Playlist" },
32
+ content: {
33
+ component: CustomContent,
34
+ props: {
35
+ inputLabel: "Name your playlist",
36
+ buttonLabel: "Create",
37
+ defaultValue: "",
38
+ submitAction: { type: "sendCloudEvent", options: {} },
39
+ },
40
+ },
41
+ },
42
+ } as any);
43
+
44
+ expect(result).toBe(ActionResult.Success);
45
+ expect(openBottomSheetModal).toHaveBeenCalledTimes(1);
46
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
47
+
48
+ const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
49
+ // Default ModalComponent provides the standard header
50
+ expect(args.ModalBottomSheetContent).toBeUndefined();
51
+
52
+ expect(args.modalBottomSheetContentProps).toMatchObject({
53
+ title: "Create Playlist",
54
+ contentComponent: CustomContent,
55
+ contentComponentProps: {
56
+ inputLabel: "Name your playlist",
57
+ buttonLabel: "Create",
58
+ defaultValue: "",
59
+ },
60
+ });
61
+ });
62
+
63
+ it("returns Error when neither list data nor content.component is provided", async () => {
64
+ const result = await openBottomSheetAction({
65
+ type: "openBottomSheet",
66
+ options: {
67
+ header: { title: "Empty" },
68
+ content: {},
69
+ },
70
+ } as any);
71
+
72
+ expect(result).toBe(ActionResult.Error);
73
+ expect(openBottomSheetModal).not.toHaveBeenCalled();
74
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
75
+ });
76
+ });