diff --git a/e2e/src/tests/Ads.spec.ts b/e2e/src/tests/Ads.spec.ts index 497de47a3..899c54eee 100644 --- a/e2e/src/tests/Ads.spec.ts +++ b/e2e/src/tests/Ads.spec.ts @@ -1,15 +1,9 @@ import { TestScope } from 'cavy'; -import { AdDescription, AdEventType, PlayerEventType, SourceDescription, AdEvent } from 'react-native-theoplayer'; -import hls from '../res/hls.json'; -import ads from '../res/ads.json'; +import { AdEventType, PlayerEventType, AdEvent } from 'react-native-theoplayer'; import { getTestPlayer } from '../components/TestableTHEOplayerView'; -import { waitForPlayerEvents, waitForPlayerEventTypes } from '../utils/Actions'; +import { waitForPlayerEvents } from '../utils/Actions'; import { TestSourceDescription, TestSources } from '../utils/SourceUtils'; -function extendSourceWithAds(source: SourceDescription, ad: AdDescription): SourceDescription { - return { ...source, ads: [ad] }; -} - export default function (spec: TestScope) { TestSources() .withAds() @@ -17,20 +11,20 @@ export default function (spec: TestScope) { spec.describe(`Set ${testSource.description} and auto-play`, function () { spec.it('dispatches sourcechange, play, playing and ad events', async function () { const player = await getTestPlayer(); - const playEventsPromise = waitForPlayerEventTypes(player, [PlayerEventType.SOURCE_CHANGE, PlayerEventType.PLAY, PlayerEventType.PLAYING]); - - const adEventsPromise = waitForPlayerEvents(player, [ + const eventsPromise = waitForPlayerEvents(player, [ + { type: PlayerEventType.SOURCE_CHANGE }, + { type: PlayerEventType.PLAY }, + { type: PlayerEventType.PLAYING }, { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BREAK_BEGIN } as AdEvent, { type: PlayerEventType.AD_EVENT, subType: AdEventType.AD_BEGIN } as AdEvent, ]); // Start autoplay player.autoplay = true; - player.source = extendSourceWithAds(hls[0], ads[0] as AdDescription); + player.source = testSource.source; - // Expect events. - await playEventsPromise; - await adEventsPromise; + // Expect events in order. + await eventsPromise; }); }); }); diff --git a/e2e/src/utils/Actions.ts b/e2e/src/utils/Actions.ts index ef2aee501..e7c74dfe3 100644 --- a/e2e/src/utils/Actions.ts +++ b/e2e/src/utils/Actions.ts @@ -1,7 +1,18 @@ // noinspection JSUnusedGlobalSymbols -import { ErrorEvent, type Event, PlayerEventType, SourceDescription, THEOplayer } from 'react-native-theoplayer'; +import { + AdEvent, + AdEventType, + ErrorEvent, + type Event, + EventMap, + PlayerEventType, + SourceDescription, + StringKeyOf, + THEOplayer, +} from 'react-native-theoplayer'; import { getTestPlayer } from '../components/TestableTHEOplayerView'; +import { logPlayerBuffer } from './PlayerUtils'; export interface TestOptions { timeout: number; @@ -66,54 +77,64 @@ export const waitForPlayerEvents = async >( options = defaultTestOptions, ): Promise[]> => { const receivedEvents: Event[] = []; - return withEventTimeOut( - new Promise[]>((resolve, reject) => { - const onError = (err: ErrorEvent) => { + const eventsPromise = new Promise[]>((resolve, reject) => { + const onError = (err: ErrorEvent) => { + console.error('[waitForPlayerEvents]', err); + player.removeEventListener(PlayerEventType.ERROR, onError); + reject(err); + }; + const onAdError = (e: AdEvent) => { + if (e.subType === AdEventType.AD_ERROR) { + const err = 'Ad error'; console.error('[waitForPlayerEvents]', err); - player.removeEventListener(PlayerEventType.ERROR, onError); + player.removeEventListener(PlayerEventType.AD_EVENT, onAdError); reject(err); - }; - let eventMap = expectedEvents.map((_expected: Partial) => ({ - event: _expected as Event, - onEvent(receivedEvent: Event) { - if (!eventMap.length) { - // No more events expected - return; + } + }; + let eventMap = expectedEvents.map((_expected: Partial) => ({ + event: _expected as Event, + onEvent(receivedEvent: Event) { + if (!eventMap.length) { + // No more events expected + return; + } + const expectedEvent = eventMap[0].event; + receivedEvents.push(receivedEvent); + 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; + + // Check order + if (inOrder && eventMap.length && !isExpected) { + const err = `Expected event '${expectedEvent.type}' but received '${receivedEvent.type}'`; + console.error('[waitForPlayerEvents]', err); + reject(err); + } + eventMap = eventMap.filter((entry) => { + if (entry.event.type === expectedEvent.type) { + player.removeEventListener(expectedEvent.type, entry.onEvent); } - const expectedEvent = eventMap[0].event; - receivedEvents.push(receivedEvent); - 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; - - // Check order - if (inOrder && eventMap.length && !isExpected) { - const err = `Expected event '${expectedEvent.type}' but received '${receivedEvent.type}'`; - console.error('[waitForPlayerEvents]', err); - reject(err); - } - eventMap = eventMap.filter((entry) => { - if (entry.event.type === expectedEvent.type) { - player.removeEventListener(expectedEvent.type, entry.onEvent); - } - return entry.event.type !== expectedEvent.type; - }); - if (!eventMap.length) { - // Done - resolve(receivedEvents); - } - }, - })); - player.addEventListener(PlayerEventType.ERROR, onError); - eventMap.forEach(({ event, onEvent }) => player.addEventListener(event.type, onEvent)); - }), - options.timeout, - expectedEvents, - receivedEvents, - ); + return entry.event.type !== expectedEvent.type; + }); + if (!eventMap.length) { + // Done + resolve(receivedEvents); + } + }, + })); + player.addEventListener(PlayerEventType.ERROR, onError); + player.addEventListener(PlayerEventType.AD_EVENT, onAdError); + eventMap.forEach(({ event, onEvent }) => player.addEventListener(event.type, onEvent)); + }); + + // Add rejection on time-out + const timeOutPromise = withEventTimeOut(eventsPromise, options.timeout, expectedEvents, receivedEvents); + + // Add extra logging on error + return withPlayerStateLogOnError(player, timeOutPromise); }; -const withEventTimeOut = >( +const withEventTimeOut = >, EType extends Event>( promise: Promise, timeout: number, expectedEvents: Partial[], @@ -137,6 +158,19 @@ const withEventTimeOut = >( }); }; +const withPlayerStateLogOnError = async (player: THEOplayer, promise: Promise) => { + try { + return await promise; + } catch (e) { + throw ( + (typeof e === 'string' ? e : JSON.stringify(e)) + + ` buffer: ${logPlayerBuffer(player)};` + + ` currentTime: ${player.currentTime};` + + ` paused: ${player.paused};` + ); + } +}; + export function expect(actual: any, desc?: string) { const descPrefix = desc ? `${desc}: ` : ''; diff --git a/e2e/src/utils/PlayerUtils.ts b/e2e/src/utils/PlayerUtils.ts new file mode 100644 index 000000000..db4d43cca --- /dev/null +++ b/e2e/src/utils/PlayerUtils.ts @@ -0,0 +1,10 @@ +import { THEOplayer } from 'react-native-theoplayer'; + +export function logPlayerBuffer(player: THEOplayer): string { + let buffer = '[ '; + for (let i = 0; i < player.buffered.length; i++) { + buffer += `${player.buffered[i].start} - ${player.buffered[i].end} ${i === player.buffered.length - 1 ? '' : ', '}`; + } + buffer += ']'; + return buffer; +}