Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Commit

Permalink
Unit tests for library (#156)
Browse files Browse the repository at this point in the history
* add unit test coverage reports to gitignore

* add jest dependencies

* add jest config

* add Config unit tests

* add AFKController unit tests

* mock video codecs list

* test for PixelStreaming SettingsChangedEvent

* mock/unmock functions for RTCRtpReceiver

* mock WebSocket

* test for PixelStreaming.connect()

* unit test for disconnect

* unit tests for reconnect

* added eventemitter events in tests

* test that listStreamers is sent on WS connect

* mock RTCPeerConnection and RTCIceCandidate

* test webRtcConnected event

* test RTCPeerConnect close

* fixed variable name for consistency

* check for navigator.getGamepads before calling it

* mock addTransceiver, createAnswer, getTransceivers, setRemoteDescription

* test for receiving a connection offer

* add null checks for peerConnection since it might be null if connectiong closed

* mock addIceCandidate

* test for receiving ICE candidate message

* mock setLocalDescription, getStats

* test for statistics

* test webRtcDisconnected event

* mock RTCDataChannel and RTCDataChannelEvent

* test for dataChannelOpen event

* mock RTCTrackEvent

* mock MediaStream and MediaStreamTrack

* mock video element play()

* test playStream and playStreamRejected events

* mock video readyState

* mock WebRTC data channel send()

* test emitCommand

* test emitUIInteraction

* test emitConsoleCommand

* Jest mock for HTMLMediaElement.play()

* check boolean return value of all emit* commands

* test dataChannelClose event

* added TextEncoder and TextDecoder to Jest globals

* mock datachannel onmessage

* test UE -> browser data channel message

* clarified in test description that we are using a Response message

* extracted commonly used setup steps as util functions

* triggerSdpOffer -> triggerSdpOfferMessage for consistency

* run unit tests as a Github action

* Revert "break one of the tests to test the GH action"

This reverts commit 05f5742.

* run tests only if files changed under Frontend/library

* added unit test run instructions to README.md
  • Loading branch information
hmuurine authored Mar 17, 2023
1 parent dbc8dd1 commit f58b32c
Show file tree
Hide file tree
Showing 17 changed files with 9,985 additions and 2,457 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/run-library-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run library unit tests

on:
push:
branches: ['master']
paths: ['Frontend/library/**']
pull_request:
branches: ['master']
paths: ['Frontend/library/**']

jobs:

build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./

permissions:
contents: write
steps:
- name: "Checkout source code"
uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'

- name: Install library deps
working-directory: ./Frontend/library
run: npm ci

- name: Run frontend lib tests
working-directory: ./Frontend/library
run: npm run test
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ node_modules/
node.zip
SignallingWebServer/Public/
SignallingWebServer/certificates
.vscode
.vscode
coverage/
6 changes: 6 additions & 0 deletions Frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ We recommend studying [/ui-library](/Frontend/ui-library) and [player.ts](/Front
- `cd implementation/your_implementation`
- `npm build-all`

## Unit tests

The [/library](/Frontend/library) project has unit tests that test the Pixel Streaming functionality against a mocked connection. To run the tests manually, run:
- `cd library`
- `npm install`
- `npm run test`

## Legal

Expand Down
18 changes: 18 additions & 0 deletions Frontend/library/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
preset: "ts-jest/presets/js-with-ts",
testEnvironment: "jsdom",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: "tsconfig.jest.json",
},
],
},
modulePathIgnorePatterns: ["<rootDir>/build/"],
testPathIgnorePatterns: ["<rootDir>/build/", "/node_modules/"],
globals: {
TextDecoder: TextDecoder,
TextEncoder: TextEncoder,
}
};
10,792 changes: 8,356 additions & 2,436 deletions Frontend/library/package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion Frontend/library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
"build": "npx webpack --config webpack.prod.js",
"build-dev": "npx webpack --config webpack.dev.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"test": "jest --detectOpenHandles --coverage=true",
"spellcheck": "cspell \"{README.md,.github/*.md,src/**/*.ts}\""
},
"devDependencies": {
"@types/jest": "29.4.0",
"@types/webxr": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"@types/webxr": "^0.5.1",
"cspell": "^4.1.0",
"eslint": "^8.11.0",
"jest": "^29.4.0",
"jest-environment-jsdom": "29.4.0",
"prettier": "2.8.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.4.2",
"typedoc": "^0.23.24",
"typescript": "^4.9.4",
Expand Down
162 changes: 162 additions & 0 deletions Frontend/library/src/AFK/AFKController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Config, Flags, NumericParameters } from '../Config/Config';
import { PixelStreaming } from '../PixelStreaming/PixelStreaming';
import { AfkTimedOutEvent, AfkWarningActivateEvent, AfkWarningUpdateEvent, AfkWarningDeactivateEvent } from '../Util/EventEmitter';
import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver';
import {
AFKController
} from './AFKController';

