Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Write more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Sep 26, 2022
1 parent 285e950 commit 3089b6e
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 75 deletions.
159 changes: 93 additions & 66 deletions test/components/views/voip/CallView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { CallView as _CallView } from "../../../../src/components/views/voip/CallView";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../src/stores/CallStore";
import { ConnectionState } from "../../../../src/models/Call";
import { Call, ConnectionState } from "../../../../src/models/Call";

const CallView = wrapInMatrixClientContext(_CallView);

Expand All @@ -53,8 +53,6 @@ describe("CallLobby", () => {

let client: Mocked<MatrixClient>;
let room: Room;
let call: MockedCall;
let widget: Widget;
let alice: RoomMember;

beforeEach(() => {
Expand All @@ -75,79 +73,117 @@ describe("CallLobby", () => {

setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);

MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;

widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
});

afterEach(() => {
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});

const renderView = async (): Promise<void> => {
render(<CallView room={room} resizing={false} waitForCall={true} />);
render(<CallView room={room} resizing={false} waitForCall={false} />);
await act(() => Promise.resolve()); // Let effects settle
};

it("calls clean on mount", async () => {
const cleanSpy = jest.spyOn(call, "clean");
await renderView();
expect(cleanSpy).toHaveBeenCalled();
});
describe("with an existing call", () => {
let call: MockedCall;
let widget: Widget;

it("shows lobby and keeps widget loaded when disconnected", async () => {
await renderView();
screen.getByRole("button", { name: "Join" });
screen.getAllByText(/\bwidget\b/i);
});
beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;

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);
});
widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
});

afterEach(() => {
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});

it("calls clean on mount", async () => {
const cleanSpy = jest.spyOn(call, "clean");
await renderView();
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);
});

it("tracks participants", async () => {
const bob = mkRoomMember(room.roomId, "@bob:example.org");
const carol = mkRoomMember(room.roomId, "@carol:example.org");
it("tracks participants", async () => {
const bob = mkRoomMember(room.roomId, "@bob:example.org");
const carol = mkRoomMember(room.roomId, "@carol:example.org");

const expectAvatars = (userIds: string[]) => {
const avatars = screen.queryAllByRole("button", { name: "Avatar" });
expect(userIds.length).toBe(avatars.length);
const expectAvatars = (userIds: string[]) => {
const avatars = screen.queryAllByRole("button", { name: "Avatar" });
expect(userIds.length).toBe(avatars.length);

for (const [userId, avatar] of zip(userIds, avatars)) {
fireEvent.focus(avatar!);
screen.getByRole("tooltip", { name: userId });
}
};
for (const [userId, avatar] of zip(userIds, avatars)) {
fireEvent.focus(avatar!);
screen.getByRole("tooltip", { name: userId });
}
};

await renderView();
expect(screen.queryByLabelText(/joined/)).toBe(null);
expectAvatars([]);
await renderView();
expect(screen.queryByLabelText(/joined/)).toBe(null);
expectAvatars([]);

act(() => { call.participants = new Set([alice]); });
screen.getByText("1 person joined");
expectAvatars([alice.userId]);
act(() => { call.participants = new Set([alice]); });
screen.getByText("1 person joined");
expectAvatars([alice.userId]);

act(() => { call.participants = new Set([alice, bob, carol]); });
screen.getByText("3 people joined");
expectAvatars([alice.userId, bob.userId, carol.userId]);
act(() => { call.participants = new Set([alice, bob, carol]); });
screen.getByText("3 people joined");
expectAvatars([alice.userId, bob.userId, carol.userId]);

act(() => { call.participants = new Set(); });
expect(screen.queryByLabelText(/joined/)).toBe(null);
expectAvatars([]);
act(() => { call.participants = new Set(); });
expect(screen.queryByLabelText(/joined/)).toBe(null);
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" }));
await waitFor(() => expect(connectSpy).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 waitFor(() => expect(CallStore.instance.get(room.roomId)).not.toBeNull());
const call = CallStore.instance.get(room.roomId)!;

const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));

cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});
});

describe("device buttons", () => {
Expand Down Expand Up @@ -196,13 +232,4 @@ describe("CallLobby", () => {
screen.getByRole("menuitem", { name: "Audio input 2" });
});
});

describe("join button", () => {
it("works", async () => {
await renderView();
const connectSpy = jest.spyOn(call, "connect");
fireEvent.click(screen.getByRole("button", { name: "Join" }));
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
});
});
});
110 changes: 102 additions & 8 deletions test/models/Call-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
jest.spyOn(MediaDeviceHandler, "getAudioInput").mockReturnValue("1");
jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");

