Refactor element call lobby + skip lobby (#12057)

* Refactor ElementCall to use the widget lobby.
 - expose skip lobby
 - use the widget.data to build the widget url

Signed-off-by: Timo K <toger5@hotmail.de>

* Use shiftKey click to skip the lobby

Signed-off-by: Timo K <toger5@hotmail.de>

* remove Lobby component

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests + remove EW lobby related tests

Signed-off-by: Timo K <toger5@hotmail.de>

* remove lobby device button tests

Signed-off-by: Timo K <toger5@hotmail.de>

* i18n

Signed-off-by: Timo K <toger5@hotmail.de>

* use voip participant label

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests
Signed-off-by: Timo K <toger5@hotmail.de>

* fix rounded corners in pip

Signed-off-by: Timo K <toger5@hotmail.de>

* allow joining call in legacy room header (without banner)

Signed-off-by: Timo K <toger5@hotmail.de>

* Introduce new connection states for calls.
And use them for integrated lobby.

Signed-off-by: Timo K <toger5@hotmail.de>

* New room header call join
Fix broken top container element call.

Signed-off-by: Timo K <toger5@hotmail.de>

* i18n

Signed-off-by: Timo K <toger5@hotmail.de>

* Fix closing element call in lobby view.
(should destroy call if there the user never managed to connect (not clicked join in lobby)

Signed-off-by: Timo K <toger5@hotmail.de>

* all cases for connection state

Signed-off-by: Timo K <toger5@hotmail.de>

* add correct LiveContentSummary labels

Signed-off-by: Timo K <toger5@hotmail.de>

* Theme widget loading (no rounded corner)
destroy call when switching room while a call is loading.

Signed-off-by: Timo K <toger5@hotmail.de>

* temp

Signed-off-by: Timo K <toger5@hotmail.de>

* usei view room dispatcher instead of emitter

Signed-off-by: Timo K <toger5@hotmail.de>

* tidy up

Signed-off-by: Timo K <toger5@hotmail.de>

* returnToLobby + remove StartCallView

Signed-off-by: Timo K <toger5@hotmail.de>

* comment cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* disconnect ongoing calls before making widget sticky.

Signed-off-by: Timo K <toger5@hotmail.de>

* linter + jitsi as videoChannel

Signed-off-by: Timo K <toger5@hotmail.de>

* stickyPromise type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix legacy call (jistsi, cisco, bbb) reopen
when clicking call button

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests and connect resolves

Signed-off-by: Timo K <toger5@hotmail.de>

* fix "waits for messaging when connecting" test

Signed-off-by: Timo K <toger5@hotmail.de>

* Allow to skip awaiting Call session events.
This option is used in tests to spare mocking the
events emitted when EC updates the room state

Signed-off-by: Timo K <toger5@hotmail.de>

* add sticky test

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for looby tile rendering

Signed-off-by: Timo K <toger5@hotmail.de>

* fix flaky test

Signed-off-by: Timo K <toger5@hotmail.de>

* add reconnect after disconnect test (video room)

Signed-off-by: Timo K <toger5@hotmail.de>

* add shift click test to call toast

Signed-off-by: Timo K <toger5@hotmail.de>

* test for allowVoipWithNoMedia in widget url

Signed-off-by: Timo K <toger5@hotmail.de>

* fix e2e tests to search for the right element

Signed-off-by: Timo K <toger5@hotmail.de>

* destroy call after test so next test does not fail

Signed-off-by: Timo K <toger5@hotmail.de>

* new call test (connection failed)

Signed-off-by: Timo K <toger5@hotmail.de>

* reset to real timers

Signed-off-by: Timo K <toger5@hotmail.de>

* dont use skipSessionAwait for tests

Signed-off-by: Timo K <toger5@hotmail.de>

* code quality (sonar)

Signed-off-by: Timo K <toger5@hotmail.de>

* refactor call.disconnect tests (dont use skipSessionAwait)

Signed-off-by: Timo K <toger5@hotmail.de>

* miscellaneous cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* only send call notify after the call has been joined (not when just opening the lobby)

Signed-off-by: Timo K <toger5@hotmail.de>

* update call notify tests to expect notify on connect.
Not on widget creation.

Signed-off-by: Timo K <toger5@hotmail.de>

* Update playwright/e2e/room/room-header.spec.ts

Co-authored-by: Robin <robin@robin.town>

* Update src/components/views/voip/CallView.tsx

Co-authored-by: Robin <robin@robin.town>

* review
rename connect -> start
isVideoRoom not dependant on feature flags
rename allOtherCallsDisconnected -> disconnectAllOtherCalls

Signed-off-by: Timo K <toger5@hotmail.de>

* check for EC widget

Signed-off-by: Timo K <toger5@hotmail.de>

* dep array

Signed-off-by: Timo K <toger5@hotmail.de>

* rename in spyOn

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
Timo 2024-01-29 17:06:12 +01:00 committed by GitHub
parent 3f7e21e08d
commit a370a5cfa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 693 additions and 767 deletions

View file

@ -140,6 +140,7 @@ const setUpWidget = (
audioMutedSpy: jest.SpyInstance<boolean, []>;
videoMutedSpy: jest.SpyInstance<boolean, []>;
} => {
call.widget.data = { ...call.widget, skipLobby: true };
const widget = new Widget(call.widget);
const eventEmitter = new EventEmitter();
@ -253,7 +254,7 @@ describe("JitsiCall", () => {
audioMutedSpy.mockReturnValue(true);
videoMutedSpy.mockReturnValue(true);
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
audioInput: null,
@ -266,7 +267,7 @@ describe("JitsiCall", () => {
audioMutedSpy.mockReturnValue(false);
videoMutedSpy.mockReturnValue(false);
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
audioInput: "Headphones",
@ -280,21 +281,63 @@ describe("JitsiCall", () => {
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
expect(call.connectionState).toBe(ConnectionState.Disconnected);
const connect = call.connect();
expect(call.connectionState).toBe(ConnectionState.Connecting);
const connect = call.start();
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
await connect;
expect(call.connectionState).toBe(ConnectionState.Connected);
});
it("doesn't stop messaging when connecting", async () => {
// Temporarily remove the messaging to simulate connecting while the
// widget is still initializing
jest.useFakeTimers();
const oldSendMock = messaging.transport.send;
mocked(messaging.transport).send.mockImplementation(async (action: string): Promise<any> => {
if (action === ElementWidgetActions.JoinCall) {
await new Promise((resolve) => setTimeout(resolve, 100));
messaging.emit(
`action:${ElementWidgetActions.JoinCall}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
}
});
expect(call.connectionState).toBe(ConnectionState.Disconnected);
const connect = call.start();
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
async function runTimers() {
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(1000);
}
async function runStopMessaging() {
await new Promise((resolve) => setTimeout(resolve, 1000));
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
}
runStopMessaging();
runTimers();
let connectError;
try {
await connect;
} catch (e) {
console.log(e);
connectError = e;
}
expect(connectError).toBeDefined();
// const connect2 = await connect;
// expect(connect2).toThrow();
messaging.transport.send = oldSendMock;
jest.useRealTimers();
});
it("fails to connect if the widget returns an error", async () => {
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
await expect(call.connect()).rejects.toBeDefined();
await expect(call.start()).rejects.toBeDefined();
});
it("fails to disconnect if the widget returns an error", async () => {
await call.connect();
await call.start();
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
await expect(call.disconnect()).rejects.toBeDefined();
});
@ -302,56 +345,55 @@ describe("JitsiCall", () => {
it("handles remote disconnection", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
const callback = jest.fn();
call.on(CallEvent.ConnectionState, callback);
messaging.emit(
`action:${ElementWidgetActions.HangupCall}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
});
it("handles instant remote disconnection when connecting", async () => {
mocked(messaging.transport).send.mockImplementation(async (action): Promise<any> => {
if (action === ElementWidgetActions.JoinCall) {
// Emit the hangup event *before* the join event to fully
// exercise the race condition
messaging.emit(
`action:${ElementWidgetActions.HangupCall}`,
new CustomEvent("widgetapirequest", { detail: {} }),
await waitFor(() => {
expect(callback).toHaveBeenNthCalledWith(1, ConnectionState.Disconnected, ConnectionState.Connected),
expect(callback).toHaveBeenNthCalledWith(
2,
ConnectionState.WidgetLoading,
ConnectionState.Disconnected,
);
messaging.emit(
`action:${ElementWidgetActions.JoinCall}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
}
return {};
expect(callback).toHaveBeenNthCalledWith(3, ConnectionState.Connecting, ConnectionState.WidgetLoading);
});
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.connect();
expect(call.connectionState).toBe(ConnectionState.Connected);
// Should disconnect on its own almost instantly
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
// in video rooms we expect the call to immediately reconnect
call.off(CallEvent.ConnectionState, callback);
});
it("disconnects", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
await call.disconnect();
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("disconnects when we leave the room", async () => {
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
room.emit(RoomEvent.MyMembership, room, "leave");
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("reconnects after disconnect in video rooms", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
await call.disconnect();
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("remains connected if we stay in the room", async () => {
await call.connect();
await call.start();
expect(call.connectionState).toBe(ConnectionState.Connected);
room.emit(RoomEvent.MyMembership, room, "join");
expect(call.connectionState).toBe(ConnectionState.Connected);
@ -377,7 +419,7 @@ describe("JitsiCall", () => {
// Now, stub out client.sendStateEvent so we can test our local echo
client.sendStateEvent.mockReset();
await call.connect();
await call.start();
expect(call.participants).toEqual(
new Map([
[alice, new Set(["alices_device"])],
@ -391,7 +433,7 @@ describe("JitsiCall", () => {
it("updates room state when connecting and disconnecting", async () => {
const now1 = Date.now();
await call.connect();
await call.start();
await waitFor(
() =>
expect(
@ -418,7 +460,7 @@ describe("JitsiCall", () => {
});
it("repeatedly updates room state while connected", async () => {
await call.connect();
await call.start();
await waitFor(
() =>
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
@ -448,11 +490,13 @@ describe("JitsiCall", () => {
const onConnectionState = jest.fn();
call.on(CallEvent.ConnectionState, onConnectionState);
await call.connect();
await call.start();
await call.disconnect();
expect(onConnectionState.mock.calls).toEqual([
[ConnectionState.Connecting, ConnectionState.Disconnected],
[ConnectionState.Connected, ConnectionState.Connecting],
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
[ConnectionState.Lobby, ConnectionState.Connecting],
[ConnectionState.Connected, ConnectionState.Lobby],
[ConnectionState.Disconnecting, ConnectionState.Connected],
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
]);
@ -464,7 +508,7 @@ describe("JitsiCall", () => {
const onParticipants = jest.fn();
call.on(CallEvent.Participants, onParticipants);
await call.connect();
await call.start();
await call.disconnect();
expect(onParticipants.mock.calls).toEqual([
[new Map([[alice, new Set(["alices_device"])]]), new Map()],
@ -477,7 +521,7 @@ describe("JitsiCall", () => {
});
it("switches to spotlight layout when the widget becomes a PiP", async () => {
await call.connect();
await call.start();
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Dock);
@ -521,7 +565,7 @@ describe("JitsiCall", () => {
});
it("doesn't clean up valid devices", async () => {
await call.connect();
await call.start();
await client.sendStateEvent(
room.roomId,
JitsiCall.MEMBER_EVENT_TYPE,
@ -582,11 +626,62 @@ describe("ElementCall", () => {
let room: Room;
let alice: RoomMember;
function setRoomMembers(memberIds: string[]) {
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
}
const callConnectProcedure: (call: ElementCall) => Promise<void> = async (call) => {
async function sessionConnect() {
await new Promise<void>((r) => {
setTimeout(() => r(), 400);
});
client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, call.roomId, {
sessionId: undefined,
} as unknown as MatrixRTCSession);
call.session?.emit(
MatrixRTCSessionEvent.MembershipsChanged,
[],
[{ sender: client.getUserId() } as CallMembership],
);
}
async function runTimers() {
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
}
sessionConnect();
const promise = call.start();
runTimers();
await promise;
};
const callDisconnectionProcedure: (call: ElementCall) => Promise<void> = async (call) => {
async function sessionDisconnect() {
await new Promise<void>((r) => {
setTimeout(() => r(), 400);
});
client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, call.roomId, {
sessionId: undefined,
} as unknown as MatrixRTCSession);
call.session?.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []);
}
async function runTimers() {
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
}
sessionDisconnect();
const promise = call.disconnect();
runTimers();
await promise;
};
beforeEach(() => {
jest.useFakeTimers();
({ client, room, alice } = setUpClientRoomAndStores());
});
afterEach(() => cleanUpClientRoomAndStores(client, room));
afterEach(() => {
jest.useRealTimers();
cleanUpClientRoomAndStores(client, room);
});
describe("get", () => {
it("finds no calls", () => {
@ -700,6 +795,28 @@ describe("ElementCall", () => {
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
expect(urlParams.get("analyticsID")).toBe("");
call.destroy();
});
it("passes feature_allow_screen_share_only_mode setting to allowVoipWithNoMedia url param", async () => {
// Now test with the preference set to true
const originalGetValue = SettingsStore.getValue;
SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => {
switch (name) {
case "feature_allow_screen_share_only_mode":
return true as T;
default:
return originalGetValue<T>(name, roomId, excludeDefault);
}
};
await ElementCall.create(room);
const call = Call.get(room);
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
expect(urlParams.get("allowVoipWithNoMedia")).toBe("true");
SettingsStore.getValue = originalGetValue;
call.destroy();
});
it("passes empty analyticsID if the id is not in the account data", async () => {
@ -729,7 +846,7 @@ describe("ElementCall", () => {
jest.useFakeTimers();
jest.setSystemTime(0);
await ElementCall.create(room);
await ElementCall.create(room, true);
const maybeCall = ElementCall.get(room);
if (maybeCall === null) throw new Error("Failed to create call");
call = maybeCall;
@ -738,41 +855,18 @@ describe("ElementCall", () => {
});
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
it("connects muted", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
audioMutedSpy.mockReturnValue(true);
videoMutedSpy.mockReturnValue(true);
await call.connect();
expect(call.connectionState).toBe(ConnectionState.Connected);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
audioInput: null,
videoInput: null,
});
});
it("connects unmuted", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
audioMutedSpy.mockReturnValue(false);
videoMutedSpy.mockReturnValue(false);
await call.connect();
expect(call.connectionState).toBe(ConnectionState.Connected);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
audioInput: "Headphones",
videoInput: "Built-in webcam",
});
});
// TODO refactor initial device configuration to use the EW settings.
// Add tests for passing EW device configuration to the widget.
it("waits for messaging when connecting", async () => {
// Temporarily remove the messaging to simulate connecting while the
// widget is still initializing
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
expect(call.connectionState).toBe(ConnectionState.Disconnected);
const connect = call.connect();
expect(call.connectionState).toBe(ConnectionState.Connecting);
const connect = callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
await connect;
@ -780,12 +874,14 @@ describe("ElementCall", () => {
});
it("fails to connect if the widget returns an error", async () => {
// we only send a JoinCall action if the widget is preloading
call.widget.data = { ...call.widget, preload: true };
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
await expect(call.connect()).rejects.toBeDefined();
await expect(call.start()).rejects.toBeDefined();
});
it("fails to disconnect if the widget returns an error", async () => {
await call.connect();
await callConnectProcedure(call);
mocked(messaging.transport).send.mockRejectedValue(new Error("never!!1! >:("));
await expect(call.disconnect()).rejects.toBeDefined();
});
@ -793,7 +889,7 @@ describe("ElementCall", () => {
it("handles remote disconnection", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.connect();
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
messaging.emit(
@ -805,35 +901,35 @@ describe("ElementCall", () => {
it("disconnects", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await call.connect();
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
await call.disconnect();
await callDisconnectionProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("disconnects when we leave the room", async () => {
await call.connect();
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
room.emit(RoomEvent.MyMembership, room, "leave");
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("remains connected if we stay in the room", async () => {
await call.connect();
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
room.emit(RoomEvent.MyMembership, room, "join");
expect(call.connectionState).toBe(ConnectionState.Connected);
});
it("disconnects if the widget dies", async () => {
await call.connect();
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("tracks layout", async () => {
await call.connect();
await callConnectProcedure(call);
expect(call.layout).toBe(Layout.Tile);
messaging.emit(
@ -850,7 +946,7 @@ describe("ElementCall", () => {
});
it("sets layout", async () => {
await call.connect();
await callConnectProcedure(call);
await call.setLayout(Layout.Spotlight);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
@ -860,13 +956,15 @@ describe("ElementCall", () => {
});
it("emits events when connection state changes", async () => {
// const wait = jest.spyOn(CallModule, "waitForEvent");
const onConnectionState = jest.fn();
call.on(CallEvent.ConnectionState, onConnectionState);
await call.connect();
await call.disconnect();
await callConnectProcedure(call);
await callDisconnectionProcedure(call);
expect(onConnectionState.mock.calls).toEqual([
[ConnectionState.Connecting, ConnectionState.Disconnected],
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
[ConnectionState.Connected, ConnectionState.Connecting],
[ConnectionState.Disconnecting, ConnectionState.Connected],
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
@ -887,7 +985,7 @@ describe("ElementCall", () => {
});
it("emits events when layout changes", async () => {
await call.connect();
await callConnectProcedure(call);
const onLayout = jest.fn();
call.on(CallEvent.Layout, onLayout);
@ -905,10 +1003,10 @@ describe("ElementCall", () => {
});
it("ends the call immediately if the session ended", async () => {
await call.connect();
await callConnectProcedure(call);
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
await callDisconnectionProcedure(call);
// this will be called automatically
// disconnect -> widget sends state event -> session manager notices no-one left
client.matrixRTC.emit(
@ -935,25 +1033,47 @@ describe("ElementCall", () => {
// should create call with perParticipantE2EE flag
ElementCall.create(room);
expect(addWidgetSpy.mock.calls[0][0].url).toContain("perParticipantE2EE=true");
ElementCall.get(room)?.destroy();
expect(Call.get(room)?.widget?.data?.perParticipantE2EE).toBe(true);
// should create call without perParticipantE2EE flag
enabledSettings.add("feature_disable_call_per_sender_encryption");
await ElementCall.create(room);
expect(Call.get(room)?.widget?.data?.perParticipantE2EE).toBe(false);
enabledSettings.delete("feature_disable_call_per_sender_encryption");
expect(addWidgetSpy.mock.calls[1][0].url).not.toContain("perParticipantE2EE=true");
client.isRoomEncrypted.mockClear();
addWidgetSpy.mockRestore();
});
it("sends notify event on connect in a room with more than two members", async () => {
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
await ElementCall.create(room);
await callConnectProcedure(Call.get(room) as ElementCall);
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
"application": "m.call",
"call_id": "",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "notify",
});
});
it("sends ring on create in a DM (two participants) room", async () => {
setRoomMembers(["@user:example.com", "@user2:example.com"]);
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
await ElementCall.create(room);
await callConnectProcedure(Call.get(room) as ElementCall);
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
"application": "m.call",
"call_id": "",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "ring",
});
});
});
describe("instance in a video room", () => {
let call: ElementCall;
let widget: Widget;
let messaging: Mocked<ClientWidgetApi>;
let audioMutedSpy: jest.SpyInstance<boolean, []>;
let videoMutedSpy: jest.SpyInstance<boolean, []>;
@ -968,49 +1088,71 @@ describe("ElementCall", () => {
if (maybeCall === null) throw new Error("Failed to create call");
call = maybeCall;
({ widget, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
});
afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));
it("doesn't end the call when the last participant leaves", async () => {
await call.connect();
await callConnectProcedure(call);
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
await callDisconnectionProcedure(call);
expect(onDestroy).not.toHaveBeenCalled();
call.off(CallEvent.Destroy, onDestroy);
});
it("connect to call with ongoing session", async () => {
// Mock membership getter used by `roomSessionForRoom`.
// This makes sure the roomSession will not be empty.
jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockImplementation(() => [
{ fakeVal: "fake membership", getMsUntilExpiry: () => 1000 } as unknown as CallMembership,
]);
// Create ongoing session
const roomSession = MatrixRTCSession.roomSessionForRoom(client, room);
const roomSessionEmitSpy = jest.spyOn(roomSession, "emit");
// Make sure the created session ends up in the call.
// `getActiveRoomSession` will be used during `call.connect`
// `getRoomSession` will be used during `Call.get`
client.matrixRTC.getActiveRoomSession.mockImplementation(() => {
return roomSession;
});
client.matrixRTC.getRoomSession.mockImplementation(() => {
return roomSession;
});
await ElementCall.create(room);
const call = Call.get(room);
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
expect(call.session).toBe(roomSession);
await callConnectProcedure(call);
expect(roomSessionEmitSpy).toHaveBeenCalledWith(
"memberships_changed",
[],
[{ sender: "@alice:example.org" }],
);
expect(call.connectionState).toBe(ConnectionState.Connected);
call.destroy();
});
it("handles remote disconnection and reconnect right after", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
await callConnectProcedure(call);
expect(call.connectionState).toBe(ConnectionState.Connected);
messaging.emit(
`action:${ElementWidgetActions.HangupCall}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
// We want the call to be connecting after the hangup.
waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connecting), { interval: 5 });
});
});
describe("create call", () => {
function setRoomMembers(memberIds: string[]) {
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
}
beforeEach(async () => {
setRoomMembers(["@user:example.com", "@user2:example.com", "@user4:example.com"]);
});
it("sends notify event on create in a room with more than two members", async () => {
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
await ElementCall.create(room);
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
"application": "m.call",
"call_id": "",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "notify",
});
});
it("sends ring on create in a DM (two participants) room", async () => {
setRoomMembers(["@user:example.com", "@user2:example.com"]);
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
await ElementCall.create(room);
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
"application": "m.call",
"call_id": "",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "ring",
});
});
it("don't sent notify event if there are existing room call members", async () => {
jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockReturnValue([
{ application: "m.call", callId: "" } as unknown as CallMembership,