@applicaster/zapp-react-native-ui-components 16.0.0-rc.8 → 16.0.0-rc.80

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 (131) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/CellRendererResolver/index.ts +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  28. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  29. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  30. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  31. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  32. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  33. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  34. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  35. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  46. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  47. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  48. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  49. package/Components/MasterCell/dataAdapter.ts +15 -5
  50. package/Components/MasterCell/hooks/index.ts +2 -0
  51. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  52. package/Components/MasterCell/index.tsx +30 -5
  53. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  54. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  55. package/Components/MasterCell/utils/index.ts +85 -46
  56. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +54 -0
  57. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +21 -0
  58. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  59. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +58 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +395 -0
  62. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  63. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +874 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  67. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
  68. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  69. package/Components/ModalComponent/BottomSheetModalContent.tsx +383 -42
  70. package/Components/ModalComponent/Button/index.tsx +5 -3
  71. package/Components/ModalComponent/Header/index.tsx +20 -7
  72. package/Components/ModalComponent/Header/utils.ts +1 -1
  73. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  74. package/Components/ModalComponent/SortableList.tsx +142 -0
  75. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  76. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  77. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  78. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  79. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  80. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  81. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  82. package/Components/ModalComponent/index.tsx +11 -0
  83. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  84. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  85. package/Components/ModalComponent/presets/index.ts +27 -0
  86. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  87. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  88. package/Components/ModalComponent/presets/types.ts +35 -0
  89. package/Components/ModalComponent/showTextInput.ts +43 -0
  90. package/Components/ModalComponent/utils.ts +76 -20
  91. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  92. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  93. package/Components/OfflineHandler/hooks.ts +123 -0
  94. package/Components/OfflineHandler/index.tsx +51 -73
  95. package/Components/OfflineHandler/utils/index.ts +1 -13
  96. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  97. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  98. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  99. package/Components/PreloaderWrapper/index.tsx +1 -1
  100. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  101. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  102. package/Components/Screen/index.tsx +8 -4
  103. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  104. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  105. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  106. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  107. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +20 -3
  109. package/Components/TopCutoffOverlay/index.tsx +2 -2
  110. package/Components/Transitioner/Transitioner.tsx +28 -17
  111. package/Components/VideoLive/LiveImageManager.ts +1 -1
  112. package/Components/VideoModal/utils.ts +6 -1
  113. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  114. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  115. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  116. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  117. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  118. package/Helpers/index.js +0 -39
  119. package/Hooks/useEntryActionsExpander.ts +63 -0
  120. package/package.json +6 -5
  121. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  122. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  123. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  124. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  125. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  126. package/Components/Screen/utils.ts +0 -16
  127. package/Helpers/Analytics/index.js +0 -95
  128. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  129. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  130. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  131. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -9,6 +9,7 @@ import {
9
9
  platformSelect,
10
10
  isAndroidTVPlatform,
11
11
  isTvOSPlatform,
12
+ isApplePlatform,
12
13
  } from "@applicaster/zapp-react-native-utils/reactUtils";