describe('AFKController', () => {
let mockPixelStreaming: PixelStreaming;

beforeEach(() => {
mockRTCRtpReceiver();
jest.useFakeTimers();
mockPixelStreaming = {
dispatchEvent: jest.fn()
} as any as PixelStreaming;
});

afterEach(() => {
unmockRTCRtpReceiver();
jest.resetAllMocks();
});

it('should not activate AFK timer if it has been disabled from settings', () => {
const config = new Config({ initialSettings: { [Flags.AFKDetection]: false } });
const onDismissAfk = jest.fn();
const afkController = new AFKController(config, mockPixelStreaming, onDismissAfk);

afkController.startAfkWarningTimer();
expect(afkController.active).toBe(false);

jest.advanceTimersByTime(1000000 * 1000);
expect(mockPixelStreaming.dispatchEvent).not.toHaveBeenCalled();
});

it('should activate AFK timer and trigger it after specified delay if it has been enabled from settings', () => {
const timeoutSeconds = 100;

const config = new Config({ initialSettings: { [Flags.AFKDetection]: true, [NumericParameters.AFKTimeoutSecs]: timeoutSeconds} });
const onDismissAfk = jest.fn();
const afkController = new AFKController(config, mockPixelStreaming, onDismissAfk);

afkController.startAfkWarningTimer();
expect(afkController.active).toBe(true);

// Advance to 1 second before AFK event:
jest.advanceTimersByTime((timeoutSeconds - 1) * 1000);
expect(mockPixelStreaming.dispatchEvent).not.toHaveBeenCalled();

// advance 1 more second to trigger AFK warning
jest.advanceTimersByTime(1000);
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningActivateEvent({
countDown: 0,
dismissAfk: expect.anything()
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 10,
}));

// advance 10 more seconds to trigger AFK countdown updates and eventually timeout
jest.advanceTimersByTime(10 * 1000);
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 9,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 8,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 7,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 6,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 5,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 4,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 3,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 2,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 1,
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkTimedOutEvent());
});

it('should postpone AFK activation each time resetAfkWarningTimer is called', () => {
const timeoutSeconds = 100;

const config = new Config({ initialSettings: { [Flags.AFKDetection]: true, [NumericParameters.AFKTimeoutSecs]: timeoutSeconds} });
const onDismissAfk = jest.fn();
const afkController = new AFKController(config, mockPixelStreaming, onDismissAfk);

afkController.startAfkWarningTimer();

// Advance to 1 second before AFK event:
jest.advanceTimersByTime((timeoutSeconds - 1) * 1000);
expect(mockPixelStreaming.dispatchEvent).not.toHaveBeenCalled();

afkController.resetAfkWarningTimer();

// advance 1 more second and ensure that AFK warning is not triggered since reset was called
jest.advanceTimersByTime(1000);
expect(mockPixelStreaming.dispatchEvent).not.toHaveBeenCalled();

// reset AFK timer once more and ensure it is triggered exactly after timeoutSeconds
afkController.resetAfkWarningTimer();

// Advance to 1 second before AFK event:
jest.advanceTimersByTime((timeoutSeconds - 1) * 1000);
expect(mockPixelStreaming.dispatchEvent).not.toHaveBeenCalled();

// advance 1 more second to trigger AFK warning
jest.advanceTimersByTime(1000);
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningActivateEvent({
countDown: 0,
dismissAfk: expect.anything()
}));
});

it('should dismiss AFK warning countdown if onAfkClick is called', () => {
const timeoutSeconds = 100;

const config = new Config({ initialSettings: { [Flags.AFKDetection]: true, [NumericParameters.AFKTimeoutSecs]: timeoutSeconds} });
const onDismissAfk = jest.fn();
const afkController = new AFKController(config, mockPixelStreaming, onDismissAfk);

afkController.startAfkWarningTimer();

// Advance to AFK event:
jest.advanceTimersByTime(timeoutSeconds * 1000);
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningActivateEvent({
countDown: 0,
dismissAfk: expect.anything()
}));
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 10,
}));

// Advance one more second and call onAfkClick
jest.advanceTimersByTime(1000);

expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningUpdateEvent({
countDown: 9,
}));

afkController.onAfkClick();
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledWith(new AfkWarningDeactivateEvent());

// advance 10 more seconds and ensure there are no more countdown/timeout events emitted
jest.advanceTimersByTime(10 * 1000);
expect(mockPixelStreaming.dispatchEvent).toHaveBeenCalledTimes(4);
});


});
Loading

0 comments on commit f58b32c

Please sign in to comment.