@applicaster/zapp-react-native-utils 16.0.0-alpha.1334507601 → 16.0.0-alpha.1444784746
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/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
- package/actionsExecutor/actions/navigateToScreen.ts +39 -47
- package/actionsExecutor/actions/openBottomSheet.ts +14 -5
- 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/__tests__/playerToolbox.test.ts +97 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/index.ts +2 -0
- package/appUtils/playerManager/player.ts +10 -0
- package/appUtils/playerManager/playerNative.ts +22 -0
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +192 -0
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +61 -0
- package/configurationUtils/modalBlocksParser.ts +14 -0
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
- package/modalState/types.ts +2 -0
- package/package.json +2 -2
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
- package/uiActionsRegistrator/registry.ts +16 -0
|
@@ -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
|
|
|
@@ -59,13 +59,6 @@ export function createNavigateToScreenAction(
|
|
|
59
59
|
context?: ActionExecutionContext
|
|
60
60
|
): Promise<ActionResult> {
|
|
61
61
|
const screenType = action.options?.typeMapping;
|
|
62
|
-
|
|
63
|
-
if (!screenType) {
|
|
64
|
-
log_error("navigateToScreen: typeMapping option is missing");
|
|
65
|
-
|
|
66
|
-
return ActionResult.Error;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
62
|
const navigationAction = action.options?.navigationAction;
|
|
70
63
|
const entrySource = action.options?.entry;
|
|
71
64
|
const navigationOptions = action.options?.navigationOptions;
|
|
@@ -76,61 +69,60 @@ export function createNavigateToScreenAction(
|
|
|
76
69
|
: entrySource
|
|
77
70
|
: null;
|
|
78
71
|
|
|
72
|
+
let navigate =
|
|
73
|
+
navigationAction === "push" ? navigator.push : navigator.replace;
|
|
74
|
+
|
|
75
|
+
let navigaingEntry = entry;
|
|
76
|
+
|
|
79
77
|
if (entry) {
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
`navigateToScreen:
|
|
78
|
+
if (screenType) {
|
|
79
|
+
log_info(
|
|
80
|
+
`navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
|
|
83
81
|
);
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
navigaingEntry = {
|
|
84
|
+
...entry,
|
|
85
|
+
type: {
|
|
86
|
+
value: screenType,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (navigationAction === "push") {
|
|
91
|
+
navigate = navigator.push;
|
|
92
|
+
} else {
|
|
93
|
+
// Use replaceTop for entries since replace() only accepts ZappRiver
|
|
94
|
+
navigate = navigator.replaceTop;
|
|
95
|
+
}
|
|
86
96
|
}
|
|
97
|
+
} else {
|
|
98
|
+
if (!screenType) {
|
|
99
|
+
log_error("navigateToScreen: typeMapping and entry option is missing");
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
`navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const overriddenEntry: ZappEntry = {
|
|
93
|
-
...entry,
|
|
94
|
-
type: {
|
|
95
|
-
value: screenType,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (navigationAction === "push") {
|
|
100
|
-
navigator.push(overriddenEntry, navigationOptions);
|
|
101
|
-
} else {
|
|
102
|
-
// Use replaceTop for entries since replace() only accepts ZappRiver
|
|
103
|
-
navigator.replaceTop(overriddenEntry, navigationOptions);
|
|
101
|
+
return ActionResult.Error;
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const screenId = contentTypes?.[screenType]?.screen_id || null;
|
|
104
|
+
const screenId = contentTypes?.[screenType]?.screen_id || null;
|
|
105
|
+
const river = rivers?.[screenId];
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (!screenId) {
|
|
108
|
+
log_error(
|
|
109
|
+
`navigateToScreen: can not resolve screen type mapping: ${screenType}`
|
|
110
|
+
);
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
return ActionResult.Error;
|
|
113
|
+
}
|
|
118
114
|
|
|
119
|
-
|
|
115
|
+
if (!river) {
|
|
116
|
+
log_error("navigateToScreen: can not resolve river");
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
return ActionResult.Error;
|
|
119
|
+
}
|
|
123
120
|
|
|
124
|
-
|
|
121
|
+
navigaingEntry = river;
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
context?.callback?.({ success: false, error: null, abort: true });
|
|
128
|
-
|
|
129
|
-
if (navigationAction === "push") {
|
|
130
|
-
navigator.push(river, navigationOptions);
|
|
131
|
-
} else {
|
|
132
|
-
navigator.replace(river, navigationOptions);
|
|
133
|
-
}
|
|
125
|
+
navigate(navigaingEntry, navigationOptions);
|
|
134
126
|
|
|
135
127
|
return ActionResult.Success;
|
|
136
128
|
};
|
|
@@ -227,7 +227,18 @@ export async function openBottomSheetAction(
|
|
|
227
227
|
|
|
228
228
|
let dynamicCollection: DynamicCollectionOptions | undefined;
|
|
229
229
|
|
|
230
|
-
|
|
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
|
+
) {
|
|
231
242
|
log_info("openBottomSheet: using inline items from action options", {
|
|
232
243
|
count: inlineItems.length,
|
|
233
244
|
});
|
|
@@ -239,10 +250,7 @@ export async function openBottomSheetAction(
|
|
|
239
250
|
entryToMenuItem(entry, behavior?.currentSelection ?? [])
|
|
240
251
|
);
|
|
241
252
|
|
|
242
|
-
dynamicCollection = parseDynamicCollectionOptions(
|
|
243
|
-
content?.dynamicCollection ??
|
|
244
|
-
content?.extensions?.dynamic_collection_options
|
|
245
|
-
);
|
|
253
|
+
dynamicCollection = parseDynamicCollectionOptions(rawDynamicCollection);
|
|
246
254
|
} else if (itemsUrl) {
|
|
247
255
|
log_info(`openBottomSheet: loading items from source: ${itemsUrl}`);
|
|
248
256
|
|
|
@@ -280,6 +288,7 @@ export async function openBottomSheetAction(
|
|
|
280
288
|
dynamicCollection,
|
|
281
289
|
themePluginId,
|
|
282
290
|
styleOverrides: styleOverrides ?? content?.styleOverrides,
|
|
291
|
+
emptyMessage: content?.emptyMessage,
|
|
283
292
|
context,
|
|
284
293
|
},
|
|
285
294
|
};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NativeModules } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
PlayerNativeCommandTypes,
|
|
4
|
+
PlayerNativeSendCommand,
|
|
5
|
+
} from "../playerNativeCommand";
|
|
6
|
+
|
|
7
|
+
describe("PlayerNativeSendCommand", () => {
|
|
8
|
+
const reactUtils = require("../../../reactUtils");
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("calls NativeModules.PlayerModule.command on Apple platform with playerId", () => {
|
|
15
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(true);
|
|
16
|
+
|
|
17
|
+
NativeModules.PlayerModule = {
|
|
18
|
+
command: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
PlayerNativeSendCommand(
|
|
22
|
+
PlayerNativeCommandTypes.setPlayerControls,
|
|
23
|
+
"player-123",
|
|
24
|
+
{ transport: ["play"] }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(NativeModules.PlayerModule.command).toHaveBeenCalledWith(
|
|
28
|
+
"setPlayerControls",
|
|
29
|
+
"player-123",
|
|
30
|
+
{ transport: ["play"] }
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not call NativeModules.PlayerModule.command on non-Apple platform", () => {
|
|
35
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(false);
|
|
36
|
+
|
|
37
|
+
NativeModules.PlayerModule = {
|
|
38
|
+
command: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
PlayerNativeSendCommand(
|
|
42
|
+
PlayerNativeCommandTypes.setPlayerControls,
|
|
43
|
+
"player-123",
|
|
44
|
+
{ transport: ["play"] }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(NativeModules.PlayerModule.command).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does nothing when playerId or command is missing", () => {
|
|
51
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(true);
|
|
52
|
+
|
|
53
|
+
NativeModules.PlayerModule = {
|
|
54
|
+
command: jest.fn(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
PlayerNativeSendCommand("", "player-123");
|
|
58
|
+
PlayerNativeSendCommand("setPlayerControls", "");
|
|
59
|
+
|
|
60
|
+
expect(NativeModules.PlayerModule.command).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const mockRegister = jest.fn();
|
|
2
|
+
let mockXray: Record<string, unknown> | null = {};
|
|
3
|
+
|
|
4
|
+
jest.mock("../../../logger", () => ({
|
|
5
|
+
getXray: () => mockXray,
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const loadToolbox = () => {
|
|
9
|
+
let module;
|
|
10
|
+
|
|
11
|
+
jest.isolateModules(() => {
|
|
12
|
+
module = require("../playerToolbox");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return module as typeof import("../playerToolbox");
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const player = (overrides = {}) => ({
|
|
19
|
+
getSeekableDuration: () => 100,
|
|
20
|
+
getContentPosition: () => 40,
|
|
21
|
+
seekTo: jest.fn(),
|
|
22
|
+
...overrides,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const manager = (active) =>
|
|
26
|
+
({ getActivePlayer: () => active, isCasting: () => false }) as any;
|
|
27
|
+
|
|
28
|
+
const registeredTools = async (active) => {
|
|
29
|
+
mockRegister.mockResolvedValue(undefined);
|
|
30
|
+
mockXray = { registerReactToolbox: mockRegister };
|
|
31
|
+
await loadToolbox().registerPlayerToolbox(manager(active));
|
|
32
|
+
|
|
33
|
+
const { tools } = mockRegister.mock.calls[0][0];
|
|
34
|
+
|
|
35
|
+
return Object.fromEntries(tools.map((tool) => [tool.name, tool.handler]));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("playerToolbox", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockRegister.mockReset();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does nothing when X-Ray has no React toolbox API", async () => {
|
|
44
|
+
mockXray = {};
|
|
45
|
+
|
|
46
|
+
await expect(
|
|
47
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
48
|
+
).resolves.toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does nothing when getXray returns null", async () => {
|
|
52
|
+
mockXray = null;
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
56
|
+
).resolves.toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("swallows a failed registration", async () => {
|
|
60
|
+
mockRegister.mockRejectedValue(new Error("no native bridge"));
|
|
61
|
+
mockXray = { registerReactToolbox: mockRegister };
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
65
|
+
).resolves.toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("seeks to an absolute position, from the end when negative, clamped", async () => {
|
|
69
|
+
const active = player();
|
|
70
|
+
const tools = await registeredTools(active);
|
|
71
|
+
|
|
72
|
+
expect(tools.seek({ position: 30 })).toEqual({ position: 30 });
|
|
73
|
+
expect(tools.seek({ position: -10 })).toEqual({ position: 90 });
|
|
74
|
+
expect(tools.seek({ position: 500 })).toEqual({ position: 100 });
|
|
75
|
+
expect(tools.seek({ position: -500 })).toEqual({ position: 0 });
|
|
76
|
+
expect(active.seekTo.mock.calls).toEqual([[30], [90], [100], [0]]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("offsets from the current position and refuses zero", async () => {
|
|
80
|
+
const tools = await registeredTools(player());
|
|
81
|
+
|
|
82
|
+
expect(tools.offset({ seconds: 15 })).toEqual({ from: 40, position: 55 });
|
|
83
|
+
expect(tools.offset({ seconds: -60 })).toEqual({ from: 40, position: 0 });
|
|
84
|
+
|
|
85
|
+
expect(() => tools.offset({ seconds: 0 })).toThrow(
|
|
86
|
+
"Ignored zero relative seek"
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("refuses to seek a stream that is not seekable", async () => {
|
|
91
|
+
const tools = await registeredTools(
|
|
92
|
+
player({ getSeekableDuration: () => 0 })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(() => tools.seek({ position: 10 })).toThrow("Seek is not supported");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -59,6 +59,19 @@ export enum SharedPlayerCallBacksKeys {
|
|
|
59
59
|
// PIP
|
|
60
60
|
OnPlayerDetached = "onPlayerDetached",
|
|
61
61
|
OnPlayerAttached = "onPlayerAttached",
|
|
62
|
+
|
|
63
|
+
OnRemoteCommand = "onRemoteCommand",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Ids native sends in onRemoteCommand (lock screen / Control Center / CarPlay buttons)
|
|
67
|
+
export enum RemoteCommand {
|
|
68
|
+
Play = "play",
|
|
69
|
+
Pause = "pause",
|
|
70
|
+
Stop = "stop",
|
|
71
|
+
NextTrack = "nextTrack",
|
|
72
|
+
PreviousTrack = "previousTrack",
|
|
73
|
+
SkipForward = "skipForward",
|
|
74
|
+
SkipBackward = "skipBackward",
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export enum PlayerRole {
|