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 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() {
|
||||||
|
@ -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();
|
||||||
|
@ -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", () => {
|
||||||
|
Reference in New Issue
Block a user