@applicaster/zapp-react-native-utils 16.0.0-alpha.6593152532 → 16.0.0-alpha.7343517668

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 (48) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +4 -3
  2. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  3. package/analyticsUtils/PlayerAnalyticsManager.ts +11 -1
  4. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  5. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  6. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  7. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  8. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  9. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  10. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  11. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  12. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  13. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  14. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  15. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  16. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  17. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  18. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  19. package/analyticsUtils/analyticsMapper.ts +4 -1
  20. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  21. package/appUtils/HooksManager/index.ts +3 -8
  22. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  23. package/appUtils/playerManager/__tests__/playerFactory.test.ts +150 -0
  24. package/appUtils/playerManager/playerFactory.ts +17 -34
  25. package/cellUtils/index.ts +3 -5
  26. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  27. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  28. package/colorUtils/index.ts +50 -0
  29. package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
  30. package/package.json +2 -2
  31. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  32. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  33. package/pipesUtils/index.ts +1 -0
  34. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  35. package/playerUtils/index.ts +24 -2
  36. package/reactHooks/cell-click/index.ts +15 -7
  37. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  38. package/reactHooks/feed/index.ts +5 -1
  39. package/reactHooks/feed/useBatchLoading.ts +9 -1
  40. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  41. package/reactHooks/screen/__tests__/useIsStandaloneFullscreen.test.ts +114 -0
  42. package/reactHooks/screen/index.ts +2 -0
  43. package/reactHooks/screen/useIsStandaloneFullscreen.ts +12 -0
  44. package/reactHooks/utils/index.ts +3 -2
  45. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  46. package/riverComponetsMeasurementProvider/index.tsx +13 -13
  47. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +1 -1
  48. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
@@ -44,7 +44,7 @@ export const { log_error, log_info, log_debug } = createLogger({
44
44
  category: "General",
45
45
  });
46
46
 
