@applicaster/zapp-react-native-utils 16.0.0-alpha.2024822855 → 16.0.0-alpha.2105365027
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.
- package/actionUtils/index.ts +3 -1
- package/actionsExecutor/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +29 -1
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
- package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
- package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +201 -1
- package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
- package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
- package/actionsExecutor/actions/goBack.ts +60 -0
- package/actionsExecutor/actions/goHome.ts +31 -0
- package/actionsExecutor/actions/index.ts +8 -0
- package/actionsExecutor/actions/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +35 -16
- package/actionsExecutor/actions/presentScreen.ts +78 -0
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
- package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
- package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
- package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/index.ts +7 -0
- package/appUtils/playerManager/player.ts +10 -0
- package/appUtils/playerManager/playerNative.ts +27 -3
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +157 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
- package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
- package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
- package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
- package/cellUtils/index.ts +74 -19
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +286 -1
- package/configurationUtils/index.ts +77 -73
- package/configurationUtils/manifestKeyParser.ts +12 -1
- package/configurationUtils/modalBlocksParser.ts +78 -4
- package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
- package/entryActions/aliasPresentation.ts +108 -0
- package/entryActions/buildEntryActions.ts +167 -0
- package/entryActions/index.ts +15 -0
- package/entryActions/types.ts +39 -0
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/_internals/index.js +8 -3
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +30 -1
- package/manifestUtils/keys.js +6 -1
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +2 -1
- package/modalState/ModalOrchestrator.ts +22 -9
- package/modalState/RemoteEditableCollection.ts +9 -1
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
- package/modalState/__tests__/index.test.ts +30 -1
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/index.ts +6 -1
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +31 -2
- package/modalState/useBottomSheetContent.ts +24 -17
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +2 -2
- package/perfTracker.ts +203 -0
- package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
- package/playerUtils/index.ts +17 -1
- package/playerUtils/resolvePlayerActions.ts +71 -0
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +67 -2
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
- package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
- package/reactHooks/navigation/index.ts +2 -0
- package/reactHooks/navigation/usePresentScreen.ts +157 -0
- package/reactHooks/navigation/useRoute.ts +22 -5
- package/reactHooks/usePluginConfiguration.ts +84 -46
- package/riverComponetsMeasurementProvider/index.tsx +8 -1
- package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
- package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
- package/uiActionsRegistrator/index.ts +25 -203
- package/uiActionsRegistrator/registry.ts +351 -0
- package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
- package/uiActionsRegistrator/usableActionItems.ts +66 -0
- package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
- package/playerUtils/getPlayerActionButtons.ts +0 -17
package/actionUtils/index.ts
CHANGED
|
@@ -3,5 +3,7 @@ import { isTV } from "../reactUtils";
|
|
|
3
3
|
|
|
4
4
|
export const resolveDefaultAssetUri = (source: string | number): string =>
|
|
5
5
|
isTV()
|
|
6
|
-
?
|
|
6
|
+
? typeof source === "number"
|
|
7
|
+
? Image.resolveAssetSource(source)?.uri || ""
|
|
8
|
+
: (source as string)
|
|
7
9
|
: Image.resolveAssetSource(source as number)?.uri || "";
|
|
@@ -56,7 +56,6 @@ export 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 @@ export 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;
|
|
@@ -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,11 +18,15 @@ import { createLogger } from "../logger";
|
|
|
18
18
|
import {
|
|
19
19
|
appRestartAction,
|
|
20
20
|
confirmDialogAction,
|
|
21
|
+
showAlertAction,
|
|
22
|
+
createGoBackAction,
|
|
23
|
+
createGoHomeAction,
|
|
21
24
|
createNavigateToScreenAction,
|
|
22
25
|
dismissBottomSheetAction,
|
|
23
26
|
localStorageRemoveAction,
|
|
24
27
|
localStorageSetAction,
|
|
25
28
|
localStorageToggleFlagAction,
|
|
29
|
+
createPresentScreenAction,
|
|
26
30
|
openBottomSheetAction,
|
|
27
31
|
refreshComponentAction,
|
|
28
32
|
screenSetVariableAction,
|
|
@@ -107,6 +111,8 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
107
111
|
confirmDialogAction
|
|
108
112
|
);
|
|
109
113
|
|
|
114
|
+
actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
|
|
115
|
+
|
|
110
116
|
actionExecutor.registerAction(
|
|
111
117
|
ACTION_TYPES.SEND_CLOUD_EVENT,
|
|
112
118
|
sendCloudEventAction
|
|
@@ -155,6 +161,7 @@ export function withActionExecutor(Component) {
|
|
|
155
161
|
const navigator = useNavigation();
|
|
156
162
|
const rivers = useRivers();
|
|
157
163
|
const contentTypes = useContentTypes();
|
|
164
|
+
const presentScreen = usePresentScreen();
|
|
158
165
|
|
|
159
166
|
const handlers = useMemo(() => {
|
|
160
167
|
return {
|
|
@@ -189,6 +196,27 @@ export function withActionExecutor(Component) {
|
|
|
189
196
|
);
|
|
190
197
|
}, [navigator, rivers, contentTypes]);
|
|
191
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
|
+
|
|
192
220
|
return (
|
|
193
221
|
<ActionExecutorContext.Provider value={handlers}>
|
|
194
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,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
|
+
});
|
|
@@ -83,9 +83,7 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
// non-push path falls back to replaceTop().
|
|
88
|
-
it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
|
|
86
|
+
it("forwards navigationOptions to replace when navigating with an entry", async () => {
|
|
89
87
|
const navigator = createNavigator();
|
|
90
88
|
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
91
89
|
|
|
@@ -100,12 +98,12 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
100
98
|
|
|
101
99
|
await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
|
|
102
100
|
|
|
103
|
-
expect(navigator.
|
|
101
|
+
expect(navigator.replace).toHaveBeenCalledWith(
|
|
104
102
|
{ id: "entry-id", type: { value: "parentLock" } },
|
|
105
103
|
{ pinFlow: "change-pin" }
|
|
106
104
|
);
|
|
107
105
|
|
|
108
|
-
expect(navigator.
|
|
106
|
+
expect(navigator.replaceTop).not.toHaveBeenCalled();
|
|
109
107
|
});
|
|
110
108
|
|
|
111
109
|
// Regression guard: the no-options path must keep working unchanged.
|
|
@@ -131,3 +129,139 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
131
129
|
expect(result).toBe(ActionResult.Success);
|
|
132
130
|
});
|
|
133
131
|
});
|
|
132
|
+
|
|
133
|
+
describe("navigateToScreen target resolution", () => {
|
|
134
|
+
function run(options, context?) {
|
|
135
|
+
const navigator = createNavigator();
|
|
136
|
+
|
|
137
|
+
const result = createNavigateToScreenAction(
|
|
138
|
+
navigator,
|
|
139
|
+
rivers,
|
|
140
|
+
contentTypes
|
|
141
|
+
)({ type: "navigateToScreen", options }, context);
|
|
142
|
+
|
|
143
|
+
return { navigator, result };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
it("overrides entry.type with typeMapping when both are provided", async () => {
|
|
147
|
+
const entry = {
|
|
148
|
+
id: "entry-id",
|
|
149
|
+
type: { value: "video" },
|
|
150
|
+
} as unknown as ZappEntry;
|
|
151
|
+
|
|
152
|
+
const { navigator, result } = run({
|
|
153
|
+
typeMapping: "parentLock",
|
|
154
|
+
navigationAction: "push",
|
|
155
|
+
entry,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(await result).toBe(ActionResult.Success);
|
|
159
|
+
|
|
160
|
+
expect(navigator.push).toHaveBeenCalledWith(
|
|
161
|
+
{ id: "entry-id", type: { value: "parentLock" } },
|
|
162
|
+
undefined
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(entry.type).toEqual({ value: "video" });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("navigates with the entry's own type when typeMapping is not provided", async () => {
|
|
169
|
+
const entry = {
|
|
170
|
+
id: "entry-id",
|
|
171
|
+
type: { value: "video" },
|
|
172
|
+
} as unknown as ZappEntry;
|
|
173
|
+
|
|
174
|
+
const { navigator, result } = run({ navigationAction: "push", entry });
|
|
175
|
+
|
|
176
|
+
expect(await result).toBe(ActionResult.Success);
|
|
177
|
+
|
|
178
|
+
expect(navigator.push).toHaveBeenCalledWith(
|
|
179
|
+
{ id: "entry-id", type: { value: "video" } },
|
|
180
|
+
undefined
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("resolves the context entry when entry is @{entry/} and typeMapping is not provided", async () => {
|
|
185
|
+
const entry = {
|
|
186
|
+
id: "context-entry-id",
|
|
187
|
+
type: { value: "video" },
|
|
188
|
+
} as unknown as ZappEntry;
|
|
189
|
+
|
|
190
|
+
const { navigator, result } = run({ entry: "@{entry/}" }, { entry });
|
|
191
|
+
|
|
192
|
+
expect(await result).toBe(ActionResult.Success);
|
|
193
|
+
|
|
194
|
+
expect(navigator.replace).toHaveBeenCalledWith(
|
|
195
|
+
{ id: "context-entry-id", type: { value: "video" } },
|
|
196
|
+
undefined
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("returns an error when neither typeMapping nor entry type is provided", async () => {
|
|
201
|
+
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
202
|
+
|
|
203
|
+
const { navigator, result } = run({ navigationAction: "push", entry });
|
|
204
|
+
|
|
205
|
+
expect(await result).toBe(ActionResult.Error);
|
|
206
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
207
|
+
expect(navigator.replace).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns an error when neither typeMapping nor entry is provided", async () => {
|
|
211
|
+
const { navigator, result } = run({ navigationAction: "push" });
|
|
212
|
+
|
|
213
|
+
expect(await result).toBe(ActionResult.Error);
|
|
214
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns an error when typeMapping has no screen_id mapping", async () => {
|
|
218
|
+
const callback = jest.fn();
|
|
219
|
+
|
|
220
|
+
const { navigator, result } = run(
|
|
221
|
+
{ typeMapping: "unknownType", navigationAction: "push" },
|
|
222
|
+
{ callback }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(await result).toBe(ActionResult.Error);
|
|
226
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
227
|
+
expect(callback).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns an error when the mapped screen_id has no river", async () => {
|
|
231
|
+
const navigator = createNavigator();
|
|
232
|
+
const callback = jest.fn();
|
|
233
|
+
|
|
234
|
+
const result = await createNavigateToScreenAction(navigator, rivers, {
|
|
235
|
+
orphan: { screen_id: "missing-screen-id" },
|
|
236
|
+
} as unknown as ZappContentTypes)(
|
|
237
|
+
{
|
|
238
|
+
type: "navigateToScreen",
|
|
239
|
+
options: { typeMapping: "orphan", navigationAction: "push" },
|
|
240
|
+
},
|
|
241
|
+
{ callback }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(result).toBe(ActionResult.Error);
|
|
245
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
246
|
+
expect(callback).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("aborts the action pipeline before navigating to a resolved river", async () => {
|
|
250
|
+
const callback = jest.fn();
|
|
251
|
+
|
|
252
|
+
const { navigator, result } = run(
|
|
253
|
+
{ typeMapping: "parentLock" },
|
|
254
|
+
{ callback }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(await result).toBe(ActionResult.Success);
|
|
258
|
+
|
|
259
|
+
expect(callback).toHaveBeenCalledWith({
|
|
260
|
+
success: false,
|
|
261
|
+
error: null,
|
|
262
|
+
abort: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(navigator.replace).toHaveBeenCalledWith(river, undefined);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -60,6 +60,30 @@ describe("openBottomSheetAction custom content", () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("passes themePluginId through to the name sheet", async () => {
|
|
64
|
+
const CustomContent = function CustomContent() {
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await openBottomSheetAction({
|
|
69
|
+
type: "openBottomSheet",
|
|
70
|
+
options: {
|
|
71
|
+
header: { title: "Create New Playlist" },
|
|
72
|
+
themePluginId: "playlist-experience",
|
|
73
|
+
content: {
|
|
74
|
+
component: CustomContent,
|
|
75
|
+
props: { buttonLabel: "Create" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
|
|
81
|
+
|
|
82
|
+
expect(args.modalBottomSheetContentProps.themePluginId).toBe(
|
|
83
|
+
"playlist-experience"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
63
87
|
it("returns Error when neither list data nor content.component is provided", async () => {
|
|
64
88
|
const result = await openBottomSheetAction({
|
|
65
89
|
type: "openBottomSheet",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { openBottomSheetAction } from "../openBottomSheet";
|
|
2
|
-
import { ActionResult } from "../../ActionExecutor";
|
|
2
|
+
import { actionExecutor, ActionResult } from "../../ActionExecutor";
|
|
3
3
|
import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
|
|
4
4
|
|
|
5
5
|
jest.mock("../../../modalState/store", () => ({
|
|
@@ -77,6 +77,94 @@ describe("openBottomSheetAction inline items", () => {
|
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it("opens an empty inline collection when dynamic collection options are present", async () => {
|
|
81
|
+
const result = await openBottomSheetAction({
|
|
82
|
+
type: "openBottomSheet",
|
|
83
|
+
options: {
|
|
84
|
+
header: { title: "Queue" },
|
|
85
|
+
content: {
|
|
86
|
+
items: [],
|
|
87
|
+
emptyMessage: "Queued items will be added here",
|
|
88
|
+
dynamicCollection: {
|
|
89
|
+
operations: ["remove"],
|
|
90
|
+
provider: "@{provider/queue}",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
expect(result).toBe(ActionResult.Success);
|
|
97
|
+
|
|
98
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
99
|
+
|
|
100
|
+
expect(menu.content.items).toEqual([]);
|
|
101
|
+
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
102
|
+
|
|
103
|
+
expect(menu.content.dynamicCollection).toMatchObject({
|
|
104
|
+
operations: ["remove"],
|
|
105
|
+
provider: "@{provider/queue}",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("accepts empty inline items when dynamic_collection_options live on extensions", async () => {
|
|
110
|
+
const result = await openBottomSheetAction({
|
|
111
|
+
type: "openBottomSheet",
|
|
112
|
+
options: {
|
|
113
|
+
content: {
|
|
114
|
+
items: [],
|
|
115
|
+
extensions: {
|
|
116
|
+
dynamic_collection_options: {
|
|
117
|
+
operations: "remove,reorder",
|
|
118
|
+
provider: "@{provider/offline}",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
} as any);
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(ActionResult.Success);
|
|
126
|
+
|
|
127
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
128
|
+
|
|
129
|
+
expect(menu.content.items).toEqual([]);
|
|
130
|
+
|
|
131
|
+
expect(menu.content.dynamicCollection).toMatchObject({
|
|
132
|
+
operations: ["remove", "reorder"],
|
|
133
|
+
provider: "@{provider/offline}",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns an error for an empty items array with no source", async () => {
|
|
138
|
+
const result = await openBottomSheetAction({
|
|
139
|
+
type: "openBottomSheet",
|
|
140
|
+
options: {
|
|
141
|
+
content: { items: [] },
|
|
142
|
+
},
|
|
143
|
+
} as any);
|
|
144
|
+
|
|
145
|
+
expect(result).toBe(ActionResult.Error);
|
|
146
|
+
expect(modalOrchestrator.open).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("forwards content.emptyMessage onto the menu", async () => {
|
|
150
|
+
const result = await openBottomSheetAction({
|
|
151
|
+
type: "openBottomSheet",
|
|
152
|
+
options: {
|
|
153
|
+
header: { title: "Queue" },
|
|
154
|
+
content: {
|
|
155
|
+
items: [{ id: "t1", title: "Track 1" }],
|
|
156
|
+
emptyMessage: "Queued items will be added here",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
} as any);
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(ActionResult.Success);
|
|
162
|
+
|
|
163
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
164
|
+
|
|
165
|
+
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
166
|
+
});
|
|
167
|
+
|
|
80
168
|
it("pushes a new sheet by default even when another sheet is already open", async () => {
|
|
81
169
|
(modalOrchestrator as any).stackSize = 1;
|
|
82
170
|
|
|
@@ -97,4 +185,116 @@ describe("openBottomSheetAction inline items", () => {
|
|
|
97
185
|
expect(modalOrchestrator.replace).toHaveBeenCalledTimes(1);
|
|
98
186
|
expect(modalOrchestrator.open).not.toHaveBeenCalled();
|
|
99
187
|
});
|
|
188
|
+
|
|
189
|
+
it("executes all actions in tap_actions when onItemPress is called", async () => {
|
|
190
|
+
const multiActionSheet = {
|
|
191
|
+
type: "openBottomSheet",
|
|
192
|
+
options: {
|
|
193
|
+
header: { title: "Target Playlists" },
|
|
194
|
+
content: {
|
|
195
|
+
role: "collection_selector",
|
|
196
|
+
behavior: { selectMode: "none", currentSelection: [] },
|
|
197
|
+
items: [
|
|
198
|
+
{
|
|
199
|
+
id: "playlist-1",
|
|
200
|
+
title: "My Playlist",
|
|
201
|
+
extensions: {
|
|
202
|
+
tap_actions: {
|
|
203
|
+
actions: [
|
|
204
|
+
{
|
|
205
|
+
type: "sendCloudEvent",
|
|
206
|
+
options: { subject: "add_collection_to_collection" },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
type: "showToast",
|
|
210
|
+
options: { message: "Added to playlist" },
|
|
211
|
+
},
|
|
212
|
+
{ type: "dismissBottomSheet", options: {} },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
} as any;
|
|
221
|
+
|
|
222
|
+
await openBottomSheetAction(multiActionSheet);
|
|
223
|
+
|
|
224
|
+
const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
|
|
225
|
+
.calls[0];
|
|
226
|
+
|
|
227
|
+
expect(menu.content.items[0].actions).toHaveLength(3);
|
|
228
|
+
|
|
229
|
+
const handleActionsSpy = jest
|
|
230
|
+
.spyOn(actionExecutor, "handleActions")
|
|
231
|
+
.mockResolvedValue(ActionResult.Success);
|
|
232
|
+
|
|
233
|
+
await onItemPress(menu.content.items[0]);
|
|
234
|
+
|
|
235
|
+
expect(handleActionsSpy).toHaveBeenCalledWith(
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
type: "sendCloudEvent",
|
|
239
|
+
options: { subject: "add_collection_to_collection" },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "showToast",
|
|
243
|
+
options: { message: "Added to playlist" },
|
|
244
|
+
},
|
|
245
|
+
{ type: "dismissBottomSheet", options: {} },
|
|
246
|
+
],
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
entry: menu.content.items[0],
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
handleActionsSpy.mockRestore();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("executes preceding actions and excludes presentSubMenu from actionExecutor", async () => {
|
|
256
|
+
const subMenuSheet = {
|
|
257
|
+
type: "openBottomSheet",
|
|
258
|
+
options: {
|
|
259
|
+
header: { title: "Menu" },
|
|
260
|
+
content: {
|
|
261
|
+
items: [
|
|
262
|
+
{
|
|
263
|
+
id: "item-with-submenu",
|
|
264
|
+
title: "More Options",
|
|
265
|
+
extensions: {
|
|
266
|
+
tap_actions: {
|
|
267
|
+
actions: [
|
|
268
|
+
{ type: "sendAnalytics", options: { event: "click_more" } },
|
|
269
|
+
{
|
|
270
|
+
type: "presentSubMenu",
|
|
271
|
+
menu: { content: { title: "Sub Options", items: [] } },
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
} as any;
|
|
281
|
+
|
|
282
|
+
await openBottomSheetAction(subMenuSheet);
|
|
283
|
+
|
|
284
|
+
const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
|
|
285
|
+
.calls[0];
|
|
286
|
+
|
|
287
|
+
const handleActionsSpy = jest
|
|
288
|
+
.spyOn(actionExecutor, "handleActions")
|
|
289
|
+
.mockResolvedValue(ActionResult.Success);
|
|
290
|
+
|
|
291
|
+
await onItemPress(menu.content.items[0]);
|
|
292
|
+
|
|
293
|
+
expect(handleActionsSpy).toHaveBeenCalledWith(
|
|
294
|
+
[{ type: "sendAnalytics", options: { event: "click_more" } }],
|
|
295
|
+
expect.objectContaining({ entry: menu.content.items[0] })
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
handleActionsSpy.mockRestore();
|
|
299
|
+
});
|
|
100
300
|
});
|