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

Player testing: Add Player Testing framework #332

Merged
merged 22 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6210d66
feat(playertesting): add player testing framework
rolandkakonyi Nov 24, 2023
fea3d22
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 24, 2023
17c0e66
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 24, 2023
bff5d8d
feat(playertesting): add instructions when running without cavy
rolandkakonyi Nov 24, 2023
9fc60c5
feat(playertesting): refactor
rolandkakonyi Nov 24, 2023
8c469a7
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 25, 2023
6a86342
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 27, 2023
0d00e43
chore: add changelog entry
rolandkakonyi Nov 27, 2023
97999dd
feat(playertesting): rename CLI commands
rolandkakonyi Nov 27, 2023
188d3ec
chore: update contribution guide
rolandkakonyi Nov 27, 2023
f8a1ec0
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 27, 2023
0d64213
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 28, 2023
1634338
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 28, 2023
7955765
Merge branch 'player-testing/cavy-setup' into player-testing/add-play…
rolandkakonyi Nov 28, 2023
150bc5e
chore: use UUID generator library
rolandkakonyi Nov 28, 2023
fe5f4ea
feat(playertesting): make event expectation helpers more verbose
rolandkakonyi Nov 28, 2023
a4c62d7
feat(playertesting): use variadic parameters for multiple event expec…
rolandkakonyi Nov 28, 2023
23323d3
feat(playertesting): add more meaningful tests
rolandkakonyi Nov 28, 2023
9cf03b2
feat(playertesting): fix variadic parameters usage
rolandkakonyi Nov 28, 2023
3742d68
feat(playertesting): update contribution guide with real test
rolandkakonyi Nov 28, 2023
a57ae4a
fix return type of RepeatedEvent function
rolandkakonyi Nov 29, 2023
0d97cc6
feat(playertesting): rename PlayerWorld to PlayerTestWorld
rolandkakonyi Nov 29, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [0.14.2] (2023-11-27)

### Added

- Player (E2E) tests for Android and iOS can now executed via `yarn integration-test test:android` and `yarn integration-test test:ios` respectively

## [Unreleased]

### Fixed

