@applicaster/zapp-react-native-utils 16.0.0-alpha.1701179921 → 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/__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 +72 -0
- package/actionsExecutor/actions/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +2 -0
- package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
- 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/__tests__/playerDiagnostics.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/index.ts +7 -0
- package/appUtils/playerManager/player.ts +15 -0
- package/appUtils/playerManager/playerToolbox.ts +196 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
- 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 +134 -0
- package/configurationUtils/modalBlocksParser.ts +38 -0
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/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 +1 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +23 -0
- package/modalState/index.ts +2 -0
- package/modalState/types.ts +11 -0
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +3 -2
- 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/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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isNil } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
2
|
|
|
3
|
-
import { StorageLevel
|
|
3
|
+
import { StorageLevel } from "./consts";
|
|
4
4
|
import {
|
|
5
5
|
getNamespaceAndKey,
|
|
6
6
|
keyIsValid,
|
|
@@ -59,18 +59,14 @@ type Setup = {
|
|
|
59
59
|
sessionStorage?: Storage;
|
|
60
60
|
localStorage?: Storage;
|
|
61
61
|
secureStorage?: Storage;
|
|
62
|
-
referenceStorage?: Storage;
|
|
63
62
|
};
|
|
64
63
|
|
|
65
|
-
export const REFERENCE_NAMESPACE = "applicaster.v2.references";
|
|
66
|
-
|
|
67
64
|
export class ContextKeysManager {
|
|
68
65
|
private static _instance: ContextKeysManager;
|
|
69
66
|
private _logger: Logger;
|
|
70
67
|
private _sessionStorage: Storage;
|
|
71
68
|
private _localStorage: Storage;
|
|
72
69
|
private _secureStorage: Storage;
|
|
73
|
-
private _referenceStorage: Storage;
|
|
74
70
|
|
|
75
71
|
public static get instance() {
|
|
76
72
|
if (!this._instance) {
|
|
@@ -79,7 +75,6 @@ export class ContextKeysManager {
|
|
|
79
75
|
sessionStorage: SessionStorage,
|
|
80
76
|
localStorage: LocalStorage,
|
|
81
77
|
secureStorage: SecureStorage,
|
|
82
|
-
referenceStorage: SessionStorage,
|
|
83
78
|
});
|
|
84
79
|
|
|
85
80
|
this._instance = instance;
|
|
@@ -94,13 +89,11 @@ export class ContextKeysManager {
|
|
|
94
89
|
sessionStorage = SessionStorage,
|
|
95
90
|
localStorage = LocalStorage,
|
|
96
91
|
secureStorage = SecureStorage,
|
|
97
|
-
|
|
98
|
-
}: Setup) {
|
|
92
|
+
}: Setup = {}) {
|
|
99
93
|
this._logger = logger;
|
|
100
94
|
this._sessionStorage = sessionStorage;
|
|
101
95
|
this._localStorage = localStorage;
|
|
102
96
|
this._secureStorage = secureStorage;
|
|
103
|
-
this._referenceStorage = referenceStorage;
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
private parseKey(key: KeyName): ParsedKeyAndNamespace {
|
|
@@ -119,41 +112,6 @@ export class ContextKeysManager {
|
|
|
119
112
|
return { isValid: true, ...parsedKey };
|
|
120
113
|
}
|
|
121
114
|
|
|
122
|
-
// GET
|
|
123
|
-
public async getKeyByReference(
|
|
124
|
-
key: string,
|
|
125
|
-
namespace: string
|
|
126
|
-
): Promise<ValueOrNothing> {
|
|
127
|
-
// 1) look for storage
|
|
128
|
-
// 2) if storage found - look value in it
|
|
129
|
-
// 3) if storage not found return NOTHING
|
|
130
|
-
|
|
131
|
-
return this._referenceStorage
|
|
132
|
-
.getItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
|
|
133
|
-
.then((storage: StorageType | unknown) => {
|
|
134
|
-
switch (storage) {
|
|
135
|
-
case StorageType.session:
|
|
136
|
-
return this._sessionStorage.getItem(key, namespace);
|
|
137
|
-
case StorageType.local:
|
|
138
|
-
return this._localStorage.getItem(key, namespace);
|
|
139
|
-
case StorageType.secure:
|
|
140
|
-
return this._secureStorage.getItem(key, namespace);
|
|
141
|
-
default:
|
|
142
|
-
// unknown storage
|
|
143
|
-
return NOTHING;
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
.catch((error) => {
|
|
147
|
-
// something went wrong
|
|
148
|
-
this._logger.warn({
|
|
149
|
-
message: error.message,
|
|
150
|
-
data: { key, namespace },
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return NOTHING;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
115
|
public async getKeys(
|
|
158
116
|
keys: Array<KeyName>,
|
|
159
117
|
contextObj?: { key: string; required?: boolean }[]
|
|
@@ -228,19 +186,7 @@ export class ContextKeysManager {
|
|
|
228
186
|
return NOTHING;
|
|
229
187
|
}
|
|
230
188
|
|
|
231
|
-
//
|
|
232
|
-
// if not found, look in session > local > secure
|
|
233
|
-
|
|
234
|
-
const resultByReference = await this.getKeyByReference(
|
|
235
|
-
parsedKey,
|
|
236
|
-
parsedNamespace
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
if (!isNil(resultByReference)) {
|
|
240
|
-
return resultByReference;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// try to full-search
|
|
189
|
+
// look in session > local > secure
|
|
244
190
|
try {
|
|
245
191
|
const resultFromSessionStorage = await this._sessionStorage.getItem(
|
|
246
192
|
parsedKey,
|
|
@@ -284,41 +230,6 @@ export class ContextKeysManager {
|
|
|
284
230
|
// GET
|
|
285
231
|
|
|
286
232
|
// SET
|
|
287
|
-
public async setReferenceForKey(
|
|
288
|
-
key: string,
|
|
289
|
-
namespace: string,
|
|
290
|
-
storageLevel: StorageLevel
|
|
291
|
-
): Promise<boolean> {
|
|
292
|
-
return this._referenceStorage
|
|
293
|
-
.setItem(
|
|
294
|
-
buildNamespaceKey(key, namespace),
|
|
295
|
-
storageLevel,
|
|
296
|
-
REFERENCE_NAMESPACE
|
|
297
|
-
)
|
|
298
|
-
.then((result) => {
|
|
299
|
-
if (savingResultIsSuccess(result)) {
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
this._logger.warn({
|
|
304
|
-
message:
|
|
305
|
-
"While saving reference - reference storage return non expected value: true",
|
|
306
|
-
data: { key, namespace, storageLevel },
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
return false;
|
|
310
|
-
})
|
|
311
|
-
.catch((error) => {
|
|
312
|
-
// something went wrong
|
|
313
|
-
this._logger.warn({
|
|
314
|
-
message: error.message,
|
|
315
|
-
data: { key, namespace, storageLevel },
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
return false;
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
233
|
// for all keys where saving were succeed return true
|
|
323
234
|
// for others return false
|
|
324
235
|
public async setKeys(
|
|
@@ -352,29 +263,36 @@ export class ContextKeysManager {
|
|
|
352
263
|
}
|
|
353
264
|
|
|
354
265
|
try {
|
|
266
|
+
let setItemResult: boolean;
|
|
267
|
+
|
|
355
268
|
switch (storageLevel) {
|
|
356
269
|
case StorageLevel.persisted:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
270
|
+
setItemResult = await this._localStorage.setItem(
|
|
271
|
+
parsedKey,
|
|
272
|
+
value,
|
|
273
|
+
parsedNamespace
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
break;
|
|
363
277
|
case StorageLevel.secure:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
278
|
+
setItemResult = await this._secureStorage.setItem(
|
|
279
|
+
parsedKey,
|
|
280
|
+
value,
|
|
281
|
+
parsedNamespace
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
break;
|
|
370
285
|
default:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
286
|
+
setItemResult = await this._sessionStorage.setItem(
|
|
287
|
+
parsedKey,
|
|
288
|
+
value,
|
|
289
|
+
parsedNamespace
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
break;
|
|
377
293
|
}
|
|
294
|
+
|
|
295
|
+
return savingResultIsSuccess(setItemResult);
|
|
378
296
|
} catch (error) {
|
|
379
297
|
// something went wrong
|
|
380
298
|
this._logger.warn({
|
|
@@ -388,24 +306,6 @@ export class ContextKeysManager {
|
|
|
388
306
|
// SET
|
|
389
307
|
|
|
390
308
|
// REMOVE
|
|
391
|
-
public async removeReferenceForKey(
|
|
392
|
-
key: string,
|
|
393
|
-
namespace: string
|
|
394
|
-
): Promise<boolean> {
|
|
395
|
-
return this._referenceStorage
|
|
396
|
-
.removeItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
|
|
397
|
-
.then(() => true)
|
|
398
|
-
.catch((error) => {
|
|
399
|
-
// something went wrong
|
|
400
|
-
this._logger.warn({
|
|
401
|
-
message: error.message,
|
|
402
|
-
data: { key, namespace },
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
return false;
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
309
|
public async removeKey(key: KeyName): Promise<boolean> {
|
|
410
310
|
const {
|
|
411
311
|
isValid: isValidKey,
|
|
@@ -422,7 +322,6 @@ export class ContextKeysManager {
|
|
|
422
322
|
this._sessionStorage.removeItem(parsedKey, parsedNamespace),
|
|
423
323
|
this._localStorage.removeItem(parsedKey, parsedNamespace),
|
|
424
324
|
this._secureStorage.removeItem(parsedKey, parsedNamespace),
|
|
425
|
-
this.removeReferenceForKey(parsedKey, parsedNamespace),
|
|
426
325
|
])
|
|
427
326
|
.then(() => true)
|
|
428
327
|
.catch((error) => {
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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);
|