Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set/create events #71

Merged
merged 9 commits into from
Feb 14, 2024
6 changes: 5 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
"Wcme",
"WCME",
"webex",
"webrtc"
"webrtc",
"createofferonsuccess",
"createansweronsuccess",
"setlocaldescriptiononsuccess",
"setremotedescriptiononsuccess"
],
"flagWords": [],
"ignorePaths": [
Expand Down
13 changes: 12 additions & 1 deletion src/mocks/rtc-peer-connection-stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@
* This stub exists to act as a scaffold for creating a mock.
*/
class RTCPeerConnectionStub {
createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return new Promise(() => {});
}
getStats(): Promise<any> {
return new Promise(() => {});
}
setLocalDescription(): Promise<any> {
setLocalDescription(
description?: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return new Promise(() => {});
}

setRemoteDescription(
description?: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return new Promise(() => {});
}
onconnectionstatechange: () => void = () => {};
Expand Down
164 changes: 163 additions & 1 deletion src/peer-connection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserInfo } from '@webex/web-capabilities';
import { MockedObjectDeep } from 'ts-jest';
import { ConnectionState, ConnectionStateHandler } from './connection-state-handler';
import { mocked } from './mocks/mock';
import { RTCPeerConnectionStub } from './mocks/rtc-peer-connection-stub';
Expand Down Expand Up @@ -246,18 +247,112 @@ describe('PeerConnection', () => {
connectionStateHandlerListener(ConnectionState.Connecting);
});
});
describe('createAnswer', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let createAnswerSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;
const mockedReturnedAnswer: RTCSessionDescriptionInit = {
sdp: 'blah',
type: 'offer',
};

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.createAnswer.mockImplementation(() => {
return Promise.resolve(mockedReturnedAnswer);
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
createAnswerSpy = jest.spyOn(pc, 'createAnswer');
pc.on(PeerConnection.Events.CreateAnswerOnSuccess, callback);
});

it('should emit event when createAnswer called', async () => {
expect.hasAssertions();
const options: RTCAnswerOptions = {
iceRestart: true,
};
const answer = await pc.createAnswer(options);
expect(answer).toStrictEqual(mockedReturnedAnswer);
expect(createAnswerSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(mockedReturnedAnswer);
});

it('should not emit event when createAnswer failed', async () => {
expect.hasAssertions();
mockPc.createAnswer.mockImplementation(() => {
return Promise.reject(new Error());
});
const answerPromise = pc.createAnswer(null as unknown as RTCOfferOptions);
await expect(answerPromise).rejects.toThrow(Error);
expect(createAnswerSpy).toHaveBeenCalledWith(null);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('createOffer', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let createOfferSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;
const mockedReturnedOffer: RTCSessionDescriptionInit = {
sdp: 'blah',
type: 'offer',
};

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.createOffer.mockImplementation(() => {
return Promise.resolve(mockedReturnedOffer);
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
createOfferSpy = jest.spyOn(pc, 'createOffer');
pc.on(PeerConnection.Events.CreateOfferOnSuccess, callback);
});

it('should emit event when createOffer called', async () => {
expect.hasAssertions();
const options: RTCOfferOptions = {
iceRestart: true,
};
const offer = await pc.createOffer(options);
expect(offer).toStrictEqual(mockedReturnedOffer);
expect(createOfferSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(mockedReturnedOffer);
});

it('should not emit event when createOffer failed', async () => {
expect.hasAssertions();
mockPc.createOffer.mockImplementation(() => {
return Promise.reject(new Error());
});
const offerPromise = pc.createOffer(null as unknown as RTCOfferOptions);
await expect(offerPromise).rejects.toThrow(Error);
expect(createOfferSpy).toHaveBeenCalledWith(null);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('setLocalDescription', () => {
let mockPc: RTCPeerConnectionStub;
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let setLocalDescriptionSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
mockPc.setLocalDescription.mockImplementation(() => {
return Promise.resolve();
});
setLocalDescriptionSpy = jest.spyOn(mockPc, 'setLocalDescription');
pc = new PeerConnection();
pc.on(PeerConnection.Events.SetLocalDescriptionOnSuccess, callback);
});

it('sets the local description with an SDP offer', async () => {
Expand All @@ -278,5 +373,72 @@ describe('PeerConnection', () => {
pc.setLocalDescription({ type: 'offer', sdp: 'm=video 9 UDP/TLS/RTP' })
).rejects.toThrow(Error);
});

it('should emit event when setLocalDescription called', async () => {
expect.hasAssertions();
const options = {
sdp: 'blah',
};
await pc.setLocalDescription(options as unknown as RTCSessionDescriptionInit);
expect(setLocalDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(options);
});

it('should not emit event when setLocalDescription failed', async () => {
expect.hasAssertions();
mockPc.setLocalDescription.mockImplementation(() => {
return Promise.reject(new Error());
});
const options = {
sdp: 'reject',
};
const offerPromise = pc.setLocalDescription(options as unknown as RTCSessionDescriptionInit);
await expect(offerPromise).rejects.toThrow(Error);
expect(setLocalDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledTimes(0);
});
});

describe('setRemoteDescription', () => {
let mockPc: MockedObjectDeep<RTCPeerConnectionStub>;
let setRemoteDescriptionSpy: jest.SpyInstance;
const callback = jest.fn();
let pc: PeerConnection;

beforeEach(() => {
jest.clearAllMocks();
mockPc = mocked(new RTCPeerConnectionStub(), true);
mockPc.setRemoteDescription.mockImplementation(() => {
return Promise.resolve();
});
mockCreateRTCPeerConnection.mockReturnValueOnce(mockPc as unknown as RTCPeerConnection);
pc = new PeerConnection();
setRemoteDescriptionSpy = jest.spyOn(pc, 'setRemoteDescription');
pc.on(PeerConnection.Events.SetRemoteDescriptionOnSuccess, callback);
});

it('should emit event when setRemoteDescription called', async () => {
expect.hasAssertions();
const options = {
sdp: 'blah',
};
await pc.setRemoteDescription(options as unknown as RTCSessionDescriptionInit);
expect(setRemoteDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledWith(options);
});

it('should not emit event when setRemoteDescription failed', async () => {
expect.hasAssertions();
mockPc.setRemoteDescription.mockImplementation(() => {
return Promise.reject(new Error());
});
const options = {
sdp: 'reject',
};
const offerPromise = pc.setRemoteDescription(options as unknown as RTCSessionDescriptionInit);
await expect(offerPromise).rejects.toThrow(Error);
expect(setRemoteDescriptionSpy).toHaveBeenCalledWith(options);
expect(callback).toHaveBeenCalledTimes(0);
});
});
});
33 changes: 29 additions & 4 deletions src/peer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConnectionState, ConnectionStateHandler } from './connection-state-hand
import { EventEmitter, EventMap } from './event-emitter';
import { createRTCPeerConnection } from './rtc-peer-connection-factory';
import { logger } from './util/logger';

/**
* A type-safe form of the DOMString used in the MediaStreamTrack.kind field.
*/
Expand All @@ -27,11 +28,23 @@ type IceGatheringStateChangeEvent = {
enum PeerConnectionEvents {
IceGatheringStateChange = 'icegatheringstatechange',
ConnectionStateChange = 'connectionstatechange',
CreateOfferOnSuccess = 'createofferonsuccess',
CreateAnswerOnSuccess = 'createansweronsuccess',
SetLocalDescriptionOnSuccess = 'setlocaldescriptiononsuccess',
SetRemoteDescriptionOnSuccess = 'setremotedescriptiononsuccess',
}

interface PeerConnectionEventHandlers extends EventMap {
[PeerConnectionEvents.IceGatheringStateChange]: (ev: IceGatheringStateChangeEvent) => void;
[PeerConnectionEvents.ConnectionStateChange]: (state: ConnectionState) => void;
[PeerConnectionEvents.CreateOfferOnSuccess]: (offer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.CreateAnswerOnSuccess]: (answer: RTCSessionDescriptionInit) => void;
[PeerConnectionEvents.SetLocalDescriptionOnSuccess]: (
description: RTCSessionDescription | RTCSessionDescriptionInit
) => void;
[PeerConnectionEvents.SetRemoteDescriptionOnSuccess]: (
description: RTCSessionDescription | RTCSessionDescriptionInit
) => void;
}

type ConnectionType = 'UDP' | 'TCP' | 'TURN-TLS' | 'TURN-TCP' | 'TURN-UDP' | 'unknown';
Expand Down Expand Up @@ -172,7 +185,10 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
* other peer.
*/
async createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit> {
return this.pc.createAnswer(options);
return this.pc.createAnswer(options).then((answer) => {
this.emit(PeerConnection.Events.CreateAnswerOnSuccess, answer);
return answer;
});
}

/**
Expand All @@ -186,7 +202,10 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
* That received offer should be delivered through the signaling server to a remote peer.
*/
async createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
return this.pc.createOffer(options);
return this.pc.createOffer(options).then((offer) => {
this.emit(PeerConnection.Events.CreateOfferOnSuccess, offer);
return offer;
});
}

/**
Expand Down Expand Up @@ -215,7 +234,11 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
});
}

return this.pc.setLocalDescription(description);
return this.pc.setLocalDescription(description).then(() => {
if (description) {
this.emit(PeerConnection.Events.SetLocalDescriptionOnSuccess, description);
}
});
}

/**
Expand All @@ -230,7 +253,9 @@ class PeerConnection extends EventEmitter<PeerConnectionEventHandlers> {
async setRemoteDescription(
description: RTCSessionDescription | RTCSessionDescriptionInit
): Promise<void> {
return this.pc.setRemoteDescription(description);
return this.pc.setRemoteDescription(description).then(() => {
this.emit(PeerConnection.Events.SetRemoteDescriptionOnSuccess, description);
});
}

/**
Expand Down
Loading