@applicaster/zapp-react-native-utils 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921
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/actionsExecutor/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +3 -0
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
- package/actionsExecutor/actions/index.ts +2 -0
- package/actionsExecutor/actions/openBottomSheet.ts +19 -6
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +1 -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 +76 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/player.ts +10 -0
- package/appUtils/playerManager/playerNative.ts +22 -0
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +78 -69
- package/configurationUtils/modalBlocksParser.ts +24 -106
- package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
- package/entryActions/aliasPresentation.ts +26 -42
- package/entryActions/buildEntryActions.ts +1 -0
- package/entryActions/index.ts +0 -2
- package/modalState/ModalOrchestrator.ts +1 -0
- 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__/useModalStoreState.test.ts +142 -0
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +3 -0
- package/modalState/useBottomSheetContent.ts +17 -12
- package/package.json +2 -2
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
- package/reactHooks/navigation/useRoute.ts +8 -4
|
@@ -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;
|
|
@@ -18,6 +18,7 @@ import { createLogger } from "../logger";
|
|
|
18
18
|
import {
|
|
19
19
|
appRestartAction,
|
|
20
20
|
confirmDialogAction,
|
|
21
|
+
showAlertAction,
|
|
21
22
|
createGoBackAction,
|
|
22
23
|
createGoHomeAction,
|
|
23
24
|
createNavigateToScreenAction,
|
|
@@ -110,6 +111,8 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
110
111
|
confirmDialogAction
|
|
111
112
|
);
|
|
112
113
|
|
|
114
|
+
actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
|
|
115
|
+
|
|
113
116
|
actionExecutor.registerAction(
|
|
114
117
|
ACTION_TYPES.SEND_CLOUD_EVENT,
|
|
115
118
|
sendCloudEventAction
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { showAlertAction } from "../showAlert";
|
|
2
|
+
import { ActionResult } from "../../ActionExecutor";
|
|
3
|
+
import { showAlertDialog } from "../../../alertUtils";
|
|
4
|
+
|
|
5
|
+
jest.mock("../../../alertUtils", () => ({
|
|
6
|
+
showAlertDialog: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const dialog = showAlertDialog as jest.Mock;
|
|
10
|
+
|
|
11
|
+
const dismiss = () => dialog.mock.calls[0][0].completion();
|
|
12
|
+
|
|
13
|
+
describe("showAlert", () => {
|
|
14
|
+
beforeEach(() => dialog.mockReset());
|
|
15
|
+
|
|
16
|
+
it("shows the notice and resolves once it is dismissed", async () => {
|
|
17
|
+
const result = showAlertAction({
|
|
18
|
+
type: "showAlert",
|
|
19
|
+
options: { title: "Locked", message: "Ask the account owner." },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(dialog).toHaveBeenCalledWith(
|
|
23
|
+
expect.objectContaining({
|
|
24
|
+
title: "Locked",
|
|
25
|
+
message: "Ask the account owner.",
|
|
26
|
+
okButtonText: "OK",
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
dismiss();
|
|
31
|
+
|
|
32
|
+
// Dismissing a notice is not a cancellation: the chain carries on.
|
|
33
|
+
await expect(result).resolves.toBe(ActionResult.Success);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("takes the dismiss label from the action", async () => {
|
|
37
|
+
const result = showAlertAction({
|
|
38
|
+
type: "showAlert",
|
|
39
|
+
options: { title: "Locked", okButtonText: "Got it" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(dialog).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({ okButtonText: "Got it" })
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
dismiss();
|
|
47
|
+
await result;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -31,7 +31,7 @@ describe("showToastAction", () => {
|
|
|
31
31
|
message: "Added to queue",
|
|
32
32
|
extraMessage: "Extra info",
|
|
33
33
|
style: undefined,
|
|
34
|
-
timeout:
|
|
34
|
+
timeout: 5000,
|
|
35
35
|
source: "confirmation",
|
|
36
36
|
},
|
|
37
37
|
]);
|
|
@@ -78,7 +78,7 @@ describe("showToastAction", () => {
|
|
|
78
78
|
message: undefined,
|
|
79
79
|
extraMessage: undefined,
|
|
80
80
|
style: undefined,
|
|
81
|
-
timeout:
|
|
81
|
+
timeout: 5000,
|
|
82
82
|
source: "confirmation",
|
|
83
83
|
},
|
|
84
84
|
]);
|
|
@@ -15,6 +15,8 @@ export { appRestartAction } from "./appRestart";
|
|
|
15
15
|
|
|
16
16
|
export { confirmDialogAction } from "./confirmDialog";
|
|
17
17
|
|
|
18
|
+
export { showAlertAction } from "./showAlert";
|
|
19
|
+
|
|
18
20
|
export { sendCloudEventAction } from "./sendCloudEvent";
|
|
19
21
|
|
|
20
22
|
export { sessionStorageToggleFlagAction } from "./sessionStorageToggleFlag";
|
|
@@ -208,7 +208,10 @@ export async function openBottomSheetAction(
|
|
|
208
208
|
items: [],
|
|
209
209
|
onPress: () => {},
|
|
210
210
|
contentComponent: content.component,
|
|
211
|
-
contentComponentProps:
|
|
211
|
+
contentComponentProps: {
|
|
212
|
+
...(content.props || {}),
|
|
213
|
+
actionContext: context,
|
|
214
|
+
},
|
|
212
215
|
},
|
|
213
216
|
});
|
|
214
217
|
|
|
@@ -224,7 +227,18 @@ export async function openBottomSheetAction(
|
|
|
224
227
|
|
|
225
228
|
let dynamicCollection: DynamicCollectionOptions | undefined;
|
|
226
229
|
|
|
227
|
-
|
|
230
|
+
const rawDynamicCollection =
|
|
231
|
+
content?.dynamicCollection ??
|
|
232
|
+
content?.extensions?.dynamic_collection_options;
|
|
233
|
+
|
|
234
|
+
// An empty items array is a valid starting state for a provider-backed or
|
|
235
|
+
// offline collection. A sheet with neither items nor a source still errors.
|
|
236
|
+
const hasDynamicCollection = Boolean(rawDynamicCollection);
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
Array.isArray(inlineItems) &&
|
|
240
|
+
(inlineItems.length > 0 || hasDynamicCollection)
|
|
241
|
+
) {
|
|
228
242
|
log_info("openBottomSheet: using inline items from action options", {
|
|
229
243
|
count: inlineItems.length,
|
|
230
244
|
});
|
|
@@ -236,10 +250,7 @@ export async function openBottomSheetAction(
|
|
|
236
250
|
entryToMenuItem(entry, behavior?.currentSelection ?? [])
|
|
237
251
|
);
|
|
238
252
|
|
|
239
|
-
dynamicCollection = parseDynamicCollectionOptions(
|
|
240
|
-
content?.dynamicCollection ??
|
|
241
|
-
content?.extensions?.dynamic_collection_options
|
|
242
|
-
);
|
|
253
|
+
dynamicCollection = parseDynamicCollectionOptions(rawDynamicCollection);
|
|
243
254
|
} else if (itemsUrl) {
|
|
244
255
|
log_info(`openBottomSheet: loading items from source: ${itemsUrl}`);
|
|
245
256
|
|
|
@@ -277,6 +288,8 @@ export async function openBottomSheetAction(
|
|
|
277
288
|
dynamicCollection,
|
|
278
289
|
themePluginId,
|
|
279
290
|
styleOverrides: styleOverrides ?? content?.styleOverrides,
|
|
291
|
+
emptyMessage: content?.emptyMessage,
|
|
292
|
+
context,
|
|
280
293
|
},
|
|
281
294
|
};
|
|
282
295
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/// <reference types="@applicaster/applicaster-types" />
|
|
2
|
+
import { showAlertDialog } from "../../alertUtils";
|
|
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
|
+
* Options for the showAlert action.
|
|
14
|
+
*/
|
|
15
|
+
export interface ShowAlertOptions {
|
|
16
|
+
/** Dialog title (required) */
|
|
17
|
+
title: string;
|
|
18
|
+
|
|
19
|
+
/** Dialog message */
|
|
20
|
+
message?: string;
|
|
21
|
+
|
|
22
|
+
/** Dismiss button text. Defaults to "OK" */
|
|
23
|
+
okButtonText?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tells the user something and waits for them to acknowledge it.
|
|
28
|
+
*
|
|
29
|
+
* One button, because there is nothing to decide. Use `confirmDialog` when the
|
|
30
|
+
* user is choosing between going on and backing out; a `confirmDialog` without
|
|
31
|
+
* a cancel label still renders two buttons, the second one blank.
|
|
32
|
+
*
|
|
33
|
+
* Always resolves to Success: dismissing a notice is not a cancellation, so the
|
|
34
|
+
* rest of the chain runs.
|
|
35
|
+
*/
|
|
36
|
+
export const showAlertAction: ActionHandler<ShowAlertOptions> = async (
|
|
37
|
+
action
|
|
38
|
+
): Promise<ActionResult> => {
|
|
39
|
+
log_info("handleAction: showAlert event");
|
|
40
|
+
|
|
41
|
+
const options = action.options as ShowAlertOptions;
|
|
42
|
+
|
|
43
|
+
return new Promise<ActionResult>((resolve) => {
|
|
44
|
+
showAlertDialog({
|
|
45
|
+
title: options?.title,
|
|
46
|
+
message: options?.message,
|
|
47
|
+
okButtonText: options?.okButtonText || "OK",
|
|
48
|
+
completion: () => resolve(ActionResult.Success),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -5,7 +5,7 @@ import { postEvent } from "../../reactHooks/useSubscriberFor";
|
|
|
5
5
|
|
|
6
6
|
export const CONFIRMATION_TOAST_SOURCE = "confirmation";
|
|
7
7
|
|
|
8
|
-
export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT =
|
|
8
|
+
export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 5000;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Shows a mobile confirmation toast with Theme defaults from
|
|
@@ -13,7 +13,7 @@ export const CONFIRMATION_TOAST_DEFAULT_TIMEOUT = 1000;
|
|
|
13
13
|
*
|
|
14
14
|
* This action is confirmation-only by design: it always stamps
|
|
15
15
|
* `source: "confirmation"` (so Theme confirmation styles apply) and defaults
|
|
16
|
-
* `timeout` to
|
|
16
|
+
* `timeout` to 5000 ms. Non-confirmation toasts (e.g. Offline Experience)
|
|
17
17
|
* must post on the shared `"showToast"` event bus directly and omit `source`.
|
|
18
18
|
* Callers may override individual style fields; Theme supplies the rest.
|
|
19
19
|
*
|
|
@@ -2,8 +2,7 @@ import { BehaviorSubject, Observable } from "rxjs";
|
|
|
2
2
|
import { distinctUntilChanged } from "rxjs/operators";
|
|
3
3
|
import { Player } from "../player";
|
|
4
4
|
import { createLogger, utilsLogger } from "../../../logger";
|
|
5
|
-
import {
|
|
6
|
-
import { findPluginByIdentifier } from "../../../pluginUtils";
|
|
5
|
+
import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
7
6
|
import {
|
|
8
7
|
loadFeedEntry,
|
|
9
8
|
loadFeedAndPrefetchThumbnailImage,
|
|
@@ -237,11 +236,8 @@ export class OverlaysObserver {
|
|
|
237
236
|
|
|
238
237
|
preparePlayNext = async () => {
|
|
239
238
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const playNextPlugin: any = findPluginByIdentifier(
|
|
243
|
-
"QuickBrickPlayNextOverlay",
|
|
244
|
-
plugins
|
|
239
|
+
const playNextPlugin: any = getPluginByIdentifier(
|
|
240
|
+
"QuickBrickPlayNextOverlay"
|
|
245
241
|
);
|
|
246
242
|
|
|
247
243
|
if (!playNextPlugin) {
|
|
@@ -252,10 +248,7 @@ export class OverlaysObserver {
|
|
|
252
248
|
return;
|
|
253
249
|
}
|
|
254
250
|
|
|
255
|
-
const playNextFeedUrl: string = await retrieveFeedUrl(
|
|
256
|
-
this.entry,
|
|
257
|
-
plugins
|
|
258
|
-
);
|
|
251
|
+
const playNextFeedUrl: string = await retrieveFeedUrl(this.entry);
|
|
259
252
|
|
|
260
253
|
if (!playNextFeedUrl || typeof playNextFeedUrl !== "string") {
|
|
261
254
|
log_debug(
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getUpcomingQueue } from "../getUpcomingQueue";
|
|
2
|
+
|
|
3
|
+
describe("getUpcomingQueue", () => {
|
|
4
|
+
it("returns null when plugin is not provided or has no module", async () => {
|
|
5
|
+
expect(await getUpcomingQueue(undefined)).toBeNull();
|
|
6
|
+
expect(await getUpcomingQueue({} as any)).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns null when QueueManager is not present on module", async () => {
|
|
10
|
+
const plugin = {
|
|
11
|
+
identifier: "queue-action",
|
|
12
|
+
module: {},
|
|
13
|
+
} as any;
|
|
14
|
+
|
|
15
|
+
expect(await getUpcomingQueue(plugin)).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("calls setupQuickBrickQueue when QueueManager is not initialized", async () => {
|
|
19
|
+
const setupQuickBrickQueue = jest.fn();
|
|
20
|
+
const mockUpcoming = [{ id: "item_1" }];
|
|
21
|
+
|
|
22
|
+
const QueueManager = {
|
|
23
|
+
isInitialized: jest
|
|
24
|
+
.fn()
|
|
25
|
+
.mockReturnValueOnce(false)
|
|
26
|
+
.mockReturnValueOnce(true),
|
|
27
|
+
instance: {
|
|
28
|
+
getUpcomingQueue: jest.fn().mockResolvedValue(mockUpcoming),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const plugin = {
|
|
33
|
+
identifier: "queue-action",
|
|
34
|
+
module: {
|
|
35
|
+
QueueManager,
|
|
36
|
+
setupQuickBrickQueue,
|
|
37
|
+
},
|
|
38
|
+
} as any;
|
|
39
|
+
|
|
40
|
+
const result = await getUpcomingQueue(plugin);
|
|
41
|
+
|
|
42
|
+
expect(setupQuickBrickQueue).toHaveBeenCalled();
|
|
43
|
+
expect(result).toEqual(mockUpcoming);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns upcoming items when QueueManager is already initialized", async () => {
|
|
47
|
+
const mockUpcoming = [{ id: "item_1" }, { id: "item_2" }];
|
|
48
|
+
|
|
49
|
+
const QueueManager = {
|
|
50
|
+
isInitialized: jest.fn().mockReturnValue(true),
|
|
51
|
+
instance: {
|
|
52
|
+
getUpcomingQueue: jest.fn().mockResolvedValue(mockUpcoming),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const plugin = {
|
|
57
|
+
identifier: "queue-action",
|
|
58
|
+
module: {
|
|
59
|
+
QueueManager,
|
|
60
|
+
},
|
|
61
|
+
} as any;
|
|
62
|
+
|
|
63
|
+
const result = await getUpcomingQueue(plugin);
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual(mockUpcoming);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { retrieveFeedUrl, retrievePlayPreviousFeedUrl } from "../utils";
|
|
2
2
|
|
|
3
|
+
// mock for import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
4
|
+
jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
|
|
5
|
+
appStore: {
|
|
6
|
+
get: jest.fn(() => []),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
3
10
|
const entryWith = (extensions: Record<string, unknown>): ZappEntry =>
|
|
4
11
|
({ id: "1", type: { value: "video" }, extensions }) as ZappEntry;
|
|
5
12
|
|
|
@@ -7,14 +14,14 @@ describe("playlist feed url accessors", () => {
|
|
|
7
14
|
it("returns the next feed url when present", async () => {
|
|
8
15
|
const entry = entryWith({ play_next_feed_url: "https://feeds/next.json" });
|
|
9
16
|
|
|
10
|
-
await expect(retrieveFeedUrl(entry
|
|
17
|
+
await expect(retrieveFeedUrl(entry)).resolves.toBe(
|
|
11
18
|
"https://feeds/next.json"
|
|
12
19
|
);
|
|
13
20
|
});
|
|
14
21
|
|
|
15
22
|
it("resolves to null for a missing entry", async () => {
|
|
16
23
|
await expect(
|
|
17
|
-
retrieveFeedUrl(undefined as unknown as ZappEntry
|
|
24
|
+
retrieveFeedUrl(undefined as unknown as ZappEntry)
|
|
18
25
|
).resolves.toBeNull();
|
|
19
26
|
});
|
|
20
27
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
export const getUpcomingQueue = async (
|
|
2
2
|
plugin?: QuickBrickPlugin | ZappPlugin
|
|
3
3
|
): Promise<unknown[] | null> => {
|
|
4
|
-
const
|
|
4
|
+
const module = (plugin as any)?.module;
|
|
5
|
+
const QueueManager = module?.QueueManager;
|
|
6
|
+
const setupQuickBrickQueue = module?.setupQuickBrickQueue;
|
|
5
7
|
|
|
6
|
-
if (!QueueManager
|
|
8
|
+
if (!QueueManager) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
!QueueManager.isInitialized?.() &&
|
|
14
|
+
typeof setupQuickBrickQueue === "function"
|
|
15
|
+
) {
|
|
16
|
+
setupQuickBrickQueue();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!QueueManager.isInitialized?.()) {
|
|
7
20
|
return null;
|
|
8
21
|
}
|
|
9
22
|
|
|
@@ -59,13 +59,10 @@ export const parseTimeToSeconds = (timeStr: string): number => {
|
|
|
59
59
|
* on playerManager, decoupling Queue playback logic entirely from OverlaysObserver (which should
|
|
60
60
|
* strictly focus on recommendation visuals and chapter markers).
|
|
61
61
|
*/
|
|
62
|
-
export const retrieveFeedUrl = async (
|
|
63
|
-
entry: ZappEntry,
|
|
64
|
-
plugins?: ZappPlugin[]
|
|
65
|
-
): Promise<string> => {
|
|
62
|
+
export const retrieveFeedUrl = async (entry: ZappEntry): Promise<string> => {
|
|
66
63
|
if (!entry) return null;
|
|
67
64
|
|
|
68
|
-
const availablePlugins =
|
|
65
|
+
const availablePlugins = appStore.get("plugins");
|
|
69
66
|
|
|
70
67
|
const queuePlugin = findPluginByIdentifier(
|
|
71
68
|
"queue-action",
|
|
@@ -36,3 +36,79 @@ describe("PlayerNative.isAudioItem", () => {
|
|
|
36
36
|
expect(playerWithEntry(null).isAudioItem()).toBe(false);
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
describe("PlayerNative.command", () => {
|
|
41
|
+
it("invokes native function with playerId on iOS platform", () => {
|
|
42
|
+
const player = new PlayerNative({
|
|
43
|
+
entry: null,
|
|
44
|
+
player: { current: {} } as any,
|
|
45
|
+
playerId: "test-player",
|
|
46
|
+
playerPluginId: "QuickBrickPlayerPlugin",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const commandSpy = jest.fn();
|
|
50
|
+
|
|
51
|
+
jest.spyOn(player, "getPlayerModule").mockReturnValue({
|
|
52
|
+
command: commandSpy,
|
|
53
|
+
} as any);
|
|
54
|
+
|
|
55
|
+
player.command("setPlayerControls", { transport: ["play"] });
|
|
56
|
+
|
|
57
|
+
expect(commandSpy).toHaveBeenCalledWith(
|
|
58
|
+
"setPlayerControls",
|
|
59
|
+
"test-player",
|
|
60
|
+
{
|
|
61
|
+
transport: ["play"],
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("invokes native function with view tag on Android when method exists", () => {
|
|
67
|
+
const reactUtils = require("../../../reactUtils");
|
|
68
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(false);
|
|
69
|
+
|
|
70
|
+
const player = new PlayerNative({
|
|
71
|
+
entry: null,
|
|
72
|
+
player: { current: {} } as any,
|
|
73
|
+
playerId: "test-player",
|
|
74
|
+
playerPluginId: "QuickBrickPlayerPlugin",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const commandSpy = jest.fn();
|
|
78
|
+
|
|
79
|
+
jest.spyOn(player, "getPlayerModule").mockReturnValue({
|
|
80
|
+
command: commandSpy,
|
|
81
|
+
} as any);
|
|
82
|
+
|
|
83
|
+
jest.spyOn(player, "getComponentNode").mockReturnValue(12345 as any);
|
|
84
|
+
|
|
85
|
+
player.command("setPlayerControls", { transport: ["play"] });
|
|
86
|
+
|
|
87
|
+
expect(commandSpy).toHaveBeenCalledWith(12345, "setPlayerControls", {
|
|
88
|
+
transport: ["play"],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
(reactUtils.isApplePlatform as jest.Mock).mockRestore();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("handles when native module does not have command method on Android", () => {
|
|
95
|
+
const reactUtils = require("../../../reactUtils");
|
|
96
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(false);
|
|
97
|
+
|
|
98
|
+
const player = new PlayerNative({
|
|
99
|
+
entry: null,
|
|
100
|
+
player: { current: {} } as any,
|
|
101
|
+
playerId: "test-player",
|
|
102
|
+
playerPluginId: "KalturaPlayerPlugin",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
jest.spyOn(player, "getPlayerModule").mockReturnValue({} as any);
|
|
106
|
+
jest.spyOn(player, "getComponentNode").mockReturnValue(12345 as any);
|
|
107
|
+
|
|
108
|
+
expect(() => {
|
|
109
|
+
player.command("setPlayerControls", { transport: ["play"] });
|
|
110
|
+
}).not.toThrow();
|
|
111
|
+
|
|
112
|
+
(reactUtils.isApplePlatform as jest.Mock).mockRestore();
|
|
113
|
+
});
|
|
114
|
+
});
|