@applicaster/zapp-react-native-ui-components 16.0.0-rc.6 → 16.0.0-rc.61

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 (129) 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/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  5. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  6. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  7. package/Components/BackgroundImage/index.ts +1 -0
  8. package/Components/CellRendererResolver/index.ts +2 -2
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  11. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  12. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  13. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  16. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  17. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  18. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  19. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -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/Layout/TV/ScreenContainer.tsx +5 -0
  26. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  27. package/Components/Layout/TV/index.tsx +3 -4
  28. package/Components/Layout/TV/index.web.tsx +2 -3
  29. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  32. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  34. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  35. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  36. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  52. package/Components/MasterCell/dataAdapter.ts +15 -5
  53. package/Components/MasterCell/hooks/index.ts +2 -0
  54. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  55. package/Components/MasterCell/index.tsx +30 -5
  56. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  57. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  58. package/Components/MasterCell/utils/index.ts +85 -46
  59. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +395 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  62. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  63. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +874 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  66. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
  67. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  68. package/Components/ModalComponent/BottomSheetModalContent.tsx +386 -42
  69. package/Components/ModalComponent/Button/index.tsx +5 -3
  70. package/Components/ModalComponent/Header/index.tsx +20 -7
  71. package/Components/ModalComponent/Header/utils.ts +1 -1
  72. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  73. package/Components/ModalComponent/SortableList.tsx +142 -0
  74. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  75. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  76. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  77. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  78. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  79. package/Components/ModalComponent/index.tsx +11 -0
  80. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  81. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  82. package/Components/ModalComponent/presets/index.ts +27 -0
  83. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  84. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  85. package/Components/ModalComponent/presets/types.ts +35 -0
  86. package/Components/ModalComponent/showTextInput.ts +43 -0
  87. package/Components/ModalComponent/utils.ts +76 -20
  88. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  89. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  90. package/Components/OfflineHandler/hooks.ts +123 -0
  91. package/Components/OfflineHandler/index.tsx +51 -73
  92. package/Components/OfflineHandler/utils/index.ts +1 -13
  93. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  94. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  95. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  96. package/Components/PreloaderWrapper/index.tsx +1 -1
  97. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  98. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  99. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  100. package/Components/Screen/TV/hooks/index.ts +2 -0
  101. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  102. package/Components/Screen/TV/index.web.tsx +16 -7
  103. package/Components/Screen/index.tsx +8 -4
  104. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  105. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  106. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  107. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  109. package/Components/TopCutoffOverlay/index.tsx +2 -2
  110. package/Components/Transitioner/Transitioner.tsx +28 -17
  111. package/Components/VideoLive/LiveImageManager.ts +41 -43
  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 +7 -40
  119. package/package.json +6 -5
  120. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  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/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  129. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -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
@@ -0,0 +1,60 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ import { useAfterPaint } from "../useAfterPaint";
4
+
5
+ describe("useAfterPaint", () => {
6
+ let frameCallbacks: FrameRequestCallback[];
7
+ let originalRaf: typeof requestAnimationFrame;
8
+ let originalCancelRaf: typeof cancelAnimationFrame;
9
+
10
+ const flushFrame = () => {
11
+ const callbacks = frameCallbacks;
12
+ frameCallbacks = [];
13
+
14
+ act(() => {
15
+ callbacks.forEach((cb) => cb(0));
16
+ });
17
+ };
18
+
19
+ beforeEach(() => {
20
+ frameCallbacks = [];
21
+ originalRaf = global.requestAnimationFrame;
22
+ originalCancelRaf = global.cancelAnimationFrame;
23
+
24
+ global.requestAnimationFrame = jest.fn((cb: FrameRequestCallback) => {
25
+ frameCallbacks.push(cb);
26
+
27
+ return frameCallbacks.length;
28
+ }) as unknown as typeof requestAnimationFrame;
29
+
30
+ global.cancelAnimationFrame = jest.fn();
31
+ });
32
+
33
+ afterEach(() => {
34
+ global.requestAnimationFrame = originalRaf;
35
+ global.cancelAnimationFrame = originalCancelRaf;
36
+ });
37
+
38
+ it("returns false on the initial render", () => {
39
+ const { result } = renderHook(() => useAfterPaint());
40
+
41
+ expect(result.current).toBe(false);
42
+ });
43
+
44
+ it("stays false after only one animation frame (before paint completes)", () => {
45
+ const { result } = renderHook(() => useAfterPaint());
46
+
47
+ flushFrame();
48
+
49
+ expect(result.current).toBe(false);
50
+ });
51
+
52
+ it("returns true after two animation frames (a real post-paint boundary)", () => {
53
+ const { result } = renderHook(() => useAfterPaint());
54
+
55
+ flushFrame();
56
+ flushFrame();
57
+
58
+ expect(result.current).toBe(true);
59
+ });
60
+ });
@@ -1 +1,3 @@
1
1
  export { useInitialFocus } from "./useInitialFocus";