jest.spyOn(SettingsStore, "getValue").mockImplementation(settingName =>
settingName === "feature_video_rooms" || settingName === "feature_element_call_video_rooms" ? true : undefined,
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
jest.spyOn(SettingsStore, "getValue").mockImplementation(
settingName => enabledSettings.has(settingName) || undefined,
);

const setUpClientRoomAndStores = (roomType: RoomType): {
const setUpClientRoomAndStores = (): {
client: Mocked<MatrixClient>;
room: Room;
alice: RoomMember;
Expand All @@ -68,7 +69,6 @@ const setUpClientRoomAndStores = (roomType: RoomType): {
const room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(room, "getType").mockReturnValue(roomType);

const alice = mkRoomMember(room.roomId, "@alice:example.org");
const bob = mkRoomMember(room.roomId, "@bob:example.org");
Expand Down Expand Up @@ -165,7 +165,8 @@ describe("JitsiCall", () => {
let carol: RoomMember;

beforeEach(() => {
({ client, room, alice, bob, carol } = setUpClientRoomAndStores(RoomType.ElementVideo));
({ client, room, alice, bob, carol } = setUpClientRoomAndStores());
jest.spyOn(room, "getType").mockReturnValue(RoomType.ElementVideo);
});

afterEach(() => cleanUpClientRoomAndStores(client, room));
Expand All @@ -191,7 +192,7 @@ describe("JitsiCall", () => {
});
});

describe("instance", () => {
describe("instance in a video room", () => {
let call: JitsiCall;
let widget: Widget;
let messaging: Mocked<ClientWidgetApi>;
Expand Down Expand Up @@ -542,7 +543,7 @@ describe("ElementCall", () => {
let carol: RoomMember;

beforeEach(() => {
({ client, room, alice, bob, carol } = setUpClientRoomAndStores(RoomType.UnstableCall));
({ client, room, alice, bob, carol } = setUpClientRoomAndStores());
});

afterEach(() => cleanUpClientRoomAndStores(client, room));
Expand All @@ -569,7 +570,7 @@ describe("ElementCall", () => {
});
});

describe("instance", () => {
describe("instance in a non-video room", () => {
let call: ElementCall;
let widget: Widget;
let messaging: Mocked<ClientWidgetApi>;
Expand All @@ -590,6 +591,10 @@ describe("ElementCall", () => {

afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));

it("has intent m.prompt", () => {
expect(call.groupCall.getContent()["m.intent"]).toBe("m.prompt");
});

it("connects muted", async () => {
expect(call.connectionState).toBe(ConnectionState.Disconnected);
audioMutedSpy.mockReturnValue(true);
Expand Down Expand Up @@ -747,6 +752,59 @@ describe("ElementCall", () => {
expect(events).toEqual([new Set([alice]), new Set()]);
});

it("ends the call immediately if we're the last participant to leave", async () => {
await call.connect();
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
expect(onDestroy).toHaveBeenCalled();
call.off(CallEvent.Destroy, onDestroy);
});

it("ends the call after a random delay if the last participant leaves without ending it", async () => {
// Bob connects
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
{
"m.expires_ts": 1000 * 60 * 10,
"m.calls": [{
"m.call_id": call.groupCall.getStateKey()!,
"m.devices": [{ device_id: "bobweb", session_id: "1", feeds: [] }],
}],
},
bob.userId,
);

const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);

// Bob disconnects
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
{
"m.expires_ts": 1000 * 60 * 10,
"m.calls": [{
"m.call_id": call.groupCall.getStateKey()!,
"m.devices": [],
}],
},
bob.userId,
);

// Nothing should happen for at least a second, to give Bob a chance
// to end the call on his own
jest.advanceTimersByTime(1000);
expect(onDestroy).not.toHaveBeenCalled();

// Within 10 seconds, our client should end the call on behalf of Bob
jest.advanceTimersByTime(9000);
expect(onDestroy).toHaveBeenCalled();

call.off(CallEvent.Destroy, onDestroy);
});

describe("clean", () => {
const aliceWeb: IMyDevice = {
device_id: "aliceweb",
Expand Down Expand Up @@ -848,4 +906,40 @@ describe("ElementCall", () => {
});
});
});

describe("instance in a video room", () => {
let call: ElementCall;
let widget: Widget;
let audioMutedSpy: jest.SpyInstance<boolean, []>;
let videoMutedSpy: jest.SpyInstance<boolean, []>;

beforeEach(async () => {
jest.useFakeTimers();
jest.setSystemTime(0);

jest.spyOn(room, "getType").mockReturnValue(RoomType.UnstableCall);

await ElementCall.create(room);
const maybeCall = ElementCall.get(room);
if (maybeCall === null) throw new Error("Failed to create call");
call = maybeCall;

({ widget, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
});

afterEach(() => cleanUpCallAndWidget(call, widget, audioMutedSpy, videoMutedSpy));

it("has intent m.room", () => {
expect(call.groupCall.getContent()["m.intent"]).toBe("m.room");
});

it("doesn't end the call when the last participant leaves", async () => {
await call.connect();
const onDestroy = jest.fn();
call.on(CallEvent.Destroy, onDestroy);
await call.disconnect();
expect(onDestroy).not.toHaveBeenCalled();
call.off(CallEvent.Destroy, onDestroy);
});
});
});
4 changes: 3 additions & 1 deletion test/test-utils/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { MatrixWidgetType } from "matrix-widget-api";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { mkEvent } from "./test-utils";
import { Call } from "../../src/models/Call";
import { Call, ElementCall, JitsiCall } from "../../src/models/Call";

export class MockedCall extends Call {
private static EVENT_TYPE = "org.example.mocked_call";
Expand Down Expand Up @@ -91,4 +91,6 @@ export class MockedCall extends Call {
*/
export const useMockedCalls = () => {
Call.get = room => MockedCall.get(room);
JitsiCall.create = async room => MockedCall.create(room, "1");
ElementCall.create = async room => MockedCall.create(room, "1");
};

0 comments on commit 3089b6e

Please sign in to comment.