47
- type ActionExecutorContextType = {
47
+ export type ActionExecutorContextType = {
48
48
  registerAction: (
49
49
  type: string,
50
50
  handler: (
@@ -66,6 +66,7 @@ type ActionExecutorContextType = {
66
66
  context?: Record<string, any>
67
67
  ) => Promise<ActionResult>;
68
68
  };
69
+
69
70
  type Props = {
70
71
  children: React.ReactNode;
71
72
  };
@@ -129,7 +130,7 @@ const prepareDefaultActions = (actionExecutor) => {
129
130
  getInflatedDataSourceUrl({
130
131
  source,
131
132
  contexts: {
132
- entry: context?.entryContext,
133
+ entry: context?.screenEntry,
133
134
  screen: context?.screenData,
134
135
  search: getSearchContext(null, mapping),
135
136
  },
@@ -198,7 +199,7 @@ const prepareDefaultActions = (actionExecutor) => {
198
199
 
199
200
  const entry = context?.entry || {};
200
201
  const entryResolver = new EntryResolver(entry);
201
- const screenData = context?.screenStateStore.getState().data || {};
202
+ const screenData = context?.screenStateStore?.getState().data || {};
202
203
  const screenResolver = new EntryResolver(screenData || {});
203
204
 
204
205
  const data =
@@ -6,9 +6,9 @@ import { postAnalyticEvent } from "../manager";
6
6
  import { ANALYTICS_CORE_EVENTS } from "../events";
7
7
 
8
8
  type AnalyticsDefaultHelperProperties = {
9
- analyticsScreenData: AnalyticsScreenProperties;
9
+ analyticsScreenData?: AnalyticsScreenProperties;
10
10
  extraProps: any;
11
- props;
11
+ props?: any;
12
12
  };
13
13
 
14
14
  export const sendMenuClickEvent = ({
@@ -23,6 +23,15 @@ class PlayerAnalyticsTrackerFactory {
23
23
  return new PlayerAnalyticsTracker();
24
24
  }
25
25
  }
26
+
27
+ onPlayerRegistered(tracker: PlayerAnalyticsTrackerI, player: Player) {
28
+ tracker.setPlayer(player);
29
+ tracker.onPlayerPresented();
30
+ }
31
+
32
+ onPlayerUnRegistered(tracker?: PlayerAnalyticsTrackerI) {
33
+ tracker?.onPlayerClosed();
34
+ }
26
35
  }
27
36
 
28
37
  export class PlayerAnalyticsManager implements PlayerLifecycleListener {
@@ -70,7 +79,7 @@ export class PlayerAnalyticsManager implements PlayerLifecycleListener {
70
79
  }
71
80
 
72
81
  const tracker = this.getTracker(player.playerId);
73
- tracker.setPlayer(player);
82
+ this.trackerFactory.onPlayerRegistered(tracker, player);
74
83
 
75
84
  const analyticsListener = new AnalyticPlayerListener({
76
85
  analyticsTracker: tracker,
@@ -84,6 +93,7 @@ export class PlayerAnalyticsManager implements PlayerLifecycleListener {
84
93
  };
85
94
 
86
95
  onUnRegistered = (player: Player) => {
96
+ this.trackerFactory.onPlayerUnRegistered(this.trackers[player.playerId]);
87
97
  player.removeListener(AnalyticPlayerListener.analyticsListenerID);
88
98
  delete this.trackers[player.playerId];
89
99
  };
@@ -74,4 +74,39 @@ describe("Analytics Mapper", () => {
74
74
  const rules = fixtures.ACPEventRules;
75
75
  await setConfigRunMapTest(rules, fixtures.ACPEventEvents);
76
76
  });
77
+
78
+ it("Does not let an ignore rule short-circuit later events", async () => {
79
+ await setConfigRunMapTest(
80
+ fixtures.ignoreOrderingRules,
81
+ fixtures.ignoreOrderingEvents
82
+ );
83
+ });
84
+
85
+ it("Keeps the original name when rename is empty", async () => {
86
+ await setConfigRunMapTest(
87
+ fixtures.emptyRenameRules,
88
+ fixtures.emptyRenameEvents
89
+ );
90
+ });
91
+
92
+ it("Renames via a regex with no named groups", async () => {
93
+ await setConfigRunMapTest(
94
+ fixtures.regexNonNamedFullMatchRules,
95
+ fixtures.regexNonNamedFullMatchEvents
96
+ );
97
+ });
98
+
99
+ it("Substitutes every named group in a regex rename", async () => {
100
+ await setConfigRunMapTest(
101
+ fixtures.regexMultiGroupRenameRules,
102
+ fixtures.regexMultiGroupRenameEvents
103
+ );
104
+ });
105
+
106
+ it("Does not fall back to event match when a regex rule does not match", async () => {
107
+ await setConfigRunMapTest(
108
+ fixtures.regexEventNoFallbackRules,
109
+ fixtures.regexEventNoFallbackEvents
110
+ );
111
+ });
77
112
  });
@@ -23,7 +23,7 @@
23
23
  "second": {
24
24
  "name": "EventName",
25
25
  "params": {
26
- "acp": 1
26
+ "acp": "1"
27
27
  }
28
28
  }
29
29
  }
@@ -12,4 +12,4 @@
12
12
  }
13
13
  ],
14
14
  "strategy": "allowUnlisted"
15
- }
15
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "keep_me", "params": {} },
3
+ "second": { "name": "keep_me", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "event": "keep_me", "ignore": false, "rename": "", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -2,17 +2,11 @@
2
2
  {
3
3
  "first": {
4
4
  "name": "Screen viewed: Ads & redirects",
5
- "params": {
6
- "ignored_param": "removed_value",
7
- "kept_param": "kept_value"
8
- }
5
+ "params": {}
9
6
  },
10
7
  "second": {
11
8
  "name": "Screen Ads & redirects",
12
- "params": {
13
- "ignored_param": "removed_value",
14
- "kept_param": "kept_value"
15
- }
9
+ "params": {}
16
10
  }
17
11
  }
18
- ]
12
+ ]
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "first": {
4
+ "name": "screen_view",
5
+ "params": {}
6
+ },
7
+ "second": {
8
+ "name": "page_view",
9
+ "params": {}
10
+ }
11
+ },
12
+ {
13
+ "first": {
14
+ "name": "player_presented",
15
+ "params": {}
16
+ }
17
+ }
18
+ ]
@@ -0,0 +1,16 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "event": "player_presented",
5
+ "ignore": true,
6
+ "strategy": "allowUnlisted"
7
+ },
8
+ {
9
+ "event": "screen_view",
10
+ "ignore": false,
11
+ "rename": "page_view",
12
+ "strategy": "allowUnlisted"
13
+ }
14
+ ],
15
+ "strategy": "allowUnlisted"
16
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "my_event", "params": {} },
3
+ "second": { "name": "my_event", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "event": "my_event", "ignore": false, "rename": "should_not_apply", "regex": "wont_match_.*", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "play-video", "params": {} },
3
+ "second": { "name": "play_video", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "ignore": false, "rename": "$<first>_$<second>", "regex": "(?<first>[a-z]+)-(?<second>[a-z]+)", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "first": { "name": "screen_view", "params": {} },
3
+ "second": { "name": "renamed", "params": {} } }
4
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": [
3
+ { "ignore": false, "rename": "renamed", "regex": "screen_view", "strategy": "allowUnlisted" }
4
+ ],
5
+ "strategy": "allowUnlisted"
6
+ }
@@ -14,6 +14,16 @@ import blockUnlistedEventEvents from "./analytics_mapper_testBlockUnlistedEvents
14
14
  import blockUnlistedEventRules from "./analytics_mapper_testBlockUnlistedEvents_rules.json";
