1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Test that calls in a group call are retried (#2637)

* Test that calls in a group call are retried

* Add new flushpromises file
This commit is contained in:
David Baker
2022-09-05 09:45:32 +01:00
committed by GitHub
parent 0d6a93b5f6
commit c78631bdee
4 changed files with 105 additions and 15 deletions

View File

@ -0,0 +1,28 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}

View File

@ -79,6 +79,8 @@ export class MockRTCPeerConnection {
private negotiationNeededListener: () => void; private negotiationNeededListener: () => void;
private needsNegotiation = false; private needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate: () => void;
localDescription: RTCSessionDescription; localDescription: RTCSessionDescription;
signalingState: RTCSignalingState = "stable"; signalingState: RTCSignalingState = "stable";
@ -99,6 +101,10 @@ export class MockRTCPeerConnection {
toJSON: function() { }, toJSON: function() { },
}; };
this.readyToNegotiate = new Promise<void>(resolve => {
this.onReadyToNegotiate = resolve;
});
MockRTCPeerConnection.instances.push(this); MockRTCPeerConnection.instances.push(this);
} }
@ -128,11 +134,13 @@ export class MockRTCPeerConnection {
getStats() { return []; } getStats() { return []; }
addTrack(track: MockMediaStreamTrack) { addTrack(track: MockMediaStreamTrack) {
this.needsNegotiation = true; this.needsNegotiation = true;
this.onReadyToNegotiate();
return new MockRTCRtpSender(track); return new MockRTCRtpSender(track);
} }
removeTrack() { removeTrack() {
this.needsNegotiation = true; this.needsNegotiation = true;
this.onReadyToNegotiate();
} }
doNegotiation() { doNegotiation() {

View File

@ -22,6 +22,7 @@ import { MatrixClient } from "../../src/client";
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage'; import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import { IStore } from '../../src/store'; import { IStore } from '../../src/store';
import { flushPromises } from '../test-utils/flushPromises';
const FAKE_USER = "@alice:example.org"; const FAKE_USER = "@alice:example.org";
const FAKE_DEVICE_ID = "AAAAAAAA"; const FAKE_DEVICE_ID = "AAAAAAAA";
@ -47,19 +48,6 @@ enum StoreType {
IndexedDB = 'IndexedDB', IndexedDB = 'IndexedDB',
} }
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
async function flushAndRunTimersUntil(cond: () => boolean) { async function flushAndRunTimersUntil(cond: () => boolean) {
while (!cond()) { while (!cond()) {
await flushPromises(); await flushPromises();

View File

@ -40,7 +40,8 @@ import { TypedEventEmitter } from '../../../src/models/typed-event-emitter';
import { MediaHandler } from '../../../src/webrtc/mediaHandler'; import { MediaHandler } from '../../../src/webrtc/mediaHandler';
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from '../../../src/webrtc/callEventHandler'; import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from '../../../src/webrtc/callEventHandler';
import { CallFeed } from '../../../src/webrtc/callFeed'; import { CallFeed } from '../../../src/webrtc/callFeed';
import { CallState } from '../../../src/webrtc/call'; import { CallEvent, CallEventHandlerMap, CallState } from '../../../src/webrtc/call';
import { flushPromises } from '../../test-utils/flushPromises';
const FAKE_ROOM_ID = "!fake:test.dummy"; const FAKE_ROOM_ID = "!fake:test.dummy";
const FAKE_CONF_ID = "fakegroupcallid"; const FAKE_CONF_ID = "fakegroupcallid";
@ -106,7 +107,10 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<G
return groupCall; return groupCall;
}; };
class MockCallMatrixClient extends TypedEventEmitter<CallEventHandlerEvent.Incoming, CallEventHandlerEventHandlerMap> { type EmittedEvents = CallEventHandlerEvent | CallEvent;
type EmittedEventMap = CallEventHandlerEventHandlerMap & CallEventHandlerMap;
class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
public mediaHandler = new MockMediaHandler(); public mediaHandler = new MockMediaHandler();
constructor(public userId: string, public deviceId: string, public sessionId: string) { constructor(public userId: string, public deviceId: string, public sessionId: string) {
@ -494,6 +498,8 @@ describe('Group Call', function() {
}); });
afterEach(function() { afterEach(function() {
jest.useRealTimers();
MockRTCPeerConnection.resetInstances(); MockRTCPeerConnection.resetInstances();
}); });
@ -530,6 +536,66 @@ describe('Group Call', function() {
await Promise.all([groupCall1.leave(), groupCall2.leave()]); await Promise.all([groupCall1.leave(), groupCall2.leave()]);
} }
}); });
it("Retries calls", async function() {
jest.useFakeTimers();
await groupCall1.create();
try {
const toDeviceProm = new Promise<void>(resolve => {
client1.sendToDevice.mockImplementation(() => {
resolve();
return Promise.resolve({});
});
});
await Promise.all([groupCall1.enter(), groupCall2.enter()]);
MockRTCPeerConnection.triggerAllNegotiations();
await toDeviceProm;
expect(client1.sendToDevice).toHaveBeenCalled();
const oldCall = groupCall1.getCallByUserId(client2.userId);
oldCall.emit(CallEvent.Hangup, oldCall);
client1.sendToDevice.mockClear();
const toDeviceProm2 = new Promise<void>(resolve => {
client1.sendToDevice.mockImplementation(() => {
resolve();
return Promise.resolve({});
});
});
jest.advanceTimersByTime(groupCall1.retryCallInterval + 500);
// when we placed the call, we could await on enter which waited for the call to
// be made. We don't have that luxury now, so first have to wait for the call
// to even be created...
let newCall: MatrixCall;
while (
(newCall = groupCall1.getCallByUserId(client2.userId)) === undefined ||
newCall.callId == oldCall.callId
) {
await flushPromises();
}
const mockPc = newCall.peerConn as unknown as MockRTCPeerConnection;
// ...then wait for it to be ready to negotiate
await mockPc.readyToNegotiate;
MockRTCPeerConnection.triggerAllNegotiations();
// ...and then finally we can wait for the invite to be sent
await toDeviceProm2;
expect(client1.sendToDevice).toHaveBeenCalledWith(EventType.CallInvite, expect.objectContaining({}));
} finally {
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
}
});
}); });
describe("muting", () => { describe("muting", () => {