Unit tests for library (#156)
* 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 05f5742cf0.
* run tests only if files changed under Frontend/library
* added unit test run instructions to README.md
This commit is contained in:
parent
dbc8dd1600
commit
f58b32cae6
|
|
@ -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
|
||||
|
|
@ -6,4 +6,5 @@ node_modules/
|
|||
node.zip
|
||||
SignallingWebServer/Public/
|
||||
SignallingWebServer/certificates
|
||||
.vscode
|
||||
.vscode
|
||||
coverage/
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver';
|
||||
import {
|
||||
Config,
|
||||
Flags,
|
||||
FlagsKeys,
|
||||
NumericParameters,
|
||||
NumericParametersKeys,
|
||||
OptionParameters,
|
||||
OptionParametersKeys,
|
||||
TextParameters,
|
||||
TextParametersKeys
|
||||
} from './Config';
|
||||
|
||||
const allFlags = Object.keys(Flags).map((key) => Flags[key as FlagsKeys]);
|
||||
const allNumericParameters = Object.keys(NumericParameters).map(
|
||||
(key) => NumericParameters[key as NumericParametersKeys]
|
||||
);
|
||||
const allTextParameters = Object.keys(TextParameters).map(
|
||||
(key) => TextParameters[key as TextParametersKeys]
|
||||
);
|
||||
const allOptionParameters = Object.keys(OptionParameters).map(
|
||||
(key) => OptionParameters[key as OptionParametersKeys]
|
||||
);
|
||||
|
||||
const allParameters = [
|
||||
...allFlags,
|
||||
...allNumericParameters,
|
||||
...allTextParameters,
|
||||
...allOptionParameters
|
||||
];
|
||||
|
||||
describe('Config', () => {
|
||||
beforeEach(() => {
|
||||
mockRTCRtpReceiver();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockRTCRtpReceiver();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should populate initial values for all settings when initialized without parameters', () => {
|
||||
const config = new Config();
|
||||
|
||||
const settings = config.getSettings();
|
||||
expect(Object.keys(settings)).toEqual(
|
||||
expect.arrayContaining(allParameters)
|
||||
);
|
||||
});
|
||||
|
||||
it('should populate given initial setting values', () => {
|
||||
const initialSettings = {
|
||||
[Flags.AutoPlayVideo]: false,
|
||||
[NumericParameters.WebRTCMaxBitrate]: 12345,
|
||||
[TextParameters.SignallingServerUrl]: 'url'
|
||||
};
|
||||
|
||||
const config = new Config({ initialSettings });
|
||||
|
||||
expect(config.isFlagEnabled(Flags.AutoPlayVideo)).toEqual(
|
||||
initialSettings[Flags.AutoPlayVideo]
|
||||
);
|
||||
expect(
|
||||
config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
).toEqual(initialSettings[NumericParameters.WebRTCMaxBitrate]);
|
||||
expect(
|
||||
config.getTextSettingValue(TextParameters.SignallingServerUrl)
|
||||
).toEqual(initialSettings[TextParameters.SignallingServerUrl]);
|
||||
});
|
||||
|
||||
it('should replace setting values when new settings are set with setSettings', () => {
|
||||
const config = new Config();
|
||||
|
||||
const preferredCodecs = ['c1', 'c2', 'c3'];
|
||||
config.setOptionSettingOptions(
|
||||
OptionParameters.PreferredCodec,
|
||||
preferredCodecs
|
||||
);
|
||||
|
||||
const changedSettings = {
|
||||
[Flags.AutoPlayVideo]: false,
|
||||
[NumericParameters.WebRTCMaxBitrate]: 54321,
|
||||
[TextParameters.SignallingServerUrl]: 'signalling-url',
|
||||
[OptionParameters.PreferredCodec]: 'c2'
|
||||
};
|
||||
|
||||
config.setSettings(changedSettings);
|
||||
|
||||
expect(config.isFlagEnabled(Flags.AutoPlayVideo)).toEqual(
|
||||
changedSettings[Flags.AutoPlayVideo]
|
||||
);
|
||||
expect(
|
||||
config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
).toEqual(changedSettings[NumericParameters.WebRTCMaxBitrate]);
|
||||
expect(
|
||||
config.getTextSettingValue(TextParameters.SignallingServerUrl)
|
||||
).toEqual(changedSettings[TextParameters.SignallingServerUrl]);
|
||||
expect(
|
||||
config.getSettingOption(OptionParameters.PreferredCodec).selected
|
||||
).toEqual(changedSettings[OptionParameters.PreferredCodec]);
|
||||
});
|
||||
|
||||
it('should replace setting values when new settings are set with set* setters', () => {
|
||||
const config = new Config();
|
||||
|
||||
const preferredCodecs = ['c1', 'c2', 'c3'];
|
||||
config.setOptionSettingOptions(
|
||||
OptionParameters.PreferredCodec,
|
||||
preferredCodecs
|
||||
);
|
||||
|
||||
const changedSettings = {
|
||||
[Flags.AutoPlayVideo]: false,
|
||||
[NumericParameters.WebRTCMaxBitrate]: 54321,
|
||||
[TextParameters.SignallingServerUrl]: 'signalling-url',
|
||||
[OptionParameters.PreferredCodec]: 'c2'
|
||||
};
|
||||
|
||||
config.setFlagEnabled(
|
||||
Flags.AutoPlayVideo,
|
||||
changedSettings[Flags.AutoPlayVideo]
|
||||
);
|
||||
config.setNumericSetting(
|
||||
NumericParameters.WebRTCMaxBitrate,
|
||||
changedSettings[NumericParameters.WebRTCMaxBitrate]
|
||||
);
|
||||
config.setTextSetting(
|
||||
TextParameters.SignallingServerUrl,
|
||||
changedSettings[TextParameters.SignallingServerUrl]
|
||||
);
|
||||
config.setOptionSettingValue(
|
||||
OptionParameters.PreferredCodec,
|
||||
changedSettings[OptionParameters.PreferredCodec]
|
||||
);
|
||||
|
||||
expect(config.isFlagEnabled(Flags.AutoPlayVideo)).toEqual(
|
||||
changedSettings[Flags.AutoPlayVideo]
|
||||
);
|
||||
expect(
|
||||
config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
).toEqual(changedSettings[NumericParameters.WebRTCMaxBitrate]);
|
||||
expect(
|
||||
config.getTextSettingValue(TextParameters.SignallingServerUrl)
|
||||
).toEqual(changedSettings[TextParameters.SignallingServerUrl]);
|
||||
expect(
|
||||
config.getSettingOption(OptionParameters.PreferredCodec).selected
|
||||
).toEqual(changedSettings[OptionParameters.PreferredCodec]);
|
||||
});
|
||||
|
||||
it('should persist config changes to window.location URL when updateURLParams() is called', () => {
|
||||
const config = new Config({ useUrlParams: true });
|
||||
|
||||
const preferredCodecs = ['c1', 'c2', 'c3'];
|
||||
config.setOptionSettingOptions(
|
||||
OptionParameters.PreferredCodec,
|
||||
preferredCodecs
|
||||
);
|
||||
|
||||
const changedSettings = {
|
||||
[Flags.AutoPlayVideo]: false,
|
||||
[NumericParameters.WebRTCMaxBitrate]: 54321,
|
||||
[TextParameters.SignallingServerUrl]: 'signalling-url',
|
||||
[OptionParameters.PreferredCodec]: 'c2'
|
||||
};
|
||||
|
||||
config.setSettings(changedSettings);
|
||||
|
||||
config
|
||||
.getFlags()
|
||||
.find((setting) => setting.id === Flags.AutoPlayVideo)
|
||||
?.updateURLParams();
|
||||
config
|
||||
.getNumericSettings()
|
||||
.find(
|
||||
(setting) => setting.id === NumericParameters.WebRTCMaxBitrate
|
||||
)
|
||||
?.updateURLParams();
|
||||
config
|
||||
.getTextSettings()
|
||||
.find(
|
||||
(setting) => setting.id === TextParameters.SignallingServerUrl
|
||||
)
|
||||
?.updateURLParams();
|
||||
config
|
||||
.getOptionSettings()
|
||||
.find((setting) => setting.id === OptionParameters.PreferredCodec)
|
||||
?.updateURLParams();
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
expect(urlParams.get(Flags.AutoPlayVideo)).toEqual(
|
||||
changedSettings[Flags.AutoPlayVideo].toString()
|
||||
);
|
||||
expect(urlParams.get(NumericParameters.WebRTCMaxBitrate)).toEqual(
|
||||
changedSettings[NumericParameters.WebRTCMaxBitrate].toString()
|
||||
);
|
||||
expect(urlParams.get(TextParameters.SignallingServerUrl)).toEqual(
|
||||
changedSettings[TextParameters.SignallingServerUrl].toString()
|
||||
);
|
||||
expect(urlParams.get(OptionParameters.PreferredCodec)).toEqual(
|
||||
changedSettings[OptionParameters.PreferredCodec].toString()
|
||||
);
|
||||
});
|
||||
|
||||
it('should read initial config from window.location URL if initialized with useUrlParams: true', () => {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
'http://localhost/?AutoPlayVideo=false&WebRTCMaxBitrate=43210&ss=signalling-url-from-url-param'
|
||||
);
|
||||
|
||||
const config = new Config({ useUrlParams: true });
|
||||
|
||||
expect(config.isFlagEnabled(Flags.AutoPlayVideo)).toEqual(false);
|
||||
expect(
|
||||
config.getNumericSettingValue(NumericParameters.WebRTCMaxBitrate)
|
||||
).toEqual(43210);
|
||||
expect(
|
||||
config.getTextSettingValue(TextParameters.SignallingServerUrl)
|
||||
).toEqual('signalling-url-from-url-param');
|
||||
});
|
||||
});
|
||||
|
|
@ -54,9 +54,11 @@ export class GamePadController {
|
|||
);
|
||||
}
|
||||
this.controllers = [];
|
||||
for (const gamepad of navigator.getGamepads()) {
|
||||
if (gamepad) {
|
||||
this.gamePadConnectHandler(new GamepadEvent('gamepadconnected', { gamepad }));
|
||||
if (navigator.getGamepads) {
|
||||
for (const gamepad of navigator.getGamepads()) {
|
||||
if (gamepad) {
|
||||
this.gamePadConnectHandler(new GamepadEvent('gamepadconnected', { gamepad }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,11 +84,11 @@ export class PeerConnectionController {
|
|||
|
||||
this.setupTransceiversAsync(useMic).finally(() => {
|
||||
this.peerConnection
|
||||
.createOffer(offerOptions)
|
||||
?.createOffer(offerOptions)
|
||||
.then((offer: RTCSessionDescriptionInit) => {
|
||||
this.showTextOverlayConnecting();
|
||||
offer.sdp = this.mungeSDP(offer.sdp, useMic);
|
||||
this.peerConnection.setLocalDescription(offer);
|
||||
this.peerConnection?.setLocalDescription(offer);
|
||||
this.onSendWebRTCOffer(offer);
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -103,7 +103,7 @@ export class PeerConnectionController {
|
|||
async receiveOffer(offer: RTCSessionDescriptionInit, config: Config) {
|
||||
Logger.Log(Logger.GetStackTrace(), 'Receive Offer', 6);
|
||||
|
||||
this.peerConnection.setRemoteDescription(offer).then(() => {
|
||||
this.peerConnection?.setRemoteDescription(offer).then(() => {
|
||||
const isLocalhostConnection =
|
||||
location.hostname === 'localhost' ||
|
||||
location.hostname === '127.0.0.1';
|
||||
|
|
@ -123,14 +123,14 @@ export class PeerConnectionController {
|
|||
|
||||
this.setupTransceiversAsync(useMic).finally(() => {
|
||||
this.peerConnection
|
||||
.createAnswer()
|
||||
?.createAnswer()
|
||||
.then((Answer: RTCSessionDescriptionInit) => {
|
||||
Answer.sdp = this.mungeSDP(Answer.sdp, useMic);
|
||||
return this.peerConnection.setLocalDescription(Answer);
|
||||
return this.peerConnection?.setLocalDescription(Answer);
|
||||
})
|
||||
.then(() => {
|
||||
this.onSendWebRTCAnswer(
|
||||
this.peerConnection.currentLocalDescription
|
||||
this.peerConnection?.currentLocalDescription
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -158,7 +158,7 @@ export class PeerConnectionController {
|
|||
* @param answer - RTC Session Descriptor from the Signaling Server
|
||||
*/
|
||||
receiveAnswer(answer: RTCSessionDescriptionInit) {
|
||||
this.peerConnection.setRemoteDescription(answer);
|
||||
this.peerConnection?.setRemoteDescription(answer);
|
||||
// Ugly syntax, but this achieves the intersection of the browser supported list and the UE supported list
|
||||
this.config.setOptionSettingOptions(
|
||||
OptionParameters.PreferredCodec,
|
||||
|
|
@ -174,7 +174,7 @@ export class PeerConnectionController {
|
|||
* Generate Aggregated Stats and then fire a onVideo Stats event
|
||||
*/
|
||||
generateStats() {
|
||||
this.peerConnection.getStats(null).then((StatsData: RTCStatsReport) => {
|
||||
this.peerConnection?.getStats(null).then((StatsData: RTCStatsReport) => {
|
||||
this.aggregatedStats.processStats(StatsData);
|
||||
this.onVideoStats(this.aggregatedStats);
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ export class PeerConnectionController {
|
|||
}
|
||||
}
|
||||
|
||||
this.peerConnection.addIceCandidate(iceCandidate);
|
||||
this.peerConnection?.addIceCandidate(iceCandidate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -363,14 +363,14 @@ export class PeerConnectionController {
|
|||
*/
|
||||
async setupTransceiversAsync(useMic: boolean) {
|
||||
const hasTransceivers =
|
||||
this.peerConnection.getTransceivers().length > 0;
|
||||
this.peerConnection?.getTransceivers().length > 0;
|
||||
|
||||
// Setup a transceiver for getting UE video
|
||||
this.peerConnection.addTransceiver('video', { direction: 'recvonly' });
|
||||
this.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
|
||||
|
||||
// We can only set preferrec codec on Chrome
|
||||
if (RTCRtpReceiver.getCapabilities && this.preferredCodec != '') {
|
||||
for (const transceiver of this.peerConnection.getTransceivers()) {
|
||||
for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
|
||||
if (
|
||||
transceiver &&
|
||||
transceiver.receiver &&
|
||||
|
|
@ -421,7 +421,7 @@ export class PeerConnectionController {
|
|||
|
||||
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
||||
if (!useMic) {
|
||||
this.peerConnection.addTransceiver('audio', {
|
||||
this.peerConnection?.addTransceiver('audio', {
|
||||
direction: 'recvonly'
|
||||
});
|
||||
} else {
|
||||
|
|
@ -451,7 +451,7 @@ export class PeerConnectionController {
|
|||
);
|
||||
if (stream) {
|
||||
if (hasTransceivers) {
|
||||
for (const transceiver of this.peerConnection.getTransceivers()) {
|
||||
for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
|
||||
if (
|
||||
transceiver &&
|
||||
transceiver.receiver &&
|
||||
|
|
@ -469,14 +469,14 @@ export class PeerConnectionController {
|
|||
} else {
|
||||
for (const track of stream.getTracks()) {
|
||||
if (track.kind && track.kind == 'audio') {
|
||||
this.peerConnection.addTransceiver(track, {
|
||||
this.peerConnection?.addTransceiver(track, {
|
||||
direction: 'sendrecv'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.peerConnection.addTransceiver('audio', {
|
||||
this.peerConnection?.addTransceiver('audio', {
|
||||
direction: 'recvonly'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,524 @@
|
|||
import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver';
|
||||
import {
|
||||
Config,
|
||||
NumericParameters,
|
||||
} from '../Config/Config';
|
||||
import { PixelStreaming } from './PixelStreaming';
|
||||
import { SettingsChangedEvent, StreamerListMessageEvent, WebRtcConnectedEvent, WebRtcSdpEvent } from '../Util/EventEmitter';
|
||||
import { mockWebSocket, MockWebSocketSpyFunctions, MockWebSocketTriggerFunctions, unmockWebSocket } from '../__test__/mockWebSocket';
|
||||
import { MessageRecvTypes } from '../WebSockets/MessageReceive';
|
||||
import { mockRTCPeerConnection, MockRTCPeerConnectionSpyFunctions, MockRTCPeerConnectionTriggerFunctions, unmockRTCPeerConnection } from '../__test__/mockRTCPeerConnection';
|
||||
import { mockHTMLMediaElement, mockMediaStream, unmockMediaStream } from '../__test__/mockMediaStream';
|
||||
import { InitialSettings } from '../DataChannel/InitialSettings';
|
||||
|
||||
const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate);
|
||||
|
||||
describe('PixelStreaming', () => {
|
||||
let webSocketSpyFunctions: MockWebSocketSpyFunctions;
|
||||
let webSocketTriggerFunctions: MockWebSocketTriggerFunctions;
|
||||
let rtcPeerConnectionSpyFunctions: MockRTCPeerConnectionSpyFunctions;
|
||||
let rtcPeerConnectionTriggerFunctions: MockRTCPeerConnectionTriggerFunctions;
|
||||
|
||||
const mockSignallingUrl = 'ws://localhost:24680/';
|
||||
const streamerId = "MOCK_PIXEL_STREAMING";
|
||||
const streamerIdList = [streamerId];
|
||||
const sdp = "v=0\r\no=- 974006863270230083 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS pixelstreaming_audio_stream_id pixelstreaming_video_stream_id\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendonly\r\na=msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 H264/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 red/90000\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 ulpfec/90000\r\na=ssrc-group:FID 3702690738 1574960745\r\na=ssrc:3702690738 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:3702690738 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:3702690738 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:3702690738 label:pixelstreaming_video_track_label\r\na=ssrc:1574960745 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:1574960745 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:1574960745 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:1574960745 label:pixelstreaming_video_track_label\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 63 110\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 maxaveragebitrate=510000;maxplaybackrate=48000;minptime=3;sprop-stereo=1;stereo=1;usedtx=0;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:110 telephone-event/48000\r\na=maxptime:120\r\na=ptime:20\r\na=ssrc:2587776314 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:2587776314 msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=ssrc:2587776314 mslabel:pixelstreaming_audio_stream_id\r\na=ssrc:2587776314 label:pixelstreaming_audio_track_label\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n";
|
||||
const iceCandidate: RTCIceCandidateInit = {
|
||||
sdpMid: "0",
|
||||
sdpMLineIndex: null,
|
||||
usernameFragment: null,
|
||||
candidate:"candidate:2199032595 1 udp 2122260223 192.168.1.89 64674 typ host generation 0 ufrag +JE1 network-id 1"
|
||||
};
|
||||
|
||||
const triggerWebSocketOpen = () =>
|
||||
webSocketTriggerFunctions.triggerOnOpen?.();
|
||||
const triggerConfigMessage = () =>
|
||||
webSocketTriggerFunctions.triggerOnMessage?.({
|
||||
type: MessageRecvTypes.CONFIG,
|
||||
peerConnectionOptions: {}
|
||||
});
|
||||
const triggerStreamerListMessage = (streamerIdList: string[]) =>
|
||||
webSocketTriggerFunctions.triggerOnMessage?.({
|
||||
type: MessageRecvTypes.STREAMER_LIST,
|
||||
ids: streamerIdList
|
||||
});
|
||||
const triggerSdpOfferMessage = () =>
|
||||
webSocketTriggerFunctions.triggerOnMessage?.({
|
||||
type: MessageRecvTypes.OFFER,
|
||||
sdp
|
||||
});
|
||||
const triggerIceCandidateMessage = () =>
|
||||
webSocketTriggerFunctions.triggerOnMessage?.({
|
||||
type: MessageRecvTypes.ICE_CANDIDATE,
|
||||
candidate: iceCandidate
|
||||
});
|
||||
const triggerIceConnectionState = (state: RTCIceConnectionState) =>
|
||||
rtcPeerConnectionTriggerFunctions.triggerIceConnectionStateChange?.(
|
||||
state
|
||||
);
|
||||
const triggerAddTrack = () => {
|
||||
const stream = new MediaStream();
|
||||
const track = new MediaStreamTrack();
|
||||
rtcPeerConnectionTriggerFunctions.triggerOnTrack?.({
|
||||
track,
|
||||
streams: [stream]
|
||||
} as RTCTrackEventInit);
|
||||
return { stream, track };
|
||||
};
|
||||
const triggerOpenDataChannel = () => {
|
||||
const channel = new RTCDataChannel();
|
||||
rtcPeerConnectionTriggerFunctions.triggerOnDataChannel?.({
|
||||
channel
|
||||
});
|
||||
channel.onopen?.(new Event('open'));
|
||||
return { channel };
|
||||
};
|
||||
const establishMockedPixelStreamingConnection = (
|
||||
streamerIds = streamerIdList,
|
||||
iceConnectionState: RTCIceConnectionState = 'connected'
|
||||
) => {
|
||||
triggerWebSocketOpen();
|
||||
triggerConfigMessage();
|
||||
triggerStreamerListMessage(streamerIds);
|
||||
triggerSdpOfferMessage();
|
||||
triggerIceCandidateMessage();
|
||||
triggerIceConnectionState(iceConnectionState);
|
||||
const { stream, track } = triggerAddTrack();
|
||||
const { channel } = triggerOpenDataChannel();
|
||||
return { channel, stream, track };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRTCRtpReceiver();
|
||||
mockMediaStream();
|
||||
[webSocketSpyFunctions, webSocketTriggerFunctions] = mockWebSocket();
|
||||
[rtcPeerConnectionSpyFunctions, rtcPeerConnectionTriggerFunctions] = mockRTCPeerConnection();
|
||||
mockHTMLMediaElement({ ableToPlay: true });
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockRTCRtpReceiver();
|
||||
unmockMediaStream();
|
||||
unmockWebSocket();
|
||||
unmockRTCPeerConnection();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should emit settingsChanged events when the configuration is updated', () => {
|
||||
const config = new Config();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
const settingsChangedSpy = jest.fn();
|
||||
pixelStreaming.addEventListener("settingsChanged", settingsChangedSpy);
|
||||
|
||||
expect(settingsChangedSpy).not.toHaveBeenCalled();
|
||||
|
||||
config.setNumericSetting(NumericParameters.WebRTCMaxBitrate, 123);
|
||||
|
||||
expect(settingsChangedSpy).toHaveBeenCalledWith(new SettingsChangedEvent({
|
||||
id: NumericParameters.WebRTCMaxBitrate,
|
||||
target: config.getNumericSettings().find((setting) => setting.id === NumericParameters.WebRTCMaxBitrate)!,
|
||||
type: 'number',
|
||||
value: 123,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should connect to signalling server when connect is called', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
|
||||
|
||||
pixelStreaming.connect();
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
});
|
||||
|
||||
it('should autoconnect to signalling server if autoconnect setting is enabled', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
|
||||
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
});
|
||||
|
||||
it('should disconnect from signalling server if disconnect is called', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const disconnectedSpy = jest.fn();
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
|
||||
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
|
||||
|
||||
pixelStreaming.disconnect();
|
||||
|
||||
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
|
||||
expect(disconnectedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should connect immediately to signalling server if reconnect is called and connection is not up', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
|
||||
|
||||
pixelStreaming.reconnect();
|
||||
|
||||
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
});
|
||||
|
||||
it('should disconnect and reconnect to signalling server if reconnect is called and connection is up', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const autoconnectedSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
|
||||
|
||||
pixelStreaming.reconnect();
|
||||
|
||||
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
|
||||
|
||||
// delayed reconnect after 3 seconds
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
||||
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(autoconnectedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should request streamer list when connected to the signalling server', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
|
||||
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/"type":"listStreamers"/)
|
||||
);
|
||||
});
|
||||
|
||||
it('should autoselect a streamer if receiving only one streamer in streamerList message', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const streamerListSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("streamerListMessage", streamerListSpy);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
triggerConfigMessage();
|
||||
triggerStreamerListMessage(streamerIdList);
|
||||
|
||||
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
|
||||
messageStreamerList: expect.objectContaining({
|
||||
type: MessageRecvTypes.STREAMER_LIST,
|
||||
ids: streamerIdList
|
||||
}),
|
||||
autoSelectedStreamerId: streamerId
|
||||
}));
|
||||
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/"type":"subscribe".*MOCK_PIXEL_STREAMING/)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not autoselect a streamer if receiving multiple streamers in streamerList message', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const streamerId2 = "MOCK_2_PIXEL_STREAMING";
|
||||
const extendedStreamerIdList = [streamerId, streamerId2];
|
||||
const streamerListSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("streamerListMessage", streamerListSpy);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
triggerConfigMessage();
|
||||
triggerStreamerListMessage(extendedStreamerIdList);
|
||||
|
||||
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
|
||||
messageStreamerList: expect.objectContaining({
|
||||
type: MessageRecvTypes.STREAMER_LIST,
|
||||
ids: extendedStreamerIdList
|
||||
}),
|
||||
autoSelectedStreamerId: null
|
||||
}));
|
||||
expect(webSocketSpyFunctions.sendSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringMatching(/"type":"subscribe"/)
|
||||
);
|
||||
});
|
||||
|
||||
it('should set remoteDescription and emit webRtcSdp event when an offer is received', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const eventSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("webRtcSdp", eventSpy);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
triggerConfigMessage();
|
||||
triggerStreamerListMessage(streamerIdList);
|
||||
|
||||
expect(eventSpy).not.toHaveBeenCalled();
|
||||
|
||||
triggerSdpOfferMessage();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.setRemoteDescriptionSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
sdp
|
||||
}));
|
||||
expect(eventSpy).toHaveBeenCalledWith(new WebRtcSdpEvent());
|
||||
});
|
||||
|
||||
it('should add an ICE candidate when receiving a iceCandidate message', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
triggerConfigMessage();
|
||||
triggerStreamerListMessage(streamerIdList);
|
||||
triggerSdpOfferMessage();
|
||||
triggerIceCandidateMessage();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.addIceCandidateSpy).toHaveBeenCalledWith(iceCandidate)
|
||||
});
|
||||
|
||||
it('should emit webRtcConnected event when ICE connection state is connected', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const connectedSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("webRtcConnected", connectedSpy);
|
||||
|
||||
triggerWebSocketOpen();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.constructorSpy).not.toHaveBeenCalled();
|
||||
|
||||
triggerConfigMessage();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.constructorSpy).toHaveBeenCalled();
|
||||
|
||||
triggerIceCandidateMessage();
|
||||
triggerIceConnectionState('connected')
|
||||
|
||||
expect(connectedSpy).toHaveBeenCalledWith(new WebRtcConnectedEvent());
|
||||
});
|
||||
|
||||
|
||||
it('should call RTCPeerConnection close and emit webRtcDisconnected when disconnect is called', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const disconnectedSpy = jest.fn();
|
||||
const dataChannelSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy);
|
||||
pixelStreaming.addEventListener("dataChannelClose", dataChannelSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.disconnect();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.closeSpy).toHaveBeenCalled();
|
||||
expect(disconnectedSpy).toHaveBeenCalled();
|
||||
expect(dataChannelSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit statistics when connected', async () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
|
||||
const statsSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("statsReceived", statsSpy);
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
expect(statsSpy).not.toHaveBeenCalled();
|
||||
|
||||
// New stats sent at 1s intervals
|
||||
jest.advanceTimersByTime(1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(statsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(statsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
aggregatedStats: expect.objectContaining({
|
||||
candidatePair: expect.objectContaining({
|
||||
bytesReceived: 123
|
||||
}),
|
||||
localCandidates: [
|
||||
expect.objectContaining({ address: 'mock-address' })
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
await flushPromises();
|
||||
expect(statsSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should emit dataChannelOpen when data channel is opened', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const dataChannelSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("dataChannelOpen", dataChannelSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
expect(dataChannelSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit playStream when video play is called', () => {
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const streamSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("playStream", streamSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(streamSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit playStreamRejected if video play is rejected', async () => {
|
||||
mockHTMLMediaElement({ ableToPlay: false });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const streamRejectedSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("playStreamRejected", streamRejectedSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
await flushPromises();
|
||||
|
||||
expect(streamRejectedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send data through the data channel when emitCommand is called', () => {
|
||||
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
|
||||
|
||||
const commandSent = pixelStreaming.emitCommand({
|
||||
'Resolution.Width': 123,
|
||||
'Resolution.Height': 456
|
||||
});
|
||||
|
||||
expect(commandSent).toEqual(true);
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent sending console commands unless permitted by streamer', () => {
|
||||
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
|
||||
|
||||
const commandSent = pixelStreaming.emitConsoleCommand("console command");
|
||||
|
||||
expect(commandSent).toEqual(false);
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow sending console commands if permitted by streamer', () => {
|
||||
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const initialSettingsSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addEventListener("initialSettings", initialSettingsSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
|
||||
expect(initialSettingsSpy).not.toHaveBeenCalled();
|
||||
|
||||
const initialSettings = new InitialSettings();
|
||||
initialSettings.PixelStreamingSettings.AllowPixelStreamingCommands = true;
|
||||
pixelStreaming._onInitialSettings(initialSettings);
|
||||
|
||||
expect(initialSettingsSpy).toHaveBeenCalled();
|
||||
|
||||
const commandSent = pixelStreaming.emitConsoleCommand("console command");
|
||||
|
||||
expect(commandSent).toEqual(true);
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send data through the data channel when emitUIInteraction is called', () => {
|
||||
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.connect();
|
||||
|
||||
establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
|
||||
|
||||
const commandSent = pixelStreaming.emitUIInteraction({ custom: "descriptor" });
|
||||
|
||||
expect(commandSent).toEqual(true);
|
||||
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call user-provided callback if receiving a data channel Response message from the streamer', () => {
|
||||
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
||||
|
||||
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
||||
const responseListenerSpy = jest.fn();
|
||||
const pixelStreaming = new PixelStreaming(config);
|
||||
pixelStreaming.addResponseEventListener('responseListener', responseListenerSpy);
|
||||
pixelStreaming.connect();
|
||||
|
||||
const { channel } = establishMockedPixelStreamingConnection();
|
||||
|
||||
pixelStreaming.play();
|
||||
|
||||
expect(responseListenerSpy).not.toHaveBeenCalled();
|
||||
|
||||
const testMessageContents = JSON.stringify({ test: "mock-data" });
|
||||
const data = new DataView(new ArrayBuffer(1 + 2 * testMessageContents.length));
|
||||
data.setUint8(0, 1); // type 1 == Response
|
||||
let byteIdx = 1;
|
||||
for (let i = 0; i < testMessageContents.length; i++) {
|
||||
data.setUint16(byteIdx, testMessageContents.charCodeAt(i), true);
|
||||
byteIdx += 2;
|
||||
}
|
||||
channel.dispatchEvent(new MessageEvent('message', { data: data.buffer }));
|
||||
|
||||
expect(responseListenerSpy).toHaveBeenCalledWith(testMessageContents);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
export class MockMediaStreamImpl implements MediaStream {
|
||||
active: boolean;
|
||||
id: string;
|
||||
|
||||
constructor(data?: MediaStream | MediaStreamTrack[]) {
|
||||
//
|
||||
}
|
||||
|
||||
onaddtrack: ((this: MediaStream, ev: MediaStreamTrackEvent) => any) | null;
|
||||
onremovetrack: ((this: MediaStream, ev: MediaStreamTrackEvent) => any) | null;
|
||||
addTrack(track: MediaStreamTrack): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
clone(): MediaStream {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getAudioTracks(): MediaStreamTrack[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getTrackById(trackId: string): MediaStreamTrack | null {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getTracks(): MediaStreamTrack[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getVideoTracks(): MediaStreamTrack[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeTrack(track: MediaStreamTrack): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
addEventListener<K extends keyof MediaStreamEventMap>(type: K, listener: (this: MediaStream, ev: MediaStreamEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeEventListener<K extends keyof MediaStreamEventMap>(type: K, listener: (this: MediaStream, ev: MediaStreamEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
dispatchEvent(event: Event): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrackImpl implements MediaStreamTrack {
|
||||
contentHint: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
muted: boolean;
|
||||
readyState: MediaStreamTrackState;
|
||||
|
||||
constructor() {
|
||||
this.kind = 'video';
|
||||
this.readyState = 'live';
|
||||
}
|
||||
|
||||
onended: ((this: MediaStreamTrack, ev: Event) => any) | null;
|
||||
onmute: ((this: MediaStreamTrack, ev: Event) => any) | null;
|
||||
onunmute: ((this: MediaStreamTrack, ev: Event) => any) | null;
|
||||
applyConstraints(constraints?: MediaTrackConstraints | undefined): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
clone(): MediaStreamTrack {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getCapabilities(): MediaTrackCapabilities {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getConstraints(): MediaTrackConstraints {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getSettings(): MediaTrackSettings {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
stop(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
addEventListener<K extends keyof MediaStreamTrackEventMap>(type: K, listener: (this: MediaStreamTrack, ev: MediaStreamTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeEventListener<K extends keyof MediaStreamTrackEventMap>(type: K, listener: (this: MediaStreamTrack, ev: MediaStreamTrackEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
dispatchEvent(event: Event): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
const mockHTMLMediaElementPlay = (ableToPlay: boolean) => {
|
||||
if (ableToPlay) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject("mock cancel");
|
||||
};
|
||||
|
||||
const originalMediaStream = global.MediaStream;
|
||||
const originalMediaStreamTrack = global.MediaStreamTrack;
|
||||
export const mockMediaStream = () => {
|
||||
global.MediaStream = MockMediaStreamImpl;
|
||||
global.MediaStreamTrack = MockMediaStreamTrackImpl;
|
||||
}
|
||||
|
||||
export const unmockMediaStream = () => {
|
||||
global.MediaStream = originalMediaStream;
|
||||
global.MediaStreamTrack = originalMediaStreamTrack;
|
||||
}
|
||||
|
||||
export const mockHTMLMediaElement = (options: { ableToPlay: boolean, readyState?: number }) => {
|
||||
const { ableToPlay, readyState } = options;
|
||||
jest.spyOn(HTMLMediaElement.prototype, 'play').mockReturnValue(mockHTMLMediaElementPlay(ableToPlay));
|
||||
if (readyState !== undefined) {
|
||||
jest.spyOn(HTMLMediaElement.prototype, 'readyState', 'get').mockReturnValue(readyState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
export interface MockRTCPeerConnectionSpyFunctions {
|
||||
constructorSpy: null | ((config: RTCConfiguration) => void);
|
||||
closeSpy: null | (() => void);
|
||||
setRemoteDescriptionSpy: null | ((description: RTCSessionDescriptionInit) => void);
|
||||
setLocalDescriptionSpy: null | ((description: RTCLocalSessionDescriptionInit) => void);
|
||||
createAnswerSpy: null | (() => void);
|
||||
addTransceiverSpy: null | ((trackOrKind: string | MediaStreamTrack, init?: RTCRtpTransceiverInit | undefined) => void);
|
||||
addIceCandidateSpy: null | ((candidate: RTCIceCandidateInit) => void);
|
||||
sendDataSpy: null | ((data: ArrayBuffer) => void);
|
||||
}
|
||||
|
||||
export interface MockRTCPeerConnectionTriggerFunctions {
|
||||
triggerIceConnectionStateChange: null | ((state: RTCIceConnectionState) => void);
|
||||
triggerOnTrack: null | ((data: RTCTrackEventInit) => void);
|
||||
triggerOnIceCandidate: null | ((data: RTCPeerConnectionIceEventInit) => void);
|
||||
triggerOnDataChannel: null | ((data: RTCDataChannelEventInit) => void);
|
||||
}
|
||||
|
||||
const spyFunctions: MockRTCPeerConnectionSpyFunctions = {
|
||||
constructorSpy: null,
|
||||
closeSpy: null,
|
||||
setRemoteDescriptionSpy: null,
|
||||
setLocalDescriptionSpy: null,
|
||||
createAnswerSpy: null,
|
||||
addTransceiverSpy: null,
|
||||
addIceCandidateSpy: null,
|
||||
sendDataSpy: null,
|
||||
};
|
||||
|
||||
const triggerFunctions: MockRTCPeerConnectionTriggerFunctions = {
|
||||
triggerIceConnectionStateChange: null,
|
||||
triggerOnTrack: null,
|
||||
triggerOnIceCandidate: null,
|
||||
triggerOnDataChannel: null
|
||||
};
|
||||
|
||||
export class MockRTCPeerConnectionImpl implements RTCPeerConnection {
|
||||
|
||||
canTrickleIceCandidates: boolean | null;
|
||||
connectionState: RTCPeerConnectionState;
|
||||
currentLocalDescription: RTCSessionDescription | null;
|
||||
currentRemoteDescription: RTCSessionDescription | null;
|
||||
iceConnectionState: RTCIceConnectionState;
|
||||
iceGatheringState: RTCIceGatheringState;
|
||||
localDescription: RTCSessionDescription | null;
|
||||
pendingLocalDescription: RTCSessionDescription | null;
|
||||
pendingRemoteDescription: RTCSessionDescription | null;
|
||||
remoteDescription: RTCSessionDescription | null;
|
||||
sctp: RTCSctpTransport | null;
|
||||
signalingState: RTCSignalingState;
|
||||
_dataChannels: RTCDataChannel[] = [];
|
||||
|
||||
constructor(config: RTCConfiguration) {
|
||||
this.connectionState = "new";
|
||||
this.iceConnectionState = "new";
|
||||
spyFunctions.constructorSpy?.(config);
|
||||
triggerFunctions.triggerIceConnectionStateChange = this.triggerIceConnectionStateChange.bind(this);
|
||||
triggerFunctions.triggerOnTrack = this.triggerOnTrack.bind(this);
|
||||
triggerFunctions.triggerOnIceCandidate = this.triggerOnIceCandidate.bind(this);
|
||||
triggerFunctions.triggerOnDataChannel =
|
||||
this.triggerOnDataChannel.bind(this);
|
||||
}
|
||||
|
||||
onconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
ondatachannel: ((this: RTCPeerConnection, ev: RTCDataChannelEvent) => any) | null;
|
||||
onicecandidate: ((this: RTCPeerConnection, ev: RTCPeerConnectionIceEvent) => any) | null;
|
||||
onicecandidateerror: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
oniceconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
onicegatheringstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
onnegotiationneeded: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
onsignalingstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null;
|
||||
ontrack: ((this: RTCPeerConnection, ev: RTCTrackEvent) => any) | null;
|
||||
addIceCandidate(candidate?: RTCIceCandidateInit | undefined): Promise<void>;
|
||||
addIceCandidate(candidate: RTCIceCandidateInit, successCallback: VoidFunction, failureCallback: RTCPeerConnectionErrorCallback): Promise<void>;
|
||||
addIceCandidate(candidate?: unknown, successCallback?: unknown, failureCallback?: unknown): Promise<void> {
|
||||
if (this.iceConnectionState !== "connected" && this.iceConnectionState !== "completed") {
|
||||
this.iceConnectionState = "checking";
|
||||
}
|
||||
this.oniceconnectionstatechange?.(new Event("iceconnectionstatechange"));
|
||||
spyFunctions.addIceCandidateSpy?.(candidate as RTCIceCandidateInit);
|
||||
return Promise.resolve();
|
||||
}
|
||||
addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
addTransceiver(trackOrKind: string | MediaStreamTrack, init?: RTCRtpTransceiverInit | undefined): RTCRtpTransceiver {
|
||||
spyFunctions.addTransceiverSpy?.(trackOrKind, init);
|
||||
return {} as RTCRtpTransceiver;
|
||||
}
|
||||
createAnswer(options?: RTCAnswerOptions | undefined): Promise<RTCSessionDescriptionInit>;
|
||||
createAnswer(successCallback: RTCSessionDescriptionCallback, failureCallback: RTCPeerConnectionErrorCallback): Promise<void>;
|
||||
createAnswer(successCallback?: unknown, failureCallback?: unknown): Promise<void> | Promise<RTCSessionDescriptionInit> {
|
||||
spyFunctions.createAnswerSpy?.();
|
||||
const res: RTCSessionDescriptionInit = {
|
||||
type: "answer",
|
||||
sdp: "v=0\r\no=- 5791786663981007547 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 98\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:z0li\r\na=ice-pwd:DkbG5Q3dFSIygDc47cms4TGA\r\na=ice-options:trickle\r\na=fingerprint:sha-256 F9:5B:3C:AB:89:88:0E:1B:2E:63:B3:D2:B8:92:59:E2:3A:46:B6:85:09:F4:50:0E:72:4F:9F:70:6D:5F:BD:1A\r\na=setup:active\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=recvonly\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:98 H264/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 63 110\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:z0li\r\na=ice-pwd:DkbG5Q3dFSIygDc47cms4TGA\r\na=ice-options:trickle\r\na=fingerprint:sha-256 F9:5B:3C:AB:89:88:0E:1B:2E:63:B3:D2:B8:92:59:E2:3A:46:B6:85:09:F4:50:0E:72:4F:9F:70:6D:5F:BD:1A\r\na=setup:active\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=recvonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:110 telephone-event/48000\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:z0li\r\na=ice-pwd:DkbG5Q3dFSIygDc47cms4TGA\r\na=ice-options:trickle\r\na=fingerprint:sha-256 F9:5B:3C:AB:89:88:0E:1B:2E:63:B3:D2:B8:92:59:E2:3A:46:B6:85:09:F4:50:0E:72:4F:9F:70:6D:5F:BD:1A\r\na=setup:active\r\na=mid:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n"
|
||||
};
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit | undefined): RTCDataChannel {
|
||||
const dataChannel = new RTCDataChannel();
|
||||
this._dataChannels.push(dataChannel);
|
||||
return dataChannel;
|
||||
}
|
||||
createOffer(options?: RTCOfferOptions | undefined): Promise<RTCSessionDescriptionInit>;
|
||||
createOffer(successCallback: RTCSessionDescriptionCallback, failureCallback: RTCPeerConnectionErrorCallback, options?: RTCOfferOptions | undefined): Promise<void>;
|
||||
createOffer(successCallback?: unknown, failureCallback?: unknown, options?: unknown): Promise<void> | Promise<RTCSessionDescriptionInit> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getConfiguration(): RTCConfiguration {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getReceivers(): RTCRtpReceiver[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getSenders(): RTCRtpSender[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getStats(selector?: MediaStreamTrack | null | undefined): Promise<RTCStatsReport> {
|
||||
const stats = {
|
||||
forEach: function (callbackfn: (value: any) => void): void {
|
||||
callbackfn({
|
||||
type: 'candidate-pair',
|
||||
bytesReceived: 123,
|
||||
});
|
||||
callbackfn({
|
||||
type: 'local-candidate',
|
||||
address: 'mock-address',
|
||||
});
|
||||
},
|
||||
};
|
||||
return Promise.resolve(stats as RTCStatsReport);
|
||||
}
|
||||
getTransceivers(): RTCRtpTransceiver[] {
|
||||
return [];
|
||||
}
|
||||
removeTrack(sender: RTCRtpSender): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
restartIce(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
setConfiguration(configuration?: RTCConfiguration | undefined): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
setLocalDescription(description?: RTCLocalSessionDescriptionInit | undefined): Promise<void>;
|
||||
setLocalDescription(description: RTCLocalSessionDescriptionInit, successCallback: VoidFunction, failureCallback: RTCPeerConnectionErrorCallback): Promise<void>;
|
||||
setLocalDescription(description?: unknown, successCallback?: unknown, failureCallback?: unknown): Promise<void> {
|
||||
spyFunctions.setLocalDescriptionSpy?.(description as RTCLocalSessionDescriptionInit);
|
||||
return Promise.resolve();
|
||||
}
|
||||
setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void>;
|
||||
setRemoteDescription(description: RTCSessionDescriptionInit, successCallback: VoidFunction, failureCallback: RTCPeerConnectionErrorCallback): Promise<void>;
|
||||
setRemoteDescription(description: unknown, successCallback?: unknown, failureCallback?: unknown): Promise<void> {
|
||||
spyFunctions.setRemoteDescriptionSpy?.(description as RTCSessionDescriptionInit);
|
||||
return Promise.resolve();
|
||||
}
|
||||
addEventListener<K extends keyof RTCPeerConnectionEventMap>(type: K, listener: (this: RTCPeerConnection, ev: RTCPeerConnectionEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeEventListener<K extends keyof RTCPeerConnectionEventMap>(type: K, listener: (this: RTCPeerConnection, ev: RTCPeerConnectionEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
dispatchEvent(event: Event): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
static generateCertificate(keygenAlgorithm: AlgorithmIdentifier): Promise<RTCCertificate> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.connectionState = "closed";
|
||||
this.iceConnectionState = "closed";
|
||||
this.onconnectionstatechange?.(new Event(this.connectionState));
|
||||
this.oniceconnectionstatechange?.(new Event(this.iceConnectionState));
|
||||
this._dataChannels.forEach((channel) => channel.close());
|
||||
spyFunctions.closeSpy?.();
|
||||
}
|
||||
|
||||
triggerIceConnectionStateChange(state: RTCIceConnectionState) {
|
||||
this.iceConnectionState = state;
|
||||
const event = new Event(state);
|
||||
this.oniceconnectionstatechange?.(event);
|
||||
}
|
||||
|
||||
triggerOnTrack(data: RTCTrackEventInit) {
|
||||
const event = new RTCTrackEvent('track', data);
|
||||
this.ontrack?.(event);
|
||||
}
|
||||
|
||||
triggerOnIceCandidate(data: RTCPeerConnectionIceEventInit) {
|
||||
const event = new RTCPeerConnectionIceEvent('icecandidate', data);
|
||||
this.onicecandidate?.(event);
|
||||
}
|
||||
|
||||
triggerOnDataChannel(data: RTCDataChannelEventInit) {
|
||||
this._dataChannels.push(data.channel);
|
||||
const event = new RTCDataChannelEvent('datachannel', data);
|
||||
this.ondatachannel?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCIceCandidateImpl implements RTCIceCandidate {
|
||||
address: string | null;
|
||||
candidate: string;
|
||||
component: RTCIceComponent | null;
|
||||
foundation: string | null;
|
||||
port: number | null;
|
||||
priority: number | null;
|
||||
protocol: RTCIceProtocol | null;
|
||||
relatedAddress: string | null;
|
||||
relatedPort: number | null;
|
||||
sdpMLineIndex: number | null;
|
||||
sdpMid: string | null;
|
||||
tcpType: RTCIceTcpCandidateType | null;
|
||||
type: RTCIceCandidateType | null;
|
||||
usernameFragment: string | null;
|
||||
|
||||
constructor(options?: RTCIceCandidateInit) {
|
||||
this.candidate = options?.candidate || "";
|
||||
this.sdpMid = options?.sdpMid || null;
|
||||
this.sdpMLineIndex = options?.sdpMLineIndex || null;
|
||||
this.usernameFragment = options?.usernameFragment || null;
|
||||
}
|
||||
|
||||
toJSON(): RTCIceCandidateInit {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCDataChannelImpl implements RTCDataChannel {
|
||||
binaryType: BinaryType;
|
||||
bufferedAmount: number;
|
||||
bufferedAmountLowThreshold: number;
|
||||
id: number | null;
|
||||
label: string;
|
||||
maxPacketLifeTime: number | null;
|
||||
maxRetransmits: number | null;
|
||||
negotiated: boolean;
|
||||
ordered: boolean;
|
||||
protocol: string;
|
||||
readyState: RTCDataChannelState;
|
||||
|
||||
constructor() {
|
||||
this.readyState = "open";
|
||||
}
|
||||
|
||||
onbufferedamountlow: ((this: RTCDataChannel, ev: Event) => any) | null;
|
||||
onclose: ((this: RTCDataChannel, ev: Event) => any) | null;
|
||||
onclosing: ((this: RTCDataChannel, ev: Event) => any) | null;
|
||||
onerror: ((this: RTCDataChannel, ev: Event) => any) | null;
|
||||
onmessage: ((this: RTCDataChannel, ev: MessageEvent<any>) => any) | null;
|
||||
onopen: ((this: RTCDataChannel, ev: Event) => any) | null;
|
||||
close(): void {
|
||||
this.onclose?.(new Event('close'));
|
||||
}
|
||||
send(data: string): void;
|
||||
send(data: Blob): void;
|
||||
send(data: ArrayBuffer): void;
|
||||
send(data: ArrayBufferView): void;
|
||||
send(data: unknown): void {
|
||||
spyFunctions.sendDataSpy?.(data as ArrayBuffer);
|
||||
}
|
||||
addEventListener<K extends keyof RTCDataChannelEventMap>(type: K, listener: (this: RTCDataChannel, ev: RTCDataChannelEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
addEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeEventListener<K extends keyof RTCDataChannelEventMap>(type: K, listener: (this: RTCDataChannel, ev: RTCDataChannelEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
dispatchEvent(event: Event): boolean {
|
||||
if (event.type === 'message') {
|
||||
this.onmessage?.(event as MessageEvent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCDataChannelEventImpl extends Event implements RTCDataChannelEvent {
|
||||
channel: RTCDataChannel;
|
||||
constructor(name: string, data: RTCDataChannelEventInit) {
|
||||
super(name, data);
|
||||
this.channel = data.channel;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCTrackEventImpl extends Event implements RTCTrackEvent {
|
||||
receiver: RTCRtpReceiver;
|
||||
streams: readonly MediaStream[];
|
||||
track: MediaStreamTrack;
|
||||
transceiver: RTCRtpTransceiver;
|
||||
constructor(name: string, data: RTCTrackEventInit) {
|
||||
super(name, data);
|
||||
this.receiver = data.receiver;
|
||||
this.streams = data.streams || [];
|
||||
this.track = data.track;
|
||||
this.transceiver = data.transceiver;
|
||||
}
|
||||
}
|
||||
|
||||
const originalRTCPeerConnection = global.RTCPeerConnection;
|
||||
const originalRTCIceCandidate = global.RTCIceCandidate;
|
||||
const originalRTCDataChannel = global.RTCDataChannel;
|
||||
const originalRTCDataChannelEvent = global.RTCDataChannelEvent;
|
||||
const originalRTCTrackEvent = global.RTCTrackEvent;
|
||||
export const mockRTCPeerConnection = (): [
|
||||
MockRTCPeerConnectionSpyFunctions,
|
||||
MockRTCPeerConnectionTriggerFunctions
|
||||
] => {
|
||||
spyFunctions.constructorSpy = jest.fn();
|
||||
spyFunctions.closeSpy = jest.fn();
|
||||
spyFunctions.setRemoteDescriptionSpy = jest.fn();
|
||||
spyFunctions.setLocalDescriptionSpy = jest.fn();
|
||||
spyFunctions.createAnswerSpy = jest.fn();
|
||||
spyFunctions.addTransceiverSpy = jest.fn();
|
||||
spyFunctions.addIceCandidateSpy = jest.fn();
|
||||
spyFunctions.sendDataSpy = jest.fn();
|
||||
global.RTCPeerConnection = MockRTCPeerConnectionImpl;
|
||||
global.RTCIceCandidate = MockRTCIceCandidateImpl;
|
||||
global.RTCDataChannel = MockRTCDataChannelImpl;
|
||||
global.RTCDataChannelEvent = MockRTCDataChannelEventImpl;
|
||||
global.RTCTrackEvent = MockRTCTrackEventImpl;
|
||||
return [spyFunctions, triggerFunctions];
|
||||
};
|
||||
|
||||
export const unmockRTCPeerConnection = () => {
|
||||
global.RTCPeerConnection = originalRTCPeerConnection;
|
||||
global.RTCIceCandidate = originalRTCIceCandidate;
|
||||
global.RTCDataChannel = originalRTCDataChannel;
|
||||
global.RTCDataChannelEvent = originalRTCDataChannelEvent;
|
||||
global.RTCTrackEvent = originalRTCTrackEvent;
|
||||
spyFunctions.constructorSpy = null;
|
||||
spyFunctions.closeSpy = null;
|
||||
spyFunctions.setRemoteDescriptionSpy = null;
|
||||
spyFunctions.setLocalDescriptionSpy = null;
|
||||
spyFunctions.createAnswerSpy = null;
|
||||
spyFunctions.addTransceiverSpy = null;
|
||||
spyFunctions.addIceCandidateSpy = null;
|
||||
spyFunctions.sendDataSpy = null;
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export const mockRTCRtpReceiverImpl = {
|
||||
prototype: jest.fn(),
|
||||
getCapabilities: () => ({
|
||||
codecs: [
|
||||
{
|
||||
clockRate: 60,
|
||||
mimeType: "testMimeType",
|
||||
sdpFmtpLine: "AV1"
|
||||
}
|
||||
] as RTCRtpCodecCapability[],
|
||||
headerExtensions: [] as RTCRtpHeaderExtensionCapability[]
|
||||
})
|
||||
} as any as typeof global.RTCRtpReceiver;
|
||||
|
||||
const originalRTCRtpReceiver = global.RTCRtpReceiver;
|
||||
export const mockRTCRtpReceiver = () => {
|
||||
global.RTCRtpReceiver = mockRTCRtpReceiverImpl;
|
||||
}
|
||||
|
||||
export const unmockRTCRtpReceiver = () => {
|
||||
global.RTCRtpReceiver = originalRTCRtpReceiver;
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
export interface MockWebSocketSpyFunctions {
|
||||
constructorSpy: null | ((url: string) => void);
|
||||
openSpy: null | ((event: Event) => void);
|
||||
errorSpy: null | ((event: Event) => void);
|
||||
closeSpy: null | ((event: CloseEvent) => void);
|
||||
messageSpy: null | ((event: MessageEvent) => void);
|
||||
messageBinarySpy: null | ((event: MessageEvent) => void);
|
||||
sendSpy: null | ((data: string | Blob | ArrayBufferView | ArrayBufferLike) => void);
|
||||
}
|
||||
|
||||
export interface MockWebSocketTriggerFunctions {
|
||||
triggerOnOpen: null | (() => void);
|
||||
triggerOnError: null | (() => void);
|
||||
triggerOnClose: null | ((closeReason?: CloseEventInit) => void);
|
||||
triggerOnMessage: null | ((message?: object) => void);
|
||||
triggerOnMessageBinary: null | ((message?: Blob) => void);
|
||||
}
|
||||
|
||||
const spyFunctions: MockWebSocketSpyFunctions = {
|
||||
constructorSpy: null,
|
||||
openSpy: null,
|
||||
errorSpy: null,
|
||||
closeSpy: null,
|
||||
messageSpy: null,
|
||||
messageBinarySpy: null,
|
||||
sendSpy: null
|
||||
};
|
||||
|
||||
const triggerFunctions: MockWebSocketTriggerFunctions = {
|
||||
triggerOnOpen: null,
|
||||
triggerOnError: null,
|
||||
triggerOnClose: null,
|
||||
triggerOnMessage: null,
|
||||
triggerOnMessageBinary: null
|
||||
};
|
||||
|
||||
export class MockWebSocketImpl extends WebSocket {
|
||||
_readyState: number;
|
||||
|
||||
constructor(url: string | URL, protocols?: string | string[]) {
|
||||
super(url, protocols);
|
||||
this._readyState = this.OPEN;
|
||||
spyFunctions.constructorSpy?.(this.url);
|
||||
triggerFunctions.triggerOnOpen = this.triggerOnOpen.bind(this);
|
||||
triggerFunctions.triggerOnError = this.triggerOnError.bind(this);
|
||||
triggerFunctions.triggerOnClose = this.triggerOnClose.bind(this);
|
||||
triggerFunctions.triggerOnMessage = this.triggerOnMessage.bind(this);
|
||||
triggerFunctions.triggerOnMessageBinary =
|
||||
this.triggerOnMessageBinary.bind(this);
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._readyState;
|
||||
}
|
||||
|
||||
close(code?: number | undefined, reason?: string | undefined): void {
|
||||
super.close(code, reason);
|
||||
this._readyState = this.CLOSED;
|
||||
this.triggerOnClose({ code, reason });
|
||||
}
|
||||
|
||||
send(data: string | Blob | ArrayBufferView | ArrayBufferLike): void {
|
||||
spyFunctions.sendSpy?.(data);
|
||||
}
|
||||
|
||||
triggerOnOpen() {
|
||||
const event = new Event('open');
|
||||
this.onopen?.(event);
|
||||
spyFunctions.openSpy?.(event);
|
||||
}
|
||||
|
||||
triggerOnError() {
|
||||
const event = new Event('error');
|
||||
this.onerror?.(event);
|
||||
spyFunctions.errorSpy?.(event);
|
||||
}
|
||||
|
||||
triggerOnClose(closeReason?: CloseEventInit) {
|
||||
const reason = closeReason ?? { code: 1, reason: 'mock reason' };
|
||||
const event = new CloseEvent('close', reason);
|
||||
this.onclose?.(event);
|
||||
spyFunctions.closeSpy?.(event);
|
||||
}
|
||||
|
||||
triggerOnMessage(message?: object) {
|
||||
const data = message
|
||||
? JSON.stringify(message)
|
||||
: JSON.stringify({ type: 'test' });
|
||||
const event = new MessageEvent('message', { data });
|
||||
this.onmessage?.(event);
|
||||
spyFunctions.messageSpy?.(event);
|
||||
}
|
||||
|
||||
triggerOnMessageBinary(message?: Blob) {
|
||||
const data =
|
||||
message ??
|
||||
new Blob([JSON.stringify({ type: 'test' })], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const event = new MessageEvent('messagebinary', { data });
|
||||
this.onmessagebinary?.(event);
|
||||
spyFunctions.messageBinarySpy?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
const originalWebSocket = WebSocket;
|
||||
export const mockWebSocket = (): [
|
||||
MockWebSocketSpyFunctions,
|
||||
MockWebSocketTriggerFunctions
|
||||
] => {
|
||||
spyFunctions.constructorSpy = jest.fn();
|
||||
spyFunctions.openSpy = jest.fn();
|
||||
spyFunctions.errorSpy = jest.fn();
|
||||
spyFunctions.closeSpy = jest.fn();
|
||||
spyFunctions.messageSpy = jest.fn();
|
||||
spyFunctions.messageBinarySpy = jest.fn();
|
||||
spyFunctions.sendSpy = jest.fn();
|
||||
global.WebSocket = MockWebSocketImpl;
|
||||
return [spyFunctions, triggerFunctions];
|
||||
};
|
||||
|
||||
export const unmockWebSocket = () => {
|
||||
global.WebSocket = originalWebSocket;
|
||||
spyFunctions.constructorSpy = null;
|
||||
spyFunctions.openSpy = null;
|
||||
spyFunctions.errorSpy = null;
|
||||
spyFunctions.closeSpy = null;
|
||||
spyFunctions.messageSpy = null;
|
||||
spyFunctions.messageBinarySpy = null;
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"lib": ["es2015"],
|
||||
"include": ["./src/*.ts"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"typedocOptions": {
|
||||
"exclude": "src/index.*",
|
||||
"entryPoints": ["src/pixelstreamingfrontend.ts"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue