111
This commit is contained in:
16
WebApp/client/test/domrect.js
Normal file
16
WebApp/client/test/domrect.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// mock class
|
||||
|
||||
export class DOMRect {
|
||||
constructor(x, y, width, height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
get left() {
|
||||
return this.x;
|
||||
}
|
||||
get top() {
|
||||
return this.y;
|
||||
}
|
||||
}
|
||||
11
WebApp/client/test/domvideoelement.js
Normal file
11
WebApp/client/test/domvideoelement.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// mock class
|
||||
|
||||
export class DOMHTMLVideoElement {
|
||||
constructor(rect) {
|
||||
this.rect = rect;
|
||||
}
|
||||
|
||||
getBoundingClientRect() {
|
||||
return this.rect;
|
||||
}
|
||||
}
|
||||
172
WebApp/client/test/inputdevice.test.js
Normal file
172
WebApp/client/test/inputdevice.test.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
FourCC,
|
||||
Mouse,
|
||||
Keyboard,
|
||||
Touchscreen,
|
||||
Gamepad,
|
||||
KeyboardState,
|
||||
MouseState,
|
||||
TouchscreenState,
|
||||
GamepadState,
|
||||
StateEvent,
|
||||
InputEvent,
|
||||
TextEvent
|
||||
} from "../src/inputdevice.js";
|
||||
|
||||
describe(`FourCC`, () => {
|
||||
test('toInt32', () => {
|
||||
const number = new FourCC('A', 'A', 'A', 'A').toInt32();
|
||||
expect(number).toBe(0x41414141);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`MouseState`, () => {
|
||||
describe(`with MouseEvent`, () => {
|
||||
let event;
|
||||
beforeEach(() => {
|
||||
event = new MouseEvent('click', { buttons:1, clientX:0, clientY:0});
|
||||
});
|
||||
test('format', () => {
|
||||
const format = new MouseState(event).format;
|
||||
expect(format).toBe(0x4d4f5553);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const state = new MouseState(event);
|
||||
expect(state.buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
describe(`with WheelEvent`, () => {
|
||||
let event;
|
||||
beforeEach(() => {
|
||||
event = new WheelEvent('wheel', { deltaX:0, deltaY:0 });
|
||||
});
|
||||
test('format', () => {
|
||||
const format = new MouseState(event).format;
|
||||
expect(format).toBe(0x4d4f5553);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const state = new MouseState(event);
|
||||
expect(state.buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`KeyboardState`, () => {
|
||||
let event;
|
||||
beforeEach(() => {
|
||||
event = new KeyboardEvent('keydown', { code: 'KeyA' });
|
||||
});
|
||||
test('format', () => {
|
||||
const format = new KeyboardState(event).format;
|
||||
expect(format).toBe(0x4b455953);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const state = new KeyboardState(event);
|
||||
expect(state.buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`TouchscreenState`, () => {
|
||||
let event;
|
||||
beforeEach(() => {
|
||||
event = new TouchEvent("touchstart", {
|
||||
changedTouches: [{ // InputInit
|
||||
identifier: 0,
|
||||
target: null,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
rotationAngle: 0,
|
||||
force: 0,
|
||||
altitudeAngle: 0,
|
||||
azimuthAngle:0,
|
||||
touchType: "direct"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test('format', () => {
|
||||
const format = new TouchscreenState(event, null, Date.now()).format;
|
||||
expect(format).toBe(0x54534352);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const state = new TouchscreenState(event, null, Date.now());
|
||||
expect(state.buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`GamepadState`, () => {
|
||||
let event;
|
||||
beforeEach(() => {
|
||||
event = {
|
||||
type: 'gamepadupdated',
|
||||
gamepad : {
|
||||
id: 1,
|
||||
buttons: Array(16).fill({ pressed: false, value: 1 }),
|
||||
axes:[0, 0, 0, 0]
|
||||
}};
|
||||
});
|
||||
test('format', () => {
|
||||
const format = new GamepadState(event).format;
|
||||
expect(format).toBe(0x47504144);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const state = new GamepadState(event);
|
||||
expect(state.buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`StateEvent`, () => {
|
||||
let state;
|
||||
beforeEach(() => {
|
||||
const event = new KeyboardEvent('keydown', { code: 'KeyA' });
|
||||
state = new KeyboardState(event);
|
||||
});
|
||||
test('buffer', () => {
|
||||
const stateEvent = StateEvent.fromState(state, 0, Date.now());
|
||||
expect(new Int32Array(stateEvent.buffer.slice(0, 4))[0]).toBe(StateEvent.format);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`TextEvent`, () => {
|
||||
test('buffer', () => {
|
||||
const event = new KeyboardEvent('keydown', { code: 'KeyA', key: "a"});
|
||||
const textEvent = TextEvent.create(0, event, Date.now());
|
||||
expect(new Int32Array(textEvent.buffer.slice(0, 4))[0]).toBe(TextEvent.format);
|
||||
const offset = InputEvent.size;
|
||||
// 'a' is 97
|
||||
expect(new Uint32Array(textEvent.buffer.slice(offset, offset+4))[0]).toBe(97);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Mouse`, () => {
|
||||
test('alignedSizeInBytes', () => {
|
||||
let device = new Mouse("Mouse", "Mouse", 1, null, null);
|
||||
expect(device).toBeInstanceOf(Mouse);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Keyboard`, () => {
|
||||
test('alignedSizeInBytes', () => {
|
||||
let device = new Keyboard("Keyboard", "Keyboard", 1, null, null);
|
||||
expect(device).toBeInstanceOf(Keyboard);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Touchscreen`, () => {
|
||||
test('alignedSizeInBytes', () => {
|
||||
let device = new Touchscreen("Touchscreen", "Touchscreen", 1, null, null);
|
||||
expect(device).toBeInstanceOf(Touchscreen);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Gamepad`, () => {
|
||||
test('alignedSizeInBytes', () => {
|
||||
let device = new Gamepad("Gamepad", "Gamepad", 1, null, null);
|
||||
expect(device).toBeInstanceOf(Gamepad);
|
||||
});
|
||||
});
|
||||
132
WebApp/client/test/inputremoting.test.js
Normal file
132
WebApp/client/test/inputremoting.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
InputDevice,
|
||||
MouseState,
|
||||
KeyboardState,
|
||||
TouchscreenState,
|
||||
GamepadState
|
||||
} from "../src/inputdevice.js";
|
||||
|
||||
import {
|
||||
MessageType,
|
||||
NewDeviceMsg,
|
||||
NewEventsMsg,
|
||||
RemoveDeviceMsg,
|
||||
InputRemoting,
|
||||
} from "../src/inputremoting.js";
|
||||
|
||||
import {
|
||||
Sender,
|
||||
Observer
|
||||
} from "../src/sender.js";
|
||||
|
||||
import {DOMRect} from "./domrect.js";
|
||||
|
||||
describe(`InputRemoting`, () => {
|
||||
let sender = null;
|
||||
let inputRemoting = null;
|
||||
let observer = null;
|
||||
beforeEach(async () => {
|
||||
document.getBoundingClientRect = function(){ return new DOMRect(0,0,0,0); };
|
||||
sender = new Sender(document);
|
||||
inputRemoting = new InputRemoting(sender);
|
||||
let dc = null;
|
||||
observer = new Observer(dc);
|
||||
});
|
||||
test('startSending', () => {
|
||||
expect.assertions(0);
|
||||
inputRemoting.startSending();
|
||||
});
|
||||
test('stopSending', () => {
|
||||
expect.assertions(0);
|
||||
inputRemoting.startSending();
|
||||
inputRemoting.stopSending();
|
||||
});
|
||||
test('subscribe', () => {
|
||||
expect.assertions(0);
|
||||
inputRemoting.subscribe(observer);
|
||||
});
|
||||
});
|
||||
|
||||
test('create NewDeviceMsg', () => {
|
||||
const device = new InputDevice("Keyboard", "Keyboard", 0, null, null);
|
||||
const msg = NewDeviceMsg.create(device);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.NewDevice);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('create NewEventMsg', () => {
|
||||
test('using MouseState', () => {
|
||||
const event = new MouseEvent('click', { buttons:0, clientX:0, clientY:0} );
|
||||
const state = new MouseState(event);
|
||||
const msg = NewEventsMsg.create(state);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.NewEvents);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
test('using KeyboardState', () => {
|
||||
const event = new KeyboardEvent("keydown", { code: 'KeyA' });
|
||||
const state = new KeyboardState(event);
|
||||
const msg = NewEventsMsg.create(state);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.NewEvents);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
test('using TouchscreenState', () => {
|
||||
const event = new TouchEvent("touchstart", {
|
||||
changedTouches: [{ // InputInit
|
||||
identifier: 0,
|
||||
target: null,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
rotationAngle: 0,
|
||||
force: 0,
|
||||
altitudeAngle: 0,
|
||||
azimuthAngle:0,
|
||||
touchType: "direct"
|
||||
}]
|
||||
});
|
||||
const state = new TouchscreenState(event, null, Date.now());
|
||||
expect(state.touchData).not.toBeNull();
|
||||
expect(state.touchData).toHaveLength(1);
|
||||
const msg = NewEventsMsg.create(state.touchData[0]);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.NewEvents);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
test('using GamepadState', () => {
|
||||
const event = {
|
||||
type: 'gamepadupdated',
|
||||
gamepad : {
|
||||
id: 1,
|
||||
buttons: Array(16).fill({ pressed: false, value: 1 }),
|
||||
axes:[1, 1, 1, 1]
|
||||
}};
|
||||
const state = new GamepadState(event);
|
||||
const msg = NewEventsMsg.create(state);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.NewEvents);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('create RemoveDeviceMsg', () => {
|
||||
const device = new InputDevice("Keyboard", "Keyboard", 0, null, null);
|
||||
const msg = RemoveDeviceMsg.create(device);
|
||||
expect(msg.participant_id).toBe(0);
|
||||
expect(msg.type).toBe(MessageType.RemoveDevice);
|
||||
expect(msg.data).toBeInstanceOf(ArrayBuffer);
|
||||
expect(msg.data.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
67
WebApp/client/test/memoryhelper.test.js
Normal file
67
WebApp/client/test/memoryhelper.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
MemoryHelper
|
||||
} from "../src/memoryhelper.js";
|
||||
|
||||
describe(`MemoryHelper.writeSingleBit`, () => {
|
||||
test('turn on with offset 0', () => {
|
||||
let bytes = new ArrayBuffer(3);
|
||||
MemoryHelper.writeSingleBit(bytes, 0, false);
|
||||
|
||||
// check 00 00 00
|
||||
const view = new Uint8Array(bytes);
|
||||
expect(view[0]).toBe(0);
|
||||
expect(view[1]).toBe(0);
|
||||
expect(view[2]).toBe(0);
|
||||
});
|
||||
test('turn off with offset 0', () => {
|
||||
let bytes = new ArrayBuffer(3);
|
||||
MemoryHelper.writeSingleBit(bytes, 0, true);
|
||||
|
||||
// check 00 00 01
|
||||
const view = new Uint8Array(bytes);
|
||||
expect(view[0]).toBe(1);
|
||||
expect(view[1]).toBe(0);
|
||||
expect(view[2]).toBe(0);
|
||||
|
||||
MemoryHelper.writeSingleBit(bytes, 0, false);
|
||||
|
||||
// check 00 00 00
|
||||
expect(view[0]).toBe(0);
|
||||
expect(view[1]).toBe(0);
|
||||
expect(view[2]).toBe(0);
|
||||
});
|
||||
test('turn on with offset 32', () => {
|
||||
let bytes = new ArrayBuffer(3);
|
||||
MemoryHelper.writeSingleBit(bytes, 8, true);
|
||||
|
||||
// check 00 01 00
|
||||
const view = new Uint8Array(bytes);
|
||||
expect(view[0]).toBe(0);
|
||||
expect(view[1]).toBe(1);
|
||||
expect(view[2]).toBe(0);
|
||||
|
||||
MemoryHelper.writeSingleBit(bytes, 0, true);
|
||||
|
||||
// check 00 01 01
|
||||
expect(view[0]).toBe(1);
|
||||
expect(view[1]).toBe(1);
|
||||
expect(view[2]).toBe(0);
|
||||
});
|
||||
test('turn on with offset 15', () => {
|
||||
let bytes = new ArrayBuffer(3);
|
||||
MemoryHelper.writeSingleBit(bytes, 15, true);
|
||||
|
||||
// check 00 80 00
|
||||
const view = new Uint8Array(bytes);
|
||||
expect(view[0]).toBe(0);
|
||||
expect(view[1]).toBe(128);
|
||||
expect(view[2]).toBe(0);
|
||||
|
||||
MemoryHelper.writeSingleBit(bytes, 15, false);
|
||||
|
||||
// check 00 00 00
|
||||
expect(view[0]).toBe(0);
|
||||
expect(view[1]).toBe(0);
|
||||
expect(view[2]).toBe(0);
|
||||
});
|
||||
});
|
||||
224
WebApp/client/test/mocksignaling.js
Normal file
224
WebApp/client/test/mocksignaling.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import { sleep } from "./testutils";
|
||||
|
||||
/** @type {MockPrivateSignalingManager | MockPublicSignalingManager} */
|
||||
let manager;
|
||||
|
||||
export function reset(isPrivate) {
|
||||
manager = isPrivate ? new MockPrivateSignalingManager() : new MockPublicSignalingManager();
|
||||
}
|
||||
|
||||
export class MockSignaling extends EventTarget {
|
||||
|
||||
constructor(interval = 1000) {
|
||||
super();
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
async start() {
|
||||
await manager.add(this);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await manager.remove(this);
|
||||
}
|
||||
|
||||
async createConnection(connectionId) {
|
||||
await manager.openConnection(this, connectionId);
|
||||
}
|
||||
|
||||
async deleteConnection(connectionId) {
|
||||
await manager.closeConnection(this, connectionId);
|
||||
}
|
||||
|
||||
async sendOffer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
await manager.offer(this, data);
|
||||
}
|
||||
|
||||
async sendAnswer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
await manager.answer(this, data);
|
||||
}
|
||||
|
||||
async sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid) {
|
||||
const data = {
|
||||
'candidate': candidate,
|
||||
'sdpMLineIndex': sdpMLineIndex,
|
||||
'sdpMid': sdpMid,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
await manager.candidate(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
class MockPublicSignalingManager {
|
||||
constructor() {
|
||||
this.list = new Set();
|
||||
this.delay = async () => await sleep(10);
|
||||
}
|
||||
|
||||
async add(signaling) {
|
||||
await this.delay();
|
||||
this.list.add(signaling);
|
||||
signaling.dispatchEvent(new Event("start"));
|
||||
}
|
||||
|
||||
async remove(signaling) {
|
||||
await this.delay();
|
||||
this.list.delete(signaling);
|
||||
signaling.dispatchEvent(new Event("end"));
|
||||
}
|
||||
|
||||
async openConnection(signaling, connectionId) {
|
||||
await this.delay();
|
||||
const data = { connectionId: connectionId, polite: true };
|
||||
signaling.dispatchEvent(new CustomEvent("connect", { detail: data }));
|
||||
}
|
||||
|
||||
async closeConnection(signaling, connectionId) {
|
||||
await this.delay();
|
||||
const data = { connectionId: connectionId };
|
||||
for (const element of this.list) {
|
||||
element.dispatchEvent(new CustomEvent("disconnect", { detail: data }));
|
||||
}
|
||||
}
|
||||
|
||||
async offer(owner, data) {
|
||||
await this.delay();
|
||||
data.polite = false;
|
||||
for (const signaling of this.list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("offer", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async answer(owner, data) {
|
||||
await this.delay();
|
||||
for (const signaling of this.list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("answer", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async candidate(owner, data) {
|
||||
await this.delay();
|
||||
for (const signaling of this.list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("candidate", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockPrivateSignalingManager {
|
||||
constructor() {
|
||||
// structure Map<string:connectionId, Set<MockSignaling>> connectionIds
|
||||
this.connectionIds = new Map();
|
||||
this.delay = async () => await sleep(10);
|
||||
}
|
||||
|
||||
async add(signaling) {
|
||||
await this.delay();
|
||||
signaling.dispatchEvent(new Event("start"));
|
||||
}
|
||||
|
||||
async remove(signaling) {
|
||||
await this.delay();
|
||||
signaling.dispatchEvent(new Event("end"));
|
||||
}
|
||||
|
||||
async openConnection(signaling, connectionId) {
|
||||
await this.delay();
|
||||
const peerExists = this.connectionIds.has(connectionId);
|
||||
if (!peerExists) {
|
||||
this.connectionIds.set(connectionId, new Set());
|
||||
}
|
||||
|
||||
const list = this.connectionIds.get(connectionId);
|
||||
list.add(signaling);
|
||||
|
||||
const data = { connectionId: connectionId, polite: peerExists };
|
||||
signaling.dispatchEvent(new CustomEvent("connect", { detail: data }));
|
||||
}
|
||||
|
||||
async closeConnection(signaling, connectionId) {
|
||||
await this.delay();
|
||||
const peerExists = this.connectionIds.has(connectionId);
|
||||
const list = this.connectionIds.get(connectionId);
|
||||
if (!peerExists || !list.has(signaling)) {
|
||||
console.error(`${connectionId} This connection id is not used.`);
|
||||
}
|
||||
|
||||
const data = { connectionId: connectionId };
|
||||
for (const element of list) {
|
||||
element.dispatchEvent(new CustomEvent("disconnect", { detail: data }));
|
||||
}
|
||||
|
||||
list.delete(signaling);
|
||||
if (list.size == 0) {
|
||||
this.connectionIds.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
findList(owner, connectionId) {
|
||||
if (!this.connectionIds.has(connectionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const list = new Set(this.connectionIds.get(connectionId));
|
||||
list.delete(owner);
|
||||
if (list.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
async offer(owner, data) {
|
||||
await this.delay();
|
||||
const list = this.findList(owner, data.connectionId);
|
||||
if (list == null) {
|
||||
console.warn(`${data.connectionId} This connection id is not ready other session.`);
|
||||
return;
|
||||
}
|
||||
|
||||
data.polite = true;
|
||||
for (const signaling of list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("offer", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async answer(owner, data) {
|
||||
await this.delay();
|
||||
const list = this.findList(owner, data.connectionId);
|
||||
if (list == null) {
|
||||
console.warn(`${data.connectionId} This connection id is not ready other session.`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const signaling of list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("answer", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async candidate(owner, data) {
|
||||
await this.delay();
|
||||
const list = this.findList(owner, data.connectionId);
|
||||
if (list == null) {
|
||||
console.warn(`${data.connectionId} This connection id is not ready other session.`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const signaling of list) {
|
||||
if (signaling != owner) {
|
||||
signaling.dispatchEvent(new CustomEvent("candidate", { detail: data }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
250
WebApp/client/test/peerconnection.test.js
Normal file
250
WebApp/client/test/peerconnection.test.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import Peer from "../src/peer.js";
|
||||
import { waitFor, sleep, getUniqueId, getRTCConfiguration } from "./testutils.js";
|
||||
|
||||
|
||||
describe(`peer connection test`, () => {
|
||||
const connectionId = "12345";
|
||||
|
||||
test(`constructor`, () => {
|
||||
const peer = new Peer(connectionId, true);
|
||||
expect(peer).not.toBeNull();
|
||||
|
||||
const rtcPeer = peer.pc;
|
||||
expect(rtcPeer).not.toBeNull();
|
||||
expect(rtcPeer.ontrack).not.toBeNull();
|
||||
expect(rtcPeer.onicecandidate).not.toBeNull();
|
||||
expect(rtcPeer.onnegotiationneeded).not.toBeNull();
|
||||
expect(rtcPeer.onsignalingstatechange).not.toBeNull();
|
||||
expect(rtcPeer.oniceconnectionstatechange).not.toBeNull();
|
||||
expect(rtcPeer.onicegatheringstatechange).not.toBeNull();
|
||||
});
|
||||
|
||||
test(`close peer`, async () => {
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
|
||||
peer.close();
|
||||
expect(peer.connectionId).toBeNull();
|
||||
expect(peer.pc).toBeNull();
|
||||
});
|
||||
|
||||
test(`transceiver direction is sendrecv if using addtrack`, () => {
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
const sender = peer.addTrack(connectionId, track);
|
||||
const transceiver = peer.getTransceivers(connectionId).find(t => t.sender == sender);
|
||||
expect(transceiver.direction).toBe("sendrecv");
|
||||
});
|
||||
|
||||
test(`fire trackevent when addtrack`, async () => {
|
||||
let trackEvent;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('trackevent', (e) => trackEvent = e.detail);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => trackEvent != null);
|
||||
expect(trackEvent.track).toBe(track);
|
||||
});
|
||||
|
||||
test(`fire trackevent when on got offer description include track`, async () => {
|
||||
let trackEvent;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('trackevent', (e) => trackEvent = e.detail);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await waitFor(() => trackEvent != null);
|
||||
expect(trackEvent.track).not.toBeNull();
|
||||
});
|
||||
|
||||
test(`fire sendoffer when addtrack`, async () => {
|
||||
let offer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`fire sendoffer when addTransceiver`, async () => {
|
||||
let offer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
|
||||
peer.addTransceiver(connectionId, "video");
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`fire sendoffer when createDataChannel`, async () => {
|
||||
let offer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
|
||||
peer.createDataChannel(connectionId, "testChannel");
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`re-fire sendoffer if get answer not yet`, async () => {
|
||||
let sendOfferCount = 0;
|
||||
let offer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config, 100);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => {
|
||||
offer = e.detail;
|
||||
sendOfferCount++;
|
||||
});
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => sendOfferCount > 2);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
expect(sendOfferCount).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
test(`fire sendanswer when on got offer description in polite`, async () => {
|
||||
let answer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendanswer', (e) => answer = e.detail);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await waitFor(() => answer != null);
|
||||
expect(answer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`fire sendanswer when on got offer description in polite that have offer`, async () => {
|
||||
let offer;
|
||||
let answer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
peer.addEventListener('sendanswer', (e) => answer = e.detail);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await waitFor(() => answer != null);
|
||||
expect(answer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`fire sendanswer when on got offer description in impolite that don't have offer`, async () => {
|
||||
let answer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, false, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendanswer', (e) => answer = e.detail);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await waitFor(() => answer != null);
|
||||
expect(answer.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`don't fire sendanswer when on got offer description in impolite that have offer`, async () => {
|
||||
let offer;
|
||||
let answer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, false, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
peer.addEventListener('sendanswer', (e) => answer = e.detail);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await sleep(100);
|
||||
expect(answer).toBeUndefined();
|
||||
});
|
||||
|
||||
test(`fire nagotiated when on got answer description that have offer`, async () => {
|
||||
let offer;
|
||||
let negotiated = false;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendoffer', (e) => offer = e.detail);
|
||||
peer.pc.addEventListener('negotiated', () => negotiated = true);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
peer.addTrack(connectionId, track);
|
||||
await waitFor(() => offer != null);
|
||||
expect(offer.connectionId).toBe(connectionId);
|
||||
|
||||
const answerDesc = { type: "answer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, answerDesc);
|
||||
await waitFor(() => negotiated);
|
||||
expect(negotiated).toBeTruthy();
|
||||
});
|
||||
|
||||
test(`fire sendcandidate when on addTransceiver`, async () => {
|
||||
let candidate;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, true, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendcandidate', (e) => candidate = e.detail);
|
||||
|
||||
peer.addTransceiver(connectionId, { id: getUniqueId(), kind: "video" });
|
||||
await waitFor(() => candidate != null);
|
||||
expect(candidate.connectionId).toBe(connectionId);
|
||||
});
|
||||
|
||||
test(`accept candidate when on got candidate that have remote description`, async () => {
|
||||
let answer;
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, false, config);
|
||||
expect(peer).not.toBeNull();
|
||||
peer.addEventListener('sendanswer', (e) => answer = e.detail);
|
||||
|
||||
const testDesc = { type: "offer", sdp: "newtracksdp" };
|
||||
peer.onGotDescription(connectionId, testDesc);
|
||||
await waitFor(() => answer != null);
|
||||
expect(answer.connectionId).toBe(connectionId);
|
||||
|
||||
const testCandidate = { candidate: getUniqueId(), sdpMLineIndex: 0, sdpMid: 0 };
|
||||
peer.onGotCandidate(connectionId, testCandidate);
|
||||
await waitFor(() => peer.pc.candidates.length > 0);
|
||||
expect(peer.pc.candidates.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test(`don't accept candidate when on got candidate that don't have remote description`, async () => {
|
||||
const config = getRTCConfiguration();
|
||||
const peer = new Peer(connectionId, false, config);
|
||||
expect(peer).not.toBeNull();
|
||||
|
||||
const testCandidate = { candidate: getUniqueId(), sdpMLineIndex: 0, sdpMid: 0 };
|
||||
peer.onGotCandidate(connectionId, testCandidate);
|
||||
await sleep(100);
|
||||
expect(peer.pc.candidates.length).toBe(0);
|
||||
});
|
||||
});
|
||||
316
WebApp/client/test/peerconnectionmock.js
Normal file
316
WebApp/client/test/peerconnectionmock.js
Normal file
@@ -0,0 +1,316 @@
|
||||
import { sleep, getUniqueId } from './testutils';
|
||||
|
||||
export class PeerConnectionMock extends EventTarget {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.delay = async () => await sleep(10);
|
||||
this.config = config;
|
||||
this.ontrack = undefined;
|
||||
this.ondatachannel = undefined;
|
||||
this.onicecandidate = undefined;
|
||||
this.onnegotiationneeded = undefined;
|
||||
this.onsignalingstatechange = undefined;
|
||||
this.oniceconnectionstatechange = undefined;
|
||||
this.onicegatheringstatechange = undefined;
|
||||
this.pendingLocalDescription = null;
|
||||
this.currentLocalDescription = null;
|
||||
this.pendingRemoteDescription = null;
|
||||
this.currentRemoteDescription = null;
|
||||
this.candidates = [];
|
||||
this.signalingState = "stable";
|
||||
this.iceConnectionState = "new";
|
||||
this.iceGatheringState = "new";
|
||||
this.audioTracks = new Map();
|
||||
this.videoTracks = new Map();
|
||||
this.channels = new Map();
|
||||
this.transceiverCount = 0;
|
||||
this.transceivers = new Map();
|
||||
}
|
||||
|
||||
get localDescription() {
|
||||
if (this.pendingLocalDescription) {
|
||||
return this.pendingLocalDescription;
|
||||
}
|
||||
|
||||
return this.currentLocalDescription;
|
||||
}
|
||||
|
||||
get remoteDescription() {
|
||||
if (this.pendingRemoteDescription) {
|
||||
return this.pendingRemoteDescription;
|
||||
}
|
||||
|
||||
return this.currentRemoteDescription;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.ontrack = undefined;
|
||||
this.ondatachannel = undefined;
|
||||
this.onicecandidate = undefined;
|
||||
this.onnegotiationneeded = undefined;
|
||||
this.onsignalingstatechange = undefined;
|
||||
this.oniceconnectionstatechange = undefined;
|
||||
this.onicegatheringstatechange = undefined;
|
||||
this.pendingLocalDescription = null;
|
||||
this.currentLocalDescription = null;
|
||||
this.pendingRemoteDescription = null;
|
||||
this.currentRemoteDescription = null;
|
||||
this.candidates = [];
|
||||
this.signalingState = "close";
|
||||
this.iceConnectionState = "closed";
|
||||
this.audioTracks.clear();
|
||||
this.videoTracks.clear();
|
||||
this.channels.clear();
|
||||
this.transceiverCount = 0;
|
||||
this.transceivers.clear();
|
||||
}
|
||||
|
||||
fireOnNegotiationNeeded() {
|
||||
if (this.onnegotiationneeded) {
|
||||
this.onnegotiationneeded();
|
||||
}
|
||||
}
|
||||
|
||||
getTransceivers() {
|
||||
return Array.from(this.transceivers.values());
|
||||
}
|
||||
|
||||
addTrack(track) {
|
||||
if (track.kind == "audio") {
|
||||
this.audioTracks.set(track.id, track);
|
||||
} else {
|
||||
this.videoTracks.set(track.id, track);
|
||||
}
|
||||
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
|
||||
this.transceivers.set(this.transceiverCount++, transceiver);
|
||||
this.fireOnNegotiationNeeded();
|
||||
return transceiver.sender;
|
||||
}
|
||||
|
||||
addTransceiver(trackOrKind) {
|
||||
if (typeof trackOrKind == "string") {
|
||||
const track = { id: getUniqueId(), kind: trackOrKind };
|
||||
if (track.kind == "audio") {
|
||||
this.audioTracks.set(track.id, track);
|
||||
} else {
|
||||
this.videoTracks.set(track.id, track);
|
||||
}
|
||||
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
|
||||
this.transceivers.set(this.transceiverCount++, transceiver);
|
||||
this.fireOnNegotiationNeeded();
|
||||
return transceiver;
|
||||
}
|
||||
|
||||
if (trackOrKind.kind == "audio") {
|
||||
this.audioTracks.set(trackOrKind.id, trackOrKind);
|
||||
} else {
|
||||
this.videoTracks.set(trackOrKind.id, trackOrKind);
|
||||
}
|
||||
const transceiver = { direction: "sendrecv", sender: { track: trackOrKind }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
|
||||
this.transceivers.set(this.transceiverCount++, transceiver);
|
||||
this.fireOnNegotiationNeeded();
|
||||
return transceiver;
|
||||
}
|
||||
|
||||
createDataChannel(label) {
|
||||
const channel = { id: getUniqueId(), label: label };
|
||||
this.channels.set(channel.id, channel);
|
||||
this.fireOnNegotiationNeeded();
|
||||
return channel;
|
||||
}
|
||||
|
||||
async setLocalDescription(description = null) {
|
||||
if (description == null) {
|
||||
description = this._createSessionDescription();
|
||||
}
|
||||
await this.delay();
|
||||
this._setSessionDescription(description, false);
|
||||
}
|
||||
|
||||
async setRemoteDescription(description) {
|
||||
await this.delay();
|
||||
if (description.type == "offer" && this.signalingState == "have-local-offer") {
|
||||
this._setSessionDescription({ type: "rollback", sdp: "" }, true);
|
||||
}
|
||||
this._setSessionDescription(description, true);
|
||||
}
|
||||
|
||||
_createSessionDescription() {
|
||||
let dummySdp = "testsdp";
|
||||
if (this.videoTracks.size > 0) {
|
||||
dummySdp += "videotrack";
|
||||
}
|
||||
if (this.audioTracks.size > 0) {
|
||||
dummySdp += "audiotrack";
|
||||
}
|
||||
if (this.channels.size > 0) {
|
||||
dummySdp += "datachannel";
|
||||
}
|
||||
|
||||
if (this.signalingState == "stable" || this.signalingState == "have-local-offer" || this.signalingState == "have-remote-pranswer") {
|
||||
return { type: "offer", sdp: dummySdp };
|
||||
}
|
||||
return { type: "answer", sdp: dummySdp };
|
||||
}
|
||||
|
||||
_setSessionDescription(description, remote) {
|
||||
if (description.type == "rollback"
|
||||
&& (this.signalingState == "stable" || this.signalingState == "have-local-pranswer" || this.signalingState == "have-remote-pranswer")) {
|
||||
throw "InvalidStateError";
|
||||
}
|
||||
|
||||
if (description.type != "rollback") {
|
||||
if (remote) {
|
||||
if (description.type == "offer") {
|
||||
this.pendingRemoteDescription = description;
|
||||
this.signalingState = "have-remote-offer";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
// if sdp contains track string, create dummy track
|
||||
if (description.sdp.includes("track")) {
|
||||
const isVideo = description.sdp.includes("video");
|
||||
const kind = isVideo ? "video" : "audio";
|
||||
this._createTrackAndTransceiver(kind);
|
||||
}
|
||||
if (description.sdp.includes("datachannel")) {
|
||||
const channel = { id: getUniqueId(), label: "dummychannel" };
|
||||
this.channels.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
if (description.type == "answer") {
|
||||
this.currentRemoteDescription = description;
|
||||
this.currentLocalDescription = this.pendingLocalDescription;
|
||||
this.pendingLocalDescription = null;
|
||||
this.pendingRemoteDescription = null;
|
||||
this.signalingState = "stable";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
}
|
||||
if (description.type == "pranswer") {
|
||||
this.pendingRemoteDescription = description;
|
||||
this.signalingState = "have-remote-pranswer";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
}
|
||||
} else {
|
||||
if (description.type == "offer") {
|
||||
this.pendingLocalDescription = description;
|
||||
this.signalingState = "have-local-offer";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
}
|
||||
if (description.type == "answer") {
|
||||
this.currentLocalDescription = description;
|
||||
this.currentRemoteDescription = this.pendingRemoteDescription;
|
||||
this.pendingLocalDescription = null;
|
||||
this.pendingRemoteDescription = null;
|
||||
this.signalingState = "stable";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
// if sdp contains track string, create dummy track
|
||||
if (description.sdp.includes("track")) {
|
||||
const isVideo = description.sdp.includes("video");
|
||||
const kind = isVideo ? "video" : "audio";
|
||||
this._createTrackAndTransceiver(kind);
|
||||
}
|
||||
if (description.sdp.includes("datachannel")) {
|
||||
const channel = { id: getUniqueId(), label: "dummychannel" };
|
||||
this.channels.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
if (description.type == "pranswer") {
|
||||
this.pendingLocalDescription = description;
|
||||
this.signalingState = "have-local-pranswer";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.pendingLocalDescription = null;
|
||||
this.pendingRemoteDescription = null;
|
||||
this.signalingState = "stable";
|
||||
this.onsignalingstatechange(this.signalingState);
|
||||
}
|
||||
|
||||
if (this.videoTracks.size != 0 || this.audioTracks.size != 0) {
|
||||
this._mockGatheringIceCandidate(this.videoTracks.size + this.audioTracks.size);
|
||||
}
|
||||
|
||||
//fire ontrack with new tracks, after using tracks clear.
|
||||
if (this.ontrack) {
|
||||
for (const track of this.videoTracks.values()) {
|
||||
this.ontrack({ track: track });
|
||||
}
|
||||
this.videoTracks.clear();
|
||||
|
||||
for (const track of this.audioTracks.values()) {
|
||||
this.ontrack({ track: track });
|
||||
}
|
||||
this.audioTracks.clear();
|
||||
}
|
||||
|
||||
if (this.ondatachannel) {
|
||||
for (const channel of this.channels.values()) {
|
||||
this.ondatachannel({ channel: channel });
|
||||
}
|
||||
this.channels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async _mockGatheringIceCandidate(count) {
|
||||
this.iceGatheringState = "gathering";
|
||||
if (this.onicegatheringstatechange) {
|
||||
this.onicegatheringstatechange(this.iceGatheringState);
|
||||
}
|
||||
for (let index = 0; index < count; index++) {
|
||||
await this.delay();
|
||||
const newCandidate = { candidate: getUniqueId(), sdpMLineIndex: index, sdpMid: index };
|
||||
if (this.onicecandidate) {
|
||||
this.onicecandidate(newCandidate);
|
||||
}
|
||||
}
|
||||
this.iceGatheringState = "complete";
|
||||
if (this.onicegatheringstatechange) {
|
||||
this.onicegatheringstatechange(this.iceGatheringState);
|
||||
}
|
||||
if (this.onicecandidate) {
|
||||
this.onicecandidate({ candidate: null, sdpMLineIndex: null, sdpMid: null });
|
||||
}
|
||||
}
|
||||
|
||||
async addIceCandidate(candidate) {
|
||||
await this.delay();
|
||||
if (this.remoteDescription == null) {
|
||||
throw "InvalidStateError";
|
||||
}
|
||||
this.candidates.push(candidate);
|
||||
}
|
||||
|
||||
_createTrackAndTransceiver(kind) {
|
||||
const track = { id: getUniqueId(), kind: kind };
|
||||
if (kind == "video") {
|
||||
this.videoTracks.set(track.id, track);
|
||||
} else {
|
||||
this.audioTracks.set(track.id, track);
|
||||
}
|
||||
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
|
||||
this.transceivers.set(this.transceiverCount++, transceiver);
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionDescriptionMock {
|
||||
|
||||
constructor(object) {
|
||||
this.sdp = object.sdp;
|
||||
this.type = object.type;
|
||||
}
|
||||
|
||||
sdp;
|
||||
type;
|
||||
}
|
||||
|
||||
export class IceCandidateMock {
|
||||
constructor(object) {
|
||||
this.candidate = object.candidate;
|
||||
this.sdpMLineIndex = object.sdpMLineIndex;
|
||||
this.sdpMid = object.sdpMid;
|
||||
}
|
||||
|
||||
candidate;
|
||||
sdpMLineIndex;
|
||||
sdpMid;
|
||||
}
|
||||
45
WebApp/client/test/pointercorrect.test.js
Normal file
45
WebApp/client/test/pointercorrect.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
LetterBoxType,
|
||||
PointerCorrector
|
||||
} from "../src/pointercorrect.js";
|
||||
|
||||
import {DOMRect} from "./domrect.js";
|
||||
import {DOMHTMLVideoElement} from "./domvideoelement.js";
|
||||
|
||||
describe(`PointerCorrector.map`, () => {
|
||||
test('letterboxType', () => {
|
||||
const rect = new DOMRect(10, 10, 200, 200);
|
||||
const element = new DOMHTMLVideoElement(rect);
|
||||
let corrector = new PointerCorrector(50, 100, element);
|
||||
expect(corrector.letterBoxType).toBe(LetterBoxType.Vertical);
|
||||
corrector.reset(100, 50, element);
|
||||
expect(corrector.letterBoxType).toBe(LetterBoxType.Horizontal);
|
||||
});
|
||||
test('letterboxSize', () => {
|
||||
const rect = new DOMRect(0, 0, 100, 100);
|
||||
const element = new DOMHTMLVideoElement(rect);
|
||||
let corrector = new PointerCorrector(50, 100, element);
|
||||
expect(corrector.letterBoxSize).toBe(25);
|
||||
});
|
||||
test('contentRect', () => {
|
||||
const rect = new DOMRect(0, 0, 100, 100);
|
||||
const element = new DOMHTMLVideoElement(rect);
|
||||
let corrector = new PointerCorrector(50, 100, element);
|
||||
expect(corrector.contentRect.x).toBe(25);
|
||||
expect(corrector.contentRect.y).toBe(0);
|
||||
expect(corrector.contentRect.width).toBe(50);
|
||||
expect(corrector.contentRect.height).toBe(100);
|
||||
});
|
||||
test('mapping', () => {
|
||||
const rect = new DOMRect(10, 10, 200, 200);
|
||||
const element = new DOMHTMLVideoElement(rect);
|
||||
const videoWidth = 100;
|
||||
const videoHeight = 100;
|
||||
let corrector = new PointerCorrector(videoWidth, videoHeight, element);
|
||||
const position = [10, 10];
|
||||
const newPosition = corrector.map(position);
|
||||
expect(newPosition[0]).toBe(0);
|
||||
expect(newPosition[1]).toBe(100);
|
||||
});
|
||||
|
||||
});
|
||||
187
WebApp/client/test/renderstreaming.test.js
Normal file
187
WebApp/client/test/renderstreaming.test.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import { MockSignaling, reset } from "./mocksignaling.js";
|
||||
import { waitFor, getUniqueId, getRTCConfiguration } from "./testutils.js";
|
||||
import { RenderStreaming } from "../src/renderstreaming.js";
|
||||
|
||||
describe.each([
|
||||
{ mode: "private" },
|
||||
{ mode: "public" }
|
||||
])('renderstreaming test', ({ mode }) => {
|
||||
const connectionId1 = "12345";
|
||||
|
||||
test(`createConnection in ${mode} mode`, async () => {
|
||||
reset(mode == "private");
|
||||
const config = getRTCConfiguration();
|
||||
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
|
||||
await renderstreaming.start();
|
||||
|
||||
let isConnect = false;
|
||||
renderstreaming.onConnect = () => isConnect = true;
|
||||
await renderstreaming.createConnection(connectionId1);
|
||||
await waitFor(() => isConnect);
|
||||
expect(isConnect).toBe(true);
|
||||
|
||||
await renderstreaming.stop();
|
||||
});
|
||||
|
||||
test(`addTrack in ${mode} mode`, async () => {
|
||||
reset(mode == "private");
|
||||
const config = getRTCConfiguration();
|
||||
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
|
||||
await renderstreaming.start();
|
||||
|
||||
let isConnect = false;
|
||||
renderstreaming.onConnect = () => isConnect = true;
|
||||
await renderstreaming.createConnection(connectionId1);
|
||||
await waitFor(() => isConnect);
|
||||
expect(isConnect).toBe(true);
|
||||
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(0);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
renderstreaming.addTrack(track);
|
||||
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(1);
|
||||
|
||||
let isDisconnect = false;
|
||||
renderstreaming.onDisconnect = () => isDisconnect = true;
|
||||
await renderstreaming.deleteConnection();
|
||||
await waitFor(() => isDisconnect);
|
||||
expect(isDisconnect).toBe(true);
|
||||
|
||||
await renderstreaming.stop();
|
||||
});
|
||||
|
||||
test(`createChannel in ${mode} mode`, async () => {
|
||||
reset(mode == "private");
|
||||
const config = getRTCConfiguration();
|
||||
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
|
||||
await renderstreaming.start();
|
||||
|
||||
let isConnect = false;
|
||||
renderstreaming.onConnect = () => isConnect = true;
|
||||
await renderstreaming.createConnection(connectionId1);
|
||||
await waitFor(() => isConnect);
|
||||
expect(isConnect).toBe(true);
|
||||
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(0);
|
||||
|
||||
const label = "testlabel";
|
||||
const channel = renderstreaming.createDataChannel(label);
|
||||
expect(channel.label).toBe(label);
|
||||
|
||||
let isDisconnect = false;
|
||||
renderstreaming.onDisconnect = () => isDisconnect = true;
|
||||
await renderstreaming.deleteConnection();
|
||||
await waitFor(() => isDisconnect);
|
||||
expect(isDisconnect).toBe(true);
|
||||
|
||||
await renderstreaming.stop();
|
||||
});
|
||||
|
||||
test(`onTrackEvent in ${mode} mode`, async () => {
|
||||
reset(mode == "private");
|
||||
|
||||
const config = getRTCConfiguration();
|
||||
const renderstreaming1 = new RenderStreaming(new MockSignaling(), config);
|
||||
const renderstreaming2 = new RenderStreaming(new MockSignaling(), config);
|
||||
await renderstreaming1.start();
|
||||
await renderstreaming2.start();
|
||||
|
||||
let isConnect1 = false;
|
||||
renderstreaming1.onConnect = () => isConnect1 = true;
|
||||
let isConnect2 = false;
|
||||
renderstreaming2.onConnect = () => isConnect2 = true;
|
||||
|
||||
await renderstreaming1.createConnection(connectionId1);
|
||||
await renderstreaming2.createConnection(connectionId1);
|
||||
await waitFor(() => isConnect1 && isConnect2);
|
||||
expect(isConnect1).toBe(true);
|
||||
expect(isConnect2).toBe(true);
|
||||
|
||||
let isGotOffer1 = false;
|
||||
let isOnTrack1 = false;
|
||||
let isGotAnswer2 = false;
|
||||
renderstreaming1.onGotOffer = () => { isGotOffer1 = true; };
|
||||
renderstreaming1.onTrackEvent = () => { isOnTrack1 = true; };
|
||||
renderstreaming2.onGotAnswer = () => { isGotAnswer2 = true; };
|
||||
|
||||
expect(renderstreaming1.getTransceivers(connectionId1).length).toBe(0);
|
||||
|
||||
const track = { id: getUniqueId(), kind: "audio" };
|
||||
renderstreaming2.addTrack(track);
|
||||
expect(renderstreaming2.getTransceivers(connectionId1).length).toBe(1);
|
||||
await waitFor(() => isGotOffer1);
|
||||
expect(isGotOffer1).toBe(true);
|
||||
|
||||
await waitFor(() => isOnTrack1);
|
||||
expect(isOnTrack1).toBe(true);
|
||||
expect(renderstreaming1.getTransceivers(connectionId1).length).toBe(1);
|
||||
|
||||
await waitFor(() => isGotAnswer2);
|
||||
expect(isGotAnswer2).toBe(true);
|
||||
|
||||
let isDisconnect1 = false;
|
||||
renderstreaming1.onDisconnect = () => isDisconnect1 = true;
|
||||
let isDisconnect2 = false;
|
||||
renderstreaming2.onDisconnect = () => isDisconnect2 = true;
|
||||
|
||||
await renderstreaming1.deleteConnection();
|
||||
await renderstreaming2.deleteConnection();
|
||||
await waitFor(() => isDisconnect1 && isDisconnect2);
|
||||
expect(isDisconnect1).toBe(true);
|
||||
expect(isDisconnect2).toBe(true);
|
||||
|
||||
await renderstreaming1.stop();
|
||||
await renderstreaming2.stop();
|
||||
});
|
||||
|
||||
test(`onAddDataChannel in ${mode} mode`, async () => {
|
||||
reset(mode == "private");
|
||||
|
||||
const config = getRTCConfiguration();
|
||||
const renderstreaming1 = new RenderStreaming(new MockSignaling(), config);
|
||||
const renderstreaming2 = new RenderStreaming(new MockSignaling(), config);
|
||||
await renderstreaming1.start();
|
||||
await renderstreaming2.start();
|
||||
|
||||
let isConnect1 = false;
|
||||
renderstreaming1.onConnect = () => isConnect1 = true;
|
||||
let isConnect2 = false;
|
||||
renderstreaming2.onConnect = () => isConnect2 = true;
|
||||
|
||||
await renderstreaming1.createConnection(connectionId1);
|
||||
await renderstreaming2.createConnection(connectionId1);
|
||||
await waitFor(() => isConnect1 && isConnect2);
|
||||
expect(isConnect1).toBe(true);
|
||||
expect(isConnect2).toBe(true);
|
||||
|
||||
let isGotOffer1 = false;
|
||||
let isAddChannel1 = false;
|
||||
let isGotAnswer2 = false;
|
||||
renderstreaming1.onGotOffer = () => { isGotOffer1 = true; };
|
||||
renderstreaming1.onAddChannel = () => { isAddChannel1 = true; };
|
||||
renderstreaming2.onGotAnswer = () => { isGotAnswer2 = true; };
|
||||
|
||||
renderstreaming2.createDataChannel("testchannel");
|
||||
await waitFor(() => isGotOffer1);
|
||||
expect(isGotOffer1).toBe(true);
|
||||
|
||||
await waitFor(() => isAddChannel1);
|
||||
expect(isAddChannel1).toBe(true);
|
||||
|
||||
await waitFor(() => isGotAnswer2);
|
||||
expect(isGotAnswer2).toBe(true);
|
||||
|
||||
let isDisconnect1 = false;
|
||||
renderstreaming1.onDisconnect = () => isDisconnect1 = true;
|
||||
let isDisconnect2 = false;
|
||||
renderstreaming2.onDisconnect = () => isDisconnect2 = true;
|
||||
|
||||
await renderstreaming1.deleteConnection();
|
||||
await renderstreaming2.deleteConnection();
|
||||
await waitFor(() => isDisconnect1 && isDisconnect2);
|
||||
expect(isDisconnect1).toBe(true);
|
||||
expect(isDisconnect2).toBe(true);
|
||||
|
||||
await renderstreaming1.stop();
|
||||
await renderstreaming2.stop();
|
||||
});
|
||||
|
||||
});
|
||||
18
WebApp/client/test/resizeobservermock.js
Normal file
18
WebApp/client/test/resizeobservermock.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// mock class
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
let instanceResize = null;
|
||||
/* eslint-disable no-unused-vars */
|
||||
let callbackResize = null;
|
||||
|
||||
export default class ResizeObserverMock {
|
||||
constructor(callback) {
|
||||
instanceResize = this;
|
||||
callbackResize = callback;
|
||||
}
|
||||
disconnect() { }
|
||||
/* eslint-disable no-unused-vars */
|
||||
observe(target, options) { }
|
||||
/* eslint-disable no-unused-vars */
|
||||
unobserve(target) { }
|
||||
}
|
||||
143
WebApp/client/test/sender.test.js
Normal file
143
WebApp/client/test/sender.test.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
InputRemoting,
|
||||
} from "../src/inputremoting.js";
|
||||
|
||||
import {
|
||||
Sender,
|
||||
Observer
|
||||
} from "../src/sender.js";
|
||||
|
||||
import {jest} from '@jest/globals';
|
||||
import {DOMRect} from "./domrect.js";
|
||||
|
||||
// mock
|
||||
|
||||
class RTCDataChannel {
|
||||
get readyState() {
|
||||
return "open";
|
||||
}
|
||||
/* eslint-disable no-unused-vars */
|
||||
send(message) {
|
||||
}
|
||||
}
|
||||
|
||||
describe(`Sender`, () => {
|
||||
let inputRemoting = null;
|
||||
let sender = null;
|
||||
let observer = null;
|
||||
let events = {};
|
||||
let dc = null;
|
||||
beforeEach(async () => {
|
||||
// Empty our events before each test case
|
||||
events = {};
|
||||
|
||||
// Define the addEventListener method with a Jest mock function
|
||||
document.addEventListener = jest.fn((event, callback) => {
|
||||
events[event] = callback;
|
||||
});
|
||||
|
||||
document.removeEventListener = jest.fn((event, callback) => {
|
||||
delete events[event];
|
||||
});
|
||||
document.getBoundingClientRect = function(){ return new DOMRect(0,0,0,0); };
|
||||
sender = new Sender(document);
|
||||
inputRemoting = new InputRemoting(sender);
|
||||
dc = new RTCDataChannel();
|
||||
observer = new Observer(dc);
|
||||
});
|
||||
test('devices', () => {
|
||||
sender.addMouse();
|
||||
expect(sender.devices.length).toBe(1);
|
||||
sender.addKeyboard();
|
||||
expect(sender.devices.length).toBe(2);
|
||||
});
|
||||
test('send messages while called startSending', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addMouse();
|
||||
sender.addKeyboard();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
expect(dc.send).toHaveBeenCalled();
|
||||
});
|
||||
describe('mouse', () => {
|
||||
test('click', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addMouse();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.click(
|
||||
new MouseEvent('click', { buttons:1, clientX:0, clientY:0} ));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
test('mousemove', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addMouse();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.mousemove(
|
||||
new MouseEvent('mousemove', { buttons:1, deltaX:0, deltaY:0 }));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
test('wheel', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addMouse();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.wheel(
|
||||
new WheelEvent('wheel', { wheelDelta:0, deltaX:0, deltaY:0 }));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
});
|
||||
describe('keyboard', () => {
|
||||
test('keydown', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addKeyboard();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.keydown(
|
||||
new KeyboardEvent('keydown', { code: 'KeyA' }));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
test('keydown repeat', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addKeyboard();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.keydown(
|
||||
new KeyboardEvent('keydown', { code: 'KeyA', repeat: true }));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
});
|
||||
describe('touchscreen', () => {
|
||||
test('touchstart', () => {
|
||||
jest.spyOn(dc, 'send');
|
||||
sender.addTouchscreen();
|
||||
inputRemoting.subscribe(observer);
|
||||
inputRemoting.startSending();
|
||||
events.touchstart(
|
||||
new TouchEvent("touchstart", {
|
||||
changedTouches: [{ // InputInit
|
||||
identifier: 0,
|
||||
target: null,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
rotationAngle: 0,
|
||||
force: 0,
|
||||
altitudeAngle: 0,
|
||||
azimuthAngle:0,
|
||||
touchType: "direct"
|
||||
}]
|
||||
}));
|
||||
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
|
||||
});
|
||||
});
|
||||
describe('gamepad', () => {
|
||||
//todo
|
||||
});
|
||||
});
|
||||
484
WebApp/client/test/signaling.test.js
Normal file
484
WebApp/client/test/signaling.test.js
Normal file
@@ -0,0 +1,484 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import * as Path from 'path';
|
||||
import { setup, teardown } from 'jest-dev-server';
|
||||
import { Signaling, WebSocketSignaling } from "../src/signaling.js";
|
||||
import { MockSignaling, reset } from "./mocksignaling.js";
|
||||
import { waitFor, sleep, serverExeName } from "./testutils.js";
|
||||
|
||||
const portNumber = 8081;
|
||||
jest.setTimeout(10000);
|
||||
|
||||
describe.each([
|
||||
{ mode: "mock" },
|
||||
{ mode: "http" },
|
||||
{ mode: "websocket" },
|
||||
])('signaling test in public mode', ({ mode }) => {
|
||||
let signaling1;
|
||||
let signaling2;
|
||||
const connectionId1 = "12345";
|
||||
const connectionId2 = "67890";
|
||||
const testsdp = "test sdp";
|
||||
const testcandidate = "test candidate";
|
||||
|
||||
beforeAll(async () => {
|
||||
if (mode == "mock") {
|
||||
reset(false);
|
||||
signaling1 = new MockSignaling(1);
|
||||
signaling2 = new MockSignaling(1);
|
||||
} else {
|
||||
const path = Path.resolve(`../bin~/${serverExeName()}`);
|
||||
let cmd = `${path} -p ${portNumber}`;
|
||||
if (mode == "http") {
|
||||
cmd += " -t http";
|
||||
}
|
||||
|
||||
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
|
||||
|
||||
if (mode == "http") {
|
||||
signaling1 = new Signaling(1);
|
||||
signaling2 = new Signaling(1);
|
||||
}
|
||||
|
||||
if (mode == "websocket") {
|
||||
signaling1 = new WebSocketSignaling(1);
|
||||
signaling2 = new WebSocketSignaling(1);
|
||||
}
|
||||
}
|
||||
|
||||
await signaling1.start();
|
||||
await signaling2.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await signaling1.stop();
|
||||
await signaling2.stop();
|
||||
signaling1 = null;
|
||||
signaling2 = null;
|
||||
|
||||
if (mode == "mock") {
|
||||
return;
|
||||
}
|
||||
|
||||
await teardown();
|
||||
// work around for linux, waitng kill server process
|
||||
await sleep(1000);
|
||||
});
|
||||
|
||||
test(`onConnect using ${mode}`, async () => {
|
||||
const signaling1Spy = jest.spyOn(signaling1, 'dispatchEvent');
|
||||
let connectRes;
|
||||
let disconnectRes;
|
||||
signaling1.addEventListener('connect', (e) => connectRes = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId1);
|
||||
await waitFor(() => connectRes != null);
|
||||
expect(connectRes.connectionId).toBe(connectionId1);
|
||||
expect(connectRes.polite).toBe(true);
|
||||
|
||||
await signaling1.deleteConnection(connectionId1);
|
||||
await waitFor(() => disconnectRes != null);
|
||||
expect(disconnectRes.connectionId).toBe(connectionId1);
|
||||
|
||||
const disconnectCalledCount = signaling1Spy.mock.calls.map(x => x[0].type).filter(x => x == "disconnect").length;
|
||||
expect(disconnectCalledCount).toBe(1);
|
||||
|
||||
signaling1Spy.mockRestore();
|
||||
});
|
||||
|
||||
test(`onOffer using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId1);
|
||||
await signaling2.createConnection(connectionId2);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
expect(connectRes1.connectionId).toBe(connectionId1);
|
||||
expect(connectRes2.connectionId).toBe(connectionId2);
|
||||
|
||||
await signaling1.sendOffer(connectionId1, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId1);
|
||||
expect(offerRes2.polite).toBe(false);
|
||||
|
||||
await signaling1.deleteConnection(connectionId1);
|
||||
await waitFor(() => disconnectRes1 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId1);
|
||||
await signaling2.deleteConnection(connectionId2);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId2);
|
||||
});
|
||||
|
||||
test(`onAnswer using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
let answerRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId1);
|
||||
await signaling2.createConnection(connectionId2);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
|
||||
await signaling1.sendOffer(connectionId1, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId1);
|
||||
expect(offerRes2.sdp).toBe(testsdp);
|
||||
|
||||
signaling2.sendAnswer(connectionId1, testsdp);
|
||||
await waitFor(() => answerRes1 != null);
|
||||
expect(answerRes1.connectionId).toBe(connectionId1);
|
||||
expect(answerRes1.sdp).toBe(testsdp);
|
||||
|
||||
await signaling1.deleteConnection(connectionId1);
|
||||
await waitFor(() => disconnectRes1 != null);
|
||||
await signaling2.deleteConnection(connectionId2);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
|
||||
test(`onCandidate using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
let answerRes1;
|
||||
let candidateRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
|
||||
signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
let candidateRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId1);
|
||||
await signaling2.createConnection(connectionId2);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
|
||||
await signaling1.sendOffer(connectionId1, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId1);
|
||||
expect(offerRes2.sdp).toBe(testsdp);
|
||||
|
||||
signaling2.sendAnswer(connectionId1, testsdp);
|
||||
await waitFor(() => answerRes1 != null);
|
||||
expect(answerRes1.connectionId).toBe(connectionId1);
|
||||
expect(answerRes1.sdp).toBe(testsdp);
|
||||
|
||||
await signaling2.sendCandidate(connectionId1, testcandidate, 1, 1);
|
||||
await waitFor(() => candidateRes1 != null);
|
||||
expect(candidateRes1.connectionId).toBe(connectionId1);
|
||||
expect(candidateRes1.candidate).toBe(testcandidate);
|
||||
expect(candidateRes1.sdpMid).toBe(1);
|
||||
expect(candidateRes1.sdpMLineIndex).toBe(1);
|
||||
|
||||
await signaling1.sendCandidate(connectionId1, testcandidate, 1, 1);
|
||||
await waitFor(() => candidateRes2 != null);
|
||||
expect(candidateRes2.connectionId).toBe(connectionId1);
|
||||
expect(candidateRes2.candidate).toBe(testcandidate);
|
||||
expect(candidateRes2.sdpMid).toBe(1);
|
||||
expect(candidateRes2.sdpMLineIndex).toBe(1);
|
||||
|
||||
await signaling1.deleteConnection(connectionId1);
|
||||
await waitFor(() => disconnectRes1 != null);
|
||||
await signaling2.deleteConnection(connectionId2);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ mode: "mock" },
|
||||
{ mode: "http" },
|
||||
{ mode: "websocket" },
|
||||
])('signaling test in private mode', ({ mode }) => {
|
||||
let signaling1;
|
||||
let signaling2;
|
||||
const connectionId = "12345";
|
||||
const testsdp = "test sdp";
|
||||
const testcandidate = "test candidate";
|
||||
|
||||
beforeAll(async () => {
|
||||
if (mode == "mock") {
|
||||
reset(true);
|
||||
signaling1 = new MockSignaling(1);
|
||||
signaling2 = new MockSignaling(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = Path.resolve(`../bin~/${serverExeName()}`);
|
||||
let cmd = `${path} -p ${portNumber} -m private`;
|
||||
if (mode == "http") {
|
||||
cmd += " -t http";
|
||||
}
|
||||
|
||||
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
|
||||
|
||||
if (mode == "http") {
|
||||
signaling1 = new Signaling(1);
|
||||
signaling2 = new Signaling(1);
|
||||
}
|
||||
|
||||
if (mode == "websocket") {
|
||||
signaling1 = new WebSocketSignaling(1);
|
||||
signaling2 = new WebSocketSignaling(1);
|
||||
}
|
||||
|
||||
await signaling1.start();
|
||||
await signaling2.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await signaling1.stop();
|
||||
await signaling2.stop();
|
||||
signaling1 = null;
|
||||
signaling2 = null;
|
||||
|
||||
if (mode == "mock") {
|
||||
return;
|
||||
}
|
||||
|
||||
await teardown();
|
||||
// work around for linux, waitng kill server process
|
||||
await sleep(1000);
|
||||
});
|
||||
|
||||
test(`onConnect using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId);
|
||||
await waitFor(() => connectRes1 != null);
|
||||
expect(connectRes1.connectionId).toBe(connectionId);
|
||||
expect(connectRes1.polite).toBe(false);
|
||||
|
||||
await signaling2.createConnection(connectionId);
|
||||
await waitFor(() => connectRes2 != null);
|
||||
expect(connectRes2.connectionId).toBe(connectionId);
|
||||
expect(connectRes2.polite).toBe(true);
|
||||
|
||||
await sleep(signaling1.interval * 2);
|
||||
|
||||
await signaling1.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
disconnectRes2 = null;
|
||||
await signaling2.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
|
||||
test(`onOffer using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId);
|
||||
await waitFor(() => connectRes1 != null);
|
||||
expect(connectRes1.connectionId).toBe(connectionId);
|
||||
|
||||
signaling1.sendOffer(connectionId, testsdp);
|
||||
await sleep(signaling1.interval * 2);
|
||||
// Do not receive offer other signaling if not connected same sendoffer connectionId in private mode
|
||||
expect(offerRes2).toBeUndefined();
|
||||
|
||||
await signaling2.createConnection(connectionId);
|
||||
await waitFor(() => connectRes2 != null);
|
||||
expect(connectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
await signaling1.sendOffer(connectionId, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId);
|
||||
expect(offerRes2.polite).toBe(true);
|
||||
|
||||
await signaling1.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
disconnectRes2 = null;
|
||||
await signaling2.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
|
||||
test(`onAnswer using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
let answerRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId);
|
||||
await signaling2.createConnection(connectionId);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
|
||||
await signaling1.sendOffer(connectionId, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId);
|
||||
expect(offerRes2.sdp).toBe(testsdp);
|
||||
|
||||
await signaling2.sendAnswer(connectionId, testsdp);
|
||||
await waitFor(() => answerRes1 != null);
|
||||
expect(answerRes1.connectionId).toBe(connectionId);
|
||||
expect(answerRes1.sdp).toBe(testsdp);
|
||||
|
||||
await signaling1.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
disconnectRes2 = null;
|
||||
await signaling2.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
|
||||
test(`onCandidate using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
let answerRes1;
|
||||
let candidateRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
|
||||
signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
let candidateRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId);
|
||||
await signaling2.createConnection(connectionId);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
|
||||
await signaling1.sendOffer(connectionId, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
expect(offerRes2.connectionId).toBe(connectionId);
|
||||
expect(offerRes2.sdp).toBe(testsdp);
|
||||
|
||||
await signaling2.sendAnswer(connectionId, testsdp);
|
||||
await waitFor(() => answerRes1 != null);
|
||||
expect(answerRes1.connectionId).toBe(connectionId);
|
||||
expect(answerRes1.sdp).toBe(testsdp);
|
||||
|
||||
await signaling2.sendCandidate(connectionId, testcandidate, 1, 1);
|
||||
await waitFor(() => candidateRes1 != null);
|
||||
expect(candidateRes1.connectionId).toBe(connectionId);
|
||||
expect(candidateRes1.candidate).toBe(testcandidate);
|
||||
expect(candidateRes1.sdpMLineIndex).toBe(1);
|
||||
expect(candidateRes1.sdpMid).toBe(1);
|
||||
|
||||
await signaling1.sendCandidate(connectionId, testcandidate, 1, 1);
|
||||
await waitFor(() => candidateRes2 != null);
|
||||
expect(candidateRes2.connectionId).toBe(connectionId);
|
||||
expect(candidateRes2.candidate).toBe(testcandidate);
|
||||
expect(candidateRes2.sdpMLineIndex).toBe(1);
|
||||
expect(candidateRes2.sdpMid).toBe(1);
|
||||
|
||||
await signaling1.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
disconnectRes2 = null;
|
||||
await signaling2.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
|
||||
test(`notReceiveOwnOfferAnswer using ${mode}`, async () => {
|
||||
let connectRes1;
|
||||
let disconnectRes1;
|
||||
let offerRes1;
|
||||
let answerRes1;
|
||||
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
|
||||
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
|
||||
|
||||
let connectRes2;
|
||||
let disconnectRes2;
|
||||
let offerRes2;
|
||||
let answerRes2;
|
||||
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
|
||||
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
|
||||
|
||||
await signaling1.createConnection(connectionId);
|
||||
await signaling2.createConnection(connectionId);
|
||||
await waitFor(() => connectRes1 != null && connectRes2 != null);
|
||||
|
||||
signaling1.addEventListener('offer', (e) => offerRes1 = e.detail);
|
||||
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
|
||||
await signaling1.sendOffer(connectionId, testsdp);
|
||||
await waitFor(() => offerRes2 != null);
|
||||
await sleep(signaling1.interval * 2);
|
||||
expect(offerRes1).toBeUndefined();
|
||||
expect(offerRes2).not.toBeUndefined();
|
||||
expect(offerRes2.connectionId).toBe(connectionId);
|
||||
expect(offerRes2.sdp).toBe(testsdp);
|
||||
|
||||
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
|
||||
signaling2.addEventListener('answer', (e) => answerRes2 = e.detail);
|
||||
await signaling2.sendAnswer(connectionId, testsdp);
|
||||
await waitFor(() => answerRes1 != null);
|
||||
await sleep(signaling2.interval * 2);
|
||||
expect(answerRes1).not.toBeUndefined();
|
||||
expect(answerRes1.connectionId).toBe(connectionId);
|
||||
expect(answerRes1.sdp).toBe(testsdp);
|
||||
expect(answerRes2).toBeUndefined();
|
||||
|
||||
await signaling1.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
|
||||
expect(disconnectRes1.connectionId).toBe(connectionId);
|
||||
expect(disconnectRes2.connectionId).toBe(connectionId);
|
||||
|
||||
disconnectRes2 = null;
|
||||
await signaling2.deleteConnection(connectionId);
|
||||
await waitFor(() => disconnectRes2 != null);
|
||||
});
|
||||
});
|
||||
39
WebApp/client/test/testutils.js
Normal file
39
WebApp/client/test/testutils.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import process from "process";
|
||||
|
||||
export function waitFor(conditionFunction) {
|
||||
|
||||
const poll = resolve => {
|
||||
if (conditionFunction()) resolve();
|
||||
else setTimeout(() => poll(resolve), 100);
|
||||
};
|
||||
|
||||
return new Promise(poll);
|
||||
}
|
||||
|
||||
export async function sleep(milisecond) {
|
||||
return new Promise(resolve => setTimeout(resolve, milisecond));
|
||||
}
|
||||
|
||||
export function serverExeName() {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'webserver.exe';
|
||||
case 'darwin':
|
||||
return 'webserver_mac';
|
||||
case 'linux':
|
||||
return 'webserver';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUniqueId() {
|
||||
return new Date().getTime().toString(16) + Math.floor(1000 * Math.random()).toString(16);
|
||||
}
|
||||
|
||||
export function getRTCConfiguration() {
|
||||
let config = {};
|
||||
config.sdpSemantics = 'unified-plan';
|
||||
config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }];
|
||||
return config;
|
||||
}
|
||||
Reference in New Issue
Block a user