You've already forked matrix-js-sdk
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:
28
spec/test-utils/flushPromises.ts
Normal file
28
spec/test-utils/flushPromises.ts
Normal 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);
|
||||
});
|
||||
}
|
@ -79,6 +79,8 @@ export class MockRTCPeerConnection {
|
||||
|
||||
private negotiationNeededListener: () => void;
|
||||
private needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate: () => void;
|
||||
localDescription: RTCSessionDescription;
|
||||
signalingState: RTCSignalingState = "stable";
|
||||
|
||||
@ -99,6 +101,10 @@ export class MockRTCPeerConnection {
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
@ -128,11 +134,13 @@ export class MockRTCPeerConnection {
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
this.needsNegotiation = true;
|
||||
this.onReadyToNegotiate();
|
||||
return new MockRTCRtpSender(track);
|
||||
}
|
||||
|
||||
removeTrack() {
|
||||
this.needsNegotiation = true;
|
||||
this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
doNegotiation() {
|
||||
|
@ -22,6 +22,7 @@ import { MatrixClient } from "../../src/client";
|
||||
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
|
||||
import { logger } from '../../src/logger';
|
||||
import { IStore } from '../../src/store';
|
||||
import { flushPromises } from '../test-utils/flushPromises';
|
||||
|
||||
const FAKE_USER = "@alice:example.org";
|
||||
const FAKE_DEVICE_ID = "AAAAAAAA";
|
||||
@ -47,19 +48,6 @@ enum StoreType {
|
||||
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) {
|
||||
while (!cond()) {
|
||||
await flushPromises();
|
||||
|
@ -40,7 +40,8 @@ import { TypedEventEmitter } from '../../../src/models/typed-event-emitter';
|
||||
import { MediaHandler } from '../../../src/webrtc/mediaHandler';
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from '../../../src/webrtc/callEventHandler';
|
||||
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_CONF_ID = "fakegroupcallid";
|
||||
@ -106,7 +107,10 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<G
|
||||
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();
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
@ -494,6 +498,8 @@ describe('Group Call', function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jest.useRealTimers();
|
||||
|
||||
MockRTCPeerConnection.resetInstances();
|
||||
});
|
||||
|
||||
@ -530,6 +536,66 @@ describe('Group Call', function() {
|
||||
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", () => {
|
||||
|
Reference in New Issue
Block a user