13
14
  import {
14
15
  isAudioItem,
@@ -60,6 +61,7 @@ import {
60
61
  PlayerNativeSendCommand,
61
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
62
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { PLAYER_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
63
65
 
64
66
  type Props = {
65
67
  Player: React.ComponentType<any>;
@@ -188,12 +190,10 @@ const renderApplePlayer = ({
188
190
  return null;
189
191
  }
190
192
 
191
- const { title, summary } = item || {};
192
-
193
+ // Title/Subtitle are not resolved here: `ProgramInfo` reads them from the
194
+ // player manager's content channel via `usePlayerContent`.
193
195
  return (
194
196
  <ProgramInfo
195
- title={title}
196
- subtitle={summary}
197
197
  plugin_configuration={pluginConfiguration}
198
198
  videoStyle={videoStyle}
199
199
  />
@@ -265,14 +265,12 @@ const PlayerContainerComponent = (props: Props) => {
265
265
  navigator.goBack();
266
266
  }, [isModal, state.playerId, showNavBar, navigator]);
267
267
 
268
- const pluginConfiguration = React.useMemo(() => {
269
- return (
270
- playerManager.getPluginConfiguration() ||
271
- R.prop("__plugin_configuration", Player)
272
- );
273
- }, [playerManager.isRegistered()]);
268
+ const pluginConfiguration = React.useMemo(
269
+ () => player?.getPluginConfiguration(),
270
+ [player]
271
+ );
274
272
 
275
- const playEntry = (entry) => navigator.replaceTop(entry, { mode });
273
+ const playEntry = (entry: ZappEntry) => navigator.replaceTop(entry, { mode });
276
274
 
277
275
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
278
276
  if (!playNextOverlayState.entry) {
@@ -284,12 +282,16 @@ const PlayerContainerComponent = (props: Props) => {
284
282
  }
285
283
 
286
284
  log_debug(
287
- `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}`
285
+ `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}, outgoing item id: ${item?.id}`
288
286
  );
289
287
 
288
+ playNextOverlayState.hidePlayNext();
289
+
290
+ playerEvent(PLAYER_EVENTS.PlayNextTriggered, { id: item?.id });
291
+
290
292
  setIsLoadingNextVideo(true);
291
293
  playEntry(playNextOverlayState.entry);
292
- }, [playNextOverlayState]);
294
+ }, [playNextOverlayState, item?.id]);
293
295
 
294
296
  const playNextData: Nullable<PlayNextData> = React.useMemo(() => {
295
297
  if (!playNextOverlayState) {
@@ -346,11 +348,15 @@ const PlayerContainerComponent = (props: Props) => {
346
348
  setIsLoadingNextVideo(false);
347
349
 
348
350
  const resumeTime = Number(item?.extensions?.resumeTime);
351
+ const activePlayer = playerManager.getActivePlayer();
349
352
 
350
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
353
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
354
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
355
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
351
356
  if (
352
- !playerManager.getActivePlayer()?.hasResumePosition() &&
353
- !isNaN(resumeTime)
357
+ !activePlayer?.hasResumePosition() &&
358
+ resumeTime > 0 &&
359
+ !activePlayer?.isLive()
354
360
  ) {
355
361
  player?.seekTo(resumeTime);
356
362
  }
@@ -468,6 +474,8 @@ const PlayerContainerComponent = (props: Props) => {
468
474
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
469
475
  if (disableMiniPlayer) {
470
476
  navigator.closeVideoModal();
477
+ } else {
478
+ navigator.minimiseVideoModal();
471
479
  }
472
480
  }
473
481
 
@@ -491,7 +499,7 @@ const PlayerContainerComponent = (props: Props) => {
491
499
  const isAudioContent = isAudioItem(item);
492
500
 
493
501
  useEffect(() => {
494
- if (!isAudioContent) {
502
+ if (!isAudioContent && isApplePlatform()) {
495
503
  sendQuickBrickEvent(
496
504
  QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
497
505
  {
@@ -7,12 +7,11 @@ import * as React from "react";
7
7
  import { Text, TextStyle, View, ViewStyle } from "react-native";
8
8
  import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
9
9
 
10
+ import { usePlayerContent } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent";
10
11
  import * as programStyles from "./styles";
11
12
 
12
13
  type Props = {
13
14
  logo?: string;
14
- title: string;
15
- subtitle: string;
16
15
  isAudioContent: boolean;
17
16
  styles: {};
18
17
  plugin_configuration: {
@@ -28,13 +27,13 @@ const defaultColor = "#EFEFEF";
28
27
 
29
28
  function ProgramInfoComponent({
30
29
  logo,
31
- title = "",
32
- subtitle = "",
33
30
  isAudioContent,
34
31
  styles,
35
32
  plugin_configuration = {},
36
33
  videoStyle = {},
37
34
  }: Props) {
35
+ const { title = "", summary: subtitle = "" } = usePlayerContent();
36
+
38
37
  const platformKey = styleKeys.style_namespace;
39
38
 
40
39
  const colorTitle =
@@ -0,0 +1,337 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+
4
+ jest.mock(
5
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager",
6
+ () => ({
7
+ playerManager: {
8
+ invokeHandler: jest.fn(),
9
+ getPluginConfiguration: jest.fn(() => ({})),
10
+ isRegistered: jest.fn(() => true),
11
+ on: jest.fn().mockReturnThis(),
12
+ removeHandler: jest.fn().mockReturnThis(),
13
+ getActivePlayer: jest.fn(() => null),
14
+ closeNativePlayer: jest.fn(),
15
+ },
16
+ })
17
+ );
18
+
19
+ jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
20
+ // Defaults to `false` for the hardware-back-handling tests below, which
21
+ // key off `isTV`. The ProgramInfo/content-context describe block below
22
+ // overrides this per test (`isTV()` is also what PlayerContainer's
23
+ // `playerContentValue` uses to pick TV vs mobile data keys, decoupled
24
+ // from `isTvOSPlatform` below).
25
+ isTV: jest.fn(() => false),
26
+ isAndroidTVPlatform: jest.fn(() => false),
27
+ // `isTvOSPlatform` backs a module-level constant in PlayerContainer.tsx that
28
+ // is read once at import time, so it must already be `true` here in order
29
+ // to exercise `renderApplePlayer`'s tvOS-only ProgramInfo branch in this
30
+ // test file. It does not affect the hardware-back-handling tests below,
31
+ // which key off `isTV` instead.
32
+ isTvOSPlatform: jest.fn(() => true),
33
+ isApplePlatform: jest.fn(() => true),
34
+ platformSelect: jest.fn(({ native }) => native),
35
+ }));
36
+
37
+ jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => {
38
+ // Keep the real data-key resolver (createConfigValueGetter,
39
+ // resolvePlayerContentText, VIDEO_PLAYER_CONTENT_DATA_KEYS) so the tvOS
40
+ // ProgramInfo data-key tests exercise actual resolution logic, not a stub.
41
+ const actual = jest.requireActual(
42
+ "@applicaster/zapp-react-native-utils/playerUtils"
43
+ );
44
+
45
+ return {
46
+ ...actual,
47
+ isAudioItem: jest.fn(() => false),
48
+ isInlineTV: jest.fn(() => false),
49
+ };
50
+ });
51
+
52
+ jest.mock(
53
+ "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
54
+ () => ({
55
+ TVEventHandlerComponent: ({ children }) => children,
56
+ })
57
+ );
58
+
59
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({
60
+ usePrevious: jest.fn(),
61
+ }));
62
+
63
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => {
64
+ const navigator = {
65
+ closeVideoModal: jest.fn(),
66
+ minimiseVideoModal: jest.fn(),
67
+ maximiseVideoModal: jest.fn(),
68
+ fullscreenVideoModal: jest.fn(),
69
+ isVideoModalDocked: jest.fn(() => false),
70
+ goBack: jest.fn(),
71
+ replaceTop: jest.fn(),
72
+ push: jest.fn(),
73
+ setPlayNextOverlay: jest.fn(),
74
+ currentRoute: "/playable/entry-1",
75
+ };
76
+
77
+ const backHandlerRef: { current: (() => boolean) | null } = {
78
+ current: null,
79
+ };
80
+
81
+ return {
82
+ useBackHandler: jest.fn((cb) => {
83
+ backHandlerRef.current = cb;
84
+ }),
85
+ useNavigation: jest.fn(() => navigator),
86
+ __navigator: navigator,
87
+ __backHandlerRef: backHandlerRef,
88
+ };
89
+ });
90
+
91
+ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
92
+ QUICK_BRICK_EVENTS: {
93
+ IDLE_TIMER_DISABLED: "IDLE_TIMER_DISABLED",
94
+ MOVE_APP_TO_BACKGROUND: "MOVE_APP_TO_BACKGROUND",
95
+ },
96
+ sendQuickBrickEvent: jest.fn(),
97
+ }));
98
+
99
+ // Stubbed so PlayerContainer can render without ProgramInfo's own
100
+ // redux/style plumbing. It no longer needs to capture Title/Subtitle: those
101
+ // come from the player manager's content channel, and the key-set selection
102
+ // they depend on is covered directly in
103
+ // `playerManager/__tests__/playerContent.test.ts`.
104
+ jest.mock("../ProgramInfo", () => ({
105
+ ProgramInfo: () => null,
106
+ }));
107
+
108
+ jest.mock("../../AudioPlayer", () => ({
109
+ AudioPlayer: () => null,
110
+ }));
111
+
112
+ jest.mock("../logger", () => ({
113
+ log_debug: jest.fn(),
114
+ log_info: jest.fn(),
115
+ log_warning: jest.fn(),
116
+ playerContainerLogger: { error: jest.fn() },
117
+ }));
118
+
119
+ jest.mock(
120
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
121
+ () => {
122
+ const player = {
123
+ addListener: jest.fn(),
124
+ removeListener: jest.fn(),
125
+ isPaused: jest.fn(() => false),
126
+ isAd: jest.fn(() => false),
127
+ seekTo: jest.fn(),
128
+ getPluginConfiguration: jest.fn(() => ({})),
129
+ getOverlayObservable: jest.fn(() => ({ getPlayNextEntry: () => null })),
130
+ };
131
+
132
+ return {
133
+ usePlayer: jest.fn(() => player),
134
+ __player: player,
135
+ };
136
+ }
137
+ );
138
+
139
+ jest.mock(
140
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay",
141
+ () => ({
142
+ usePlayNextOverlay: jest.fn(() => null),
143
+ })
144
+ );
145
+
146
+ jest.mock(
147
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand",
148
+ () => ({
149
+ PlayerNativeCommandTypes: { clearPlayerData: "clearPlayerData" },
150
+ PlayerNativeSendCommand: jest.fn(),
151
+ })
152
+ );
153
+
154
+ jest.mock(
155
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
156
+ () => ({
157
+ useSetNavbarState: jest.fn(() => ({ setVisible: jest.fn() })),
158
+ })
159
+ );
160
+
161
+ jest.mock(
162
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData",
163
+ () => ({
164
+ useTargetScreenData: jest.fn(() => ({ id: "screen-id", styles: {} })),
165
+ })
166
+ );
167
+
168
+ jest.mock("../PlayerContainerContext", () => {
169
+ const ReactActual = require("react");
170
+
171
+ const PlayerContainerContext = ReactActual.createContext({
172
+ isLanguageOverlayVisible: false,
173
+ bottomFocusableId: "bottom",
174
+ showComponentsContainer: false,
175
+ refs: null,
176
+ });
177
+
178
+ return {
179
+ PlayerContainerContext,
180
+ // Mirrors just enough of the real provider for these tests: renders an
181
+ // actual `PlayerContainerContext.Provider` so the real
182
+ // `PlayerContainerContext.Consumer` inside PlayerContainer.tsx sees the
183
+ // container fields a real provider would give it, instead of a bare
184
+ // `({ children }) => children` passthrough.
185
+ PlayerContainerContextProvider: ({ children }: any) =>
186
+ ReactActual.createElement(
187
+ PlayerContainerContext.Provider,
188
+ {
189
+ value: {
190
+ isLanguageOverlayVisible: false,
191
+ bottomFocusableId: "bottom",
192
+ showComponentsContainer: false,
193
+ refs: null,
194
+ },
195
+ },
196
+ children
197
+ ),
198
+ };
199
+ });
200
+
201
+ jest.mock(
202
+ "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup",
203
+ () => ({
204
+ FocusableGroup: ({ children }) => children,
205
+ })
206
+ );
207
+
208
+ jest.mock("../WappersView/PlayerFocusableWrapperView", () => ({
209
+ PlayerFocusableWrapperView: ({ children }) => children,
210
+ }));
211
+
212
+ jest.mock("../WappersView/ComponentFocusableWrapperView", () => ({
213
+ ComponentFocusableWrapperView: () => null,
214
+ }));
215
+
216
+ jest.mock("../index", () => ({
217
+ FocusableGroupMainContainerId: "player-container-general",
218
+ }));
219
+
220
+ jest.mock(
221
+ "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers",
222
+ () => ({
223
+ isPlayable: jest.fn(() => true),
224
+ })
225
+ );
226
+
227
+ jest.mock("../../GeneralContentScreen", () => ({
228
+ GeneralContentScreen: () => null,
229
+ }));
230
+
231
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
232
+ useAppData: jest.fn(() => ({ isTabletPortrait: false })),
233
+ }));
234
+
235
+ import { PlayerContainer, VideoModalMode } from "../PlayerContainer";
236
+
237
+ const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } =
238
+ jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks");
239
+
240
+ const { playerManager: mockPlayerManager } = jest.requireMock(
241
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager"
242
+ );
243
+
244
+ const { __player: mockPlayer } = jest.requireMock(
245
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
246
+ );
247
+
248
+ const { isTV: mockIsTV } = jest.requireMock(
249
+ "@applicaster/zapp-react-native-utils/reactUtils"
250
+ );
251
+
252
+ // Renders children so that `renderApplePlayer`'s returned <ProgramInfo>
253
+ // element (passed as this component's `children` prop) actually mounts —
254
+ // otherwise React never invokes the ProgramInfo function component at all.
255
+ const Player = React.forwardRef((props: any, _ref) => props.children ?? null);
256
+
257
+ const item = {
258
+ id: "entry-1",
259
+ title: "Test entry",
260
+ summary: "",
261
+ content: { src: "https://example.com/video.m3u8" },
262
+ extensions: {},
263
+ } as any;
264
+
265
+ const renderPlayerContainer = (props = {}) =>
266
+ render(
267
+ <PlayerContainer
268
+ Player={Player}
269
+ item={item}
270
+ style={{}}
271
+ loadPipesData={jest.fn()}
272
+ isModal={true}
273
+ mode={VideoModalMode.MAXIMIZED}
274
+ {...props}
275
+ />
276
+ );
277
+
278
+ const pressHardwareBack = () => {
279
+ if (!mockBackHandlerRef.current) {
280
+ throw new Error("back handler was not registered via useBackHandler");
281
+ }
282
+
283
+ return mockBackHandlerRef.current();
284
+ };
285
+
286
+ describe("PlayerContainer hardware back handling", () => {
287
+ beforeEach(() => {
288
+ jest.clearAllMocks();
289
+ mockIsTV.mockReturnValue(false);
290
+ mockBackHandlerRef.current = null;
291
+ mockPlayer.getPluginConfiguration.mockReturnValue({});
292
+ mockPlayer.isPaused.mockReturnValue(false);
293
+ mockPlayer.isAd.mockReturnValue(false);
294
+
295
+ mockPlayer.getOverlayObservable.mockReturnValue({
296
+ getPlayNextEntry: () => null,
297
+ });
298
+
299
+ mockPlayerManager.getPluginConfiguration.mockReturnValue({});
300
+ mockPlayerManager.on.mockReturnThis();
301
+ mockPlayerManager.removeHandler.mockReturnThis();
302
+ });
303
+
304
+ it("minimises the video modal on back press when mini player is enabled", () => {
305
+ renderPlayerContainer();
306
+
307
+ const handled = pressHardwareBack();
308
+
309
+ expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled();
310
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
311
+ expect(handled).toBe(true);
312
+ });
313
+
314
+ it("closes the video modal on back press when mini player is disabled", () => {
315
+ mockPlayer.getPluginConfiguration.mockReturnValue({
316
+ disable_mini_player_when_inline: true,
317
+ });
318
+
319
+ renderPlayerContainer();
320
+
321
+ const handled = pressHardwareBack();
322
+
323
+ expect(mockNavigator.closeVideoModal).toHaveBeenCalled();
324
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
325
+ expect(handled).toBe(true);
326
+ });
327
+
328
+ it("does not handle back press when the player is already minimised", () => {
329
+ renderPlayerContainer({ mode: VideoModalMode.MINIMIZED });
330
+
331
+ const handled = pressHardwareBack();
332
+
333
+ expect(handled).toBe(false);
334
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
335
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
336
+ });
337
+ });
@@ -2,7 +2,7 @@ import React from "react";
2
2
 
3
3
  type PreloaderWrapperProps = {
4
4
  showPreloader?: boolean;
5
- children?: React.ReactNode;
5
+ children: React.ReactElement | null;
6
6
  };
7
7
 
8
8
  export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
@@ -9,7 +9,7 @@ import { useScreenConfiguration } from "../useScreenConfiguration";
9
9
  import { RefreshControl } from "../RefreshControl";
10
10
  import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
11
11
  import {
12
- usePipesCacheReset,
12
+ useMarkPipesDataStale,
13
13
  useProfilerLogging,
14
14
  } from "@applicaster/zapp-react-native-utils/reactHooks";
15
15
  import { useLoadingState } from "./hooks/useLoadingState";
@@ -154,7 +154,7 @@ function ComponentsMapComponent(props: Props) {
154
154
  [flatListHeight]
155
155
  );
156
156
 
157
- usePipesCacheReset(riverId, riverComponents);
157
+ useMarkPipesDataStale(riverId, riverComponents);
158
158
 
159
159
  const refreshControl = React.useMemo(
160
160
  () => (pullToRefreshEnabled ? <RefreshControl /> : null),
@@ -10,11 +10,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
10
10
  >
11
11
  <View
12
12
  onLayout={[Function]}
13
- style={
14
- {
15
- "flex": 1,
16
- }
17
- }
13
+ style={{}}
18
14
  >
19
15
  <RCTScrollView
20
16
  ListFooterComponent={
@@ -167,11 +163,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
167
163
  >
168
164
  <View
169
165
  onLayout={[Function]}
170
- style={
171
- {
172
- "flex": 1,
173
- }
174
- }
166
+ style={{}}
175
167
  >
176
168
  <View />
177
169
  </View>
@@ -183,11 +175,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
183
175
  >
184
176
  <View
185
177
  onLayout={[Function]}
186
- style={
187
- {
188
- "flex": 1,
189
- }
190
- }
178
+ style={{}}
191
179
  />
192
180
  </View>
193
181
  <View
@@ -18,10 +18,13 @@ import {
18
18
  useScreenData,
19
19
  } from "@applicaster/zapp-react-native-utils/reactHooks";
20
20
  import { getNavigationPluginModule } from "@applicaster/zapp-react-native-app/App/Layout/layoutHelpers";
21
+ import {
22
+ isValidColor,
23
+ isTransparentColor,
24
+ } from "@applicaster/zapp-react-native-utils/colorUtils";
21
25
 
22
26
  import { RouteManager } from "../RouteManager";
23
27
  import { useScreenConfiguration } from "../River/useScreenConfiguration";
24
- import { isValidColor } from "./utils";
25
28
  import { useWaitForValidOrientation } from "./hooks";
26
29
 
27
30
  const screenStyles = {
@@ -82,9 +85,10 @@ export function Screen(_props: Props) {
82
85
  const style = React.useMemo(
83
86
  () => ({
84
87
  ...screenStyles,
85
- backgroundColor: isValidColor(backgroundColor)
86
- ? backgroundColor
87
- : theme.app_background_color,
88
+ backgroundColor:
89
+ isValidColor(backgroundColor) && !isTransparentColor(backgroundColor)
90
+ ? backgroundColor
91
+ : theme.app_background_color,
88
92
  }),
89
93
  [theme.app_background_color, backgroundColor]
90
94
  );
@@ -8,9 +8,11 @@ import { componentsLogger } from "../../Helpers/logger";
8
8
  const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
9
9
 
10
10
  /** Loads and provides `feedData` and store to */
11
- export const ScreenFeedLoader: React.FC<
12
- React.PropsWithChildren<{ id: string; feedData: any }>
13
- > = ({ id, feedData, children }) => {
11
+ export const ScreenFeedLoader: React.FC<{
12
+ id: string;
13
+ feedData: any;
14
+ children: React.ReactElement | null;
15
+ }> = ({ id, feedData, children }) => {
14
16
  const { source: feedUrl, mapping } = feedData;
15
17
 
16
18
  const { data, loading, error } = useFeedLoader({
@@ -8,7 +8,7 @@ export const ScreenResolverFeedProvider = ({
8
8
  useFeedData,
9
9
  }: {
10
10
  id: string;
11
- children: React.ReactNode;
11
+ children: React.ReactElement | null;
12
12
  useFeedData: (id: string) => Option<ZappDataSource>;
13
13
  }) => {
14
14
  const feedData = useFeedData(id);
@@ -42,9 +42,25 @@ export class ScreenRevealManager {
42
42
  private subject$ = new Subject<void>();
43
43
  private subscription: Subscription;
44
44
 
45
- constructor(componentsToRender: ZappUIComponent[], callback: Callback) {
45
+ /**
46
+ * @param initialNumberToLoad how many components to wait for before revealing
47
+ * the screen. Staggering exists so a screen full of network-backed components
48
+ * does not load them all at once. A caller that has nothing to stagger - see
49
+ * `disableIncrementalLoading` on the HOC - passes the full count here so the
50
+ * reveal waits for exactly what gets rendered. Omitted, the count is derived
51
+ * from the components themselves.
52
+ */
53
+ constructor(
54
+ componentsToRender: ZappUIComponent[],
55
+ callback: Callback,
56
+ initialNumberToLoad?: number
57
+ ) {
46
58
  this.numberOfComponentsWaitToLoadBeforePresent =
47
- getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender);
59
+ initialNumberToLoad == null
60
+ ? getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender)
61
+ : // Clamped like the computed path: waiting for components that do not
62
+ // exist would hold the screen behind its overlay until the timeout.
63
+ Math.min(initialNumberToLoad, componentsToRender.length);
48
64
 
49
65
  this.renderingState = makeListOf<ComponentLoadingState>(
50
66
  COMPONENT_LOADING_STATE.UNKNOWN,
@@ -22,6 +22,42 @@ import { makeListOf } from "@applicaster/zapp-react-native-utils/arrayUtils";
22
22
  import { isFirstComponentGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
23
23
  import { withTimeout$ } from "@applicaster/zapp-react-native-utils/idleUtils";
24
24
 
25
+ describe("ScreenRevealManager explicit component count", () => {
26
+ beforeEach(() => {
27
+ (isFirstComponentGallery as jest.Mock).mockReturnValue(false);
28
+
29
+ (makeListOf as jest.Mock).mockImplementation((value, length) =>
30
+ Array(length).fill(value)
31
+ );
32
+
33
+ (withTimeout$ as jest.Mock).mockReturnValue(new Subject());
34
+ });
35
+
36
+ it("waits for the number of components it is given, not the computed default", () => {
37
+ const components = Array(6).fill({}) as any;
38
+
39
+ const manager = new ScreenRevealManager(components, jest.fn(), 6);
40
+
41
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(6);
42
+ });
43
+
44
+ it("never waits for more components than it was handed", () => {
45
+ const components = Array(3).fill({}) as any;
46
+
47
+ const manager = new ScreenRevealManager(components, jest.fn(), 10);
48
+
49
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
50
+ });
51
+
52
+ it("falls back to the computed default when given no explicit count", () => {
53
+ const components = Array(6).fill({}) as any;
54
+
55
+ const manager = new ScreenRevealManager(components, jest.fn());
56
+
57
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
58
+ });
59
+ });
60
+
25
61
  describe("ScreenRevealManager", () => {
26
62
  let mockCallback: jest.Mock;
27
63
  let timeout$: Subject<void>;