From ab39ee37d61dbfc44b5de2269a907c466170a4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Sep 2022 12:02:41 +0200 Subject: [PATCH] Add more `MatrixCall` tests (#2697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner Signed-off-by: Šimon Brandner --- spec/test-utils/webrtc.ts | 3 + spec/unit/webrtc/call.spec.ts | 124 +++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index c190d22bf..09849d4f3 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -103,6 +103,7 @@ export class MockRTCPeerConnection { private negotiationNeededListener: () => void; public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void; + public onTrackListener?: (e: RTCTrackEvent) => void; private needsNegotiation = false; public readyToNegotiate: Promise; private onReadyToNegotiate: () => void; @@ -143,6 +144,8 @@ export class MockRTCPeerConnection { this.negotiationNeededListener = listener; } else if (type == 'icecandidate') { this.iceCandidateListener = listener; + } else if (type == 'track') { + this.onTrackListener = listener; } } createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; } diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index a470fcdd7..5d29fe15a 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -26,7 +26,13 @@ import { CallState, CallParty, } from '../../../src/webrtc/call'; -import { SDPStreamMetadata, SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes'; +import { + MCallAnswer, + MCallHangupReject, + SDPStreamMetadata, + SDPStreamMetadataKey, + SDPStreamMetadataPurpose, +} from '../../../src/webrtc/callEventTypes'; import { DUMMY_SDP, MockMediaHandler, @@ -102,6 +108,8 @@ describe('Call', function() { // We retain a reference to this in the correct Mock type let mockSendEvent: jest.Mock, [string, string, IContent, string, Callback]>; + const errorListener = () => {}; + beforeEach(function() { prevNavigator = global.navigator; prevDocument = global.document; @@ -135,7 +143,7 @@ describe('Call', function() { roomId: FAKE_ROOM_ID, }); // call checks one of these is wired up - call.on(CallEvent.Error, () => {}); + call.on(CallEvent.Error, errorListener); }); afterEach(function() { @@ -1276,4 +1284,116 @@ describe('Call', function() { expect(call.terminate).toHaveBeenCalledWith(CallParty.Local, CallErrorCode.Transfered, true); }); }); + + describe("onTrack", () => { + it("ignores streamless track", async () => { + // @ts-ignore Mock pushRemoteFeed() is private + jest.spyOn(call, "pushRemoteFeed"); + + await call.placeVoiceCall(); + + (call.peerConn as unknown as MockRTCPeerConnection).onTrackListener( + { streams: [], track: new MockMediaStreamTrack("track_ev", "audio") } as unknown as RTCTrackEvent, + ); + + // @ts-ignore Mock pushRemoteFeed() is private + expect(call.pushRemoteFeed).not.toHaveBeenCalled(); + }); + + it("correctly pushes", async () => { + // @ts-ignore Mock pushRemoteFeed() is private + jest.spyOn(call, "pushRemoteFeed"); + + await call.placeVoiceCall(); + await call.onAnswerReceived(makeMockEvent("@test:foo", { + version: 1, + call_id: call.callId, + party_id: 'the_correct_party_id', + answer: { + sdp: DUMMY_SDP, + }, + })); + + const stream = new MockMediaStream("stream_ev", [new MockMediaStreamTrack("track_ev", "audio")]); + (call.peerConn as unknown as MockRTCPeerConnection).onTrackListener( + { streams: [stream], track: stream.getAudioTracks()[0] } as unknown as RTCTrackEvent, + ); + + // @ts-ignore Mock pushRemoteFeed() is private + expect(call.pushRemoteFeed).toHaveBeenCalledWith(stream); + // @ts-ignore Mock pushRemoteFeed() is private + expect(call.removeTrackListeners.has(stream)).toBe(true); + }); + }); + + describe("onHangupReceived()", () => { + it("ends call on onHangupReceived() if state is ringing", async () => { + expect(call.callHasEnded()).toBe(false); + + call.state = CallState.Ringing; + call.onHangupReceived({} as MCallHangupReject); + + expect(call.callHasEnded()).toBe(true); + }); + + it("ends call on onHangupReceived() if party id matches", async () => { + expect(call.callHasEnded()).toBe(false); + + await call.initWithInvite({ + getContent: jest.fn().mockReturnValue({ + version: "1", + call_id: "call_id", + party_id: "remote_party_id", + lifetime: CALL_LIFETIME, + offer: { + sdp: DUMMY_SDP, + }, + }), + getSender: () => "@test:foo", + } as unknown as MatrixEvent); + call.onHangupReceived({ version: "1", party_id: "remote_party_id" } as MCallHangupReject); + + expect(call.callHasEnded()).toBe(true); + }); + }); + + it.each( + Object.values(CallState), + )("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => { + expect(call.callHasEnded()).toBe(false); + + call.state = state; + call.onRejectReceived({} as MCallHangupReject); + + expect(call.callHasEnded()).toBe( + [CallState.InviteSent, CallState.Ringing, CallState.Ended].includes(state), + ); + }); + + it("terminates call when answered elsewhere", async () => { + await call.placeVoiceCall(); + + expect(call.callHasEnded()).toBe(false); + + call.onAnsweredElsewhere({} as MCallAnswer); + + expect(call.callHasEnded()).toBe(true); + }); + + it("throws when there is no error listener", async () => { + call.off(CallEvent.Error, errorListener); + + expect(call.placeVoiceCall()).rejects.toThrow(); + }); + + describe("hasPeerConnection()", () => { + it("hasPeerConnection() returns false if there is no peer connection", () => { + expect(call.hasPeerConnection).toBe(false); + }); + + it("hasPeerConnection() returns true if there is a peer connection", async () => { + await call.placeVoiceCall(); + expect(call.hasPeerConnection).toBe(true); + }); + }); });