15
15
  import ACPEventEvents from "./analytics_mapper_testACP_events.json";
16
16
  import ACPEventRules from "./analytics_mapper_testACP_rules.json";
17
+ import emptyRenameRules from "./analytics_mapper_testEmptyRenameKeepsName_rules.json";
18
+ import emptyRenameEvents from "./analytics_mapper_testEmptyRenameKeepsName_events.json";
19
+ import ignoreOrderingRules from "./analytics_mapper_testIgnoreOrdering_rules.json";
20
+ import ignoreOrderingEvents from "./analytics_mapper_testIgnoreOrdering_events.json";
21
+ import regexEventNoFallbackRules from "./analytics_mapper_testRegexEventNoFallback_rules.json";
22
+ import regexEventNoFallbackEvents from "./analytics_mapper_testRegexEventNoFallback_events.json";
23
+ import regexMultiGroupRenameRules from "./analytics_mapper_testRegexMultiGroupRename_rules.json";
24
+ import regexMultiGroupRenameEvents from "./analytics_mapper_testRegexMultiGroupRename_events.json";
25
+ import regexNonNamedFullMatchRules from "./analytics_mapper_testRegexNonNamedFullMatch_rules.json";
26
+ import regexNonNamedFullMatchEvents from "./analytics_mapper_testRegexNonNamedFullMatch_events.json";
17
27
 
18
28
  export const fixtures = {
19
29
  renameRules,
@@ -32,4 +42,14 @@ export const fixtures = {
32
42
  blockUnlistedEventRules,
33
43
  ACPEventEvents,
34
44
  ACPEventRules,
45
+ emptyRenameRules,
46
+ emptyRenameEvents,
47
+ ignoreOrderingRules,
48
+ ignoreOrderingEvents,
49
+ regexEventNoFallbackRules,
50
+ regexEventNoFallbackEvents,
51
+ regexMultiGroupRenameRules,
52
+ regexMultiGroupRenameEvents,
53
+ regexNonNamedFullMatchRules,
54
+ regexNonNamedFullMatchEvents,
35
55
  };
@@ -76,7 +76,10 @@ class StateHolder {
76
76
  delete this.pluckedParams[prefix];
77
77
  }
78
78
 
79
- return this._cap?.[suffix];
79
+ // Stringify so values match the native iOS/Android mappers (which coerce
80
+ // analyticsCustomProperties values to strings) — keeps GA4 data consistent
81
+ // across web/LG/Samsung and native platforms.
82
+ return this._cap?.[suffix]?.toString() ?? null;
80
83
  }
81
84
 