2
+
3
+ export { useAfterPaint } from "./useAfterPaint";
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+
3
+ export const useAfterPaint = (): boolean => {
4
+ const [painted, setPainted] = React.useState(false);
5
+
6
+ React.useEffect(() => {
7
+ let secondFrame: number | undefined;
8
+
9
+ const firstFrame = requestAnimationFrame(() => {
10
+ secondFrame = requestAnimationFrame(() => setPainted(true));
11
+ });
12
+
13
+ return () => {
14
+ cancelAnimationFrame(firstFrame);
15
+
16
+ if (secondFrame !== undefined) {
17
+ cancelAnimationFrame(secondFrame);
18
+ }
19
+ };
20
+ }, []);
21
+
22
+ return painted;
23
+ };
@@ -21,7 +21,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
21
21
 
22
22
  import { NavBarContainer } from "../../Layout/TV/NavBarContainer";
23
23
  import { ScreenResolver } from "../../ScreenResolver";
24
- import { useInitialFocus } from "./hooks";
24
+ import { useInitialFocus, useAfterPaint } from "./hooks";
25
25
  import { isPlayerPlugin } from "@applicaster/zapp-react-native-utils/pluginUtils";
26
26
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
27
27
  import { FreezeWithCallback } from "../../FreezeWithCallback";
@@ -157,6 +157,13 @@ export const Screen = ({ route, Components }: Props) => {
157
157
 
158
158
  const isScreenActive = useIsScreenActive();
159
159
 
160
+ // We need to render menu first and then proceed with screen content,
161
+ // otherwise screen will stay black until everything is loaded and screen
162
+ // rendering put huge load the CPU pushing rendering even further.
163
+ // With this approach, menu will be rendered immediately and screen content
164
+ // will be rendered after paint, which makes it more responsive and prevents black screen.
165
+ const isContentReady = useAfterPaint();
166
+
160
167
  return (
161
168
  <FreezeWithCallback freeze={!isScreenActive} onRelease={onRelease}>
162
169
  <View style={[styles.container, { backgroundColor }]}>
@@ -167,12 +174,14 @@ export const Screen = ({ route, Components }: Props) => {
167
174
  navigationProps={navigationProps}
168
175
  />
169
176
  </NavBarContainer>
170
- <ScreenResolver
171
- screenType={screenType}
172
- screenId={screenId}
173
- screenData={screenData}
174
- groupId={route}
175
- />
177
+ {isContentReady ? (
178
+ <ScreenResolver
179
+ screenType={screenType}
180
+ screenId={screenId}
181
+ screenData={screenData}
182
+ groupId={route}
183
+ />
184
+ ) : null}
176
185
  </View>
177
186
  </FreezeWithCallback>
178
187
  );
@@ -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);
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { Animated, StyleSheet } from "react-native";
3
+
4
+ import { BackgroundImage } from "@applicaster/zapp-react-native-ui-components/Components/BackgroundImage";
5
+
6
+ const styles = StyleSheet.create({
7
+ container: {
8
+ ...StyleSheet.absoluteFillObject,
9
+ position: "absolute",
10
+ },
11
+ });
12
+
13
+ export const Overlay = ({
14
+ opacity,
15
+ backgroundColor,
16
+ }: {
17
+ opacity: Animated.Value;
18
+ backgroundColor: string;
19
+ }) => {
20
+ return (
21
+ <Animated.View
22
+ style={[
23
+ styles.container,
24
+ {
25
+ opacity,
26
+ backgroundColor,
27
+ },
28
+ ]}
29
+ testID="animated-component"
30
+ >
31
+ <BackgroundImage />
32
+ </Animated.View>
33
+ );
34
+ };