@applicaster/zapp-react-native-utils 16.0.0-alpha.1929433645 → 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.
Files changed (46) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
  3. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  4. package/actionsExecutor/actions/navigateToScreen.ts +29 -45
  5. package/actionsExecutor/actions/openBottomSheet.ts +1 -0
  6. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  7. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  8. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  9. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  10. package/appUtils/playerManager/index.ts +7 -0
  11. package/appUtils/playerManager/player.ts +1 -0
  12. package/appUtils/playerManager/playerNative.ts +22 -0
  13. package/appUtils/playerManager/playerNativeCommand.ts +2 -6
  14. package/appUtils/playerManager/playerToolbox.ts +157 -0
  15. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  16. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  17. package/arrayUtils/index.ts +26 -43
  18. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  19. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  20. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  21. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  22. package/cellUtils/index.ts +74 -19
  23. package/configurationUtils/__tests__/modalBlocksParser.test.ts +123 -0
  24. package/configurationUtils/index.ts +77 -73
  25. package/configurationUtils/modalBlocksParser.ts +38 -0
  26. package/focusManager/FocusManager.ts +26 -0
  27. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  28. package/localizationUtils/index.ts +12 -6
  29. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  30. package/logger/xrayToolbox.ts +67 -0
  31. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  32. package/manifestUtils/keys.js +6 -1
  33. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  34. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  35. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  36. package/navigationUtils/xrayNavigator.ts +12 -0
  37. package/package.json +2 -2
  38. package/perfTracker.ts +203 -0
  39. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  40. package/reactHooks/connection/index.tsx +8 -5
  41. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  42. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  43. package/reactHooks/usePluginConfiguration.ts +84 -46
  44. package/riverComponetsMeasurementProvider/index.tsx +8 -1
  45. package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
  46. package/uiActionsRegistrator/registry.ts +16 -0
@@ -3,5 +3,7 @@ import { isTV } from "../reactUtils";
3
3
 
4
4
  export const resolveDefaultAssetUri = (source: string | number): string =>
5
5
  isTV()
6
- ? (source as string)
6
+ ? typeof source === "number"
7
+ ? Image.resolveAssetSource(source)?.uri || ""
8
+ : (source as string)
7
9
  : Image.resolveAssetSource(source as number)?.uri || "";
@@ -83,9 +83,7 @@ describe("navigateToScreen navigationOptions", () => {
83
83
  );
84
84
  });
85
85
 
