Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ jobs:
with:
node-version: 14
- run: yarn
- run: yarn test
- run: yarn build
1 change: 1 addition & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: yarn
- run: yarn test
- name: Set version from git tag
run: yarn version --no-git-tag-version --new-version ${GITHUB_REF#refs/tags/}
- run: yarn build
Expand Down
8 changes: 8 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable */

module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable */

module.exports = {
setupFiles: ["./src/testUtils/setupFakeDom.ts"],
};
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "dist/index.js",
"unpkg": "dist/browser/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest",
"lint": "yarn lint:json && yarn lint:ts",
"lint:json": "prettier --list-different .eslintrc *.json",
"lint:ts": "eslint --ext .js,.ts --ignore-path .gitignore .",
Expand All @@ -16,15 +16,27 @@
"author": "Clockwork Dog <info@clockwork.dog>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.17.2",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@testing-library/dom": "^8.11.3",
"@types/howler": "^2.2.4",
"@types/jest": "^27.4.0",
"@types/jsdom": "^16.2.14",
"@types/ws": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"babel-jest": "^27.5.1",
"browserify": "^17.0.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1",
"jdom": "^3.2.5",
"jest": "^27.5.1",
"prettier": "^2.2.1",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"wait-for-expect": "^3.0.2",
"ws": "^8.5.0"
},
"dependencies": {
"howler": "^2.2.3",
Expand Down
8 changes: 5 additions & 3 deletions src/VideoPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,12 @@ export default class VideoPlayer {
}

private notifyStateListeners() {
const VideoState: VideoState = {
const videoState: VideoState = {
globalVolume: this.globalVolume,
isPlaying: this.activeClip ? !this.videoClipPlayers[this.activeClip.path].videoElement?.paused : false,
clips: { ...this.videoClipPlayers },
clips: Object.fromEntries(
Object.entries(this.videoClipPlayers).map(([path, player]) => [path, { config: player.config, volume: player.volume }] as const)
),
activeClip: this.activeClip
? {
path: this.activeClip.path,
Expand All @@ -280,7 +282,7 @@ export default class VideoPlayer {
}
: undefined,
};
this.dispatchEvent('state', VideoState);
this.dispatchEvent('state', videoState);
}

private notifyClipStateListeners(playId: string, file: string, status: MediaStatus) {
Expand Down
162 changes: 162 additions & 0 deletions src/__tests__/VideoPlayer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import waitForExpect from 'wait-for-expect';
import { MediaClipStateMessage } from '..';
import { VideoState } from '../types/VideoState';
import VideoPlayer from '../VideoPlayer';
import FakeCogsConnection from '../testUtils/FakeCogsConnection';

let fakeCogsConnection: FakeCogsConnection;
let videoPlayer: VideoPlayer;
let stateListener: jest.Mock<void, [VideoState]>;
let clipStateListener: jest.Mock<void, [MediaClipStateMessage]>;

beforeEach(async () => {
stateListener = jest.fn();
clipStateListener = jest.fn();

fakeCogsConnection = new FakeCogsConnection();
await fakeCogsConnection.isOpen;

videoPlayer = new VideoPlayer(fakeCogsConnection);
videoPlayer.addEventListener('state', ({ detail }) => stateListener(detail));
videoPlayer.addEventListener('videoClipState', ({ detail }) => clipStateListener(detail));
});

afterEach(() => {
fakeCogsConnection.close();
});

describe('config update', () => {
test('initial clip states empty', async () => {
await waitForExpect(() => {
expect(fakeCogsConnection.fakeServerMessageListener).toHaveBeenCalledWith({
allMediaClipStates: {
files: [],
mediaType: 'video',
},
});
});
});

test('empty config => state empty', async () => {
fakeCogsConnection.fakeMessageFromServer({
type: 'media_config_update',
files: {},
globalVolume: 1,
});

await waitForExpect(() => {
expect(stateListener).toHaveBeenLastCalledWith({
globalVolume: 1,
isPlaying: false,
clips: {},
});
});
});

test('preload one file', async () => {
fakeCogsConnection.fakeMessageFromServer({
type: 'media_config_update',
files: {
'preload-me.mp4': { type: 'video', preload: true },
},
globalVolume: 1,
});

await waitForExpect(() => {
expect(stateListener).toHaveBeenLastCalledWith({
globalVolume: 1,
isPlaying: false,
clips: {
'preload-me.mp4': {
config: { type: 'video', ephemeral: false, fit: expect.any(String), preload: true },
volume: 1,
},
},
});
});

expect(clipStateListener).not.toHaveBeenCalled();
});
});

describe('ephemeral', () => {
test('play', async () => {
fakeCogsConnection.fakeMessageFromServer({
type: 'video_play',
file: 'foo.mp4',
playId: 'video1',
volume: 1,
fit: 'contain',
});

await waitForExpect(() => {
expect(stateListener).toHaveBeenLastCalledWith({
globalVolume: 1,
isPlaying: true,
activeClip: {
path: 'foo.mp4',
loop: false,
volume: 1,
state: 'playing',
},
clips: {
'foo.mp4': {
config: {
ephemeral: true,
fit: 'contain',
preload: false,
},
volume: 1,
},
},
});
expect(clipStateListener).toHaveBeenLastCalledWith({
playId: 'video1',
mediaType: 'video',
file: 'foo.mp4',
status: 'playing',
});
expect(fakeCogsConnection.fakeServerMessageListener).toHaveBeenLastCalledWith({
mediaClipState: {
playId: 'video1',
mediaType: 'video',
file: 'foo.mp4',
status: 'playing',
},
});
});
});

test('play, stop', async () => {
fakeCogsConnection.fakeMessageFromServer({
type: 'video_play',
file: 'foo.mp4',
playId: 'video2',
volume: 1,
fit: 'contain',
});
fakeCogsConnection.fakeMessageFromServer({ type: 'video_stop' });

await waitForExpect(() => {
expect(stateListener).toHaveBeenLastCalledWith({
globalVolume: 1,
isPlaying: false,
clips: {},
});
expect(clipStateListener).toHaveBeenCalledWith({
playId: 'video2',
mediaType: 'video',
file: 'foo.mp4',
status: 'stopped',
});
expect(fakeCogsConnection.fakeServerMessageListener).toHaveBeenCalledWith({
mediaClipState: {
playId: 'video2',
mediaType: 'video',
file: 'foo.mp4',
status: 'stopped',
},
});
});
});
});
42 changes: 42 additions & 0 deletions src/testUtils/FakeCogsConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import WS, { WebSocketServer } from 'ws';
import { CogsClientMessage, MediaClipStateMessage } from '..';
import CogsConnection from '../CogsConnection';
import AllMediaClipStatesMessage from '../types/AllMediaClipStatesMessage';

export default class FakeCogsConnection extends CogsConnection {
port: number;
isOpen: Promise<void>;
fakeServerMessageListener: jest.Mock<
void,
[
{
allMediaClipStates?: AllMediaClipStatesMessage;
mediaClipStates?: MediaClipStateMessage;
}
]
> = jest.fn();

private fakeServer: WebSocketServer;
private fakeClientConnection: WS | undefined;
private messageListener = (data: WS.RawData) => this.fakeServerMessageListener(JSON.parse(data.toString()));

constructor(port = 2000 + Math.floor(Math.random() * 8000)) {
super({ hostname: 'localhost', port });
this.port = port;
this.fakeServer = new WebSocketServer({ port });
this.isOpen = (async () => {
this.fakeClientConnection = await new Promise<WS>((resolve) => this.fakeServer.once('connection', resolve));
this.fakeClientConnection.on('message', this.messageListener);
})();
}

close(): void {
this.fakeClientConnection?.off('message', this.messageListener);
this.fakeClientConnection?.close();
this.fakeServer.close();
}

fakeMessageFromServer(message: CogsClientMessage): void {
this.fakeClientConnection?.send(JSON.stringify({ message }));
}
}
26 changes: 26 additions & 0 deletions src/testUtils/setupFakeDom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import WS from 'ws';
import { JSDOM } from 'jsdom';

const dom = new JSDOM();
global.document = dom.window.document;
global.window = dom.window as any;
global.EventTarget = dom.window.EventTarget;
global.CustomEvent = dom.window.CustomEvent;

dom.window.WebSocket = WS as any;
global.WebSocket = dom.window.WebSocket;

jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(async function (this: HTMLMediaElement) {
this.dispatchEvent(new dom.window.Event('playing', { bubbles: true }));

let paused = false;
Object.defineProperty(this, 'paused', {
get: () => paused,
set: (value) => (paused = value),
});

jest.spyOn(this, 'pause').mockImplementation(() => {
paused = true;
this.dispatchEvent(new dom.window.Event('paused', { bubbles: true }));
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exclude": ["dist", "src/testUtils", "**/__tests__/*", "**/*.test.ts"],
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
Expand All @@ -13,7 +14,7 @@
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
Expand Down
Loading