mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
All the test bits and pieces
This commit is contained in:
@@ -5,47 +5,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
MockRoom,
|
||||
MockRTCSession,
|
||||
TestReactionsWrapper,
|
||||
} from "../utils/testReactions";
|
||||
import { MockRoom } from "../utils/testReactions";
|
||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||
import { ElementCallReactionEventType } from "../reactions";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
||||
import { MockRTCSession } from "../utils/test";
|
||||
import { ReactionsProvider } from "../useReactions";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberEventAlice = "$membership-alice:example.org";
|
||||
|
||||
const membership: Record<string, string> = {
|
||||
[memberEventAlice]: memberUserIdAlice,
|
||||
};
|
||||
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
||||
|
||||
function TestComponent({
|
||||
rtcSession,
|
||||
vm,
|
||||
}: {
|
||||
rtcSession: MockRTCSession;
|
||||
vm: CallViewModel;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<ReactionToggleButton userId={memberUserIdAlice} />
|
||||
</TestReactionsWrapper>
|
||||
<ReactionsProvider rtcSession={rtcSession as unknown as MatrixRTCSession}>
|
||||
<ReactionToggleButton vm={vm} identifier={localIdent} />
|
||||
</ReactionsProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
test("Can open menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -53,40 +51,68 @@ test("Can open menu", async () => {
|
||||
|
||||
test("Can raise hand", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.raise_hand"));
|
||||
expect(room.testSentEvents).toEqual([
|
||||
[
|
||||
undefined,
|
||||
"m.reaction",
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: memberEventAlice,
|
||||
key: "🖐️",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
"m.reaction",
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: localRtcMember.eventId,
|
||||
key: "🖐️",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
],
|
||||
]);
|
||||
},
|
||||
);
|
||||
await act(() => {
|
||||
vm.updateReactions({
|
||||
raisedHands: {
|
||||
[localIdent]: new Date(),
|
||||
},
|
||||
reactions: {},
|
||||
});
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("Can lower hand", async () => {
|
||||
test.only("Can lower hand", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
const reactionEvent = room.testSendHandRaise(memberEventAlice, membership);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.raise_hand"));
|
||||
await act(() => {
|
||||
vm.updateReactions({
|
||||
raisedHands: {
|
||||
[localIdent]: new Date(),
|
||||
},
|
||||
reactions: {},
|
||||
});
|
||||
});
|
||||
await user.click(getByLabelText("action.lower_hand"));
|
||||
expect(room.testRedactedEvents).toEqual([[undefined, reactionEvent]]);
|
||||
expect(rtcSession.room.client.redactEvent).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
"m.reaction",
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: localRtcMember.eventId,
|
||||
key: "🖐️",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
},
|
||||
);
|
||||
await act(() => {
|
||||
vm.updateReactions({
|
||||
raisedHands: {},
|
||||
reactions: {},
|
||||
});
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -137,9 +137,9 @@ exports[`Can raise hand 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r1j:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="secondary"
|
||||
aria-labelledby=":r0:"
|
||||
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -153,9 +153,7 @@ exports[`Can raise hand 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
fill-rule="evenodd"
|
||||
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -14,44 +14,24 @@ import {
|
||||
test,
|
||||
vitest,
|
||||
} from "vitest";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { ConnectionState } from "livekit-client";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { afterEach } from "node:test";
|
||||
import { act, ReactNode } from "react";
|
||||
import {
|
||||
CallMembership,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { act } from "react";
|
||||
import { CallMembership } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import {
|
||||
mockLivekitRoom,
|
||||
mockLocalParticipant,
|
||||
mockMatrixRoom,
|
||||
mockMatrixRoomMember,
|
||||
mockRemoteParticipant,
|
||||
mockRtcMembership,
|
||||
MockRTCSession,
|
||||
} from "../utils/test";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { mockRtcMembership } from "../utils/test";
|
||||
import {
|
||||
CallEventAudioRenderer,
|
||||
MAX_PARTICIPANT_COUNT_FOR_SOUND,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { TestReactionsWrapper } from "../utils/testReactions";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
|
||||
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
||||
const local = mockMatrixRoomMember(localRtcMember);
|
||||
const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA");
|
||||
const alice = mockMatrixRoomMember(aliceRtcMember);
|
||||
const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
||||
const localParticipant = mockLocalParticipant({ identity: "" });
|
||||
const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`;
|
||||
const aliceParticipant = mockRemoteParticipant({ identity: aliceId });
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import {
|
||||
alice,
|
||||
aliceRtcMember,
|
||||
bobRtcMember,
|
||||
local,
|
||||
} from "../utils/test-fixtures";
|
||||
|
||||
vitest.mock("../useAudioContext");
|
||||
vitest.mock("../soundUtils");
|
||||
@@ -78,66 +58,6 @@ beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
function TestComponent({
|
||||
rtcSession,
|
||||
vm,
|
||||
}: {
|
||||
rtcSession: MockRTCSession;
|
||||
vm: CallViewModel;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<TestReactionsWrapper
|
||||
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
||||
>
|
||||
<CallEventAudioRenderer vm={vm} />
|
||||
</TestReactionsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function getMockEnv(
|
||||
members: RoomMember[],
|
||||
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
|
||||
): {
|
||||
vm: CallViewModel;
|
||||
session: MockRTCSession;
|
||||
remoteRtcMemberships: BehaviorSubject<CallMembership[]>;
|
||||
} {
|
||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||
const remoteParticipants = of([aliceParticipant]);
|
||||
const liveKitRoom = mockLivekitRoom(
|
||||
{ localParticipant },
|
||||
{ remoteParticipants },
|
||||
);
|
||||
const matrixRoom = mockMatrixRoom({
|
||||
client: {
|
||||
getUserId: () => localRtcMember.sender,
|
||||
getDeviceId: () => localRtcMember.deviceId,
|
||||
on: vitest.fn(),
|
||||
off: vitest.fn(),
|
||||
} as Partial<MatrixClient> as MatrixClient,
|
||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||
});
|
||||
|
||||
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>(
|
||||
initialRemoteRtcMemberships,
|
||||
);
|
||||
|
||||
const session = new MockRTCSession(
|
||||
matrixRoom,
|
||||
localRtcMember,
|
||||
).withMemberships(remoteRtcMemberships);
|
||||
|
||||
const vm = new CallViewModel(
|
||||
session as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
of(ConnectionState.Connected),
|
||||
);
|
||||
return { vm, session, remoteRtcMemberships };
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to play a sound when loading the call state
|
||||
* because typically this occurs in two stages. We first join
|
||||
@@ -146,8 +66,12 @@ function getMockEnv(
|
||||
* a noise every time.
|
||||
*/
|
||||
test("plays one sound when entering a call", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
// Joining a call usually means remote participants are added later.
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
||||
@@ -155,10 +79,12 @@ test("plays one sound when entering a call", () => {
|
||||
expect(playSound).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
// TODO: Same test?
|
||||
test("plays a sound when a user joins", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
||||
@@ -168,8 +94,11 @@ test("plays a sound when a user joins", () => {
|
||||
});
|
||||
|
||||
test("plays a sound when a user leaves", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([]);
|
||||
@@ -185,12 +114,12 @@ test("plays no sound when the participant list is more than the maximum size", (
|
||||
);
|
||||
}
|
||||
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv(
|
||||
const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment(
|
||||
[local, alice],
|
||||
mockRtcMemberships,
|
||||
);
|
||||
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
expect(playSound).not.toBeCalled();
|
||||
act(() => {
|
||||
remoteRtcMemberships.next(
|
||||
@@ -201,9 +130,9 @@ test("plays no sound when the participant list is more than the maximum size", (
|
||||
});
|
||||
|
||||
test("plays one sound when a hand is raised", () => {
|
||||
const { session, vm } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
// Joining a call usually means remote participants are added later.
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
raisedHands: {
|
||||
@@ -216,9 +145,9 @@ test("plays one sound when a hand is raised", () => {
|
||||
});
|
||||
|
||||
test("should not play a sound when a hand raise is retracted", () => {
|
||||
const { session, vm } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
// Joining a call usually means remote participants are added later.
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
raisedHands: {
|
||||
|
||||
@@ -7,45 +7,19 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { act, ReactNode } from "react";
|
||||
import { act } from "react";
|
||||
import { afterEach } from "node:test";
|
||||
|
||||
import {
|
||||
MockRoom,
|
||||
MockRTCSession,
|
||||
TestReactionsWrapper,
|
||||
} from "../utils/testReactions";
|
||||
import { showReactions } from "../settings/settings";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import { ReactionSet } from "../reactions";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberUserIdBob = "@bob:example.org";
|
||||
const memberUserIdCharlie = "@charlie:example.org";
|
||||
const memberEventAlice = "$membership-alice:example.org";
|
||||
const memberEventBob = "$membership-bob:example.org";
|
||||
const memberEventCharlie = "$membership-charlie:example.org";
|
||||
|
||||
const membership: Record<string, string> = {
|
||||
[memberEventAlice]: memberUserIdAlice,
|
||||
[memberEventBob]: memberUserIdBob,
|
||||
[memberEventCharlie]: memberUserIdCharlie,
|
||||
};
|
||||
|
||||
function TestComponent({
|
||||
rtcSession,
|
||||
}: {
|
||||
rtcSession: MockRTCSession;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<ReactionsOverlay />
|
||||
</TestReactionsWrapper>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
import {
|
||||
local,
|
||||
alice,
|
||||
aliceRtcMember,
|
||||
bobRtcMember,
|
||||
} from "../utils/test-fixtures";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
|
||||
afterEach(() => {
|
||||
showReactions.setValue(showReactions.defaultValue);
|
||||
@@ -53,22 +27,21 @@ afterEach(() => {
|
||||
|
||||
test("defaults to showing no reactions", () => {
|
||||
showReactions.setValue(true);
|
||||
const rtcSession = new MockRTCSession(
|
||||
new MockRoom(memberUserIdAlice),
|
||||
membership,
|
||||
);
|
||||
const { container } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { container } = render(<ReactionsOverlay vm={vm} />);
|
||||
expect(container.getElementsByTagName("span")).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("shows a reaction when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
const reaction = ReactionSet[0];
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { getByRole } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, reaction, membership);
|
||||
vm.updateReactions({
|
||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
||||
raisedHands: {},
|
||||
});
|
||||
});
|
||||
const span = getByRole("presentation");
|
||||
expect(getByRole("presentation")).toBeTruthy();
|
||||
@@ -78,29 +51,33 @@ test("shows a reaction when sent", () => {
|
||||
test("shows two of the same reaction when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const reaction = ReactionSet[0];
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, reaction, membership);
|
||||
});
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventBob, reaction, membership);
|
||||
vm.updateReactions({
|
||||
reactions: {
|
||||
[aliceRtcMember.deviceId]: reaction,
|
||||
[bobRtcMember.deviceId]: reaction,
|
||||
},
|
||||
raisedHands: {},
|
||||
});
|
||||
});
|
||||
expect(getAllByRole("presentation")).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("shows two different reactions when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const [reactionA, reactionB] = ReactionSet;
|
||||
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, reactionA, membership);
|
||||
});
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventBob, reactionB, membership);
|
||||
vm.updateReactions({
|
||||
reactions: {
|
||||
[aliceRtcMember.deviceId]: reactionA,
|
||||
[bobRtcMember.deviceId]: reactionB,
|
||||
},
|
||||
raisedHands: {},
|
||||
});
|
||||
});
|
||||
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
||||
expect(reactionElementA.innerHTML).toEqual(reactionA.emoji);
|
||||
@@ -110,11 +87,13 @@ test("shows two different reactions when sent", () => {
|
||||
test("hides reactions when reaction animations are disabled", () => {
|
||||
showReactions.setValue(false);
|
||||
const reaction = ReactionSet[0];
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { container } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, reaction, membership);
|
||||
vm.updateReactions({
|
||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
||||
raisedHands: {},
|
||||
});
|
||||
});
|
||||
const { container } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
expect(container.getElementsByTagName("span")).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, vi, onTestFinished, it } from "vitest";
|
||||
import { test, vi, onTestFinished, it, vitest } from "vitest";
|
||||
import {
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
@@ -684,3 +685,64 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add presenters and speakers?
|
||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// There should always be one tile for each MatrixRTCSession
|
||||
const expectedLayoutMarbles = "a";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
schedule("ah", {
|
||||
a: () => {
|
||||
// We imagine that only three tiles (the first three) will be visible
|
||||
// on screen at a time
|
||||
vm.layout.subscribe((layout) => {
|
||||
console.log(layout);
|
||||
if (layout.type === "grid") {
|
||||
for (let i = 0; i < layout.grid.length; i++)
|
||||
layout.grid[i].setVisible(i <= 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
h: () => {
|
||||
vm.updateReactions({
|
||||
reactions: {},
|
||||
raisedHands: {
|
||||
[`${bobRtcMember.sender}:${bobRtcMember.deviceId}`]: new Date(),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
"@bob:example.org:BBBB:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
],
|
||||
},
|
||||
h: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
"@bob:example.org:BBBB:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
switchMap,
|
||||
switchScan,
|
||||
take,
|
||||
tap,
|
||||
timer,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
@@ -635,13 +636,14 @@ export class CallViewModel extends ViewModel {
|
||||
[
|
||||
m.speaker,
|
||||
m.presenter,
|
||||
m.vm.handRaised,
|
||||
m.vm.videoEnabled,
|
||||
m.vm instanceof LocalUserMediaViewModel
|
||||
? m.vm.alwaysShow
|
||||
: of(false),
|
||||
m.vm.handRaised,
|
||||
],
|
||||
(speaker, presenter, handRaised, video, alwaysShow) => {
|
||||
(speaker, presenter, video, alwaysShow, handRaised) => {
|
||||
console.log(m.vm.id, handRaised);
|
||||
let bin: SortingBin;
|
||||
if (m.vm.local)
|
||||
bin = alwaysShow
|
||||
@@ -664,6 +666,12 @@ export class CallViewModel extends ViewModel {
|
||||
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
|
||||
);
|
||||
}),
|
||||
tap((v) =>
|
||||
console.log(
|
||||
"final grid",
|
||||
v.map((v) => v.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
private readonly spotlight: Observable<MediaViewModel[]> =
|
||||
@@ -1116,10 +1124,12 @@ export class CallViewModel extends ViewModel {
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
private readonly handsRaisedSubject = new Subject<Record<string, Date>>();
|
||||
private readonly reactionsSubject = new Subject<
|
||||
private readonly handsRaisedSubject = new BehaviorSubject<
|
||||
Record<string, Date>
|
||||
>({});
|
||||
private readonly reactionsSubject = new BehaviorSubject<
|
||||
Record<string, ReactionOption>
|
||||
>();
|
||||
>({});
|
||||
|
||||
public readonly handsRaised = this.handsRaisedSubject.asObservable();
|
||||
public readonly reactions = this.reactionsSubject.asObservable();
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
createHandRaisedReaction,
|
||||
createRedaction,
|
||||
MockRoom,
|
||||
MockRTCSession,
|
||||
ReactionsMockRTCSession,
|
||||
TestReactionsWrapper,
|
||||
} from "./utils/testReactions";
|
||||
|
||||
@@ -55,7 +55,7 @@ const TestComponent: FC = () => {
|
||||
|
||||
describe("useReactions", () => {
|
||||
test("starts with an empty list", () => {
|
||||
const rtcSession = new MockRTCSession(
|
||||
const rtcSession = new ReactionsMockRTCSession(
|
||||
new MockRoom(memberUserIdAlice),
|
||||
membership,
|
||||
);
|
||||
@@ -68,7 +68,7 @@ describe("useReactions", () => {
|
||||
});
|
||||
test("handles incoming raised hand", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -81,7 +81,7 @@ describe("useReactions", () => {
|
||||
});
|
||||
test("handles incoming unraised hand", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -105,7 +105,7 @@ describe("useReactions", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -119,7 +119,7 @@ describe("useReactions", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -133,7 +133,7 @@ describe("useReactions", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -151,7 +151,7 @@ describe("useReactions", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, memberUserIdBob),
|
||||
]);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
@@ -161,7 +161,7 @@ describe("useReactions", () => {
|
||||
});
|
||||
test("ignores invalid sender for new event", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
|
||||
@@ -349,6 +349,7 @@ export const ReactionsProvider = ({
|
||||
]);
|
||||
|
||||
const toggleRaisedHand = useCallback(async () => {
|
||||
console.log("toggleRaisedHand", myMembershipIdentifier);
|
||||
if (!myMembershipIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
17
src/utils/test-fixtures.ts
Normal file
17
src/utils/test-fixtures.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
mockRtcMembership,
|
||||
mockMatrixRoomMember,
|
||||
mockRemoteParticipant,
|
||||
mockLocalParticipant,
|
||||
} from "./test";
|
||||
|
||||
export const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA");
|
||||
export const alice = mockMatrixRoomMember(aliceRtcMember);
|
||||
export const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`;
|
||||
export const aliceParticipant = mockRemoteParticipant({ identity: aliceId });
|
||||
|
||||
export const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
||||
export const local = mockMatrixRoomMember(localRtcMember);
|
||||
export const localParticipant = mockLocalParticipant({ identity: "" });
|
||||
|
||||
export const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
||||
71
src/utils/test-viewmodel.ts
Normal file
71
src/utils/test-viewmodel.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ConnectionState } from "livekit-client";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { CallMembership, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { vitest } from "vitest";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { mockLivekitRoom, mockMatrixRoom, MockRTCSession } from "./test";
|
||||
import {
|
||||
aliceRtcMember,
|
||||
aliceParticipant,
|
||||
localParticipant,
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
|
||||
/**
|
||||
* Construct a basic CallViewModel to test components that make use of it.
|
||||
* @param members
|
||||
* @param initialRemoteRtcMemberships
|
||||
* @returns
|
||||
*/
|
||||
export function getBasicCallViewModelEnvironment(
|
||||
members: RoomMember[],
|
||||
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
|
||||
): {
|
||||
vm: CallViewModel;
|
||||
remoteRtcMemberships: BehaviorSubject<CallMembership[]>;
|
||||
rtcSession: MockRTCSession;
|
||||
} {
|
||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||
const remoteParticipants = of([aliceParticipant]);
|
||||
const liveKitRoom = mockLivekitRoom(
|
||||
{ localParticipant },
|
||||
{ remoteParticipants },
|
||||
);
|
||||
const matrixRoom = mockMatrixRoom({
|
||||
relations: {
|
||||
getChildEventsForEvent: vitest.fn(),
|
||||
} as Partial<RelationsContainer> as RelationsContainer,
|
||||
client: {
|
||||
getUserId: () => localRtcMember.sender,
|
||||
getDeviceId: () => localRtcMember.deviceId,
|
||||
sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||
redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||
on: vitest.fn(),
|
||||
off: vitest.fn(),
|
||||
} as Partial<MatrixClient> as MatrixClient,
|
||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||
});
|
||||
|
||||
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>(
|
||||
initialRemoteRtcMemberships,
|
||||
);
|
||||
|
||||
const rtcSession = new MockRTCSession(
|
||||
matrixRoom,
|
||||
localRtcMember,
|
||||
).withMemberships(remoteRtcMemberships);
|
||||
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
of(ConnectionState.Connected),
|
||||
);
|
||||
return { vm, remoteRtcMemberships, rtcSession };
|
||||
}
|
||||
@@ -4,19 +4,21 @@ Copyright 2023, 2024 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
import { map, Observable, of, SchedulerLike } from "rxjs";
|
||||
import { BehaviorSubject, map, Observable, of, SchedulerLike } from "rxjs";
|
||||
import { RunHelpers, TestScheduler } from "rxjs/testing";
|
||||
import { expect, vi } from "vitest";
|
||||
import { expect, vi, vitest } from "vitest";
|
||||
import {
|
||||
RoomMember,
|
||||
Room as MatrixRoom,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
TypedEventEmitter,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
CallMembership,
|
||||
Focus,
|
||||
MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
MatrixRTCSessionEventHandlerMap,
|
||||
SessionMembershipData,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
RemoteParticipant,
|
||||
RemoteTrackPublication,
|
||||
Room as LivekitRoom,
|
||||
ConnectionState,
|
||||
} from "livekit-client";
|
||||
|
||||
import {
|
||||
@@ -36,6 +39,14 @@ import {
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { DEFAULT_CONFIG, ResolvedConfigOptions } from "../config/ConfigOptions";
|
||||
import { Config } from "../config/Config";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import {
|
||||
aliceParticipant,
|
||||
aliceRtcMember,
|
||||
localParticipant,
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export function withFakeTimers(continuation: () => void): void {
|
||||
vi.useFakeTimers();
|
||||
@@ -129,6 +140,7 @@ export function mockRtcMembership(
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
sender: typeof user === "string" ? user : user.userId,
|
||||
event_id: `$-ev-${randomUUID()}:example.org`,
|
||||
});
|
||||
return new CallMembership(event, data);
|
||||
}
|
||||
|
||||
@@ -27,53 +27,41 @@ import {
|
||||
ElementCallReactionEventType,
|
||||
ReactionOption,
|
||||
} from "../reactions";
|
||||
import { MockRTCSession } from "./test";
|
||||
|
||||
export const TestReactionsWrapper = ({
|
||||
rtcSession,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
rtcSession: MockRTCSession | MatrixRTCSession;
|
||||
}>): ReactNode => {
|
||||
return (
|
||||
<ReactionsProvider rtcSession={rtcSession as unknown as MatrixRTCSession}>
|
||||
{children}
|
||||
</ReactionsProvider>
|
||||
);
|
||||
};
|
||||
// export class ReactionsMockRTCSession extends EventEmitter {
|
||||
// public memberships: {
|
||||
// sender: string;
|
||||
// eventId: string;
|
||||
// createdTs: () => Date;
|
||||
// }[];
|
||||
|
||||
export class MockRTCSession extends EventEmitter {
|
||||
public memberships: {
|
||||
sender: string;
|
||||
eventId: string;
|
||||
createdTs: () => Date;
|
||||
}[];
|
||||
// public constructor(
|
||||
// public readonly room: MockRoom,
|
||||
// membership: Record<string, string>,
|
||||
// ) {
|
||||
// super();
|
||||
// this.memberships = Object.entries(membership).map(([eventId, sender]) => ({
|
||||
// sender,
|
||||
// eventId,
|
||||
// createdTs: (): Date => new Date(),
|
||||
// }));
|
||||
// }
|
||||
|
||||
public constructor(
|
||||
public readonly room: MockRoom,
|
||||
membership: Record<string, string>,
|
||||
) {
|
||||
super();
|
||||
this.memberships = Object.entries(membership).map(([eventId, sender]) => ({
|
||||
sender,
|
||||
eventId,
|
||||
createdTs: (): Date => new Date(),
|
||||
}));
|
||||
}
|
||||
// public testRemoveMember(userId: string): void {
|
||||
// this.memberships = this.memberships.filter((u) => u.sender !== userId);
|
||||
// this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
// }
|
||||
|
||||
public testRemoveMember(userId: string): void {
|
||||
this.memberships = this.memberships.filter((u) => u.sender !== userId);
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
}
|
||||
|
||||
public testAddMember(sender: string): void {
|
||||
this.memberships.push({
|
||||
sender,
|
||||
eventId: `!fake-${randomUUID()}:event`,
|
||||
createdTs: (): Date => new Date(),
|
||||
});
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
}
|
||||
}
|
||||
// public testAddMember(sender: string): void {
|
||||
// this.memberships.push({
|
||||
// sender,
|
||||
// eventId: `!fake-${randomUUID()}:event`,
|
||||
// createdTs: (): Date => new Date(),
|
||||
// });
|
||||
// this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
// }
|
||||
// }
|
||||
|
||||
export function createHandRaisedReaction(
|
||||
parentMemberEvent: string,
|
||||
@@ -126,6 +114,7 @@ export class MockRoom extends EventEmitter {
|
||||
public get client(): MatrixClient {
|
||||
return {
|
||||
getUserId: (): string => this.ownUserId,
|
||||
getDeviceId: (): string => "ABCDEF",
|
||||
sendEvent: async (
|
||||
...props: Parameters<MatrixClient["sendEvent"]>
|
||||
): ReturnType<MatrixClient["sendEvent"]> => {
|
||||
|
||||
Reference in New Issue
Block a user