@applicaster/zapp-react-native-utils 16.0.0-alpha.1593888345 → 16.0.0-alpha.1733304298
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 +3 -0
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
- package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
- package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -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 +160 -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/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +21 -6
- package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +1 -0
- package/actionsExecutor/feedDecorator.ts +6 -0
- package/analyticsUtils/__tests__/playerAnalyticsSafeguard.test.ts +62 -0
- package/analyticsUtils/playerAnalyticsTracker.ts +1 -0
- package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
- package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
- package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
- package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
- package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
- package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
- package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
- package/appUtils/contextKeysManager/index.ts +28 -129
- 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__/playerDiagnostics.test.ts +62 -0
- 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 +281 -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 +25 -0
- package/appUtils/playerManager/playerNative.ts +22 -0
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +196 -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 +40 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +189 -46
- package/configurationUtils/modalBlocksParser.ts +57 -101
- 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/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/defaultManifestConfigurations/player.js +8 -0
- package/manifestUtils/keys.js +7 -2
- 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 +2 -0
- package/modalState/RemoteEditableCollection.ts +9 -1
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +39 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/index.ts +2 -0
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +14 -0
- package/modalState/useBottomSheetContent.ts +17 -12
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +3 -2
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/quickBrickAssets/__tests__/index.test.ts +18 -0
- package/quickBrickAssets/index.ts +35 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +29 -23
- 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__/feedInvalidation.test.tsx +338 -0
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useFeedLoader.tsx +28 -1
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
- package/reactHooks/navigation/useRoute.ts +8 -4
- package/reactHooks/usePluginConfiguration.ts +84 -46
- package/riverComponetsMeasurementProvider/index.tsx +3 -1
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
- package/uiActionsRegistrator/registry.ts +16 -0
- package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
- package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
|
@@ -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",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Player } from "../player";
|
|
2
|
+
|
|
3
|
+
const entry = (id: string) => ({ id, title: id }) as unknown as ZappEntry;
|
|
4
|
+
|
|
5
|
+
const makePlayer = () => {
|
|
6
|
+
const player = new Player({ playerId: "test-player" });
|
|
7
|
+
player.entry = entry("first");
|
|
8
|
+
|
|
9
|
+
return player;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("Player per-entry diagnostics", () => {
|
|
13
|
+
it("keeps the latest of onError and onVideoError, and clears it when a new entry loads", () => {
|
|
14
|
+
const player = makePlayer();
|
|
15
|
+
const second = { message: "second" };
|
|
16
|
+
|
|
17
|
+
player.onError({ message: "first" } as any);
|
|
18
|
+
player.onVideoError(second as any);
|
|
19
|
+
|
|
20
|
+
expect(player.lastError).toBe(second);
|
|
21
|
+
|
|
22
|
+
player.entry = entry("second");
|
|
23
|
+
|
|
24
|
+
expect(player.lastError).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reads the next entry without creating the overlay observer", () => {
|
|
28
|
+
const player = makePlayer();
|
|
29
|
+
|
|
30
|
+
expect(player.getPlayNextEntry()).toBeNull();
|
|
31
|
+
|
|
32
|
+
jest
|
|
33
|
+
.spyOn(player.getOverlayObservable(), "getPlayNextEntry")
|
|
34
|
+
.mockReturnValue(entry("next"));
|
|
35
|
+
|
|
36
|
+
expect(player.getPlayNextEntry()).toEqual(entry("next"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps the content clock while a client-side ad plays", () => {
|
|
40
|
+
const player = makePlayer();
|
|
41
|
+
|
|
42
|
+
player.onVideoProgress({
|
|
43
|
+
currentTime: 300,
|
|
44
|
+
duration: 1800,
|
|
45
|
+
seekableDuration: 1800,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
player.onAdBegin({});
|
|
49
|
+
|
|
50
|
+
player.onVideoProgress({
|
|
51
|
+
currentTime: 5,
|
|
52
|
+
duration: 15,
|
|
53
|
+
seekableDuration: 15,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(player.getPosition()).toBe(5);
|
|
57
|
+
expect(player.getDuration()).toBe(15);
|
|
58
|
+
expect(player.getContentPosition()).toBe(300);
|
|
59
|
+
expect(player.getContentDuration()).toBe(1800);
|
|
60
|
+
expect(player.getSeekableDuration()).toBe(1800);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -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,281 @@
|
|
|
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
|
+
isLive: () => false,
|
|
22
|
+
getPlayNextEntry: () => null,
|
|
23
|
+
playNextHandler: jest.fn(),
|
|
24
|
+
seekTo: jest.fn(),
|
|
25
|
+
...overrides,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const manager = (active) =>
|
|
29
|
+
({ getActivePlayer: () => active, isCasting: () => false }) as any;
|
|
30
|
+
|
|
31
|
+
const registeredTools = async (active) => {
|
|
32
|
+
mockRegister.mockResolvedValue(undefined);
|
|
33
|
+
mockXray = { registerReactToolbox: mockRegister };
|
|
34
|
+
await loadToolbox().registerPlayerToolbox(manager(active));
|
|
35
|
+
|
|
36
|
+
const { tools } = mockRegister.mock.lastCall[0];
|
|
37
|
+
|
|
38
|
+
return Object.fromEntries(tools.map((tool) => [tool.name, tool.handler]));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("playerToolbox", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockRegister.mockReset();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("does nothing when X-Ray has no React toolbox API", async () => {
|
|
47
|
+
mockXray = {};
|
|
48
|
+
|
|
49
|
+
await expect(
|
|
50
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
51
|
+
).resolves.toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does nothing when getXray returns null", async () => {
|
|
55
|
+
mockXray = null;
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
59
|
+
).resolves.toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("swallows a failed registration", async () => {
|
|
63
|
+
mockRegister.mockRejectedValue(new Error("no native bridge"));
|
|
64
|
+
mockXray = { registerReactToolbox: mockRegister };
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
loadToolbox().registerPlayerToolbox(manager(null))
|
|
68
|
+
).resolves.toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("seeks to an absolute position, from the end when negative, clamped", async () => {
|
|
72
|
+
const active = player();
|
|
73
|
+
const tools = await registeredTools(active);
|
|
74
|
+
|
|
75
|
+
expect(tools.seek({ position: 30 })).toEqual({ position: 30 });
|
|
76
|
+
expect(tools.seek({ position: -10 })).toEqual({ position: 90 });
|
|
77
|
+
expect(tools.seek({ position: 500 })).toEqual({ position: 100 });
|
|
78
|
+
expect(tools.seek({ position: -500 })).toEqual({ position: 0 });
|
|
79
|
+
expect(active.seekTo.mock.calls).toEqual([[30], [90], [100], [0]]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("offsets from the current position and refuses zero", async () => {
|
|
83
|
+
const tools = await registeredTools(player());
|
|
84
|
+
|
|
85
|
+
expect(tools.offset({ seconds: 15 })).toEqual({ from: 40, position: 55 });
|
|
86
|
+
expect(tools.offset({ seconds: -60 })).toEqual({ from: 40, position: 0 });
|
|
87
|
+
|
|
88
|
+
expect(() => tools.offset({ seconds: 0 })).toThrow(
|
|
89
|
+
"Ignored zero relative seek"
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("state", () => {
|
|
94
|
+
const playing = (overrides = {}, entry: any = { id: "e1", title: "T" }) =>
|
|
95
|
+
player({
|
|
96
|
+
playerId: "p1",
|
|
97
|
+
playerRole: "Primary",
|
|
98
|
+
getEntry: () => entry,
|
|
99
|
+
getPosition: () => 40,
|
|
100
|
+
getDuration: () => 100,
|
|
101
|
+
getContentDuration: () => 100,
|
|
102
|
+
isReadyToPlay: () => true,
|
|
103
|
+
isPlaying: () => true,
|
|
104
|
+
isBuffering: () => false,
|
|
105
|
+
isAd: () => false,
|
|
106
|
+
isLive: () => false,
|
|
107
|
+
getIsMuted: () => false,
|
|
108
|
+
...overrides,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const state = async (overrides = {}, entry?: any) =>
|
|
112
|
+
(await registeredTools(playing(overrides, entry))).state();
|
|
113
|
+
|
|
114
|
+
it("reports inactive when nothing plays", async () => {
|
|
115
|
+
expect((await registeredTools(null)).state()).toEqual({
|
|
116
|
+
active: false,
|
|
117
|
+
casting: false,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("keeps the existing fields and adds the content clock for a VOD", async () => {
|
|
122
|
+
expect(await state()).toEqual({
|
|
123
|
+
active: true,
|
|
124
|
+
playerId: "p1",
|
|
125
|
+
entry: { id: "e1", title: "T" },
|
|
126
|
+
position: 40,
|
|
127
|
+
duration: 100,
|
|
128
|
+
playing: true,
|
|
129
|
+
buffering: false,
|
|
130
|
+
readyToPlay: true,
|
|
131
|
+
ad: false,
|
|
132
|
+
live: false,
|
|
133
|
+
muted: false,
|
|
134
|
+
role: "Primary",
|
|
135
|
+
plugin: null,
|
|
136
|
+
engine: null,
|
|
137
|
+
casting: false,
|
|
138
|
+
seekableDuration: 100,
|
|
139
|
+
contentPosition: 40,
|
|
140
|
+
contentDuration: 100,
|
|
141
|
+
drm: null,
|
|
142
|
+
lastError: null,
|
|
143
|
+
next: null,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("reports the content's clock next to the ad's during a client-side ad", async () => {
|
|
148
|
+
const result = await state({
|
|
149
|
+
isAd: () => true,
|
|
150
|
+
getPosition: () => 5,
|
|
151
|
+
getDuration: () => 15,
|
|
152
|
+
getContentPosition: () => 300,
|
|
153
|
+
getContentDuration: () => 1800,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result).toMatchObject({
|
|
157
|
+
ad: true,
|
|
158
|
+
position: 5,
|
|
159
|
+
duration: 15,
|
|
160
|
+
contentPosition: 300,
|
|
161
|
+
contentDuration: 1800,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("reports non-finite numbers as null", async () => {
|
|
166
|
+
expect(
|
|
167
|
+
await state({
|
|
168
|
+
getSeekableDuration: () => NaN,
|
|
169
|
+
getContentPosition: () => null,
|
|
170
|
+
getContentDuration: () => Infinity,
|
|
171
|
+
})
|
|
172
|
+
).toMatchObject({
|
|
173
|
+
seekableDuration: null,
|
|
174
|
+
contentPosition: null,
|
|
175
|
+
contentDuration: null,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("reports the player plugin and its web engine", async () => {
|
|
180
|
+
expect(
|
|
181
|
+
await state({
|
|
182
|
+
playerPluginId: "QuickBrickPlayerPlugin",
|
|
183
|
+
engine: "hls.js",
|
|
184
|
+
})
|
|
185
|
+
).toMatchObject({ plugin: "QuickBrickPlayerPlugin", engine: "hls.js" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("lists the entry's DRM key systems, or null", async () => {
|
|
189
|
+
const drm = { widevine: { license_url: "w" }, playready: {} };
|
|
190
|
+
|
|
191
|
+
expect((await state({}, { id: "e", extensions: { drm } })).drm).toEqual([
|
|
192
|
+
"widevine",
|
|
193
|
+
"playready",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect((await state({}, { id: "e", extensions: { drm: {} } })).drm).toBe(
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect((await state({}, { id: "e", extensions: {} })).drm).toBe(null);
|
|
201
|
+
expect((await state({}, null)).drm).toBe(null);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("reports no last error as null", async () => {
|
|
205
|
+
expect((await state({ lastError: null })).lastError).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("play next", () => {
|
|
210
|
+
const nextEntry = { id: "n1", title: "Next", extensions: {} };
|
|
211
|
+
|
|
212
|
+
const stateWith = async (overrides) =>
|
|
213
|
+
(
|
|
214
|
+
await registeredTools(
|
|
215
|
+
player({
|
|
216
|
+
getEntry: () => null,
|
|
217
|
+
getPosition: () => 0,
|
|
218
|
+
getDuration: () => 0,
|
|
219
|
+
getContentDuration: () => 0,
|
|
220
|
+
isReadyToPlay: () => true,
|
|
221
|
+
isPlaying: () => true,
|
|
222
|
+
isBuffering: () => false,
|
|
223
|
+
isAd: () => false,
|
|
224
|
+
getIsMuted: () => false,
|
|
225
|
+
...overrides,
|
|
226
|
+
})
|
|
227
|
+
)
|
|
228
|
+
).state();
|
|
229
|
+
|
|
230
|
+
it("reports the entry Play Next would play", async () => {
|
|
231
|
+
expect(
|
|
232
|
+
(await stateWith({ getPlayNextEntry: () => nextEntry })).next
|
|
233
|
+
).toEqual({ id: "n1", title: "Next" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("reports no next entry as null", async () => {
|
|
237
|
+
expect((await stateWith({})).next).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("plays the next entry through the player's Play Next action", async () => {
|
|
241
|
+
const active = player({ getPlayNextEntry: () => nextEntry });
|
|
242
|
+
const tools = await registeredTools(active);
|
|
243
|
+
|
|
244
|
+
expect(tools.next()).toEqual({ id: "n1", title: "Next" });
|
|
245
|
+
expect(active.playNextHandler).toHaveBeenCalledTimes(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("fails without a next entry", async () => {
|
|
249
|
+
const active = player();
|
|
250
|
+
const tools = await registeredTools(active);
|
|
251
|
+
|
|
252
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
253
|
+
expect(active.playNextHandler).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("fails when no Play Next action is registered", async () => {
|
|
257
|
+
const tools = await registeredTools(
|
|
258
|
+
player({
|
|
259
|
+
getPlayNextEntry: () => nextEntry,
|
|
260
|
+
playNextHandler: null,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("fails without an active player", async () => {
|
|
268
|
+
const tools = await registeredTools(null);
|
|
269
|
+
|
|
270
|
+
expect(() => tools.next()).toThrow("No active player");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("refuses to seek a stream that is not seekable", async () => {
|
|
275
|
+
const tools = await registeredTools(
|
|
276
|
+
player({ getSeekableDuration: () => 0 })
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
expect(() => tools.seek({ position: 10 })).toThrow("Seek is not supported");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
|
|
3
|
+
import { usePlayerControllerSetup } from "../usePlayerControllerSetup";
|
|
4
|
+
import { PlayerAnalyticsManager } from "../../../analyticsUtils/PlayerAnalyticsManager";
|
|
5
|
+
|
|
6
|
+
const calls: string[] = [];
|
|
7
|
+
|
|
8
|
+
jest.mock("../index", () => ({
|
|
9
|
+
playerManager: {
|
|
10
|
+
isPlayerRegistered: jest.fn(() => false),
|
|
11
|
+
registerPlayer: jest.fn(() => calls.push("registerPlayer")),
|
|
12
|
+
unregisterPlayer: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../../../analyticsUtils/PlayerAnalyticsManager", () => ({
|
|
17
|
+
PlayerAnalyticsManager: {
|
|
18
|
+
getInstance: jest.fn(() => calls.push("analyticsManager")),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("../../../reactHooks/useConfiguration", () => ({
|
|
23
|
+
useConfiguration: () => ({}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("../../../playerUtils/useValidatePlayerConfig", () => ({
|
|
27
|
+
useValidatePlayerConfig: jest.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("../../../reactUtils", () => ({
|
|
31
|
+
isTV: () => false,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock(
|
|
35
|
+
"@applicaster/zapp-react-native-ui-components/Helpers/Platform",
|
|
36
|
+
() => ({
|
|
37
|
+
isAppleTV: () => false,
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
class Controller {
|
|
42
|
+
destroy = jest.fn();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("usePlayerControllerSetup", () => {
|
|
46
|
+
it("creates the analytics manager before the player registers", () => {
|
|
47
|
+
renderHook(() =>
|
|
48
|
+
usePlayerControllerSetup({
|
|
49
|
+
props: { playerId: "player-1", entry: { id: "entry-1" } },
|
|
50
|
+
ControllerType: Controller,
|
|
51
|
+
ref: null,
|
|
52
|
+
manifest: {},
|
|
53
|
+
pluginId: "plugin",
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// The manager attaches analytics to players as they register, so a player
|
|
58
|
+
// registered before it exists would send no player analytics events
|
|
59
|
+
expect(PlayerAnalyticsManager.getInstance).toHaveBeenCalled();
|
|
60
|
+
expect(calls).toEqual(["analyticsManager", "registerPlayer"]);
|
|
61
|
+
});
|
|
62
|
+
});
|