@applicaster/zapp-react-native-utils 15.1.12-rc.0 → 15.1.13-alpha.1034774223

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 (66) hide show
  1. package/actionsExecutor/ActionExecutor.ts +40 -2
  2. package/actionsExecutor/ActionExecutorContext.tsx +70 -16
  3. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  4. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  5. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  6. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +212 -0
  7. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  8. package/actionsExecutor/actions/index.ts +4 -0
  9. package/actionsExecutor/actions/openBottomSheet.ts +325 -0
  10. package/actionsExecutor/actions/refreshComponent.ts +6 -0
  11. package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
  12. package/actionsExecutor/consts.ts +23 -0
  13. package/actionsExecutor/types.ts +1 -1
  14. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +5 -2
  15. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +55 -2
  16. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  17. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  18. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  19. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  20. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  21. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  22. package/arrayUtils/index.ts +79 -0
  23. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  24. package/configurationUtils/__tests__/modalBlocksParser.test.ts +260 -0
  25. package/configurationUtils/manifestKeyParser.ts +50 -10
  26. package/configurationUtils/modalBlocksParser.ts +296 -0
  27. package/entryActions/__tests__/buildEntryActions.test.ts +156 -0
  28. package/entryActions/aliasPresentation.ts +124 -0
  29. package/entryActions/buildEntryActions.ts +166 -0
  30. package/entryActions/index.ts +17 -0
  31. package/entryActions/types.ts +39 -0
  32. package/manifestUtils/defaultManifestConfigurations/player.js +22 -1
  33. package/manifestUtils/index.js +3 -0
  34. package/manifestUtils/modalBlocks.js +304 -0
  35. package/modalState/ContentViewModel.ts +113 -0
  36. package/modalState/EditableCollection.ts +17 -0
  37. package/modalState/EditableCollectionRegistry.ts +51 -0
  38. package/modalState/ModalOrchestrator.ts +211 -0
  39. package/modalState/RemoteEditableCollection.ts +227 -0
  40. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  41. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  42. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  43. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  44. package/modalState/__tests__/RemoteEditableCollection.test.ts +326 -0
  45. package/modalState/__tests__/index.test.ts +30 -1
  46. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  47. package/modalState/index.ts +35 -83
  48. package/modalState/store.ts +102 -0
  49. package/modalState/types.ts +159 -0
  50. package/modalState/useBottomSheetContent.ts +104 -0
  51. package/package.json +2 -2
  52. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  53. package/playerUtils/index.ts +17 -1
  54. package/playerUtils/resolvePlayerActions.ts +71 -0
  55. package/reactHooks/actions/index.ts +67 -2
  56. package/reactHooks/usePluginConfiguration.ts +6 -3
  57. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  58. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  59. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  60. package/uiActionsRegistrator/index.ts +25 -203
  61. package/uiActionsRegistrator/registry.ts +335 -0
  62. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  63. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  64. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  65. package/playerUtils/getPlayerActionButtons.ts +0 -17
  66. /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();
