Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintenance/e2e ad test #454

Closed
wants to merge 5 commits into from
Closed
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
24 changes: 9 additions & 15 deletions e2e/src/tests/Ads.spec.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
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()
.forEach((testSource: TestSourceDescription) => {
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;
});
});
});
Expand Down
122 changes: 78 additions & 44 deletions e2e/src/utils/Actions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,54 +77,64 @@ export const waitForPlayerEvents = async <EType extends Event<PlayerEventType>>(
options = defaultTestOptions,
): Promise<Event<PlayerEventType>[]> => {
const receivedEvents: Event<PlayerEventType>[] = [];
return withEventTimeOut(
new Promise<Event<PlayerEventType>[]>((resolve, reject) => {
const onError = (err: ErrorEvent) => {
const eventsPromise = new Promise<Event<PlayerEventType>[]>((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<EType>) => ({
event: _expected as Event<PlayerEventType>,
onEvent(receivedEvent: Event<PlayerEventType>) {
if (!eventMap.length) {
// No more events expected
return;
}
};
let eventMap = expectedEvents.map((_expected: Partial<EType>) => ({
event: _expected as Event<PlayerEventType>,
onEvent(receivedEvent: Event<PlayerEventType>) {
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 = <EType extends Event<PlayerEventType>>(
const withEventTimeOut = <TType extends StringKeyOf<EventMap<string>>, EType extends Event<TType>>(
promise: Promise<any>,
timeout: number,
expectedEvents: Partial<EType>[],
Expand All @@ -137,6 +158,19 @@ const withEventTimeOut = <EType extends Event<PlayerEventType>>(
});
};

const withPlayerStateLogOnError = async (player: THEOplayer, promise: Promise<any>) => {
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}: ` : '';

Expand Down
10 changes: 10 additions & 0 deletions e2e/src/utils/PlayerUtils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading