import * as React from "react"; import { render } from "@testing-library/react-native"; jest.mock( "@applicaster/zapp-react-native-utils/appUtils/playerManager", () => ({ playerManager: { invokeHandler: jest.fn(), getPluginConfiguration: jest.fn(() => ({})), isRegistered: jest.fn(() => true), on: jest.fn().mockReturnThis(), removeHandler: jest.fn().mockReturnThis(), getActivePlayer: jest.fn(() => null), closeNativePlayer: jest.fn(), }, }) ); jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({ // Defaults to `false` for the hardware-back-handling tests below, which // key off `isTV`. The ProgramInfo/content-context describe block below // overrides this per test (`isTV()` is also what PlayerContainer's // `playerContentValue` uses to pick TV vs mobile data keys, decoupled // from `isTvOSPlatform` below). isTV: jest.fn(() => false), isAndroidTVPlatform: jest.fn(() => false), // `isTvOSPlatform` backs a module-level constant in PlayerContainer.tsx that // is read once at import time, so it must already be `true` here in order // to exercise `renderApplePlayer`'s tvOS-only ProgramInfo branch in this // test file. It does not affect the hardware-back-handling tests below, // which key off `isTV` instead. isTvOSPlatform: jest.fn(() => true), isApplePlatform: jest.fn(() => true), platformSelect: jest.fn(({ native }) => native), })); jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => { // Keep the real data-key resolver (createConfigValueGetter, // resolvePlayerContentText, VIDEO_PLAYER_CONTENT_DATA_KEYS) so the tvOS // ProgramInfo data-key tests exercise actual resolution logic, not a stub. const actual = jest.requireActual( "@applicaster/zapp-react-native-utils/playerUtils" ); return { ...actual, isAudioItem: jest.fn(() => false), isInlineTV: jest.fn(() => false), }; }); jest.mock( "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent", () => ({ TVEventHandlerComponent: ({ children }) => children, }) ); jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({ usePrevious: jest.fn(), })); jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => { const navigator = { closeVideoModal: jest.fn(), minimiseVideoModal: jest.fn(), maximiseVideoModal: jest.fn(), fullscreenVideoModal: jest.fn(), isVideoModalDocked: jest.fn(() => false), goBack: jest.fn(), replaceTop: jest.fn(), push: jest.fn(), setPlayNextOverlay: jest.fn(), currentRoute: "/playable/entry-1", }; const backHandlerRef: { current: (() => boolean) | null } = { current: null, }; return { useBackHandler: jest.fn((cb) => { backHandlerRef.current = cb; }), useNavigation: jest.fn(() => navigator), __navigator: navigator, __backHandlerRef: backHandlerRef, }; }); jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({ QUICK_BRICK_EVENTS: { IDLE_TIMER_DISABLED: "IDLE_TIMER_DISABLED", MOVE_APP_TO_BACKGROUND: "MOVE_APP_TO_BACKGROUND", }, sendQuickBrickEvent: jest.fn(), })); // Stubbed so PlayerContainer can render without ProgramInfo's own // redux/style plumbing. It no longer needs to capture Title/Subtitle: those // come from the player manager's content channel, and the key-set selection // they depend on is covered directly in // `playerManager/__tests__/playerContent.test.ts`. jest.mock("../ProgramInfo", () => ({ ProgramInfo: () => null, })); jest.mock("../../AudioPlayer", () => ({ AudioPlayer: () => null, })); jest.mock("../logger", () => ({ log_debug: jest.fn(), log_info: jest.fn(), log_warning: jest.fn(), playerContainerLogger: { error: jest.fn() }, })); jest.mock( "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer", () => { const player = { addListener: jest.fn(), removeListener: jest.fn(), isPaused: jest.fn(() => false), isAd: jest.fn(() => false), seekTo: jest.fn(), getPluginConfiguration: jest.fn(() => ({})), getOverlayObservable: jest.fn(() => ({ getPlayNextEntry: () => null })), }; return { usePlayer: jest.fn(() => player), __player: player, }; } ); jest.mock( "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay", () => ({ usePlayNextOverlay: jest.fn(() => null), }) ); jest.mock( "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand", () => ({ PlayerNativeCommandTypes: { clearPlayerData: "clearPlayerData" }, PlayerNativeSendCommand: jest.fn(), }) ); jest.mock( "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext", () => ({ useSetNavbarState: jest.fn(() => ({ setVisible: jest.fn() })), }) ); jest.mock( "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData", () => ({ useTargetScreenData: jest.fn(() => ({ id: "screen-id", styles: {} })), }) ); jest.mock("../PlayerContainerContext", () => { const ReactActual = require("react"); const PlayerContainerContext = ReactActual.createContext({ isLanguageOverlayVisible: false, bottomFocusableId: "bottom", showComponentsContainer: false, refs: null, }); return { PlayerContainerContext, // Mirrors just enough of the real provider for these tests: renders an // actual `PlayerContainerContext.Provider` so the real // `PlayerContainerContext.Consumer` inside PlayerContainer.tsx sees the // container fields a real provider would give it, instead of a bare // `({ children }) => children` passthrough. PlayerContainerContextProvider: ({ children }: any) => ReactActual.createElement( PlayerContainerContext.Provider, { value: { isLanguageOverlayVisible: false, bottomFocusableId: "bottom", showComponentsContainer: false, refs: null, }, }, children ), }; }); jest.mock( "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup", () => ({ FocusableGroup: ({ children }) => children, }) ); jest.mock("../WappersView/PlayerFocusableWrapperView", () => ({ PlayerFocusableWrapperView: ({ children }) => children, })); jest.mock("../WappersView/ComponentFocusableWrapperView", () => ({ ComponentFocusableWrapperView: () => null, })); jest.mock("../index", () => ({ FocusableGroupMainContainerId: "player-container-general", })); jest.mock( "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers", () => ({ isPlayable: jest.fn(() => true), }) ); jest.mock("../../GeneralContentScreen", () => ({ GeneralContentScreen: () => null, })); jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({ usePickFromState: jest.fn(() => ({ appData: { isTabletPortrait: false }, })), })); import { PlayerContainer, VideoModalMode } from "../PlayerContainer"; const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } = jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks"); const { playerManager: mockPlayerManager } = jest.requireMock( "@applicaster/zapp-react-native-utils/appUtils/playerManager" ); const { __player: mockPlayer } = jest.requireMock( "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer" ); const { isTV: mockIsTV } = jest.requireMock( "@applicaster/zapp-react-native-utils/reactUtils" ); // Renders children so that `renderApplePlayer`'s returned // element (passed as this component's `children` prop) actually mounts — // otherwise React never invokes the ProgramInfo function component at all. const Player = React.forwardRef((props: any, _ref) => props.children ?? null); const item = { id: "entry-1", title: "Test entry", summary: "", content: { src: "https://example.com/video.m3u8" }, extensions: {}, } as any; const renderPlayerContainer = (props = {}) => render( ); const pressHardwareBack = () => { if (!mockBackHandlerRef.current) { throw new Error("back handler was not registered via useBackHandler"); } return mockBackHandlerRef.current(); }; describe("PlayerContainer hardware back handling", () => { beforeEach(() => { jest.clearAllMocks(); mockIsTV.mockReturnValue(false); mockBackHandlerRef.current = null; mockPlayer.getPluginConfiguration.mockReturnValue({}); mockPlayer.isPaused.mockReturnValue(false); mockPlayer.isAd.mockReturnValue(false); mockPlayer.getOverlayObservable.mockReturnValue({ getPlayNextEntry: () => null, }); mockPlayerManager.getPluginConfiguration.mockReturnValue({}); mockPlayerManager.on.mockReturnThis(); mockPlayerManager.removeHandler.mockReturnThis(); }); it("minimises the video modal on back press when mini player is enabled", () => { renderPlayerContainer(); const handled = pressHardwareBack(); expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled(); expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled(); expect(handled).toBe(true); }); it("closes the video modal on back press when mini player is disabled", () => { mockPlayer.getPluginConfiguration.mockReturnValue({ disable_mini_player_when_inline: true, }); renderPlayerContainer(); const handled = pressHardwareBack(); expect(mockNavigator.closeVideoModal).toHaveBeenCalled(); expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled(); expect(handled).toBe(true); }); it("does not handle back press when the player is already minimised", () => { renderPlayerContainer({ mode: VideoModalMode.MINIMIZED }); const handled = pressHardwareBack(); expect(handled).toBe(false); expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled(); expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled(); }); });