Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Theme, css } from '@emotion/react';
import pixelsToRem from '#app/utilities/pixelsToRem';
import { getEnvConfig } from '#app/lib/utilities/getEnvConfig';
import { PLAYER_FULLSCREEN_CLASS } from '#lib/mediaPlayerFullscreen.const';

const {
SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN,
Expand All @@ -25,6 +26,10 @@ export default {
justifyContent: 'center',
overflowY: 'auto',
overflowX: 'hidden',

[`body.${PLAYER_FULLSCREEN_CLASS} &`]: {
zIndex: -1,
},
}),

backdrop: css({
Expand Down
22 changes: 1 addition & 21 deletions src/app/components/Curation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,6 @@ const getGridComponent = (componentName: string | null) => {
}
};

const enterFakeScreenCallback = () => {
const consentBanner = document.getElementById('consent-banner');
if (consentBanner) {
consentBanner.style.zIndex = '-1';
}
};

const exitFakeScreenCallback = () => {
const consentBanner = document.getElementById('consent-banner');
if (consentBanner) {
consentBanner.style.zIndex = '2147483647';
}
};

interface CurationProps extends Curation {
// keep this local so we do not change the shared bff curation data shape
experimentProps?: ComponentExperimentProps;
Expand Down Expand Up @@ -238,13 +224,7 @@ export default ({
aria-labelledby="bbcMediaPlayer0"
data-testid={mediaCollectionId}
>
<MediaLoader
blocks={mediaCollection}
eventMapping={{
enterFakeFullscreen: enterFakeScreenCallback,
exitFakeFullscreen: exitFakeScreenCallback,
}}
/>
<MediaLoader blocks={mediaCollection} />
</section>
) : null;
}
Expand Down
55 changes: 55 additions & 0 deletions src/app/components/MediaLoader/index.styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
import pixelsToRem from '#app/utilities/pixelsToRem';
import { PLAYER_FULLSCREEN_CLASS } from '#lib/mediaPlayerFullscreen.const';
import { css, Theme } from '@emotion/react';

export { PLAYER_FULLSCREEN_CLASS };
export const FAKE_FULLSCREEN_LAYER_CLASS = 'simorgh-fake-fullscreen-layer';
export const FAKE_FULLSCREEN_ACTIVE_CLASS = 'simorgh-player-fullscreen-active';
export const ACTIVE_FULLSCREEN_LOADER_STATE = 'active-fake-fullscreen';

export const fakeFullscreenStyles = `
html.${PLAYER_FULLSCREEN_CLASS} {
overflow: hidden;
}

body.${PLAYER_FULLSCREEN_CLASS} {
overflow: auto;
}

.${FAKE_FULLSCREEN_LAYER_CLASS} {
display: none;
background: #000;
position: fixed;
inset: 0;
pointer-events: none;
height: 100lvh;
width: 100vw;
outline: 1000px solid #000;
z-index: 2147483646;
}

.${FAKE_FULLSCREEN_LAYER_CLASS}.${FAKE_FULLSCREEN_ACTIVE_CLASS} {
display: block;
}

[data-simorgh-media-loader="${ACTIVE_FULLSCREEN_LOADER_STATE}"] {
position: fixed !important;
inset: 0 !important;
width: 100vw !important;
max-width: none !important;
height: 100lvh !important;
margin: 0 !important;
aspect-ratio: auto !important;
background: #000 !important;
z-index: 2147483647 !important;
}

[data-simorgh-media-loader="${ACTIVE_FULLSCREEN_LOADER_STATE}"] .media-player {
height: 100% !important;
}
`;

const commonMarginSpacing = ({ mq, spacings }: Theme) =>
css({
marginInline: 0,
Expand Down Expand Up @@ -52,6 +100,13 @@ export default {
!isEmbedded && commonMarginSpacing,
],

mediaPlayerWrapper: () =>
css({
flex: 1,
minHeight: 0,
height: '100%',
}),

audioMediaContainer: () =>
css({
height: '165px',
Expand Down
180 changes: 179 additions & 1 deletion src/app/components/MediaLoader/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
} from '#app/components/react-testing-library-with-providers';
import { Helmet } from 'react-helmet';
import useLocation from '#app/hooks/useLocation';
import { TV_PAGE } from '#app/routes/utils/pageTypes';
import { LIVE_PAGE, TV_PAGE } from '#app/routes/utils/pageTypes';
import MediaPlayer from '.';
import {
aresMediaBlocks,
livePageAudioClipMediaBlock,
onDemandTvBlocks,
onDemandTvBlocksWithOverrides,
} from './fixture';
Expand Down Expand Up @@ -121,6 +122,7 @@ describe('MediaLoader', () => {
const mockRequire = jest.fn();
const mockBump = {
player: () => ({
bind: jest.fn(),
load: jest.fn(),
}),
};
Expand All @@ -144,6 +146,182 @@ describe('MediaLoader', () => {

expect(window.mediaPlayers.testId).not.toBeNull();
});

it('adds and removes fullscreen classes on fake fullscreen enter/exit events', async () => {
const mockRequire = jest.fn();
const bind = jest.fn();
const mockBump = {
player: () => ({
bind,
load: jest.fn(),
}),
};

window.requirejs = mockRequire;

await act(async () => {
render(<MediaPlayer blocks={aresMediaBlocks as MediaBlock[]} />, {
id: 'testId',
});
});

const callbackFn = mockRequire.mock.calls[0][1];
callbackFn(mockBump);

const enterFakeFullscreen = bind.mock.calls.find(
([event]) => event === 'enterFakeFullscreen',
)?.[1];
const exitFakeFullscreen = bind.mock.calls.find(
([event]) => event === 'exitFakeFullscreen',
)?.[1];

expect(typeof enterFakeFullscreen).toBe('function');
expect(typeof exitFakeFullscreen).toBe('function');

act(() => {
enterFakeFullscreen({});
});

expect(document.documentElement.classList).toContain(
'simorgh-player-fullscreen',
);
expect(document.body.classList).toContain('simorgh-player-fullscreen');

act(() => {
exitFakeFullscreen({});
});

expect(document.documentElement.classList).not.toContain(
'simorgh-player-fullscreen',
);
expect(document.body.classList).not.toContain(
'simorgh-player-fullscreen',
);
});

it('composes caller fake fullscreen handlers with internal fullscreen handlers', async () => {
const onEnterFakeFullscreen = jest.fn();
const mockRequire = jest.fn();
const bind = jest.fn();
const mockBump = {
player: () => ({
bind,
load: jest.fn(),
}),
};

window.requirejs = mockRequire;

await act(async () => {
render(
<MediaPlayer
blocks={aresMediaBlocks as MediaBlock[]}
eventMapping={{ enterFakeFullscreen: onEnterFakeFullscreen }}
/>,
{
id: 'testId',
},
);
});

const callbackFn = mockRequire.mock.calls[0][1];
callbackFn(mockBump);

const enterFakeFullscreenBindings = bind.mock.calls.filter(
([event]) => event === 'enterFakeFullscreen',
);

expect(enterFakeFullscreenBindings).toHaveLength(2);

act(() => {
enterFakeFullscreenBindings.forEach(([, handler]) => handler({}));
});

expect(onEnterFakeFullscreen).toHaveBeenCalled();
expect(document.documentElement.classList).toContain(
'simorgh-player-fullscreen',
);
});

it('does not bind fake fullscreen handlers for audio players', async () => {
const mockRequire = jest.fn();
const bind = jest.fn();
const mockBump = {
player: () => ({
bind,
load: jest.fn(),
}),
};

window.requirejs = mockRequire;

await act(async () => {
render(
<MediaPlayer
blocks={[livePageAudioClipMediaBlock] as MediaBlock[]}
/>,
{
id: 'testId',
pageType: LIVE_PAGE,
},
);
});

const callbackFn = mockRequire.mock.calls[0][1];
callbackFn(mockBump);

const fakeFullscreenBindings = bind.mock.calls.filter(([event]) =>
['enterFakeFullscreen', 'exitFakeFullscreen'].includes(event),
);

expect(fakeFullscreenBindings).toHaveLength(0);
});

it('does not reinitialise the player when fake fullscreen state changes', async () => {
// Use the real useState implementation here so entering fake fullscreen
// actually triggers a MediaLoader re-render, reproducing the scenario
// that previously caused the Bump player to be torn down and
// recreated mid-playback (closing iOS fake fullscreen immediately).
(useState as jest.Mock).mockImplementation(
jest.requireActual('react').useState,
);

const mockRequire = jest.fn();
const bind = jest.fn();
const load = jest.fn();
const mockBump = {
player: () => ({
bind,
load,
}),
};

window.requirejs = mockRequire;

await act(async () => {
render(<MediaPlayer blocks={aresMediaBlocks as MediaBlock[]} />, {
id: 'testId',
pageType: LIVE_PAGE,
});
});

const callbackFn = mockRequire.mock.calls[0][1];
callbackFn(mockBump);

expect(mockRequire).toHaveBeenCalledTimes(1);
expect(load).toHaveBeenCalledTimes(1);

const enterFakeFullscreen = bind.mock.calls.find(
([event]) => event === 'enterFakeFullscreen',
)?.[1];

act(() => {
enterFakeFullscreen({});
});

expect(mockRequire).toHaveBeenCalledTimes(1);
expect(load).toHaveBeenCalledTimes(1);
});
});

describe('Placeholder', () => {
Expand Down
Loading
Loading