From a1c61a1196e2c7f2fc854b1d47a3fa1f8b78d653 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 23 Oct 2024 16:50:08 +0200 Subject: [PATCH 1/2] Add basic play-out tests --- e2e/src/tests/Basic.spec.ts | 77 ++++++++++++++++++++++++++++++------- e2e/src/utils/Actions.ts | 60 ++++++++++++++++++++--------- 2 files changed, 106 insertions(+), 31 deletions(-) diff --git a/e2e/src/tests/Basic.spec.ts b/e2e/src/tests/Basic.spec.ts index 1faca8dc6..e6fbb50ef 100644 --- a/e2e/src/tests/Basic.spec.ts +++ b/e2e/src/tests/Basic.spec.ts @@ -7,34 +7,85 @@ import mp4 from '../res/mp4.json'; import { getTestPlayer } from '../components/TestableTHEOplayerView'; import { expect, waitForPlayerEventType, waitForPlayerEventTypes } from '../utils/Actions'; -function testBasicPlayout(spec: TestScope, title: string, source: SourceDescription, autoplay: boolean) { +const SEEK_THRESHOLD = 1e-1; + +async function preparePlayerWithSource(source: SourceDescription, autoplay: boolean) { + const player = await getTestPlayer(); + const eventsPromise = waitForPlayerEventType(player, PlayerEventType.SOURCE_CHANGE); + const eventsPromiseAutoPlay = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); + + // Start autoplay + player.autoplay = autoplay; + player.source = source; + + // Wait for `sourcechange`, `play` and `playing` events. + if (autoplay) { + await eventsPromiseAutoPlay; + } else { + await eventsPromise; + } + return player; +} + +function testBasicPlayout(spec: TestScope, title: string, source: SourceDescription) { spec.describe(title, function () { - spec.it('dispatches sourcechange, play and playing in order', async function () { - const player = await getTestPlayer(); - const eventsPromise = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); + spec.it('dispatches sourcechange event on setting a source without autoplay', async function () { + // Set source and wait for playback + const player = await preparePlayerWithSource(source, false); - // Start autoplay - player.autoplay = autoplay; - player.source = source; + // Still playing + expect(player.paused).toBeTruthy(); + }); - // Expect events. - await eventsPromise; + spec.it('dispatches sourcechange, play and playing events in order on setting a source with autoplay', async function () { + // Set source and wait for playback + const player = await preparePlayerWithSource(source, true); // Still playing expect(player.paused).toBeFalsy(); + }); + + spec.it('dispatches a seeked event after seeking', async function () { + // Set source and wait for playback + const player = await preparePlayerWithSource(source, true); // Seek const seekPromise = waitForPlayerEventType(player, PlayerEventType.SEEKED); - player.currentTime = 10e3; + const seekTime = 10e3; + player.currentTime = seekTime; + + // Wait for `seeked` event. await seekPromise; + + // Expect currentTime to be updated. + expect(player.currentTime).toBeSmallerThanOrEqual(seekTime + SEEK_THRESHOLD); + }); + + spec.it('dispatches paused, play and playing events after pausing & resuming playback', async function () { + // Set source and wait for playback + const player = await preparePlayerWithSource(source, true); + + // Pause play-out. + const pausePromise = waitForPlayerEventType(player, PlayerEventType.PAUSE); + player.pause(); + + // Wait for 'paused' event. + await pausePromise; + + // Resume play-out. + const playPromises = waitForPlayerEventTypes(player, [PlayerEventType.PLAY, PlayerEventType.PLAYING]); + player.play(); + + // Wait for 'play' and 'playing' events. + await playPromises; }); }); } export default function (spec: TestScope) { if (Platform.OS === 'android' || Platform.OS === 'web') { - testBasicPlayout(spec, 'Set DASH source and auto-play', dash[0], true); + testBasicPlayout(spec, 'Set DASH source and auto-play', dash[0]); } - testBasicPlayout(spec, 'Set HLS source and auto-play', hls[0], true); - testBasicPlayout(spec, 'Set mp4 source and auto-play', mp4[0], true); + testBasicPlayout(spec, 'Set HLS source and auto-play', hls[0]); + testBasicPlayout(spec, 'Set mp4 source and auto-play', mp4[0]); } diff --git a/e2e/src/utils/Actions.ts b/e2e/src/utils/Actions.ts index 6a0bbc4a1..5d49a3440 100644 --- a/e2e/src/utils/Actions.ts +++ b/e2e/src/utils/Actions.ts @@ -63,7 +63,7 @@ export const waitForPlayerEvents = async >( } const expectedEvent = eventMap[0].event; events.push(receivedEvent); - console.debug('[waitForPlayerEvents]', `Received event '${JSON.stringify(receivedEvent.type)}' - waiting for ${expectedEvent.type}`); + console.debug('[waitForPlayerEvents]', `Received event ${JSON.stringify(receivedEvent.type)} - waiting for ${expectedEvent.type}`); const index = eventMap.findIndex((e) => propsMatch(e.event, receivedEvent)); const isExpected = index <= 0; @@ -108,34 +108,58 @@ const withTimeOut = (promise: Promise, timeout: number): Promise => { }); }; -export function expect(actual: any) { +export function expect(actual: any, desc?: string) { + const descPrefix = desc ? `${desc}: ` : ''; + + const logPass = (msg: string) => console.log(`${descPrefix}${msg} ✅`); + const throwErr = (msg: string) => { + throw new Error(`${descPrefix}${msg} ❌`); + }; + return { toBe(expected: any) { - if (actual !== expected) { - throw new Error(`Expected ${actual} to be ${expected} ❌`); - } - console.log(`Passed: ${actual} == ${expected} ✅`); + if (actual === expected) logPass(`${actual} == ${expected}`); + else throwErr(`Expected ${actual} to be ${expected}`); + }, + + toNotBe(expected: any) { + if (actual !== expected) logPass(`${actual} != ${expected}`); + else throwErr(`Expected ${actual} not to be ${expected}`); }, toEqual(expected: any) { - if (JSON.stringify(actual) !== JSON.stringify(expected)) { - throw new Error(`Expected ${JSON.stringify(actual)} to equal ${JSON.stringify(expected)} ❌`); - } - console.log(`Passed: ${JSON.stringify(actual)} !== ${JSON.stringify(expected)} ✅`); + if (JSON.stringify(actual) === JSON.stringify(expected)) logPass(`Expected ${actual} to equal ${expected}`); + else throwErr(`Expected ${actual} to equal ${expected}`); + }, + + toBeGreaterThan(expected: number) { + if (actual > expected) logPass(`${actual} > ${expected}`); + else throwErr(`Expected ${actual} to be greater than ${expected}`); + }, + + toBeGreaterThanOrEqual(expected: number) { + if (actual >= expected) logPass(`${actual} >= ${expected}`); + else throwErr(`Expected ${actual} to be greater than or equal to ${expected}`); + }, + + toBeSmallerThan(expected: number) { + if (actual < expected) logPass(`${actual} < ${expected}`); + else throwErr(`Expected ${actual} to be smaller than ${expected}`); + }, + + toBeSmallerThanOrEqual(expected: number) { + if (actual <= expected) logPass(`${actual} <= ${expected}`); + else throwErr(`Expected ${actual} to be smaller than or equal to ${expected}`); }, toBeTruthy() { - if (!actual) { - throw new Error(`Expected ${actual} to be truthy ❌`); - } - console.log(`Passed: ${actual} is truthy ✅`); + if (actual) logPass(`${actual} is truthy`); + else throwErr(`Expected ${actual} to be truthy`); }, toBeFalsy() { - if (actual) { - throw new Error(`Expected ${actual} to be falsy ❌`); - } - console.log(`Passed: ${actual} is falsy ✅`); + if (!actual) logPass(`${actual} is falsy`); + else throwErr(`Expected ${actual} to be falsy`); }, }; } From 7445e87619edfe1d157501b8bf038718952877d4 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 24 Oct 2024 09:48:29 +0200 Subject: [PATCH 2/2] Add presentation mode test --- e2e/src/tests/Ads.spec.ts | 6 ++-- e2e/src/tests/Basic.spec.ts | 27 +++-------------- e2e/src/tests/Connector.spec.ts | 24 +++++++-------- e2e/src/tests/PresentationMode.spec.ts | 42 ++++++++++++++++++++++++++ e2e/src/tests/index.ts | 5 +-- e2e/src/utils/Actions.ts | 21 ++++++++++++- 6 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 e2e/src/tests/PresentationMode.spec.ts diff --git a/e2e/src/tests/Ads.spec.ts b/e2e/src/tests/Ads.spec.ts index 19530a4a2..95d793495 100644 --- a/e2e/src/tests/Ads.spec.ts +++ b/e2e/src/tests/Ads.spec.ts @@ -1,5 +1,5 @@ import { TestScope } from 'cavy'; -import { AdDescription, AdEventType, PlayerEventType, SourceDescription } from 'react-native-theoplayer'; +import { AdDescription, AdEventType, PlayerEventType, SourceDescription, AdEvent } from 'react-native-theoplayer'; import hls from '../res/hls.json'; import ads from '../res/ads.json'; import { getTestPlayer } from '../components/TestableTHEOplayerView'; @@ -16,8 +16,8 @@ export default function (spec: TestScope) { const playEventsPromise = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); const adEventsPromise = waitForPlayerEvents(player, [ - { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BREAK_BEGIN }, - { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BEGIN }, + { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BREAK_BEGIN } as AdEvent, + { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BEGIN } as AdEvent, ]); // Start autoplay diff --git a/e2e/src/tests/Basic.spec.ts b/e2e/src/tests/Basic.spec.ts index e6fbb50ef..6de3f00fc 100644 --- a/e2e/src/tests/Basic.spec.ts +++ b/e2e/src/tests/Basic.spec.ts @@ -4,29 +4,10 @@ import { PlayerEventType, SourceDescription } from 'react-native-theoplayer'; import dash from '../res/dash.json'; import hls from '../res/hls.json'; import mp4 from '../res/mp4.json'; -import { getTestPlayer } from '../components/TestableTHEOplayerView'; -import { expect, waitForPlayerEventType, waitForPlayerEventTypes } from '../utils/Actions'; +import { expect, preparePlayerWithSource, waitForPlayerEventType, waitForPlayerEventTypes } from '../utils/Actions'; const SEEK_THRESHOLD = 1e-1; -async function preparePlayerWithSource(source: SourceDescription, autoplay: boolean) { - const player = await getTestPlayer(); - const eventsPromise = waitForPlayerEventType(player, PlayerEventType.SOURCE_CHANGE); - const eventsPromiseAutoPlay = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); - - // Start autoplay - player.autoplay = autoplay; - player.source = source; - - // Wait for `sourcechange`, `play` and `playing` events. - if (autoplay) { - await eventsPromiseAutoPlay; - } else { - await eventsPromise; - } - return player; -} - function testBasicPlayout(spec: TestScope, title: string, source: SourceDescription) { spec.describe(title, function () { spec.it('dispatches sourcechange event on setting a source without autoplay', async function () { @@ -39,7 +20,7 @@ function testBasicPlayout(spec: TestScope, title: string, source: SourceDescript spec.it('dispatches sourcechange, play and playing events in order on setting a source with autoplay', async function () { // Set source and wait for playback - const player = await preparePlayerWithSource(source, true); + const player = await preparePlayerWithSource(source); // Still playing expect(player.paused).toBeFalsy(); @@ -47,7 +28,7 @@ function testBasicPlayout(spec: TestScope, title: string, source: SourceDescript spec.it('dispatches a seeked event after seeking', async function () { // Set source and wait for playback - const player = await preparePlayerWithSource(source, true); + const player = await preparePlayerWithSource(source); // Seek const seekPromise = waitForPlayerEventType(player, PlayerEventType.SEEKED); @@ -63,7 +44,7 @@ function testBasicPlayout(spec: TestScope, title: string, source: SourceDescript spec.it('dispatches paused, play and playing events after pausing & resuming playback', async function () { // Set source and wait for playback - const player = await preparePlayerWithSource(source, true); + const player = await preparePlayerWithSource(source); // Pause play-out. const pausePromise = waitForPlayerEventType(player, PlayerEventType.PAUSE); diff --git a/e2e/src/tests/Connector.spec.ts b/e2e/src/tests/Connector.spec.ts index 1f325675f..bf4a4c077 100644 --- a/e2e/src/tests/Connector.spec.ts +++ b/e2e/src/tests/Connector.spec.ts @@ -14,12 +14,12 @@ type PlayerFn = (player: THEOplayer) => Promise | void; const NoOpPlayerFn: PlayerFn = (_player: THEOplayer) => {}; function testConnector(spec: TestScope, onCreate: PlayerFn, onUseAPI: PlayerFn, onDestroy: PlayerFn) { - spec.it('successfully creates the connector and connects to the player', async function () { - await onCreate(await getTestPlayer()); - }); - - spec.it('handles sourcechange, play and playing events on play-out', async function () { + spec.it('successfully creates the connector, connects to the player, uses API, and cleans up and destroys.', async function () { const player = await getTestPlayer(); + + // Create connector. + await onCreate(player); + const eventsPromise = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); // Start autoplay @@ -28,16 +28,14 @@ function testConnector(spec: TestScope, onCreate: PlayerFn, onUseAPI: PlayerFn, // Expect events. await eventsPromise; - }); - if (onUseAPI !== NoOpPlayerFn) { - spec.it('successfully uses connector API', async function () { - await onUseAPI(await getTestPlayer()); - }); - } + // Use connector API + if (onUseAPI !== NoOpPlayerFn) { + await onUseAPI(player); + } - spec.it('successfully cleans up on destroy', async function () { - await onDestroy(await getTestPlayer()); + // Clean-up and destroy connector. + await onDestroy(player); }); } diff --git a/e2e/src/tests/PresentationMode.spec.ts b/e2e/src/tests/PresentationMode.spec.ts new file mode 100644 index 000000000..4ba1f7f55 --- /dev/null +++ b/e2e/src/tests/PresentationMode.spec.ts @@ -0,0 +1,42 @@ +import { TestScope } from 'cavy'; +import { PlayerEventType, PresentationMode, PresentationModeChangeEvent } from 'react-native-theoplayer'; +import hls from '../res/hls.json'; +import { expect, preparePlayerWithSource, waitForPlayerEvent } from '../utils/Actions'; +import { sleep } from '../utils/TimeUtils'; + +export default function (spec: TestScope) { + spec.describe('Switches between presentation modes.', function () { + spec.it('dispatches presentationmodechange events between inline and fullscreen.', async function () { + const player = await preparePlayerWithSource(hls[0]); + + // Switch to fullscreen. + const fullscreenPromise = waitForPlayerEvent(player, { + type: PlayerEventType.PRESENTATIONMODE_CHANGE, + presentationMode: PresentationMode.fullscreen, + previousPresentationMode: PresentationMode.inline, + } as PresentationModeChangeEvent); + player.presentationMode = PresentationMode.fullscreen; + + // Wait for 'presentationmodechange' event. + await fullscreenPromise; + + // Play-out should not pause. + await sleep(500); + expect(player.paused).toBeFalsy(); + + // Switch back to inline. + const inlinePromise = waitForPlayerEvent(player, { + type: PlayerEventType.PRESENTATIONMODE_CHANGE, + presentationMode: PresentationMode.inline, + previousPresentationMode: PresentationMode.inline, + } as PresentationModeChangeEvent); + player.presentationMode = PresentationMode.inline; + + // Wait for 'presentationmodechange' event. + await inlinePromise; + + // Play-out should not pause. + expect(player.paused).toBeFalsy(); + }); + }); +} diff --git a/e2e/src/tests/index.ts b/e2e/src/tests/index.ts index 7aef5346e..987aaf663 100644 --- a/e2e/src/tests/index.ts +++ b/e2e/src/tests/index.ts @@ -1,5 +1,6 @@ -import Basic from './Basic.spec'; import Ads from './Ads.spec'; +import Basic from './Basic.spec'; import Connector from './Connector.spec'; +import PresentationMode from './PresentationMode.spec'; -export default [Basic, Ads, Connector]; +export default [Basic, Ads, Connector, PresentationMode]; diff --git a/e2e/src/utils/Actions.ts b/e2e/src/utils/Actions.ts index 5d49a3440..29428d78a 100644 --- a/e2e/src/utils/Actions.ts +++ b/e2e/src/utils/Actions.ts @@ -1,6 +1,7 @@ // noinspection JSUnusedGlobalSymbols -import { ErrorEvent, type Event, PlayerEventType, THEOplayer } from 'react-native-theoplayer'; +import { ErrorEvent, type Event, PlayerEventType, SourceDescription, THEOplayer } from 'react-native-theoplayer'; +import { getTestPlayer } from '../components/TestableTHEOplayerView'; export interface TestOptions { timeout: number; @@ -10,6 +11,24 @@ export const defaultTestOptions: TestOptions = { timeout: 10000, }; +export async function preparePlayerWithSource(source: SourceDescription, autoplay: boolean = true): Promise { + const player = await getTestPlayer(); + const eventsPromise = waitForPlayerEventType(player, PlayerEventType.SOURCE_CHANGE); + const eventsPromiseAutoPlay = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); + + // Start autoplay + player.autoplay = autoplay; + player.source = source; + + // Wait for `sourcechange`, `play` and `playing` events. + if (autoplay) { + await eventsPromiseAutoPlay; + } else { + await eventsPromise; + } + return player; +} + export const waitForPlayerEventType = async ( player: THEOplayer, type: PlayerEventType,