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:
@@ -179,7 +179,7 @@ describe("PipContainer", () => {
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
await act(async () => {
|
||||
await call.connect();
|
||||
await call.start();
|
||||
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ describe("CallEvent", () => {
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("@alice:example.org started a video call");
|
||||
screen.getByLabelText("2 participants");
|
||||
screen.getByLabelText("2 people joined");
|
||||
|
||||
// Test that the join button works
|
||||
const dispatcherSpy = jest.fn();
|
||||
@@ -155,7 +155,7 @@ describe("CallEvent", () => {
|
||||
}),
|
||||
);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
await act(() => call.connect());
|
||||
await act(() => call.start());
|
||||
|
||||
// Test that the leave button works
|
||||
fireEvent.click(screen.getByRole("button", { name: "Leave" }));
|
||||
|
||||
@@ -31,6 +31,10 @@ import EventEmitter from "events";
|
||||
import { setupJestCanvasMock } from "jest-canvas-mock";
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
@@ -59,11 +63,11 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import WidgetStore from "../../../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
|
||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -274,6 +278,7 @@ describe("LegacyRoomHeader", () => {
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
@@ -407,6 +412,7 @@ describe("LegacyRoomHeader", () => {
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
@@ -432,6 +438,7 @@ describe("LegacyRoomHeader", () => {
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
@@ -562,7 +569,20 @@ describe("LegacyRoomHeader", () => {
|
||||
mockEnabledSettings(["feature_group_calls"]);
|
||||
|
||||
await withCall(async (call) => {
|
||||
await call.connect();
|
||||
// We set the call to skip lobby because otherwise the connection will wait until
|
||||
// the user clicks the "join" button, inside the widget lobby which is hard to mock.
|
||||
call.widget.data = { ...call.widget.data, skipLobby: true };
|
||||
// The connect method will wait until the session actually connected. Otherwise it will timeout.
|
||||
// Emitting SessionStarted will trigger the connect method to resolve.
|
||||
setTimeout(
|
||||
() =>
|
||||
client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, {
|
||||
room,
|
||||
} as MatrixRTCSession),
|
||||
100,
|
||||
);
|
||||
await call.start();
|
||||
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget))!;
|
||||
renderHeader({ viewingCall: true, activeCall: call });
|
||||
|
||||
|
||||
@@ -331,12 +331,12 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("clicking on ongoing (unpinned) call re-pins it", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||
// allow element calls
|
||||
// allow calls
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
|
||||
const widget = {};
|
||||
const widget = { eventId: "some_id_so_it_is_interpreted_as_non_virtual_widget" };
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call);
|
||||
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
@@ -405,7 +405,7 @@ describe("RoomHeader", () => {
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||
});
|
||||
|
||||
it("calls using element calls for large rooms", async () => {
|
||||
it("calls using element call for large rooms", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
|
||||
@@ -56,6 +56,7 @@ import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { ConnectionState } from "../../../../src/models/Call";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -235,20 +236,37 @@ describe("RoomTile", () => {
|
||||
renderRoomTile();
|
||||
screen.getByText("Video");
|
||||
|
||||
let completeWidgetLoading: () => void = () => {};
|
||||
const widgetLoadingCompleted = new Promise<void>((resolve) => (completeWidgetLoading = resolve));
|
||||
|
||||
// Insert an await point in the connection method so we can inspect
|
||||
// the intermediate connecting state
|
||||
let completeConnection: () => void = () => {};
|
||||
const connectionCompleted = new Promise<void>((resolve) => (completeConnection = resolve));
|
||||
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
|
||||
|
||||
let completeLobby: () => void = () => {};
|
||||
const lobbyCompleted = new Promise<void>((resolve) => (completeLobby = resolve));
|
||||
|
||||
jest.spyOn(call, "performConnection").mockImplementation(async () => {
|
||||
call.setConnectionState(ConnectionState.WidgetLoading);
|
||||
await widgetLoadingCompleted;
|
||||
call.setConnectionState(ConnectionState.Lobby);
|
||||
await lobbyCompleted;
|
||||
call.setConnectionState(ConnectionState.Connecting);
|
||||
await connectionCompleted;
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await screen.findByText("Loading…");
|
||||
completeWidgetLoading();
|
||||
await screen.findByText("Lobby");
|
||||
completeLobby();
|
||||
await screen.findByText("Joining…");
|
||||
const joinedFound = screen.findByText("Joined");
|
||||
completeConnection();
|
||||
await joinedFound;
|
||||
await screen.findByText("Joined");
|
||||
})(),
|
||||
call.connect(),
|
||||
call.start(),
|
||||
]);
|
||||
|
||||
await Promise.all([screen.findByText("Video"), call.disconnect()]);
|
||||
@@ -274,12 +292,12 @@ describe("RoomTile", () => {
|
||||
act(() => {
|
||||
call.participants = new Map([alice]);
|
||||
});
|
||||
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||
expect(screen.getByLabelText("1 person joined").textContent).toBe("1");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map([alice, bob, carol]);
|
||||
});
|
||||
expect(screen.getByLabelText("4 participants").textContent).toBe("4");
|
||||
expect(screen.getByLabelText("4 people joined").textContent).toBe("4");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map();
|
||||
|
||||
@@ -38,8 +38,6 @@ import { CallView as _CallView } from "../../../../src/components/views/voip/Cal
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import { Call, ConnectionState } from "../../../../src/models/Call";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import MediaDeviceHandler from "../../../../src/MediaDeviceHandler";
|
||||
|
||||
const CallView = wrapInMatrixClientContext(_CallView);
|
||||
|
||||
@@ -75,8 +73,10 @@ describe("CallView", () => {
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
});
|
||||
|
||||
const renderView = async (): Promise<void> => {
|
||||
render(<CallView room={room} resizing={false} waitForCall={false} />, { wrapper: TooltipProvider });
|
||||
const renderView = async (skipLobby = false): Promise<void> => {
|
||||
render(<CallView room={room} resizing={false} waitForCall={false} skipLobby={skipLobby} />, {
|
||||
wrapper: TooltipProvider,
|
||||
});
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
@@ -108,20 +108,6 @@ describe("CallView", () => {
|
||||
expect(cleanSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
|
||||
it("only shows widget when connected", async () => {
|
||||
await renderView();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
|
||||
expect(screen.queryByRole("button", { name: "Join" })).toBe(null);
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: Fix I do not understand this test
|
||||
*/
|
||||
@@ -166,40 +152,17 @@ describe("CallView", () => {
|
||||
expectAvatars([]);
|
||||
});
|
||||
|
||||
it("connects to the call when the join button is pressed", async () => {
|
||||
await renderView();
|
||||
const connectSpy = jest.spyOn(call, "connect");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
it("automatically connects to the call when skipLobby is true", async () => {
|
||||
const connectSpy = jest.spyOn(call, "start");
|
||||
await renderView(true);
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
|
||||
});
|
||||
|
||||
it("disables join button when the participant limit has been exceeded", async () => {
|
||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
|
||||
SdkConfig.put({
|
||||
element_call: { participant_limit: 2, url: "", use_exclusively: false, brand: "Element Call" },
|
||||
});
|
||||
call.participants = new Map([
|
||||
[bob, new Set("b")],
|
||||
[carol, new Set("c")],
|
||||
]);
|
||||
|
||||
await renderView();
|
||||
const connectSpy = jest.spyOn(call, "connect");
|
||||
const joinButton = screen.getByRole("button", { name: "Join" });
|
||||
expect(joinButton).toHaveAttribute("aria-disabled", "true");
|
||||
fireEvent.click(joinButton);
|
||||
await waitFor(() => expect(connectSpy).not.toHaveBeenCalled(), { interval: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("without an existing call", () => {
|
||||
it("creates and connects to a new call when the join button is pressed", async () => {
|
||||
await renderView();
|
||||
expect(Call.get(room)).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await renderView(true);
|
||||
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
|
||||
const call = CallStore.instance.getCall(room.roomId)!;
|
||||
|
||||
@@ -214,117 +177,4 @@ describe("CallView", () => {
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device buttons", () => {
|
||||
const fakeVideoInput1: MediaDeviceInfo = {
|
||||
deviceId: "v1",
|
||||
groupId: "v1",
|
||||
label: "Webcam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
};
|
||||
const fakeVideoInput2: MediaDeviceInfo = {
|
||||
deviceId: "v2",
|
||||
groupId: "v2",
|
||||
label: "Othercam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
};
|
||||
const fakeAudioInput1: MediaDeviceInfo = {
|
||||
deviceId: "v1",
|
||||
groupId: "v1",
|
||||
label: "Headphones",
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
};
|
||||
const fakeAudioInput2: MediaDeviceInfo = {
|
||||
deviceId: "v2",
|
||||
groupId: "v2",
|
||||
label: "Tailphones",
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
};
|
||||
|
||||
it("hide when no devices are available", async () => {
|
||||
await renderView();
|
||||
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
|
||||
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
|
||||
});
|
||||
|
||||
it("hide when no access to device list", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockRejectedValue("permission denied");
|
||||
await renderView();
|
||||
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
|
||||
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
|
||||
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
|
||||
});
|
||||
|
||||
it("hide when unknown error with device list", async () => {
|
||||
const originalGetDevices = MediaDeviceHandler.getDevices;
|
||||
MediaDeviceHandler.getDevices = () => Promise.reject("unknown error");
|
||||
await renderView();
|
||||
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
|
||||
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
|
||||
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
|
||||
MediaDeviceHandler.getDevices = originalGetDevices;
|
||||
});
|
||||
|
||||
it("show without dropdown when only one device is available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1]);
|
||||
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: /camera/ });
|
||||
expect(screen.queryByRole("button", { name: "Video devices" })).toBe(null);
|
||||
});
|
||||
|
||||
it("show with dropdown when multiple devices are available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeAudioInput1, fakeAudioInput2]);
|
||||
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: /microphone/ });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Audio devices" }));
|
||||
screen.getByRole("menuitem", { name: "Headphones" });
|
||||
screen.getByRole("menuitem", { name: "Tailphones" });
|
||||
});
|
||||
|
||||
it("sets video device when selected", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1, fakeVideoInput2]);
|
||||
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: /camera/ });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Video devices" }));
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: fakeVideoInput2.label }));
|
||||
|
||||
expect(client.getMediaHandler().setVideoInput).toHaveBeenCalledWith(fakeVideoInput2.deviceId);
|
||||
});
|
||||
|
||||
it("sets audio device when selected", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeAudioInput1, fakeAudioInput2]);
|
||||
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: /microphone/ });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Audio devices" }));
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: fakeAudioInput2.label }));
|
||||
|
||||
expect(client.getMediaHandler().setAudioInput).toHaveBeenCalledWith(fakeAudioInput2.deviceId);
|
||||
});
|
||||
|
||||
it("set media muted if no access to audio device", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeAudioInput1, fakeAudioInput2]);
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
|
||||
await renderView();
|
||||
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
|
||||
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("set media muted if no access to video device", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([fakeVideoInput1, fakeVideoInput2]);
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockRejectedValue("permission rejected");
|
||||
await renderView();
|
||||
expect(MediaDeviceHandler.startWithAudioMuted).toBeTruthy();
|
||||
expect(MediaDeviceHandler.startWithVideoMuted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+264
-122
@@ -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,
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("Algorithm", () => {
|
||||
// End of setup
|
||||
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
await call.connect();
|
||||
await call.start();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
|
||||
await call.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
|
||||
@@ -17,7 +17,8 @@ limitations under the License.
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { last } from "lodash";
|
||||
import { MatrixEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientWidgetApi } from "matrix-widget-api";
|
||||
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
|
||||
import { stubClient, mkRoom, mkEvent } from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
@@ -25,6 +26,7 @@ import { StopGapWidget } from "../../../src/stores/widgets/StopGapWidget";
|
||||
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
|
||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../src/voice-broadcast";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import ActiveWidgetStore from "../../../src/stores/ActiveWidgetStore";
|
||||
|
||||
jest.mock("matrix-widget-api/lib/ClientWidgetApi");
|
||||
|
||||
@@ -114,3 +116,68 @@ describe("StopGapWidget", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("StopGapWidget with stickyPromise", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let widget: StopGapWidget;
|
||||
let messaging: MockedObject<ClientWidgetApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
widget.stopMessaging();
|
||||
});
|
||||
it("should wait for the sticky promise to resolve before starting messaging", async () => {
|
||||
jest.useFakeTimers();
|
||||
const getStickyPromise = async () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
widget = new StopGapWidget({
|
||||
app: {
|
||||
id: "test",
|
||||
creatorUserId: "@alice:example.org",
|
||||
type: "example",
|
||||
url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url",
|
||||
roomId: "!1:example.org",
|
||||
},
|
||||
room: mkRoom(client, "!1:example.org"),
|
||||
userId: "@alice:example.org",
|
||||
creatorUserId: "@alice:example.org",
|
||||
waitForIframeLoad: true,
|
||||
userWidget: false,
|
||||
stickyPromise: getStickyPromise,
|
||||
});
|
||||
|
||||
const setPersistenceSpy = jest.spyOn(ActiveWidgetStore.instance, "setWidgetPersistence");
|
||||
|
||||
// Start messaging without an iframe, since ClientWidgetApi is mocked
|
||||
widget.startMessaging(null as unknown as HTMLIFrameElement);
|
||||
const emitSticky = async () => {
|
||||
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
|
||||
messaging?.hasCapability.mockReturnValue(true);
|
||||
// messaging.transport.reply will be called but transport is undefined in this test environment
|
||||
// This just makes sure the call doesn't throw
|
||||
Object.defineProperty(messaging, "transport", { value: { reply: () => {} } });
|
||||
messaging.on.mock.calls.find(([event, listener]) => {
|
||||
if (event === `action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`) {
|
||||
listener({ preventDefault: () => {}, detail: { data: { value: true } } });
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
await emitSticky();
|
||||
expect(setPersistenceSpy).not.toHaveBeenCalled();
|
||||
// Advance the fake timer so that the sticky promise resolves
|
||||
jest.runAllTimers();
|
||||
// Use a real timer and wait for the next tick so the sticky promise can resolve
|
||||
jest.useRealTimers();
|
||||
|
||||
waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("IncomingCallEvent", () => {
|
||||
|
||||
screen.getByText("Video call started");
|
||||
screen.getByText("Video");
|
||||
screen.getByLabelText("3 participants");
|
||||
screen.getByLabelText("3 people joined");
|
||||
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getByRole("button", { name: "Close" });
|
||||
@@ -167,6 +167,30 @@ describe("IncomingCallEvent", () => {
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: false,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(toastStore.dismissToast).toHaveBeenCalledWith(
|
||||
getIncomingCallToastKey(notifyContent.call_id, room.roomId),
|
||||
),
|
||||
);
|
||||
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
});
|
||||
it("skips lobby when using shift key click", async () => {
|
||||
renderToast();
|
||||
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }), { shiftKey: true });
|
||||
await waitFor(() =>
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
skipLobby: true,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user