riot-web/test/LegacyCallHandler-test.ts

686 lines
25 KiB
TypeScript
Raw Normal View History

/*
2021-04-27 19:56:22 +02:00
Copyright 2021 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.
*/
import {
IProtocol,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MatrixEvent,
PushRuleKind,
Room,
RuleId,
TweakName,
2022-12-12 12:24:14 +01:00
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
2022-12-12 12:24:14 +01:00
import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import EventEmitter from "events";
import { mocked } from "jest-mock";
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
import LegacyCallHandler, {
2022-12-12 12:24:14 +01:00
LegacyCallHandlerEvent,
AudioID,
PROTOCOL_PSTN,
PROTOCOL_PSTN_PREFIXED,
PROTOCOL_SIP_NATIVE,
PROTOCOL_SIP_VIRTUAL,
} from "../src/LegacyCallHandler";
import { stubClient, mkStubRoom, untilDispatch } from "./test-utils";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import DMRoomMap from "../src/utils/DMRoomMap";
import SdkConfig from "../src/SdkConfig";
Make `CallHandler` more `EventEmitter`y (#6704) * sharedInstance() -> instance Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use CallState event instead of dispatching Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Simplifie some code Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method to start a call instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method instead of place_conference_call Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make terminateCallApp() and hangupCallApp() public Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupAllCalls() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make dialNumber(), startTransferToMatrixID() and startTransferToPhoneNumber() public instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use answerCall() instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupOrReject() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Update docs Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Improve TS Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Dispatch call_state, see https://github.com/vector-im/element-web/pull/18823#issuecomment-917377277 Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-11-30 19:09:13 +01:00
import { Action } from "../src/dispatcher/actions";
import { getFunctionalMembers } from "../src/utils/room/getFunctionalMembers";
2022-12-12 12:24:14 +01:00
import SettingsStore from "../src/settings/SettingsStore";
import { UIFeature } from "../src/settings/UIFeature";
import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../src/contexts/SDKContext";
import Modal from "../src/Modal";
jest.mock("../src/Modal");
// mock VoiceRecording because it contains all the audio APIs
jest.mock("../src/audio/VoiceRecording", () => ({
VoiceRecording: jest.fn().mockReturnValue({
disableMaxLength: jest.fn(),
liveData: {
onUpdate: jest.fn(),
},
off: jest.fn(),
on: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
destroy: jest.fn(),
contentType: "audio/ogg",
}),
}));
jest.mock("../src/utils/room/getFunctionalMembers", () => ({
getFunctionalMembers: jest.fn(),
}));
// The Matrix IDs that the user sees when talking to Alice & Bob
const NATIVE_ALICE = "@alice:example.org";
const NATIVE_BOB = "@bob:example.org";
const NATIVE_CHARLIE = "@charlie:example.org";
// Virtual user for Bob
const VIRTUAL_BOB = "@virtual_bob:example.org";
//const REAL_ROOM_ID = "$room1:example.org";
// The rooms the user sees when they're communicating with these users
const NATIVE_ROOM_ALICE = "$alice_room:example.org";
const NATIVE_ROOM_BOB = "$bob_room:example.org";
const NATIVE_ROOM_CHARLIE = "$charlie_room:example.org";
const FUNCTIONAL_USER = "@bot:example.com";
// The room we use to talk to virtual Bob (but that the user does not see)
// Bob has a virtual room, but Alice doesn't
const VIRTUAL_ROOM_BOB = "$virtual_bob_room:example.org";
// Bob's phone number
const BOB_PHONE_NUMBER = "01818118181";
function mkStubDM(roomId: string, userId: string) {
const room = mkStubRoom(roomId, "room", MatrixClientPeg.safeGet());
room.getJoinedMembers = jest.fn().mockReturnValue([
{
2022-12-12 12:24:14 +01:00
userId: "@me:example.org",
name: "Member",
rawDisplayName: "Member",
roomId: roomId,
membership: KnownMembership.Join,
2022-12-12 12:24:14 +01:00
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
{
userId: userId,
2022-12-12 12:24:14 +01:00
name: "Member",
rawDisplayName: "Member",
roomId: roomId,
membership: KnownMembership.Join,
2022-12-12 12:24:14 +01:00
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
{
userId: FUNCTIONAL_USER,
2022-12-12 12:24:14 +01:00
name: "Bot user",
rawDisplayName: "Bot user",
roomId: roomId,
membership: KnownMembership.Join,
2022-12-12 12:24:14 +01:00
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
]);
room.currentState.getMembers = room.getJoinedMembers;
return room;
}
class FakeCall extends EventEmitter {
roomId: string;
callId = "fake call id";
constructor(roomId: string) {
super();
this.roomId = roomId;
}
setRemoteOnHold() {}
setRemoteAudioElement() {}
placeVoiceCall() {
this.emit(CallEvent.State, CallState.Connected, null);
}
}
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
function untilCallHandlerEvent(callHandler: LegacyCallHandler, event: LegacyCallHandlerEvent): Promise<void> {
Make `CallHandler` more `EventEmitter`y (#6704) * sharedInstance() -> instance Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use CallState event instead of dispatching Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Simplifie some code Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method to start a call instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method instead of place_conference_call Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make terminateCallApp() and hangupCallApp() public Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupAllCalls() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make dialNumber(), startTransferToMatrixID() and startTransferToPhoneNumber() public instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use answerCall() instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupOrReject() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Update docs Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Improve TS Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Dispatch call_state, see https://github.com/vector-im/element-web/pull/18823#issuecomment-917377277 Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-11-30 19:09:13 +01:00
return new Promise<void>((resolve) => {
callHandler.addListener(event, () => {
resolve();
});
});
}
2022-12-12 12:24:14 +01:00
describe("LegacyCallHandler", () => {
let dmRoomMap;
let callHandler: LegacyCallHandler;
let audioElement: HTMLAudioElement;
let fakeCall: MatrixCall | null;
// what addresses the app has looked up via pstn and native lookup
let pstnLookup: string | null;
let nativeLookup: string | null;
2022-12-12 12:24:14 +01:00
const deviceId = "my-device";
beforeEach(async () => {
stubClient();
fakeCall = null;
MatrixClientPeg.safeGet().createCall = (roomId: string): MatrixCall | null => {
if (fakeCall && fakeCall.roomId !== roomId) {
throw new Error("Only one call is supported!");
}
fakeCall = new FakeCall(roomId) as unknown as MatrixCall;
return fakeCall as unknown as MatrixCall;
};
MatrixClientPeg.safeGet().deviceId = deviceId;
MatrixClientPeg.safeGet().getThirdpartyProtocols = () => {
return Promise.resolve({
"m.id.phone": {} as IProtocol,
"im.vector.protocol.sip_native": {} as IProtocol,
"im.vector.protocol.sip_virtual": {} as IProtocol,
});
};
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
callHandler = new LegacyCallHandler();
callHandler.start();
2022-12-12 12:24:14 +01:00
mocked(getFunctionalMembers).mockReturnValue([FUNCTIONAL_USER]);
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
const nativeRoomBob = mkStubDM(NATIVE_ROOM_BOB, NATIVE_BOB);
const nativeRoomCharie = mkStubDM(NATIVE_ROOM_CHARLIE, NATIVE_CHARLIE);
const virtualBobRoom = mkStubDM(VIRTUAL_ROOM_BOB, VIRTUAL_BOB);
MatrixClientPeg.safeGet().getRoom = (roomId: string): Room | null => {
switch (roomId) {
case NATIVE_ROOM_ALICE:
return nativeRoomAlice;
case NATIVE_ROOM_BOB:
return nativeRoomBob;
case NATIVE_ROOM_CHARLIE:
return nativeRoomCharie;
case VIRTUAL_ROOM_BOB:
return virtualBobRoom;
}
return null;
};
dmRoomMap = {
getUserIdForRoomId: (roomId: string) => {
if (roomId === NATIVE_ROOM_ALICE) {
return NATIVE_ALICE;
} else if (roomId === NATIVE_ROOM_BOB) {
return NATIVE_BOB;
} else if (roomId === NATIVE_ROOM_CHARLIE) {
return NATIVE_CHARLIE;
} else if (roomId === VIRTUAL_ROOM_BOB) {
return VIRTUAL_BOB;
} else {
return null;
}
},
getDMRoomsForUserId: (userId: string) => {
if (userId === NATIVE_ALICE) {
return [NATIVE_ROOM_ALICE];
} else if (userId === NATIVE_BOB) {
return [NATIVE_ROOM_BOB];
} else if (userId === NATIVE_CHARLIE) {
return [NATIVE_ROOM_CHARLIE];
} else if (userId === VIRTUAL_BOB) {
return [VIRTUAL_ROOM_BOB];
} else {
return [];
}
},
} as unknown as DMRoomMap;
DMRoomMap.setShared(dmRoomMap);
pstnLookup = null;
nativeLookup = null;
MatrixClientPeg.safeGet().getThirdpartyUser = (proto: string, params: any) => {
if ([PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED].includes(proto)) {
2022-12-12 12:24:14 +01:00
pstnLookup = params["m.id.phone"];
return Promise.resolve([
{
userid: VIRTUAL_BOB,
protocol: "m.id.phone",
fields: {
is_native: true,
lookup_success: true,
},
2022-12-12 12:24:14 +01:00
},
]);
} else if (proto === PROTOCOL_SIP_NATIVE) {
nativeLookup = params["virtual_mxid"];
if (params["virtual_mxid"] === VIRTUAL_BOB) {
return Promise.resolve([
{
userid: NATIVE_BOB,
protocol: "im.vector.protocol.sip_native",
fields: {
is_native: true,
lookup_success: true,
},
},
]);
}
return Promise.resolve([]);
} else if (proto === PROTOCOL_SIP_VIRTUAL) {
2022-12-12 12:24:14 +01:00
if (params["native_mxid"] === NATIVE_BOB) {
return Promise.resolve([
{
userid: VIRTUAL_BOB,
protocol: "im.vector.protocol.sip_virtual",
fields: {
is_virtual: true,
lookup_success: true,
},
},
2022-12-12 12:24:14 +01:00
]);
}
return Promise.resolve([]);
}
return Promise.resolve([]);
};
2022-12-12 12:24:14 +01:00
audioElement = document.createElement("audio");
audioElement.id = "remoteAudio";
document.body.appendChild(audioElement);
});
afterEach(() => {
callHandler.stop();
// @ts-ignore
DMRoomMap.setShared(null);
// @ts-ignore
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
window.mxLegacyCallHandler = null;
MatrixClientPeg.unset();
document.body.removeChild(audioElement);
SdkConfig.reset();
});
2022-12-12 12:24:14 +01:00
it("should look up the correct user and start a call in the room when a phone number is dialled", async () => {
await callHandler.dialNumber(BOB_PHONE_NUMBER);
expect(pstnLookup).toEqual(BOB_PHONE_NUMBER);
expect(nativeLookup).toEqual(VIRTUAL_BOB);
// we should have switched to the native room for Bob
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB);
// Check that a call was started: its room on the protocol level
// should be the virtual room
expect(fakeCall).not.toBeNull();
expect(fakeCall?.roomId).toEqual(VIRTUAL_ROOM_BOB);
// but it should appear to the user to be in thw native room for Bob
expect(callHandler.roomIdForCall(fakeCall!)).toEqual(NATIVE_ROOM_BOB);
});
2022-12-12 12:24:14 +01:00
it("should look up the correct user and start a call in the room when a call is transferred", async () => {
// we can pass a very minimal object as as the call since we pass consultFirst=true:
// we don't need to actually do any transferring
const mockTransferreeCall = { type: CallType.Voice } as unknown as MatrixCall;
await callHandler.startTransferToPhoneNumber(mockTransferreeCall, BOB_PHONE_NUMBER, true);
// same checks as above
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB);
expect(fakeCall).not.toBeNull();
expect(fakeCall!.roomId).toEqual(VIRTUAL_ROOM_BOB);
expect(callHandler.roomIdForCall(fakeCall!)).toEqual(NATIVE_ROOM_BOB);
});
2022-12-12 12:24:14 +01:00
it("should move calls between rooms when remote asserted identity changes", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
Make `CallHandler` more `EventEmitter`y (#6704) * sharedInstance() -> instance Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use CallState event instead of dispatching Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Simplifie some code Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method to start a call instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use a method instead of place_conference_call Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make terminateCallApp() and hangupCallApp() public Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupAllCalls() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make dialNumber(), startTransferToMatrixID() and startTransferToPhoneNumber() public instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use answerCall() instead of using the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use hangupOrReject() instead of the dispatcher Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Update docs Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Improve TS Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Dispatch call_state, see https://github.com/vector-im/element-web/pull/18823#issuecomment-917377277 Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-11-30 19:09:13 +01:00
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
// We placed the call in Alice's room so it should start off there
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);
let callRoomChangeEventCount = 0;
2022-12-12 12:24:14 +01:00
const roomChangePromise = new Promise<void>((resolve) => {
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
callHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, () => {
++callRoomChangeEventCount;
resolve();
});
});
// Now emit an asserted identity for Bob: this should be ignored
// because we haven't set the config option to obey asserted identity
expect(fakeCall).not.toBeNull();
fakeCall!.getRemoteAssertedIdentity = jest.fn().mockReturnValue({
id: NATIVE_BOB,
});
fakeCall!.emit(CallEvent.AssertedIdentityChanged, fakeCall!);
// Now set the config option
SdkConfig.add({
voip: {
obey_asserted_identity: true,
},
});
// ...and send another asserted identity event for a different user
fakeCall!.getRemoteAssertedIdentity = jest.fn().mockReturnValue({
id: NATIVE_CHARLIE,
});
fakeCall!.emit(CallEvent.AssertedIdentityChanged, fakeCall!);
await roomChangePromise;
callHandler.removeAllListeners();
// If everything's gone well, we should have seen only one room change
// event and the call should now be in Charlie's room.
// If it's not obeying any, the call will still be in NATIVE_ROOM_ALICE.
// If it incorrectly obeyed both asserted identity changes, either it will
// have just processed one and the call will be in the wrong room, or we'll
// have seen two room change dispatches.
expect(callRoomChangeEventCount).toEqual(1);
expect(callHandler.getCallForRoom(NATIVE_ROOM_BOB)).toBeNull();
expect(callHandler.getCallForRoom(NATIVE_ROOM_CHARLIE)).toBe(fakeCall);
});
describe("when listening to a voice broadcast", () => {
let voiceBroadcastPlayback: VoiceBroadcastPlayback;
beforeEach(() => {
voiceBroadcastPlayback = new VoiceBroadcastPlayback(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
SdkContextClass.instance.voiceBroadcastRecordingsStore,
);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation();
});
it("and placing a call should pause the broadcast", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
expect(voiceBroadcastPlayback.pause).toHaveBeenCalled();
});
});
describe("when recording a voice broadcast", () => {
beforeEach(() => {
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(
new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
),
);
});
it("and placing a call should show the info dialog", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
expect(Modal.createDialog).toMatchSnapshot();
});
});
});
2022-12-12 12:24:14 +01:00
describe("LegacyCallHandler without third party protocols", () => {
let dmRoomMap;
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
let callHandler: LegacyCallHandler;
let audioElement: HTMLAudioElement;
let fakeCall: MatrixCall | null;
beforeEach(() => {
stubClient();
fakeCall = null;
MatrixClientPeg.safeGet().createCall = (roomId) => {
if (fakeCall && fakeCall.roomId !== roomId) {
throw new Error("Only one call is supported!");
}
fakeCall = new FakeCall(roomId) as unknown as MatrixCall;
return fakeCall;
};
MatrixClientPeg.safeGet().getThirdpartyProtocols = () => {
throw new Error("Endpoint unsupported.");
};
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
callHandler = new LegacyCallHandler();
callHandler.start();
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
MatrixClientPeg.safeGet().getRoom = (roomId: string): Room | null => {
switch (roomId) {
case NATIVE_ROOM_ALICE:
return nativeRoomAlice;
}
return null;
};
dmRoomMap = {
getUserIdForRoomId: (roomId: string) => {
if (roomId === NATIVE_ROOM_ALICE) {
return NATIVE_ALICE;
} else {
return null;
}
},
getDMRoomsForUserId: (userId: string) => {
if (userId === NATIVE_ALICE) {
return [NATIVE_ROOM_ALICE];
} else {
return [];
}
},
} as DMRoomMap;
DMRoomMap.setShared(dmRoomMap);
MatrixClientPeg.safeGet().getThirdpartyUser = (_proto, _params) => {
throw new Error("Endpoint unsupported.");
};
2022-12-12 12:24:14 +01:00
audioElement = document.createElement("audio");
audioElement.id = "remoteAudio";
document.body.appendChild(audioElement);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
});
afterEach(() => {
callHandler.stop();
// @ts-ignore
DMRoomMap.setShared(null);
// @ts-ignore
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
window.mxLegacyCallHandler = null;
MatrixClientPeg.unset();
document.body.removeChild(audioElement);
SdkConfig.reset();
});
2022-12-12 12:24:14 +01:00
it("should still start a native call", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
Prepare for Element Call integration (#9224) * Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
2022-08-30 21:13:39 +02:00
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
// Check that a call was started: its room on the protocol level
// should be the virtual room
expect(fakeCall).not.toBeNull();
expect(fakeCall!.roomId).toEqual(NATIVE_ROOM_ALICE);
// but it should appear to the user to be in thw native room for Bob
expect(callHandler.roomIdForCall(fakeCall!)).toEqual(NATIVE_ROOM_ALICE);
});
2022-12-12 12:24:14 +01:00
describe("incoming calls", () => {
const roomId = "test-room-id";
const mockAudioElement = {
play: jest.fn(),
pause: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
muted: false,
} as unknown as HTMLMediaElement;
beforeEach(() => {
jest.clearAllMocks();
2022-12-12 12:24:14 +01:00
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === UIFeature.Voip);
jest.spyOn(MatrixClientPeg.safeGet(), "supportsVoip").mockReturnValue(true);
MatrixClientPeg.safeGet().isFallbackICEServerAllowed = jest.fn();
MatrixClientPeg.safeGet().prepareToEncrypt = jest.fn();
MatrixClientPeg.safeGet().pushRules = {
global: {
2022-12-12 12:24:14 +01:00
[PushRuleKind.Override]: [
{
rule_id: RuleId.IncomingCall,
default: false,
enabled: true,
actions: [
{
set_tweak: TweakName.Sound,
value: "ring",
},
],
},
],
},
};
2022-12-12 12:24:14 +01:00
jest.spyOn(document, "getElementById").mockReturnValue(mockAudioElement);
// silence local notifications by default
jest.spyOn(MatrixClientPeg.safeGet(), "getAccountData").mockImplementation((eventType) => {
if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
return new MatrixEvent({
type: eventType,
content: {
is_silenced: true,
},
});
}
});
});
2022-12-12 12:24:14 +01:00
it("should unmute <audio> before playing", () => {
// Test setup: set the audio element as muted
mockAudioElement.muted = true;
expect(mockAudioElement.muted).toStrictEqual(true);
callHandler.play(AudioID.Ring);
// Ensure audio is no longer muted
expect(mockAudioElement.muted).toStrictEqual(false);
// Ensure the audio was played
expect(mockAudioElement.play).toHaveBeenCalled();
});
2022-12-12 12:24:14 +01:00
it("listens for incoming call events when voip is enabled", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
// call added to call map
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
});
2022-12-12 12:24:14 +01:00
it("rings when incoming call state is ringing and notifications set to ring", () => {
// remove local notification silencing mock for this test
jest.spyOn(MatrixClientPeg.safeGet(), "getAccountData").mockReturnValue(undefined);
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
// call added to call map
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected, fakeCall!);
// ringer audio element started
expect(mockAudioElement.play).toHaveBeenCalled();
});
2022-12-12 12:24:14 +01:00
it("does not ring when incoming call state is ringing but local notifications are silenced", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
// call added to call map
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected, fakeCall!);
// ringer audio element started
expect(mockAudioElement.play).not.toHaveBeenCalled();
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
});
2022-12-12 12:24:14 +01:00
it("should force calls to silent when local notifications are silenced", async () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
expect(callHandler.isForcedSilent()).toEqual(true);
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
});
2022-12-12 12:24:14 +01:00
it("does not unsilence calls when local notifications are silenced", async () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
2022-12-12 12:24:14 +01:00
const callHandlerEmitSpy = jest.spyOn(callHandler, "emit");
cli.emit(CallEventHandlerEvent.Incoming, call);
// reset emit call count
callHandlerEmitSpy.mockClear();
callHandler.unSilenceCall(call.callId);
expect(callHandlerEmitSpy).not.toHaveBeenCalled();
// call still silenced
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
// ringer not played
expect(mockAudioElement.play).not.toHaveBeenCalled();
});
});
});