@applicaster/zapp-react-native-ui-components 16.0.0-rc.3 → 16.0.0-rc.30

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 (58) hide show
  1. package/Components/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  2. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  3. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  4. package/Components/BackgroundImage/index.ts +1 -0
  5. package/Components/CellRendererResolver/index.ts +2 -2
  6. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  14. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  15. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  16. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  17. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  18. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  19. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  20. package/Components/GeneralContentScreen/index.ts +2 -0
  21. package/Components/Layout/FullWidthRow.tsx +38 -0
  22. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  23. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  24. package/Components/Layout/TV/index.tsx +3 -4
  25. package/Components/Layout/TV/index.web.tsx +2 -3
  26. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  27. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  28. package/Components/MasterCell/dataAdapter.ts +15 -5
  29. package/Components/MasterCell/index.tsx +30 -5
  30. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  31. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  32. package/Components/MasterCell/utils/index.ts +85 -46
  33. package/Components/PlayerContainer/PlayerContainer.tsx +14 -10
  34. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +284 -0
  35. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  36. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  37. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  38. package/Components/Screen/TV/hooks/index.ts +2 -0
  39. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  40. package/Components/Screen/TV/index.web.tsx +16 -7
  41. package/Components/Screen/index.tsx +8 -4
  42. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  43. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  44. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  45. package/Components/VideoLive/LiveImageManager.ts +56 -45
  46. package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -2
  47. package/Components/VideoModal/utils.ts +6 -1
  48. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  49. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  50. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  51. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  52. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  53. package/Helpers/index.js +7 -40
  54. package/package.json +5 -5
  55. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  56. package/Components/Screen/utils.ts +0 -16
  57. package/Helpers/Analytics/index.js +0 -95
  58. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
@@ -1,22 +1,23 @@
1
1
  import React, { useMemo } from "react";
2
2
  import * as R from "ramda";
3
- import validateColor from "validate-color";
3
+ import memoizee from "memoizee";
4
+
4
5
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
6
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
6
-
7
- import { masterCellLogger } from "../logger";
8
- import { getCellState } from "../../Cell/utils";
7
+ import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
9
8
  import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
10
- import { get } from "@applicaster/zapp-react-native-utils/utils";
11
- import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
9
+ import { pathOr, get, isNil } from "@applicaster/zapp-react-native-utils/utils";
10
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
12
11
  import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
13
12
  import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
14
- import memoizee from "memoizee";
15
- import stringify from "fast-json-stable-stringify";
13
+ import { isNotEmptyString } from "@applicaster/zapp-react-native-utils/stringUtils";
14
+ import { masterCellLogger } from "../logger";
15
+ import { getCellState } from "../../Cell/utils";
16
+ import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
16
17
  import { DataProvider } from "../DefaultComponents/DataProvider";
17
18
 