@@ -21,9 +21,11 @@ import {
21
21
  createGoBackAction,
22
22
  createGoHomeAction,
23
23
  createNavigateToScreenAction,
24
+ dismissBottomSheetAction,
24
25
  localStorageRemoveAction,
25
26
  localStorageSetAction,
26
27
  localStorageToggleFlagAction,
28
+ openBottomSheetAction,
27
29
  refreshComponentAction,
28
30
  screenSetVariableAction,
29
31
  screenToggleFlagAction,
@@ -35,6 +37,8 @@ import {
35
37
  showToastAction,
36
38
  } from "./actions";
37
39
 
40
+ import { ACTION_TYPES } from "./consts";
41
+
38
42
  export const { log_error, log_info, log_debug } = createLogger({
39
43
  subsystem: "ActionExecutorContext",
40
44
  category: "General",
@@ -43,9 +47,15 @@ export const { log_error, log_info, log_debug } = createLogger({
43
47
  export type ActionExecutorContextType = {
44
48
  registerAction: <TOptions = Record<string, any>>(
45
49
  type: string,
46
- handler: ActionHandler<TOptions>
50
+ handler: ActionHandler<TOptions>,
51
+ stateResolver?: (entry?: ZappEntry) => any
47
52
  ) => void;
48
53
  unregisterAction: (type: string) => void;
54
+ registerActionState: (
55
+ type: string,
56
+ stateResolver: (entry?: ZappEntry) => any
57
+ ) => () => void;
58
+ getActionState: (type: string, entry?: ZappEntry) => any;
49
59
  handleAction: (
50
60
  action: ActionType,
51
61
  context?: ActionExecutionContext
@@ -65,34 +75,76 @@ type Props = {
65
75
  };
66
76
 
67
77
  const prepareDefaultActions = (actionExecutor) => {
68
- actionExecutor.registerAction("localStorageSet", localStorageSetAction);
69
- actionExecutor.registerAction("sessionStorageSet", sessionStorageSetAction);
70
- 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
+ );
71
92
 
72
93
  actionExecutor.registerAction(
73
- "sessionStorageRemove",
94
+ ACTION_TYPES.SESSION_STORAGE_REMOVE,
74
95
  sessionStorageRemoveAction
75
96
  );
76
97
 
77
- actionExecutor.registerAction("refreshComponent", refreshComponentAction);
78
- actionExecutor.registerAction("switchLayout", switchLayoutAction);
79
- actionExecutor.registerAction("appRestart", appRestartAction);
80
- actionExecutor.registerAction("confirmDialog", confirmDialogAction);
81
- 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
+ );
82
116
 
83
117
  actionExecutor.registerAction(
84
- "sessionStorageToggleFlag",
118
+ ACTION_TYPES.SESSION_STORAGE_TOGGLE_FLAG,
85
119
  sessionStorageToggleFlagAction
86
120
  );
87
121
 
88
122
  actionExecutor.registerAction(
89
- "localStorageToggleFlag",
123
+ ACTION_TYPES.LOCAL_STORAGE_TOGGLE_FLAG,
90
124
  localStorageToggleFlagAction
91
125
  );
92
126
 
93
- actionExecutor.registerAction("screenSetVariable", screenSetVariableAction);
94
- actionExecutor.registerAction("screenToggleFlag", screenToggleFlagAction);
95
- 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);
96
148
  };
97
149
 
98
150
  export const ActionExecutorContext =
@@ -110,6 +162,8 @@ export function withActionExecutor(Component) {
110
162
  return {
111
163
  unregisterAction: _actionExecutor.unregisterAction,
112
164
  registerAction: _actionExecutor.registerAction,
165
+ registerActionState: _actionExecutor.registerActionState,
166
+ getActionState: _actionExecutor.getActionState,
113
167
  handleAction: _actionExecutor.handleAction,
114
168
  handleActions: _actionExecutor.handleActions,
115
169
  handleEntryActions: _actionExecutor.handleEntryActions,
@@ -132,7 +186,7 @@ export function withActionExecutor(Component) {
132
186
 
133
187
  useEffect(() => {
134
188
  return _actionExecutor.registerAction(
135
- "navigateToScreen",
189
+ ACTION_TYPES.NAVIGATE_TO_SCREEN,
136
190
  createNavigateToScreenAction(navigator, rivers, contentTypes)
137
191
  );
138
192
  }, [navigator, rivers, contentTypes]);
@@ -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,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,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
+ });
@@ -0,0 +1,212 @@
1
+ import { openBottomSheetAction } from "../openBottomSheet";
2
+ import { actionExecutor, ActionResult } from "../../ActionExecutor";
3
+ import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
4
+
5
+ jest.mock("../../../modalState/store", () => ({
6
+ openBottomSheetModal: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../../../modalState/ModalOrchestrator", () => ({
10
+ modalOrchestrator: {
11
+ open: jest.fn(),
12
+ replace: jest.fn(),
13
+ stackSize: 0,
14
+ },
15
+ isPresentSubMenuAction: jest.requireActual("../../../modalState/types")
16
+ .isPresentSubMenuAction,
17
+ }));
18
+
19
+ describe("openBottomSheetAction inline items", () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ (modalOrchestrator as any).stackSize = 0;
23
+ });
24
+
25
+ const speedSheetAction = {
26
+ type: "openBottomSheet",
27
+ options: {
28
+ header: { title: "Playback Speed" },
29
+ content: {
30
+ role: "collection_selector",
31
+ behavior: {
32
+ selectMode: "single",
33
+ currentSelection: ["1"],
34
+ },
35
+ items: [
36
+ {
37
+ id: "0.8",
38
+ title: "0.8x",
39
+ extensions: {
40
+ tap_actions: {
41
+ actions: [
42
+ { type: "setPlaybackSpeed", options: { value: 0.8 } },
43
+ ],
44
+ },
45
+ },
46
+ },
47
+ {
48
+ id: "1",
49
+ title: "1x",
50
+ extensions: {
51
+ tap_actions: {
52
+ actions: [{ type: "setPlaybackSpeed", options: { value: 1 } }],
53
+ },
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ },
59
+ } as any;
60
+
61
+ it("opens orchestrator with inline items and marks current selection", async () => {
62
+ const result = await openBottomSheetAction(speedSheetAction);
63
+
64
+ expect(result).toBe(ActionResult.Success);
65
+ expect(modalOrchestrator.open).toHaveBeenCalledTimes(1);
66
+ expect(modalOrchestrator.replace).not.toHaveBeenCalled();
67
+
68
+ const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
69
+
70
+ expect(menu.content.items[0].isSelected).toBe(false);
71
+
72
+ expect(menu.content.items[1]).toMatchObject({
73
+ id: "1",
74
+ title: "1x",
75
+ isSelected: true,
76
+ action: { type: "setPlaybackSpeed", options: { value: 1 } },
77
+ });
78
+ });
79
+
80
+ it("pushes a new sheet by default even when another sheet is already open", async () => {
81
+ (modalOrchestrator as any).stackSize = 1;
82
+
83
+ await openBottomSheetAction(speedSheetAction);
84
+
85
+ expect(modalOrchestrator.open).toHaveBeenCalledTimes(1);
86
+ expect(modalOrchestrator.replace).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it("replaces the current sheet when options.replace is true", async () => {
90
+ (modalOrchestrator as any).stackSize = 1;
91
+
92
+ await openBottomSheetAction({
93
+ ...speedSheetAction,
94
+ options: { ...speedSheetAction.options, replace: true },
95
+ });
96
+
97
+ expect(modalOrchestrator.replace).toHaveBeenCalledTimes(1);
98
+ expect(modalOrchestrator.open).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it("executes all actions in tap_actions when onItemPress is called", async () => {
102
+ const multiActionSheet = {
103
+ type: "openBottomSheet",
104
+ options: {
105
+ header: { title: "Target Playlists" },
106
+ content: {
107
+ role: "collection_selector",
108
+ behavior: { selectMode: "none", currentSelection: [] },
109
+ items: [
110
+ {
111
+ id: "playlist-1",
112
+ title: "My Playlist",
113
+ extensions: {
114
+ tap_actions: {
115
+ actions: [
116
+ {
117
+ type: "sendCloudEvent",
118
+ options: { subject: "add_collection_to_collection" },
119
+ },
120
+ {
121
+ type: "showToast",
122
+ options: { message: "Added to playlist" },
123
+ },
124
+ { type: "dismissBottomSheet", options: {} },
125
+ ],
126
+ },
127
+ },
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ } as any;
133
+
134
+ await openBottomSheetAction(multiActionSheet);
135
+
136
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
137
+ .calls[0];
138
+
139
+ expect(menu.content.items[0].actions).toHaveLength(3);
140
+
141
+ const handleActionsSpy = jest
142
+ .spyOn(actionExecutor, "handleActions")
143
+ .mockResolvedValue(ActionResult.Success);
144
+
145
+ await onItemPress(menu.content.items[0]);
146
+
147
+ expect(handleActionsSpy).toHaveBeenCalledWith(
148
+ [
149
+ {
150
+ type: "sendCloudEvent",
151
+ options: { subject: "add_collection_to_collection" },
152
+ },
153
+ {
154
+ type: "showToast",
155
+ options: { message: "Added to playlist" },
156
+ },
157
+ { type: "dismissBottomSheet", options: {} },
158
+ ],
159
+ expect.objectContaining({
160
+ entry: menu.content.items[0],
161
+ })
162
+ );
163
+
164
+ handleActionsSpy.mockRestore();
165
+ });
166
+
167
+ it("executes preceding actions and excludes presentSubMenu from actionExecutor", async () => {
168
+ const subMenuSheet = {
169
+ type: "openBottomSheet",
170
+ options: {
171
+ header: { title: "Menu" },
172
+ content: {
173
+ items: [
174
+ {
175
+ id: "item-with-submenu",
176
+ title: "More Options",
177
+ extensions: {
178
+ tap_actions: {
179
+ actions: [
180
+ { type: "sendAnalytics", options: { event: "click_more" } },
181
+ {
182
+ type: "presentSubMenu",
183
+ menu: { content: { title: "Sub Options", items: [] } },
184
+ },
185
+ ],
186
+ },
187
+ },
188
+ },
189
+ ],
190
+ },
191
+ },
192
+ } as any;
193
+
194
+ await openBottomSheetAction(subMenuSheet);
195
+
196
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
197
+ .calls[0];
198
+
199
+ const handleActionsSpy = jest
200
+ .spyOn(actionExecutor, "handleActions")
201
+ .mockResolvedValue(ActionResult.Success);
202
+
203
+ await onItemPress(menu.content.items[0]);
204
+
205
+ expect(handleActionsSpy).toHaveBeenCalledWith(
206
+ [{ type: "sendAnalytics", options: { event: "click_more" } }],
207
+ expect.objectContaining({ entry: menu.content.items[0] })
208
+ );
209
+
210
+ handleActionsSpy.mockRestore();
211
+ });
212
+ });
@@ -0,0 +1,23 @@
1
+ /// <reference types="@applicaster/applicaster-types" />
2
+ import { dismissModal, modalOrchestrator } from "../../modalState";
3
+ import { ActionResult } from "../ActionExecutor";
4
+ import { ActionHandler } from "../types";
5
+ import { createLogger } from "../../logger";
6
+
7
+ const { log_info } = createLogger({
8
+ subsystem: "ActionExecutorContext",
9
+ category: "General",
10
+ });
11
+
12
+ /**
13
+ * Dismisses the active bottom sheet / modal.
14
+ */
15
+ export const dismissBottomSheetAction: ActionHandler<
16
+ any
17
+ > = async (): Promise<ActionResult> => {
18
+ log_info("handleAction: dismissBottomSheet");
19
+ modalOrchestrator.close();
20
+ dismissModal();
21
+
22
+ return ActionResult.Success;
23
+ };
@@ -32,3 +32,7 @@ export { createGoBackAction } from "./goBack";
32
32
  export { createGoHomeAction } from "./goHome";
33
33
 
34
34
  export { showToastAction } from "./showToast";
35
+
36
+ export { openBottomSheetAction } from "./openBottomSheet";
37
+
38
+ export { dismissBottomSheetAction } from "./dismissBottomSheet";