You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Test placing a call in a group call (#2593)
* Test placing a call in a group call Refactors a bit of the call testing stuff Fixes https://github.com/vector-im/element-call/issues/521 * Unused imports * Use expect.toHaveBeenCalledWith() * Types * More types * Add comment on mock typing * Use toHaveBeenCalledWith() * Initialise groupcall & room in beforeEach * Initialise mockMediahandler sensibly * Add type params to mock * Rename mute tests * Move comment * Join / leave in parallel * Remove leftover expect
This commit is contained in:
@@ -70,20 +70,41 @@ export class MockAudioContext {
|
||||
}
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
private static instances: MockRTCPeerConnection[] = [];
|
||||
|
||||
private negotiationNeededListener: () => void;
|
||||
private needsNegotiation = false;
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
public static triggerAllNegotiations() {
|
||||
for (const inst of this.instances) {
|
||||
inst.doNegotiation();
|
||||
}
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') this.negotiationNeededListener = listener;
|
||||
}
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
@@ -93,7 +114,17 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
this.needsNegotiation = true;
|
||||
return new MockRTCRtpSender(track);
|
||||
}
|
||||
|
||||
doNegotiation() {
|
||||
if (this.needsNegotiation && this.negotiationNeededListener) {
|
||||
this.needsNegotiation = false;
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
@@ -140,6 +171,10 @@ export class MockMediaStream {
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
|
||||
clone() {
|
||||
return new MockMediaStream(this.id, this.tracks);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
@@ -149,6 +184,9 @@ export class MockMediaDeviceInfo {
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
public userMediaStreams: MediaStream[] = [];
|
||||
public screensharingStreams: MediaStream[] = [];
|
||||
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
@@ -160,3 +198,32 @@ export class MockMediaHandler {
|
||||
hasAudioDevice() { return true; }
|
||||
stopAllStreams() {}
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
// @ts-ignore Mock
|
||||
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
|
||||
},
|
||||
};
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {};
|
||||
}
|
||||
|
@@ -22,9 +22,7 @@ import {
|
||||
MockMediaHandler,
|
||||
MockMediaStream,
|
||||
MockMediaStreamTrack,
|
||||
MockMediaDeviceInfo,
|
||||
MockRTCPeerConnection,
|
||||
MockAudioContext,
|
||||
installWebRTCMocks,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
|
||||
@@ -48,29 +46,7 @@ describe('Call', function() {
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
// @ts-ignore Mock
|
||||
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
|
||||
},
|
||||
};
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
installWebRTCMocks();
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
|
@@ -14,70 +14,95 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventType, GroupCallIntent, GroupCallType, Room, RoomMember } from '../../../src';
|
||||
import { EventType, GroupCallIntent, GroupCallType, MatrixEvent, Room, RoomMember } from '../../../src';
|
||||
import { GroupCall } from "../../../src/webrtc/groupCall";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { MockAudioContext, MockMediaHandler } from '../../test-utils/webrtc';
|
||||
import { installWebRTCMocks, MockMediaHandler, MockRTCPeerConnection } from '../../test-utils/webrtc';
|
||||
import { ReEmitter } from '../../../src/ReEmitter';
|
||||
import { TypedEventEmitter } from '../../../src/models/typed-event-emitter';
|
||||
import { MediaHandler } from '../../../src/webrtc/mediaHandler';
|
||||
|
||||
const FAKE_SELF_USER_ID = "@me:test.dummy";
|
||||
const FAKE_SELF_DEVICE_ID = "AAAAAA";
|
||||
const FAKE_SELF_SESSION_ID = "1";
|
||||
const FAKE_ROOM_ID = "!fake:test.dummy";
|
||||
const FAKE_CONF_ID = "fakegroupcallid";
|
||||
|
||||
const FAKE_USER_ID_1 = "@alice:test.dummy";
|
||||
const FAKE_DEVICE_ID_1 = "@AAAAAA";
|
||||
const FAKE_SESSION_ID_1 = "alice1";
|
||||
const FAKE_USER_ID_2 = "@bob:test.dummy";
|
||||
const FAKE_DEVICE_ID_2 = "@BBBBBB";
|
||||
const FAKE_SESSION_ID_2 = "bob1";
|
||||
|
||||
class MockCallMatrixClient {
|
||||
public mediaHandler: MediaHandler = new MockMediaHandler() as unknown as MediaHandler;
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
}
|
||||
|
||||
groupCallEventHandler = {
|
||||
groupCalls: new Map(),
|
||||
};
|
||||
|
||||
callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
|
||||
sendStateEvent = jest.fn();
|
||||
|
||||
getMediaHandler() { return this.mediaHandler; }
|
||||
|
||||
getUserId() { return this.userId; }
|
||||
|
||||
getDeviceId() { return this.deviceId; }
|
||||
getSessionId() { return this.sessionId; }
|
||||
|
||||
emit = jest.fn();
|
||||
on = jest.fn();
|
||||
removeListener = jest.fn();
|
||||
getTurnServers = () => [];
|
||||
isFallbackICEServerAllowed = () => false;
|
||||
reEmitter = new ReEmitter(new TypedEventEmitter());
|
||||
getUseE2eForGroupCall = () => false;
|
||||
checkTurnServers = () => null;
|
||||
}
|
||||
|
||||
describe('Group Call', function() {
|
||||
beforeEach(function() {
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
installWebRTCMocks();
|
||||
});
|
||||
|
||||
describe('Basic functionality', function() {
|
||||
let mockSendState: jest.Mock;
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let groupCall: GroupCall;
|
||||
|
||||
beforeEach(function() {
|
||||
const typedMockClient = new MockCallMatrixClient(
|
||||
FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1,
|
||||
);
|
||||
mockSendState = typedMockClient.sendStateEvent;
|
||||
|
||||
mockClient = typedMockClient as unknown as MatrixClient;
|
||||
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
|
||||
});
|
||||
|
||||
it("sends state event to room when creating", async () => {
|
||||
const mockSendState = jest.fn();
|
||||
|
||||
const mockClient = {
|
||||
sendStateEvent: mockSendState,
|
||||
groupCallEventHandler: {
|
||||
groupCalls: new Map(),
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_SELF_USER_ID);
|
||||
const groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
|
||||
|
||||
await groupCall.create();
|
||||
|
||||
expect(mockSendState.mock.calls[0][0]).toEqual(FAKE_ROOM_ID);
|
||||
expect(mockSendState.mock.calls[0][1]).toEqual(EventType.GroupCallPrefix);
|
||||
expect(mockSendState.mock.calls[0][2]["m.type"]).toEqual(GroupCallType.Video);
|
||||
expect(mockSendState.mock.calls[0][2]["m.intent"]).toEqual(GroupCallIntent.Prompt);
|
||||
expect(mockSendState).toHaveBeenCalledWith(
|
||||
FAKE_ROOM_ID, EventType.GroupCallPrefix, expect.objectContaining({
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}),
|
||||
groupCall.groupCallId,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends member state event to room on enter", async () => {
|
||||
const mockSendState = jest.fn();
|
||||
const mockMediaHandler = new MockMediaHandler();
|
||||
|
||||
const mockClient = {
|
||||
sendStateEvent: mockSendState,
|
||||
groupCallEventHandler: {
|
||||
groupCalls: new Map(),
|
||||
},
|
||||
callEventHandler: {
|
||||
calls: new Map(),
|
||||
},
|
||||
mediaHandler: mockMediaHandler,
|
||||
getMediaHandler: () => mockMediaHandler,
|
||||
getUserId: () => FAKE_SELF_USER_ID,
|
||||
getDeviceId: () => FAKE_SELF_DEVICE_ID,
|
||||
getSessionId: () => FAKE_SELF_SESSION_ID,
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_SELF_USER_ID);
|
||||
const groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
|
||||
|
||||
room.currentState.members[FAKE_SELF_USER_ID] = {
|
||||
userId: FAKE_SELF_USER_ID,
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
} as unknown as RoomMember;
|
||||
|
||||
await groupCall.create();
|
||||
@@ -85,14 +110,212 @@ describe('Group Call', function() {
|
||||
try {
|
||||
await groupCall.enter();
|
||||
|
||||
expect(mockSendState.mock.lastCall[0]).toEqual(FAKE_ROOM_ID);
|
||||
expect(mockSendState.mock.lastCall[1]).toEqual(EventType.GroupCallMemberPrefix);
|
||||
expect(mockSendState.mock.lastCall[2]['m.calls'].length).toEqual(1);
|
||||
expect(mockSendState.mock.lastCall[2]['m.calls'][0]["m.call_id"]).toEqual(groupCall.groupCallId);
|
||||
expect(mockSendState.mock.lastCall[2]['m.calls'][0]['m.devices'].length).toEqual(1);
|
||||
expect(mockSendState.mock.lastCall[2]['m.calls'][0]['m.devices'][0].device_id).toEqual(FAKE_SELF_DEVICE_ID);
|
||||
expect(mockSendState).toHaveBeenCalledWith(
|
||||
FAKE_ROOM_ID,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
expect.objectContaining({
|
||||
"m.calls": [
|
||||
expect.objectContaining({
|
||||
"m.call_id": groupCall.groupCallId,
|
||||
"m.devices": [
|
||||
expect.objectContaining({
|
||||
device_id: FAKE_DEVICE_ID_1,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
FAKE_USER_ID_1,
|
||||
);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
|
||||
it("starts with mic unmuted in regular calls", async () => {
|
||||
try {
|
||||
await groupCall.create();
|
||||
|
||||
await groupCall.initLocalCallFeed();
|
||||
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(false);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
|
||||
it("starts with mic muted in PTT calls", async () => {
|
||||
try {
|
||||
// replace groupcall with a PTT one for this test
|
||||
// we will probably want a dedicated test suite for PTT calls, so when we do,
|
||||
// this can go in there instead.
|
||||
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, true, GroupCallIntent.Prompt);
|
||||
|
||||
await groupCall.create();
|
||||
|
||||
await groupCall.initLocalCallFeed();
|
||||
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
|
||||
it("disables audio stream when audio is set to muted", async () => {
|
||||
try {
|
||||
await groupCall.create();
|
||||
|
||||
await groupCall.initLocalCallFeed();
|
||||
|
||||
await groupCall.setMicrophoneMuted(true);
|
||||
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
|
||||
it("starts with video unmuted in regular calls", async () => {
|
||||
try {
|
||||
await groupCall.create();
|
||||
|
||||
await groupCall.initLocalCallFeed();
|
||||
|
||||
expect(groupCall.isLocalVideoMuted()).toEqual(false);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
|
||||
it("disables video stream when video is set to muted", async () => {
|
||||
try {
|
||||
await groupCall.create();
|
||||
|
||||
await groupCall.initLocalCallFeed();
|
||||
|
||||
await groupCall.setLocalVideoMuted(true);
|
||||
|
||||
expect(groupCall.isLocalVideoMuted()).toEqual(true);
|
||||
} finally {
|
||||
groupCall.leave();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Placing calls', function() {
|
||||
let groupCall1: GroupCall;
|
||||
let groupCall2: GroupCall;
|
||||
let client1: MatrixClient;
|
||||
let client2: MatrixClient;
|
||||
|
||||
beforeEach(function() {
|
||||
MockRTCPeerConnection.resetInstances();
|
||||
|
||||
client1 = new MockCallMatrixClient(
|
||||
FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1,
|
||||
) as unknown as MatrixClient;
|
||||
|
||||
client2 = new MockCallMatrixClient(
|
||||
FAKE_USER_ID_2, FAKE_DEVICE_ID_2, FAKE_SESSION_ID_2,
|
||||
) as unknown as MatrixClient;
|
||||
|
||||
client1.sendStateEvent = client2.sendStateEvent = (roomId, eventType, content, statekey) => {
|
||||
if (eventType === EventType.GroupCallMemberPrefix) {
|
||||
const fakeEvent = {
|
||||
getContent: () => content,
|
||||
getRoomId: () => FAKE_ROOM_ID,
|
||||
getStateKey: () => statekey,
|
||||
} as unknown as MatrixEvent;
|
||||
|
||||
let subMap = client1Room.currentState.events.get(eventType);
|
||||
if (!subMap) {
|
||||
subMap = new Map<string, MatrixEvent>();
|
||||
client1Room.currentState.events.set(eventType, subMap);
|
||||
client2Room.currentState.events.set(eventType, subMap);
|
||||
}
|
||||
// since we cheat & use the same maps for each, we can
|
||||
// just add it once.
|
||||
subMap.set(statekey, fakeEvent);
|
||||
|
||||
groupCall1.onMemberStateChanged(fakeEvent);
|
||||
groupCall2.onMemberStateChanged(fakeEvent);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
|
||||
const client1Room = new Room(FAKE_ROOM_ID, client1, FAKE_USER_ID_1);
|
||||
|
||||
const client2Room = new Room(FAKE_ROOM_ID, client2, FAKE_USER_ID_2);
|
||||
|
||||
groupCall1 = new GroupCall(
|
||||
client1, client1Room, GroupCallType.Video, false, GroupCallIntent.Prompt, FAKE_CONF_ID,
|
||||
);
|
||||
|
||||
groupCall2 = new GroupCall(
|
||||
client2, client2Room, GroupCallType.Video, false, GroupCallIntent.Prompt, FAKE_CONF_ID,
|
||||
);
|
||||
|
||||
client1Room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
} as unknown as RoomMember;
|
||||
client1Room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
} as unknown as RoomMember;
|
||||
|
||||
client2Room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
} as unknown as RoomMember;
|
||||
client2Room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
MockRTCPeerConnection.resetInstances();
|
||||
});
|
||||
|
||||
it("Places a call to a peer", async function() {
|
||||
await groupCall1.create();
|
||||
|
||||
try {
|
||||
// keep this as its own variable so we have it typed as a mock
|
||||
// rather than its type in the client object
|
||||
const mockSendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
|
||||
const toDeviceProm = new Promise<void>(resolve => {
|
||||
mockSendToDevice.mockImplementation(() => {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
});
|
||||
});
|
||||
|
||||
client1.sendToDevice = mockSendToDevice;
|
||||
|
||||
await Promise.all([groupCall1.enter(), groupCall2.enter()]);
|
||||
|
||||
MockRTCPeerConnection.triggerAllNegotiations();
|
||||
|
||||
await toDeviceProm;
|
||||
|
||||
expect(mockSendToDevice.mock.calls[0][0]).toBe("m.call.invite");
|
||||
|
||||
const toDeviceCallContent = mockSendToDevice.mock.calls[0][1];
|
||||
expect(Object.keys(toDeviceCallContent).length).toBe(1);
|
||||
expect(Object.keys(toDeviceCallContent)[0]).toBe(FAKE_USER_ID_2);
|
||||
|
||||
const toDeviceBobDevices = toDeviceCallContent[FAKE_USER_ID_2];
|
||||
expect(Object.keys(toDeviceBobDevices).length).toBe(1);
|
||||
expect(Object.keys(toDeviceBobDevices)[0]).toBe(FAKE_DEVICE_ID_2);
|
||||
|
||||
const bobDeviceMessage = toDeviceBobDevices[FAKE_DEVICE_ID_2];
|
||||
expect(bobDeviceMessage.conf_id).toBe(FAKE_CONF_ID);
|
||||
} finally {
|
||||
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user