@applicaster/zapp-react-native-ui-components 16.0.0-rc.7 → 16.0.0-rc.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
- package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
- package/Components/AudioPlayer/tv/index.tsx +5 -1
- package/Components/Cell/TvOSCellComponent.tsx +0 -1
- package/Components/CellRendererResolver/index.ts +2 -2
- package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
- package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
- package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
- package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
- package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
- package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
- package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
- package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
- package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
- package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
- package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
- package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
- package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
- package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
- package/Components/GeneralContentScreen/index.ts +2 -0
- package/Components/Layout/FullWidthRow.tsx +38 -0
- package/Components/Layout/TV/ScreenContainer.tsx +5 -0
- package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
- package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
- package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
- package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
- package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
- package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
- package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
- package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
- package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
- package/Components/MasterCell/dataAdapter.ts +15 -5
- package/Components/MasterCell/hooks/index.ts +2 -0
- package/Components/MasterCell/hooks/usePressedState.ts +4 -0
- package/Components/MasterCell/index.tsx +30 -5
- package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
- package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
- package/Components/MasterCell/utils/index.ts +85 -46
- package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
- package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +395 -0
- package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
- package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +874 -0
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
- package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
- package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
- package/Components/ModalComponent/BottomSheetModalContent.tsx +383 -42
- package/Components/ModalComponent/Button/index.tsx +5 -3
- package/Components/ModalComponent/Header/index.tsx +20 -7
- package/Components/ModalComponent/Header/utils.ts +1 -1
- package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
- package/Components/ModalComponent/SortableList.tsx +142 -0
- package/Components/ModalComponent/SortableList.web.tsx +102 -0
- package/Components/ModalComponent/TextInputContent.tsx +100 -0
- package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
- package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
- package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
- package/Components/ModalComponent/index.tsx +11 -0
- package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
- package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
- package/Components/ModalComponent/presets/index.ts +27 -0
- package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
- package/Components/ModalComponent/presets/standardCell.tsx +41 -0
- package/Components/ModalComponent/presets/types.ts +35 -0
- package/Components/ModalComponent/showTextInput.ts +43 -0
- package/Components/ModalComponent/utils.ts +76 -20
- package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
- package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
- package/Components/OfflineHandler/hooks.ts +123 -0
- package/Components/OfflineHandler/index.tsx +51 -73
- package/Components/OfflineHandler/utils/index.ts +1 -13
- package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
- package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
- package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
- package/Components/PreloaderWrapper/index.tsx +1 -1
- package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
- package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
- package/Components/Screen/TV/hooks/index.ts +2 -0
- package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
- package/Components/Screen/TV/index.web.tsx +16 -7
- package/Components/Screen/index.tsx +8 -4
- package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
- package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
- package/Components/TopCutoffOverlay/index.tsx +2 -2
- package/Components/Transitioner/Transitioner.tsx +28 -17
- package/Components/VideoLive/LiveImageManager.ts +41 -43
- package/Components/VideoModal/utils.ts +6 -1
- package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
- package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
- package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
- package/Contexts/CachedDimensionsContext/index.ts +17 -2
- package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
- package/Helpers/index.js +0 -39
- package/package.json +6 -5
- package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
- package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
- package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
- package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
- package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
- package/Components/Screen/utils.ts +0 -16
- package/Helpers/Analytics/index.js +0 -95
- /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
- /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
- /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
- /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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
!
|
|
353
|
-
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
);
|