18
19
  const hasElementSpecificViewType = (viewType) => (element) => {
19
- if (R.isNil(element)) {
20
+ if (isNil(element)) {
20
21
  return false;
21
22
  }
22
23
 
@@ -28,12 +29,20 @@ const hasElementSpecificViewType = (viewType) => (element) => {
28
29
  return hasElementsSpecificViewType(viewType)(element.elements);
29
30
  };
30
31
 
32
+ export const hasElementsSpecificViewType = (viewType) => (elements) => {
33
+ if (isNilOrEmpty(elements)) {
34
+ return false;
35
+ }
36
+
37
+ return R.any(hasElementSpecificViewType(viewType))(elements);
38
+ };
39
+
31
40
  const logWarning = (
32
41
  colorValueFromCellStyle,
33
42
  colorFromProp,
34
43
  colorValueFromEntry
35
44
  ) => {
36
- if (R.isNil(colorValueFromEntry)) {
45
+ if (isNil(colorValueFromEntry)) {
37
46
  masterCellLogger.warn({
38
47
  message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`,
39
48
  data: {
@@ -44,15 +53,10 @@ const logWarning = (
44
53
  }
45
54
  };
46
55
 
47
- export const hasElementsSpecificViewType = (viewType) => (elements) => {
48
- if (R.isNil(elements) || R.isEmpty(elements)) {
49
- return false;
50
- }
51
-
52
- return R.any(hasElementSpecificViewType(viewType))(elements);
53
- };
54
-
55
- function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
56
+ export function resolveColorForProp(
57
+ entry: any,
58
+ colorFromProp: string | undefined
59
+ ) {
56
60
  if (!colorFromProp) {
57
61
  return undefined;
58
62
  }
@@ -62,9 +66,7 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
62
66
  colorFromProp.split(".")
63
67
  );
64
68
 
65
- const color = colorFromProp.replace(".00", "").replace(".0", ""); // https://github.com/dreamyguy/validate-color/issues/44
66
-
67
- if (nestedEntryValue === undefined && !validateColor(color)) {
69
+ if (nestedEntryValue === undefined && !isValidColor(colorFromProp)) {
68
70
  logWarning(colorFromProp, colorFromProp, nestedEntryValue);
69
71
 
70
72
  return undefined;
@@ -86,36 +88,73 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
86
88
 
87
89
  const getColorKeys = memoizee((style) => {
88
90
  const styleKeys = Object.keys(style);
89
- const colorKeys = styleKeys.filter((key) => /color/i.test(key));
90
91
 
91
- return colorKeys;
92
+ return styleKeys.filter((key) => /color/i.test(key));
92
93
  });
93
94
 
94
- export const resolveColor = memoizee(
95
- (entry, style) => {
96
- if (style === null || style === undefined) {
97
- return style;
98
- }
95
+ /**
96
+ * A color style value is either a literal color (e.g. "#FFFFFF") or a
97
+ * data-mapping path into the entry. Data mappings always point at the entry
98
+ * `extensions` object (e.g. "extensions.brandColor"), so we only need to do a
99
+ * path lookup when the value starts with "extensions" — everything else is a
100
+ * literal color and can be returned as-is.
101
+ *
102
+ * This avoids the previous, very expensive approach that ran `validateColor`
103
+ * on every value and memoized on a full `fast-json-stable-stringify` of the
104
+ * entire entry.
105
+ */
106
+ const EXTENSIONS_PREFIX = "extensions";
107
+
108
+ const isDataMappingPath = (value: string): boolean =>
109
+ typeof value === "string" && value.startsWith(EXTENSIONS_PREFIX);
110
+
111
+ export const resolveColor = (
112
+ entry,
113
+ style,
114
+ allowDynamicColorsOutsideExtensions
115
+ ) => {
116
+ if (style === null || style === undefined) {
117
+ return style;
118
+ }
119
+
120
+ const colorKeys = getColorKeys(style);
121
+
122
+ if (colorKeys.length === 0) {
123
+ return style;
124
+ }
125
+
126
+ return colorKeys.reduce(
127
+ (acc, key) => {
128
+ const value = acc[key];
99
129
 
100
- return getColorKeys(style).reduce(
101
- (acc, value) => {
102
- if (acc[value] && typeof acc[value] === "string") {
103
- const colorStyle = resolveColorForProp(entry, acc[value]);
130
+ // 1. The Expensive Edge-Case (Only for the app with root mappings)
131
+ if (allowDynamicColorsOutsideExtensions && isNotEmptyString(value)) {
132
+ const possibleColor = resolveColorForProp(entry, value);
104
133
 
105
- acc[value] = colorStyle;
106
- }
134
+ acc[key] = isValidColor(possibleColor) ? possibleColor : null;
107
135
 
108
136
  return acc;
109
- },
110
- { ...style }
111
- );
112
- },
113
- {
114
- normalizer: (args) => {
115
- return [stringify(args[0]), stringify(args[1])].join("|");
137
+ }
138
+
139
+ // 2. The Fast Path (For 99% of apps)
140
+ if (isDataMappingPath(value)) {
141
+ // resolve the mapped color from the entry; fall back to null
142
+ // (RN ignores null style values) when the path is missing
143
+ const possibleColor = pathOr(null, value.split("."), entry);
144
+
145
+ acc[key] = possibleColor;
146
+
147
+ return acc;
148
+ }
149
+
150
+ // 3. Default Case: Treat as a raw color string
151
+ acc[key] = value;
152
+
153
+ return acc;
116
154
  },
117
- }
118
- );
155
+ { ...style }
156
+ );
157
+ };
119
158
 
120
159
  export function isVideoPreviewEnabled({
121
160
  enable_video_preview = false,
@@ -238,7 +277,7 @@ const recursiveCloneElement = (focused: boolean) => (element) => {
238
277
  };
239
278
 
240
279
  export const recursiveCloneElementsWithState = (focused: boolean, children) => {
241
- if (R.isNil(children) || R.isEmpty(children)) {
280
+ if (isNilOrEmpty(children)) {
242
281
  return undefined;
243
282
  }
244
283
 
@@ -252,7 +291,7 @@ export const cloneChildrenWithIds = (
252
291
  ids: string[],
253
292
  children: React.ReactElement[]
254
293
  ) => {
255
- if (R.isNil(children) || R.isEmpty(children)) {
294
+ if (isNilOrEmpty(children)) {
256
295
  return undefined;
257
296
  }
258
297
 
@@ -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) {
@@ -346,11 +344,15 @@ const PlayerContainerComponent = (props: Props) => {
346
344
  setIsLoadingNextVideo(false);
347
345
 
348
346
  const resumeTime = Number(item?.extensions?.resumeTime);
347
+ const activePlayer = playerManager.getActivePlayer();
349
348
 
350
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
349
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
350
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
351
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
351
352
  if (
352
- !playerManager.getActivePlayer()?.hasResumePosition() &&
353
- !isNaN(resumeTime)
353
+ !activePlayer?.hasResumePosition() &&
354
+ resumeTime > 0 &&
355
+ !activePlayer?.isLive()
354
356
  ) {
355
357
  player?.seekTo(resumeTime);
356
358
  }
@@ -468,6 +470,8 @@ const PlayerContainerComponent = (props: Props) => {
468
470
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
469
471
  if (disableMiniPlayer) {
470
472
  navigator.closeVideoModal();
473
+ } else {
474
+ navigator.minimiseVideoModal();
471
475
  }
472
476
  }
473
477
 
@@ -0,0 +1,284 @@
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
+ isTV: jest.fn(() => false),
21
+ isAndroidTVPlatform: jest.fn(() => false),
22
+ isTvOSPlatform: jest.fn(() => false),
23
+ platformSelect: jest.fn(({ native }) => native),
24
+ }));
25
+
26
+ jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => ({
27
+ isAudioItem: jest.fn(() => false),
28
+ isInlineTV: jest.fn(() => false),
29
+ }));
30
+
31
+ jest.mock(
32
+ "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
33
+ () => ({
34
+ TVEventHandlerComponent: ({ children }) => children,
35
+ })
36
+ );
37
+
38
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({
39
+ usePrevious: jest.fn(),
40
+ }));
41
+
42
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => {
43
+ const navigator = {
44
+ closeVideoModal: jest.fn(),
45
+ minimiseVideoModal: jest.fn(),
46
+ maximiseVideoModal: jest.fn(),
47
+ fullscreenVideoModal: jest.fn(),
48
+ isVideoModalDocked: jest.fn(() => false),
49
+ goBack: jest.fn(),
50
+ replaceTop: jest.fn(),
51
+ push: jest.fn(),
52
+ setPlayNextOverlay: jest.fn(),
53
+ currentRoute: "/playable/entry-1",
54
+ };
55
+
56
+ const backHandlerRef: { current: (() => boolean) | null } = {
57
+ current: null,
58
+ };
59
+
60
+ return {
61
+ useBackHandler: jest.fn((cb) => {
62
+ backHandlerRef.current = cb;
63
+ }),
64
+ useNavigation: jest.fn(() => navigator),
65
+ __navigator: navigator,
66
+ __backHandlerRef: backHandlerRef,
67
+ };
68
+ });
69
+
70
+ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
71
+ QUICK_BRICK_EVENTS: {
72
+ IDLE_TIMER_DISABLED: "IDLE_TIMER_DISABLED",
73
+ MOVE_APP_TO_BACKGROUND: "MOVE_APP_TO_BACKGROUND",
74
+ },
75
+ sendQuickBrickEvent: jest.fn(),
76
+ }));
77
+
78
+ jest.mock("../ProgramInfo", () => ({
79
+ ProgramInfo: () => null,
80
+ }));
81
+
82
+ jest.mock("../../AudioPlayer", () => ({
83
+ AudioPlayer: () => null,
84
+ }));
85
+
86
+ jest.mock("../logger", () => ({
87
+ log_debug: jest.fn(),
88
+ log_info: jest.fn(),
89
+ log_warning: jest.fn(),
90
+ playerContainerLogger: { error: jest.fn() },
91
+ }));
92
+
93
+ jest.mock(
94
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
95
+ () => {
96
+ const player = {
97
+ addListener: jest.fn(),
98
+ removeListener: jest.fn(),
99
+ isPaused: jest.fn(() => false),
100
+ isAd: jest.fn(() => false),
101
+ seekTo: jest.fn(),
102
+ getPluginConfiguration: jest.fn(() => ({})),
103
+ getOverlayObservable: jest.fn(() => ({ getPlayNextEntry: () => null })),
104
+ };
105
+
106
+ return {
107
+ usePlayer: jest.fn(() => player),
108
+ __player: player,
109
+ };
110
+ }
111
+ );
112
+
113
+ jest.mock(
114
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay",
115
+ () => ({
116
+ usePlayNextOverlay: jest.fn(() => null),
117
+ })
118
+ );
119
+
120
+ jest.mock(
121
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand",
122
+ () => ({
123
+ PlayerNativeCommandTypes: { clearPlayerData: "clearPlayerData" },
124
+ PlayerNativeSendCommand: jest.fn(),
125
+ })
126
+ );
127
+
128
+ jest.mock(
129
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
130
+ () => ({
131
+ useSetNavbarState: jest.fn(() => ({ setVisible: jest.fn() })),
132
+ })
133
+ );
134
+
135
+ jest.mock(
136
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData",
137
+ () => ({
138
+ useTargetScreenData: jest.fn(() => ({ id: "screen-id", styles: {} })),
139
+ })
140
+ );
141
+
142
+ jest.mock("../PlayerContainerContext", () => {
143
+ const ReactActual = require("react");
144
+
145
+ return {
146
+ PlayerContainerContext: ReactActual.createContext({
147
+ isLanguageOverlayVisible: false,
148
+ bottomFocusableId: "bottom",
149
+ showComponentsContainer: false,
150
+ refs: null,
151
+ }),
152
+ PlayerContainerContextProvider: ({ children }) => children,
153
+ };
154
+ });
155
+
156
+ jest.mock(
157
+ "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup",
158
+ () => ({
159
+ FocusableGroup: ({ children }) => children,
160
+ })
161
+ );
162
+
163
+ jest.mock("../WappersView/PlayerFocusableWrapperView", () => ({
164
+ PlayerFocusableWrapperView: ({ children }) => children,
165
+ }));
166
+
167
+ jest.mock("../WappersView/ComponentFocusableWrapperView", () => ({
168
+ ComponentFocusableWrapperView: () => null,
169
+ }));
170
+
171
+ jest.mock("../index", () => ({
172
+ FocusableGroupMainContainerId: "player-container-general",
173
+ }));
174
+
175
+ jest.mock(
176
+ "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers",
177
+ () => ({
178
+ isPlayable: jest.fn(() => true),
179
+ })
180
+ );
181
+
182
+ jest.mock("../../GeneralContentScreen", () => ({
183
+ GeneralContentScreen: () => null,
184
+ }));
185
+
186
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
187
+ useAppData: jest.fn(() => ({ isTabletPortrait: false })),
188
+ }));
189
+
190
+ import { PlayerContainer, VideoModalMode } from "../PlayerContainer";
191
+
192
+ const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } =
193
+ jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks");
194
+
195
+ const { playerManager: mockPlayerManager } = jest.requireMock(
196
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager"
197
+ );
198
+
199
+ const { __player: mockPlayer } = jest.requireMock(
200
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
201
+ );
202
+
203
+ const Player = React.forwardRef(() => null);
204
+
205
+ const item = {
206
+ id: "entry-1",
207
+ title: "Test entry",
208
+ summary: "",
209
+ content: { src: "https://example.com/video.m3u8" },
210
+ extensions: {},
211
+ } as any;
212
+
213
+ const renderPlayerContainer = (props = {}) =>
214
+ render(
215
+ <PlayerContainer
216
+ Player={Player}
217
+ item={item}
218
+ style={{}}
219
+ loadPipesData={jest.fn()}
220
+ isModal={true}
221
+ mode={VideoModalMode.MAXIMIZED}
222
+ {...props}
223
+ />
224
+ );
225
+
226
+ const pressHardwareBack = () => {
227
+ if (!mockBackHandlerRef.current) {
228
+ throw new Error("back handler was not registered via useBackHandler");
229
+ }
230
+
231
+ return mockBackHandlerRef.current();
232
+ };
233
+
234
+ describe("PlayerContainer hardware back handling", () => {
235
+ beforeEach(() => {
236
+ jest.clearAllMocks();
237
+ mockBackHandlerRef.current = null;
238
+ mockPlayer.getPluginConfiguration.mockReturnValue({});
239
+ mockPlayer.isPaused.mockReturnValue(false);
240
+ mockPlayer.isAd.mockReturnValue(false);
241
+
242
+ mockPlayer.getOverlayObservable.mockReturnValue({
243
+ getPlayNextEntry: () => null,
244
+ });
245
+
246
+ mockPlayerManager.getPluginConfiguration.mockReturnValue({});
247
+ mockPlayerManager.on.mockReturnThis();
248
+ mockPlayerManager.removeHandler.mockReturnThis();
249
+ });
250
+
251
+ it("minimises the video modal on back press when mini player is enabled", () => {
252
+ renderPlayerContainer();
253
+
254
+ const handled = pressHardwareBack();
255
+
256
+ expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled();
257
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
258
+ expect(handled).toBe(true);
259
+ });
260
+
261
+ it("closes the video modal on back press when mini player is disabled", () => {
262
+ mockPlayer.getPluginConfiguration.mockReturnValue({
263
+ disable_mini_player_when_inline: true,
264
+ });
265
+
266
+ renderPlayerContainer();
267
+
268
+ const handled = pressHardwareBack();
269
+
270
+ expect(mockNavigator.closeVideoModal).toHaveBeenCalled();
271
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
272
+ expect(handled).toBe(true);
273
+ });
274
+
275
+ it("does not handle back press when the player is already minimised", () => {
276
+ renderPlayerContainer({ mode: VideoModalMode.MINIMIZED });
277
+
278
+ const handled = pressHardwareBack();
279
+
280
+ expect(handled).toBe(false);
281
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
282
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
283
+ });
284
+ });
@@ -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
+ };