82
85
  private async getFromLocalAndSession(
@@ -4,8 +4,14 @@ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
4
4
  import { postAnalyticEvent } from "./manager";
5
5
  import { isLive } from "../playerUtils";
6
6
  import { extensionsEvents } from "./AnalyticsEvents/helper";
7
- import { AD_EVENT, EVENT_TYPES, PLAYER_DISPLAY_STATES } from "./events";
7
+ import {
8
+ AD_EVENT,
9
+ EVENT_TYPES,
10
+ GENERAL_EVENT,
11
+ PLAYER_DISPLAY_STATES,
12
+ } from "./events";
8
13
  import { Player } from "../appUtils/playerManager/player";
14
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
9
15
 
10
16
  export interface PlayerAnalyticsTrackerI {
11
17
  handleAnalyticEvent(
@@ -13,6 +19,8 @@ export interface PlayerAnalyticsTrackerI {
13
19
  eventData?: QuickBrickPlayer.EventData
14
20
  ): void;
15
21
  setPlayer(player: Player): void;
22
+ onPlayerPresented(): void;
23
+ onPlayerClosed(): void;
16
24
  }
17
25
 
18
26
  export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
@@ -26,6 +34,15 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
26
34
  this.player = player;
27
35
  }
28
36
 
37
+ onPlayerPresented() {
38
+ this.handleAnalyticEvent(GENERAL_EVENT.player_presented);
39
+ }
40
+
41
+ onPlayerClosed() {
42
+ this.handleAnalyticEvent(GENERAL_EVENT.session_end);
43
+ this.handleAnalyticEvent(GENERAL_EVENT.player_closed);
44
+ }
45
+
29
46
  isAdBreak(event: string): boolean {
30
47
  const adBreakEvents = [
31
48
  AD_EVENT.ad_break_start,
@@ -180,7 +197,8 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
180
197
  this.entry?.extensions?.free || this.entry?.extensions?.isFree;
181
198
 
182
199
  const title = this.entry?.title;
183
- const streamType = isLive(this.entry) ? "live" : "vod"; // Todo: determine other types, channel, podcast, aod
200
+ const isStreamLive = isLive(this.entry);
201
+ const streamType = isStreamLive ? "live" : "vod"; // Todo: determine other types, channel, podcast, aod
184
202
 
185
203
  const currentPosition = this.getCurrentPosition(
186
204
  event,
@@ -201,6 +219,7 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
201
219
  );
202
220
 
203
221
  const playerState = this.getPlayerState();
222
+ const playerType = this.player?.playerPluginId;
204
223
 
205
224
  const analyticsCustomProperties = extensionsEvents(this.entry?.extensions);
206
225
 
@@ -210,7 +229,11 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
210
229
  name: title,
211
230
  media_type: mediaType,
212
231
  stream_type: streamType,
213
- duration: mediaDuration,
232
+ player_type: playerType,
233
+ // We do not pass duration for live streams anymore (it's now seekableDuration),
234
+ // but we can't mark it as optional in Analytics Mapper yet,
235
+ // so we set it to -1 for live streams
236
+ duration: isStreamLive && isNil(mediaDuration) ? -1 : mediaDuration,
214
237
  current_position: currentPosition,
215
238
  player_state: playerState,
216
239
  stream_format: contentType,
@@ -87,7 +87,7 @@ export function HooksManager({
87
87
  }: HookManagerArgs): HookManager {
88
88
  hooksManagerLogger.addContext({ targetScreenId: targetScreen.id });
89
89
 
90
- function logHookEvent(func, message, data) {
90
+ function logHookEvent(func, message, data = {}) {
91
91
  func({
92
92
  message,
93
93
  data: __DEV__ ? data : null,
@@ -344,7 +344,6 @@ export function HooksManager({
344
344
  `hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin.identifier} on home screen`,
345
345
  {
346
346
  payload,
347
- hook: hookPlugin,
348
347
  }
349
348
  );
350
349
 
@@ -356,17 +355,13 @@ export function HooksManager({
356
355
  `hookCallback: hook successfully finished: ${hookPlugin.identifier}`,
357
356
  {
358
357
  payload,
359
- hook: hookPlugin,
360
358
  }
361
359
  );
362
360
 
363
361
  if (!callback) {
364
362
  logHookEvent(
365
- hooksManagerLogger.warn,
366
- `hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`,
367
- {
368
- hookPlugin,
369
- }
363
+ hooksManagerLogger.debug,
364
+ `hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`
370
365
  );
371
366
 
372
367
  callback = hookCallback;
@@ -1,40 +1,53 @@
1
- import * as R from "ramda";
2
1
  import { DEFAULT_NAMESPACE } from "../consts";
3
2
  import { KeyName, KeyNameObj } from "../index";
4
3
 
5
- const lastDotRegex = /\.([^.]+)$/;
6
- const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
7
-
8
4
  export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
9
5
  if (typeof namespacedKey !== "string") return namespacedKey;
10
6
 
11
- const namespacedKeyProps = namespacedKey.includes(".")
12
- ? splitByLastDot(namespacedKey)
13
- : [DEFAULT_NAMESPACE, namespacedKey];
7
+ if (!namespacedKey.includes(".")) {
8
+ return { namespace: DEFAULT_NAMESPACE, key: namespacedKey };
9
+ }
10
+
11
+ // split on the last dot - the namespace itself may contain dots
12
+ const lastDotIndex = namespacedKey.lastIndexOf(".");
14
13
 
15
- return R.zipObj(["namespace", "key"], namespacedKeyProps);
14
+ return {
15
+ namespace: namespacedKey.slice(0, lastDotIndex),
16
+ key: namespacedKey.slice(lastDotIndex + 1),
17
+ };
16
18
  }
17
19
 
18
- const propIsNotNullOrUndefined = (prop) =>
19
- R.compose(R.not, R.isNil, R.prop(prop));
20
+ const hasOwn = (obj: object, prop: string): boolean =>
21
+ Object.prototype.hasOwnProperty.call(obj, prop);
22
+
23
+ const isEmpty = (value: unknown): boolean => {
24
+ if (typeof value === "string" || Array.isArray(value)) {
25
+ return value.length === 0;
26
+ }
27
+
28
+ if (value !== null && typeof value === "object") {
29
+ return Object.keys(value).length === 0;
30
+ }
31
+
32
+ return false;
33
+ };
34
+
35
+ export const keyIsValid = (parsedKey: unknown): boolean => {
36
+ if (parsedKey == null) return false;
37
+ if (typeof parsedKey === "string") return false;
38
+ if (Array.isArray(parsedKey)) return false;
20
39
 
21
- const propIsNotEmpty = (prop) => R.compose(R.not, R.isEmpty, R.prop(prop));
40
+ const obj = parsedKey as Record<string, unknown>;
22
41
 
23
- export const keyIsValid = R.allPass([
24
- R.compose(R.not, R.isNil),
25
- R.compose(R.not, R.is(String)),
26
- R.compose(R.not, R.is(Array)),
27
- R.has("key"),
28
- R.has("namespace"),
29
- propIsNotNullOrUndefined("key"),
30
- propIsNotNullOrUndefined("namespace"),
31
- propIsNotEmpty("key"),
32
- propIsNotEmpty("namespace"),
33
- ]);
42
+ if (!hasOwn(obj, "key") || !hasOwn(obj, "namespace")) return false;
43
+ if (obj.key == null || obj.namespace == null) return false;
44
+ if (isEmpty(obj.key) || isEmpty(obj.namespace)) return false;
45
+
46
+ return true;
47
+ };
34
48
 
35
49
  export const buildNamespaceKey = (key: string, namespace: string) =>
36
50
  `${namespace}.${key}`;
37
51
 
38
- export const savingResultIsSuccess = (result: unknown): boolean => {
39
- return R.is(Boolean, result) && !!result;
40
- };
52
+ export const savingResultIsSuccess = (result: unknown): boolean =>
53
+ typeof result === "boolean" && result === true;
@@ -0,0 +1,150 @@
1
+ const mockAppStoreGet = jest.fn();
2
+ const mockFindPluginByIdentifier = jest.fn();
3
+
4
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
5
+ appStore: { get: (key: string) => mockAppStoreGet(key) },
6
+ }));
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/pluginUtils", () => ({
9
+ findPluginByIdentifier: (id: string, plugins: any) =>
10
+ mockFindPluginByIdentifier(id, plugins),
11
+ }));
12
+
13
+ import { PlayerRole } from "../conts";
14
+ import { playerFactory } from "../playerFactory";
15
+
16
+ const fakeComponent = () => null;
17
+
18
+ class FakeController {
19
+ receivedConfig: any;
20
+ constructor(config: any) {
21
+ this.receivedConfig = config;
22
+ }
23
+ }
24
+
25
+ const baseConfig = {
26
+ player: {},
27
+ playerId: "p1",
28
+ autoplay: false,
29
+ entry: { id: "entry-1" } as any,
30
+ muted: false,
31
+ playerPluginId: "test-plugin",
32
+ screenConfig: { foo: "bar" },
33
+ playerRole: PlayerRole.Cell,
34
+ };
35
+
36
+ const installPluginWithProtocol = (
37
+ protocolReturn: any | ((screenConfig: any, entry: any) => any)
38
+ ) => {
39
+ const playerProtocol =
40
+ typeof protocolReturn === "function"
41
+ ? protocolReturn
42
+ : () => protocolReturn;
43
+
44
+ mockAppStoreGet.mockImplementation((key: string) =>
45
+ key === "plugins" ? { "test-plugin": {} } : null
46
+ );
47
+
48
+ mockFindPluginByIdentifier.mockReturnValue({
49
+ module: { playerProtocol },
50
+ });
51
+ };
52
+
53
+ describe("playerFactory", () => {
54
+ beforeEach(() => {
55
+ mockAppStoreGet.mockReset();
56
+ mockFindPluginByIdentifier.mockReset();
57
+ });
58
+
59
+ it("resolves with controller and Component when playerProtocol returns synchronously", async () => {
60
+ installPluginWithProtocol({
61
+ Component: fakeComponent,
62
+ controllerClass: FakeController,
63
+ });
64
+
65
+ const item = await playerFactory(baseConfig);
66
+
67
+ expect(item).not.toBeNull();
68
+ expect(item?.Component).toBe(fakeComponent);
69
+ expect(item?.controller).toBeInstanceOf(FakeController);
70
+
71
+ expect((item?.controller as FakeController).receivedConfig).toBe(
72
+ baseConfig
73
+ );
74
+ });
75
+
76
+ it("resolves with controller and Component when playerProtocol returns a Promise (delayed import)", async () => {
77
+ installPluginWithProtocol(async () => ({
78
+ Component: fakeComponent,
79
+ controllerClass: FakeController,
80
+ }));
81
+
82
+ const item = await playerFactory(baseConfig);
83
+
84
+ expect(item).not.toBeNull();
85
+ expect(item?.Component).toBe(fakeComponent);
86
+ expect(item?.controller).toBeInstanceOf(FakeController);
87
+ });
88
+
89
+ it("passes screenConfig and entry through to playerProtocol", async () => {
90
+ const playerProtocol = jest.fn(() => ({
91
+ Component: fakeComponent,
92
+ controllerClass: FakeController,
93
+ }));
94
+
95
+ mockAppStoreGet.mockImplementation((key: string) =>
96
+ key === "plugins" ? { "test-plugin": {} } : null
97
+ );
98
+
99
+ mockFindPluginByIdentifier.mockReturnValue({ module: { playerProtocol } });
100
+
101
+ await playerFactory(baseConfig);
102
+
103
+ expect(playerProtocol).toHaveBeenCalledWith(
104
+ baseConfig.screenConfig,
105
+ baseConfig.entry
106
+ );
107
+ });
108
+
109
+ it("resolves to null when playerPluginId is missing", async () => {
110
+ expect(
111
+ await playerFactory({ ...baseConfig, playerPluginId: "" } as any)
112
+ ).toBeNull();
113
+ });
114
+
115
+ it("resolves to null when appStore has no plugins", async () => {
116
+ mockAppStoreGet.mockReturnValue(null);
117
+
118
+ expect(await playerFactory(baseConfig)).toBeNull();
119
+ });
120
+
121
+ it("resolves to null when the plugin has no playerProtocol", async () => {
122
+ mockAppStoreGet.mockImplementation((key: string) =>
123
+ key === "plugins" ? { "test-plugin": {} } : null
124
+ );
125
+
126
+ mockFindPluginByIdentifier.mockReturnValue({ module: {} });
127
+
128
+ expect(await playerFactory(baseConfig)).toBeNull();
129
+ });
130
+
131
+ it("resolves to null when playerProtocol returns without a Component", async () => {
132
+ installPluginWithProtocol({ controllerClass: FakeController });
133
+
134
+ expect(await playerFactory(baseConfig)).toBeNull();
135
+ });
136
+
137
+ it("resolves to null when playerProtocol returns without a controllerClass", async () => {
138
+ installPluginWithProtocol({ Component: fakeComponent });
139
+
140
+ expect(await playerFactory(baseConfig)).toBeNull();
141
+ });
142
+
143
+ it("rejects when playerProtocol throws synchronously", async () => {
144
+ installPluginWithProtocol(() => {
145
+ throw new Error("boom");
146
+ });
147
+
148
+ await expect(playerFactory(baseConfig)).rejects.toThrow("boom");
149
+ });
150
+ });