- Android: `onEvent` callback not being called on `PlayerView`
Expand Down
44 changes: 41 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ To build and run the example app on iOS:
yarn example ios
```

To edit the Swift/Objective-C files, open `example/ios/BitmovinPlayerReactNativeExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > RNBitmovinPlayer`.
To edit the Swift/Objective-C files, open `example/ios/BitmovinPlayerReactNativeExample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > RNBitmovinPlayer`.

To edit the Kotlin files, open `example/android` in Android Studio and find the source files at `bitmovin-player-react-native` under `Android`.

Expand Down Expand Up @@ -122,10 +122,48 @@ swiftlint lint --autocorrect

## Testing

Remember to add tests for your change if possible. Run the unit tests by:
Remember to add tests for your change if possible. Run the player tests by:

```sh
yarn test
yarn integration-test test:android
yarn integration-test test:ios
```

See available API for testing [here](/integration_test/playertesting/PlayerTesting.ts).

A Player Test has the following structure always:

```ts
export default (spec: TestScope) => {
spec.describe('SCENARIO TO TEST', () => {
spec.it('EXPECTATION', async () => {
await startPlayerTest({}, async () => {
// TEST CODE
});
});
});
};
```

For example:

```ts
export default (spec: TestScope) => {
spec.describe('playing a source', () => {
spec.it('emits TimeChanged events', async () => {
await startPlayerTest({}, async () => {
await loadSourceConfig({
url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8',
type: SourceType.HLS,
});
await callPlayerAndExpectEvents((player) => {
player.play();
}, EventSequence(EventType.Play, EventType.Playing));
await expectEvents(RepeatedEvent(EventType.TimeChanged, 5));
});
});
});
};
```

## Commit message convention
Expand Down
3 changes: 2 additions & 1 deletion integration_test/app.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "IntegrationTest",
"displayName": "IntegrationTest"
"displayName": "IntegrationTest",
"licenseKey": "ENTER_LICENSE_KEY"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this new key here, to enable setting the license key easily for tests.
This can be later used for CI as well.

}
5 changes: 4 additions & 1 deletion integration_test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AppRegistry } from 'react-native';
import TestableApp from './src/TestableApp';
import { name as appName } from './app.json';
import PlayerWorld from './playertesting/PlayerWorld';
import { name as appName, licenseKey } from './app.json';

PlayerWorld.defaultLicenseKey = licenseKey;

AppRegistry.registerComponent(appName, () => TestableApp);
10 changes: 8 additions & 2 deletions integration_test/ios/IntegrationTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,11 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.bitmovin.player.reactnative.$(PRODUCT_NAME:-rfc1034identifier)";
PRODUCT_NAME = IntegrationTest;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
Expand All @@ -322,7 +325,10 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.bitmovin.player.reactnative.$(PRODUCT_NAME:-rfc1034identifier)";
PRODUCT_NAME = IntegrationTest;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
Expand Down Expand Up @@ -361,7 +367,7 @@
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=appletvsimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand Down Expand Up @@ -441,7 +447,7 @@
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=appletvsimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
Expand Down
9 changes: 5 additions & 4 deletions integration_test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"playertest:android": "yarn cavy run-android",
"playertest:ios": "yarn cavy run-ios",
"test:android": "yarn cavy run-android",
"test:ios": "yarn cavy run-ios",
"pods": "yarn pods-install || yarn pods-update",
"pods-install": "yarn pod-install",
"pods-update": "pod update --silent"
Expand All @@ -22,11 +22,12 @@
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native/metro-config": "^0.72.11",
"@types/cavy": "^3.2.7",
"babel-plugin-module-resolver": "^5.0.0",
"cavy-cli": "^3.0.0",
"metro-react-native-babel-preset": "0.76.8",
"pod-install": "^0.1.39",
"@types/cavy": "^3.2.7",
"cavy-cli": "^3.0.0"
"react-native-uuid": "^2.0.1"
},
"engines": {
"node": ">=16"
Expand Down
65 changes: 65 additions & 0 deletions integration_test/playertesting/EventType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export enum EventType {
AdBreakFinished = 'onAdBreakFinished',
AdBreakStarted = 'onAdBreakStarted',
AdClicked = 'onAdClicked',
AdError = 'onAdError',
AdFinished = 'onAdFinished',
AdManifestLoad = 'onAdManifestLoad',
AdManifestLoaded = 'onAdManifestLoaded',
AdQuartile = 'onAdQuartile',
AdScheduled = 'onAdScheduled',
AdSkipped = 'onAdSkipped',
AdStarted = 'onAdStarted',
CastAvailable = 'onCastAvailable',
CastPaused = 'onCastPaused',
CastPlaybackFinished = 'onCastPlaybackFinished',
CastPlaying = 'onCastPlaying',
CastStarted = 'onCastStarted',
CastStart = 'onCastStart',
CastStopped = 'onCastStopped',
CastTimeUpdated = 'onCastTimeUpdated',
CastWaitingForDevice = 'onCastWaitingForDevice',
Destroy = 'onDestroy',
Event = 'onEvent',
FullscreenEnabled = 'onFullscreenEnabled',
FullscreenDisabled = 'onFullscreenDisabled',
FullscreenEnter = 'onFullscreenEnter',
FullscreenExit = 'onFullscreenExit',
Muted = 'onMuted',
Paused = 'onPaused',
PictureInPictureAvailabilityChanged = 'onPictureInPictureAvailabilityChanged',
PictureInPictureEnter = 'onPictureInPictureEnter',
PictureInPictureEntered = 'onPictureInPictureEntered',
PictureInPictureExit = 'onPictureInPictureExit',
PictureInPictureExited = 'onPictureInPictureExited',
PlaybackFinished = 'onPlaybackFinished',
PlayerActive = 'onPlayerActive',
PlayerError = 'onPlayerError',
PlayerWarning = 'onPlayerWarning',
Play = 'onPlay',
Playing = 'onPlaying',
Ready = 'onReady',
Seeked = 'onSeeked',
Seek = 'onSeek',
TimeShift = 'onTimeShift',
TimeShifted = 'onTimeShifted',
StallStarted = 'onStallStarted',
StallEnded = 'onStallEnded',
SourceError = 'onSourceError',
SourceLoaded = 'onSourceLoaded',
SourceLoad = 'onSourceLoad',
SourceUnloaded = 'onSourceUnloaded',
SourceWarning = 'onSourceWarning',
AudioAdded = 'onAudioAdded',
AudioChanged = 'onAudioChanged',
AudioRemoved = 'onAudioRemoved',
SubtitleAdded = 'onSubtitleAdded',
SubtitleChanged = 'onSubtitleChanged',
SubtitleRemoved = 'onSubtitleRemoved',
TimeChanged = 'onTimeChanged',
Unmuted = 'onUnmuted',
VideoPlaybackQualityChanged = 'onVideoPlaybackQualityChanged',
DownloadFinished = 'onDownloadFinished',
VideoDownloadQualityChanged = 'onVideoDownloadQualityChanged',
PlaybackSpeedChanged = 'onPlaybackSpeedChanged',
}
95 changes: 95 additions & 0 deletions integration_test/playertesting/PlayerTesting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
Player,
PlayerConfig,
Event,
SourceConfig,
ReadyEvent,
TimeChangedEvent,
} from 'bitmovin-player-react-native';
import { EventType } from './EventType';
import PlayerWorld from './PlayerWorld';
import {
SingleEventExpectation,
MultipleEventsExpectation,
} from './expectations';

export const startPlayerTest = async (
config: PlayerConfig,
fn: () => Promise<void>
): Promise<void> => {
return await PlayerWorld.shared.startPlayerTest(config, fn);
};

export const callPlayer = async <T>(
fn: (player: Player) => Promise<T>
): Promise<T> => {
return await PlayerWorld.shared.callPlayer(fn);
};

export const expectEvent = async <T extends Event>(
expectationConvertible: SingleEventExpectation | EventType,
timeoutSeconds: number = 10
): Promise<T> => {
return await PlayerWorld.shared.expectEvent(
expectationConvertible,
timeoutSeconds
);
};

export const expectEvents = async (
expectationsConvertible: MultipleEventsExpectation | EventType[],
timeoutSeconds: number = 10
): Promise<Event[]> => {
return await PlayerWorld.shared.expectEvents(
expectationsConvertible,
timeoutSeconds
);
};

export const callPlayerAndExpectEvent = async <E extends Event, P>(
fn: (player: Player) => Promise<P>,
expectationConvertible: SingleEventExpectation | EventType,
timeoutSeconds: number = 10
): Promise<E> => {
return await PlayerWorld.shared.callPlayerAndExpectEvent(
fn,
expectationConvertible,
timeoutSeconds
);
};

export const callPlayerAndExpectEvents = async (
fn: (player: Player) => void,
expectationsConvertible: MultipleEventsExpectation | EventType[],
timeoutSeconds: number = 10
): Promise<Event[]> => {
return await PlayerWorld.shared.callPlayerAndExpectEvents(
fn,
expectationsConvertible,
timeoutSeconds
);
};

export const loadSourceConfig = async (
sourceConfig: SourceConfig,
timeoutSeconds: number = 10
): Promise<ReadyEvent> => {
return await PlayerWorld.shared.loadSourceConfig(
sourceConfig,
timeoutSeconds
);
};

export const playFor = async (
time: number,
timeoutSeconds: number = 10
): Promise<TimeChangedEvent> => {
return await PlayerWorld.shared.playFor(time, timeoutSeconds);
};

export const playUntil = async (
time: number,
timeoutSeconds: number = 10
): Promise<TimeChangedEvent> => {
return await PlayerWorld.shared.playUntil(time, timeoutSeconds);
};
Loading