diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx index a1498304..10c74981 100644 --- a/src/button/ReactionToggleButton.test.tsx +++ b/src/button/ReactionToggleButton.test.tsx @@ -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 = { - [memberEventAlice]: memberUserIdAlice, -}; +const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`; function TestComponent({ rtcSession, + vm, }: { rtcSession: MockRTCSession; + vm: CallViewModel; }): ReactNode { return ( - - - + + + ); } 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( - , + , ); 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( - , + , ); 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( - , + , ); - 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(); }); diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index dd4227e1..b1b5df35 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -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" > diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 2be2d186..11be2a06 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -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 ( - - - - ); -} - -function getMockEnv( - members: RoomMember[], - initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], -): { - vm: CallViewModel; - session: MockRTCSession; - remoteRtcMemberships: BehaviorSubject; -} { - 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 as MatrixClient, - getMember: (userId) => matrixRoomMembers.get(userId) ?? null, - }); - - const remoteRtcMemberships = new BehaviorSubject( - 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(); + const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([ + local, + alice, + ]); + render(); + // 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(); + const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([ + local, + alice, + ]); + render(); 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(); + const { vm, remoteRtcMemberships } = getBasicCallViewModelEnvironment([ + local, + alice, + ]); + render(); 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(); + render(); 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(); - // Joining a call usually means remote participants are added later. + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + render(); + 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(); - // Joining a call usually means remote participants are added later. + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + render(); + act(() => { vm.updateReactions({ raisedHands: { diff --git a/src/room/ReactionsOverlay.test.tsx b/src/room/ReactionsOverlay.test.tsx index 121594ab..04d647fd 100644 --- a/src/room/ReactionsOverlay.test.tsx +++ b/src/room/ReactionsOverlay.test.tsx @@ -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 = { - [memberEventAlice]: memberUserIdAlice, - [memberEventBob]: memberUserIdBob, - [memberEventCharlie]: memberUserIdCharlie, -}; - -function TestComponent({ - rtcSession, -}: { - rtcSession: MockRTCSession; -}): ReactNode { - return ( - - - - - - ); -} +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(); + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + const { container } = render(); expect(container.getElementsByTagName("span")).toHaveLength(0); }); test("shows a reaction when sent", () => { showReactions.setValue(true); + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + const { getByRole } = render(); const reaction = ReactionSet[0]; - const room = new MockRoom(memberUserIdAlice); - const rtcSession = new MockRTCSession(room, membership); - const { getByRole } = render(); 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(); + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + const { getAllByRole } = render(); 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(); + const { vm } = getBasicCallViewModelEnvironment([local, alice]); + const { getAllByRole } = render(); 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(); act(() => { - room.testSendReaction(memberEventAlice, reaction, membership); + vm.updateReactions({ + reactions: { [aliceRtcMember.deviceId]: reaction }, + raisedHands: {}, + }); }); - const { container } = render(); expect(container.getElementsByTagName("span")).toHaveLength(0); }); diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index 5dbfb1ca..76762047 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -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", + ], + }, + }, + ); + }, + ); + }); +}); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 06fb14bd..e0cf0aa2 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -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 = @@ -1116,10 +1124,12 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - private readonly handsRaisedSubject = new Subject>(); - private readonly reactionsSubject = new Subject< + private readonly handsRaisedSubject = new BehaviorSubject< + Record + >({}); + private readonly reactionsSubject = new BehaviorSubject< Record - >(); + >({}); public readonly handsRaised = this.handsRaisedSubject.asObservable(); public readonly reactions = this.reactionsSubject.asObservable(); diff --git a/src/useReactions.test.tsx b/src/useReactions.test.tsx index 6140793f..7e687d31 100644 --- a/src/useReactions.test.tsx +++ b/src/useReactions.test.tsx @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( diff --git a/src/useReactions.tsx b/src/useReactions.tsx index 0470ab0c..3ebdc8b6 100644 --- a/src/useReactions.tsx +++ b/src/useReactions.tsx @@ -349,6 +349,7 @@ export const ReactionsProvider = ({ ]); const toggleRaisedHand = useCallback(async () => { + console.log("toggleRaisedHand", myMembershipIdentifier); if (!myMembershipIdentifier) { return; } diff --git a/src/utils/test-fixtures.ts b/src/utils/test-fixtures.ts new file mode 100644 index 00000000..630f51a3 --- /dev/null +++ b/src/utils/test-fixtures.ts @@ -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"); diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts new file mode 100644 index 00000000..a2a1354d --- /dev/null +++ b/src/utils/test-viewmodel.ts @@ -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; + 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 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 as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const remoteRtcMemberships = new BehaviorSubject( + 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 }; +} diff --git a/src/utils/test.ts b/src/utils/test.ts index 93a4c133..a7255151 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -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); } diff --git a/src/utils/testReactions.tsx b/src/utils/testReactions.tsx index fec3e859..2b21ba34 100644 --- a/src/utils/testReactions.tsx +++ b/src/utils/testReactions.tsx @@ -27,53 +27,41 @@ import { ElementCallReactionEventType, ReactionOption, } from "../reactions"; +import { MockRTCSession } from "./test"; -export const TestReactionsWrapper = ({ - rtcSession, - children, -}: PropsWithChildren<{ - rtcSession: MockRTCSession | MatrixRTCSession; -}>): ReactNode => { - return ( - - {children} - - ); -}; +// 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, +// ) { +// super(); +// this.memberships = Object.entries(membership).map(([eventId, sender]) => ({ +// sender, +// eventId, +// createdTs: (): Date => new Date(), +// })); +// } - public constructor( - public readonly room: MockRoom, - membership: Record, - ) { - 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 ): ReturnType => {