86
- // Entries cannot go through replace(), which only accepts a river, so the
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.replaceTop).toHaveBeenCalledWith(
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.replace).not.toHaveBeenCalled();
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",
@@ -13,7 +13,7 @@ const { log_info, log_error } = createLogger({
13
13
  */
14
14
  export interface NavigateToScreenOptions {
15
15
  /** The screen type to navigate to (from content types mapping) */
16
- typeMapping: string;
16
+ typeMapping?: string;
17
17
 
18
18
  /** Navigation action: push adds to history, replace replaces current screen */
19
19
  navigationAction?: "push" | "replace";
@@ -58,14 +58,6 @@ export function createNavigateToScreenAction(
58
58
  action,
59
59
  context?: ActionExecutionContext
60
60
  ): Promise<ActionResult> {
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
61
  const navigationAction = action.options?.navigationAction;
70
62
  const entrySource = action.options?.entry;
71
63
  const navigationOptions = action.options?.navigationOptions;
@@ -76,61 +68,53 @@ export function createNavigateToScreenAction(
76
68
  : entrySource
77
69
  : null;
78
70
 
79
- if (entry) {
80
- if (typeof entry !== "object") {
81
- log_error(
82
- `navigateToScreen: entry option is not an object, entry: ${entry}`
83
- );
71
+ const screenType = action.options?.typeMapping || entry?.type?.value;
84
72
 
85
- return ActionResult.Error;
86
- }
73
+ const navigate =
74
+ navigationAction === "push" ? navigator.push : navigator.replace;
75
+
76
+ let navigatingEntry: ZappEntry | ZappRiver = entry;
87
77
 
78
+ if (entry && screenType) {
88
79
  log_info(
89
80
  `navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
90
81
  );
91
82
 
92
- const overriddenEntry: ZappEntry = {
83
+ navigatingEntry = {
93
84
  ...entry,
94
- type: {
95
- value: screenType,
96
- },
85
+ type: { value: screenType },
97
86
  };
87
+ } else {
88
+ if (!screenType) {
89
+ log_error(
90
+ "navigateToScreen: options.typeMapping or entry?.type?.value is not provided"
91
+ );
98
92
 
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);
93
+ return ActionResult.Error;
104
94
  }
105
95
 
106
- return ActionResult.Success;
107
- }
108
-
109
- const screenId = contentTypes?.[screenType]?.screen_id || null;
96
+ const screenId = contentTypes?.[screenType]?.screen_id || null;
97
+ const river = rivers?.[screenId];
110
98
 
111
- if (!screenId) {
112
- log_error(
113
- `navigateToScreen: can not resolve screen type mapping: ${screenType}`
114
- );
99
+ if (!screenId) {
100
+ log_error(
101
+ `navigateToScreen: can not resolve screen type mapping: ${screenType}`
102
+ );
115
103
 
116
- return ActionResult.Error;
117
- }
104
+ return ActionResult.Error;
105
+ }
118
106
 
119
- const river = rivers[screenId];
107
+ if (!river) {
108
+ log_error("navigateToScreen: can not resolve river");
120
109
 
121
- if (!river) {
122
- log_error("navigateToScreen: can not resolve river");
110
+ return ActionResult.Error;
111
+ }
123
112
 
124
- return ActionResult.Error;
113
+ navigatingEntry = river;
125
114
  }
126
115
 
127
116
  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
- }
117
+ navigate(navigatingEntry, navigationOptions);
134
118
 
135
119
  return ActionResult.Success;
136
120
  };
@@ -212,6 +212,7 @@ export async function openBottomSheetAction(
212
212
  ...(content.props || {}),
213
213
  actionContext: context,
214
214
  },
215
+ themePluginId,
215
216
  },
216
217
  });
217
218
 
@@ -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.toBe(false);
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.toBe(false);
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.toBe(false);
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
+ });
@@ -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
+ });
@@ -11,6 +11,7 @@ import {
11
11
  import { setUserLanguagePreference } from "./userLanguagePreference";
12
12
  import { PlayerRole } from "./const";
13
13
  import { RNPlayerManager } from "./nativePlayerManager";
14
+ import { registerPlayerToolbox } from "./playerToolbox";
14
15
 
15
16
  const { log_error, log_debug, log_warning } = createLogger({
16
17
  category: "General",
@@ -54,6 +55,7 @@ export class PlayerManager {
54
55
  public static getInstance() {
55
56
  if (!PlayerManager.instance) {
56
57
  PlayerManager.instance = new PlayerManager();
58
+ registerPlayerToolbox(PlayerManager.instance);
57
59
  }
58
60
 
59
61
  return PlayerManager.instance;
@@ -395,6 +397,11 @@ export class PlayerManager {
395
397
  return this.manager.removeHandler(event, handler);
396
398
  }
397
399
 
400
+ /** Whether anything handles `event` now, e.g. a mounted PlayerContainer for "toggleFullscreen". */
401
+ public hasHandler(event: string) {
402
+ return this.manager.handlers[event]?.length > 0;
403
+ }
404
+
398
405
  // TODO: should not be
399
406
  public invokeHandler(event, ...args) {
400
407
  return this.manager.invokeHandler(event, ...args);
@@ -841,6 +841,7 @@ export class Player {
841
841
  setPlaybackRate = (_rate) => {};
842
842
  startSleepTimer = (_timestamp) => {};
843
843
  cancelSleepTimer = () => {};
844
+ command = (_command: string, _userInfo?: Record<string, any>) => {};
844
845
 
845
846
  getPluginConfiguration = () => null;
846
847
  appStateChange = (_appState, _previousAppState) => {};