Add tests for useReactionsReader

This commit is contained in:
Half-Shot
2024-12-17 11:45:36 +00:00
parent 4164e0a61f
commit f604004856
6 changed files with 630 additions and 68 deletions

View File

@@ -198,10 +198,6 @@ export interface RaisedHandInfo {
} }
export interface ReactionInfo { export interface ReactionInfo {
/** expireAfter: Date;
* ...
*/
ttl: number;
reactionOption: ReactionOption; reactionOption: ReactionOption;
} }

View File

@@ -0,0 +1,512 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { renderHook } from "@testing-library/react";
import { afterEach, test, vitest } from "vitest";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import {
RoomEvent as MatrixRoomEvent,
MatrixEvent,
type IRoomTimelineData,
EventType,
MatrixEventEvent,
} from "matrix-js-sdk/src/matrix";
import useReactionsReader, {
REACTION_ACTIVE_TIME_MS,
} from "./useReactionsReader";
import {
alice,
aliceRtcMember,
local,
localRtcMember,
} from "../utils/test-fixtures";
import { getBasicRTCSession } from "../utils/test-viewmodel";
import { withTestScheduler } from "../utils/test";
import { ElementCallReactionEventType, ReactionSet } from ".";
afterEach(() => {
vitest.useRealTimers();
});
test("handles a hand raised reaction", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const localTimestamp = new Date();
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { raisedHands$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("ab", {
a: () => {},
b: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.Reaction,
origin_server_ts: localTimestamp.getTime(),
content: {
"m.relates_to": {
event_id: localRtcMember.eventId,
key: "🖐️",
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
});
expectObservable(raisedHands$).toBe("ab", {
a: {},
b: {
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
reactionEventId,
membershipEventId: localRtcMember.eventId,
time: localTimestamp,
},
},
});
});
});
});
test("handles a redaction", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const localTimestamp = new Date();
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { raisedHands$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("abc", {
a: () => {},
b: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.Reaction,
origin_server_ts: localTimestamp.getTime(),
content: {
"m.relates_to": {
event_id: localRtcMember.eventId,
key: "🖐️",
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
c: () => {
rtcSession.room.emit(
MatrixRoomEvent.Redaction,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.RoomRedaction,
redacts: reactionEventId,
}),
rtcSession.room,
undefined,
);
},
});
expectObservable(raisedHands$).toBe("abc", {
a: {},
b: {
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
reactionEventId,
membershipEventId: localRtcMember.eventId,
time: localTimestamp,
},
},
c: {},
});
});
});
});
test("handles waiting for event decryption", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const localTimestamp = new Date();
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { raisedHands$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("abc", {
a: () => {},
b: () => {
const encryptedEvent = new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.Reaction,
origin_server_ts: localTimestamp.getTime(),
content: {
"m.relates_to": {
event_id: localRtcMember.eventId,
key: "🖐️",
},
},
});
// Should ignore encrypted events that are still encrypting
encryptedEvent["decryptionPromise"] = Promise.resolve();
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
encryptedEvent,
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
c: () => {
rtcSession.room.client.emit(
MatrixEventEvent.Decrypted,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.Reaction,
origin_server_ts: localTimestamp.getTime(),
content: {
"m.relates_to": {
event_id: localRtcMember.eventId,
key: "🖐️",
},
},
}),
);
},
});
expectObservable(raisedHands$).toBe("a-c", {
a: {},
c: {
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
reactionEventId,
membershipEventId: localRtcMember.eventId,
time: localTimestamp,
},
},
});
});
});
});
test("hands rejecting events without a proper membership", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const localTimestamp = new Date();
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { raisedHands$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("ab", {
a: () => {},
b: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: EventType.Reaction,
origin_server_ts: localTimestamp.getTime(),
content: {
"m.relates_to": {
event_id: "$not-this-one:example.org",
key: "🖐️",
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
});
expectObservable(raisedHands$).toBe("a-", {
a: {},
});
});
});
});
test("handles a reaction", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const reaction = ReactionSet[1];
vitest.setSystemTime(0);
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { reactions$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("ab", {
a: () => {},
b: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: reaction.emoji,
name: reaction.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
c: () => {
vitest.advanceTimersByTime(5000);
},
});
expectObservable(reactions$).toBe(
`ab ${REACTION_ACTIVE_TIME_MS - 1}ms c`,
{
a: {},
b: {
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
reactionOption: reaction,
expireAfter: new Date(REACTION_ACTIVE_TIME_MS),
},
},
// Expect reaction to expire.
c: {},
},
);
});
});
});
test("ignores bad reaction events", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const reaction = ReactionSet[1];
vitest.setSystemTime(0);
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { reactions$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("ab", {
a: () => {},
b: () => {
// Missing content
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
// Wrong relates event
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: reaction.emoji,
name: reaction.name,
"m.relates_to": {
event_id: "wrong-event",
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
// Wrong rtc member event
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: aliceRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: reaction.emoji,
name: reaction.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
// No emoji
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
name: reaction.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
// Invalid emoji
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: " ",
name: reaction.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
});
expectObservable(reactions$).toBe("a-", {
a: {},
});
});
});
});
test("that reactions cannot be spammed", () => {
const { rtcSession } = getBasicRTCSession([local, alice]);
const reactionEventId = "$my_event_id:example.org";
const reactionA = ReactionSet[1];
const reactionB = ReactionSet[2];
vitest.setSystemTime(0);
withTestScheduler(({ schedule, expectObservable }) => {
renderHook(() => {
const { reactions$ } = useReactionsReader(
rtcSession as unknown as MatrixRTCSession,
);
schedule("abc", {
a: () => {},
b: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: reactionA.emoji,
name: reactionA.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
c: () => {
rtcSession.room.emit(
MatrixRoomEvent.Timeline,
new MatrixEvent({
room_id: rtcSession.room.roomId,
event_id: reactionEventId,
sender: localRtcMember.sender,
type: ElementCallReactionEventType,
content: {
emoji: reactionB.emoji,
name: reactionB.name,
"m.relates_to": {
event_id: localRtcMember.eventId,
},
},
}),
rtcSession.room,
undefined,
false,
{} as IRoomTimelineData,
);
},
});
expectObservable(reactions$).toBe(
`ab ${REACTION_ACTIVE_TIME_MS - 1}ms c`,
{
a: {},
b: {
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
reactionOption: reactionA,
expireAfter: new Date(REACTION_ACTIVE_TIME_MS),
},
},
c: {},
},
);
});
});
});

View File

@@ -15,7 +15,7 @@ import {
EventType, EventType,
RoomEvent as MatrixRoomEvent, RoomEvent as MatrixRoomEvent,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { BehaviorSubject, type Observable } from "rxjs"; import { BehaviorSubject, delay, type Observable } from "rxjs";
import { import {
ElementCallReactionEventType, ElementCallReactionEventType,
@@ -28,7 +28,7 @@ import {
import { useLatest } from "../useLatest"; import { useLatest } from "../useLatest";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
const REACTION_ACTIVE_TIME_MS = 3000; export const REACTION_ACTIVE_TIME_MS = 3000;
/** /**
* Listens for reactions from a RTCSession and populates subjects * Listens for reactions from a RTCSession and populates subjects
@@ -46,6 +46,19 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
new BehaviorSubject<Record<string, ReactionInfo>>({}), new BehaviorSubject<Record<string, ReactionInfo>>({}),
); );
reactionsSubject$.current
.pipe(delay(REACTION_ACTIVE_TIME_MS))
.subscribe((reactions) => {
const date = new Date();
const nextEntries = Object.fromEntries(
Object.entries(reactions).filter(([_, hr]) => hr.expireAfter < date),
);
if (Object.keys(reactions).length === Object.keys(nextEntries).length) {
return;
}
reactionsSubject$.current.next(nextEntries);
});
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const latestMemberships = useLatest(memberships); const latestMemberships = useLatest(memberships);
const latestRaisedHands = useLatest(raisedHandsSubject$.current); const latestRaisedHands = useLatest(raisedHandsSubject$.current);
@@ -132,7 +145,6 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
// This effect handles any *live* reaction/redactions in the room. // This effect handles any *live* reaction/redactions in the room.
useEffect(() => { useEffect(() => {
const reactionTimeouts = new Set<NodeJS.Timeout>();
const handleReactionEvent = (event: MatrixEvent): void => { const handleReactionEvent = (event: MatrixEvent): void => {
// Decrypted events might come from a different room // Decrypted events might come from a different room
if (event.getRoomId() !== room.roomId) return; if (event.getRoomId() !== room.roomId) return;
@@ -178,7 +190,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
[Symbol.iterator](); [Symbol.iterator]();
const emoji = segment.next().value?.segment; const emoji = segment.next().value?.segment;
if (!emoji) { if (!emoji?.trim()) {
logger.warn( logger.warn(
`Reaction had no emoji from ${reactionEventId} after splitting`, `Reaction had no emoji from ${reactionEventId} after splitting`,
); );
@@ -198,23 +210,11 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
// We've still got a reaction from this user, ignore it to prevent spamming // We've still got a reaction from this user, ignore it to prevent spamming
return; return;
} }
const timeout = globalThis.setTimeout(() => {
// Clear the reaction after some time.
reactionsSubject$.current.next(
Object.fromEntries(
Object.entries(reactionsSubject$.current.value).filter(
([id]) => id !== identifier,
),
),
);
reactionTimeouts.delete(timeout);
}, REACTION_ACTIVE_TIME_MS);
reactionTimeouts.add(timeout);
reactionsSubject$.current.next({ reactionsSubject$.current.next({
...currentReactions, ...currentReactions,
[identifier]: { [identifier]: {
reactionOption: reaction, reactionOption: reaction,
ttl: 0, expireAfter: new Date(Date.now() + REACTION_ACTIVE_TIME_MS),
}, },
}); });
} else if (event.getType() === EventType.Reaction) { } else if (event.getType() === EventType.Reaction) {
@@ -264,15 +264,11 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
// may still be sending. // may still be sending.
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent); room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
const innerReactionsSubject$ = reactionsSubject$.current;
return (): void => { return (): void => {
room.off(MatrixRoomEvent.Timeline, handleReactionEvent); room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
room.off(MatrixRoomEvent.Redaction, handleReactionEvent); room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
room.client.off(MatrixEventEvent.Decrypted, handleReactionEvent); room.client.off(MatrixEventEvent.Decrypted, handleReactionEvent);
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent); room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
reactionTimeouts.forEach((t) => clearTimeout(t));
// If we're clearing timeouts, we also clear all reactions.
innerReactionsSubject$.next({});
}; };
}, [ }, [
room, room,

View File

@@ -96,7 +96,10 @@ test("will play an audio sound when there is a reaction", () => {
} }
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 }, [aliceRtcMember.deviceId]: {
reactionOption: chosenReaction,
expireAfter: 0,
},
}); });
}); });
expect(playSound).toHaveBeenCalledWith(chosenReaction.name); expect(playSound).toHaveBeenCalledWith(chosenReaction.name);
@@ -119,7 +122,10 @@ test("will play the generic audio sound when there is soundless reaction", () =>
} }
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 }, [aliceRtcMember.deviceId]: {
reactionOption: chosenReaction,
expireAfter: 0,
},
}); });
}); });
expect(playSound).toHaveBeenCalledWith(GenericReaction.name); expect(playSound).toHaveBeenCalledWith(GenericReaction.name);
@@ -142,9 +148,9 @@ test("will play multiple audio sounds when there are multiple different reaction
} }
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction1, expireAfter: 0 },
[bobRtcMember.deviceId]: { reactionOption: reaction2, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reaction2, expireAfter: 0 },
[localRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 }, [localRtcMember.deviceId]: { reactionOption: reaction1, expireAfter: 0 },
}); });
}); });
expect(playSound).toHaveBeenCalledWith(reaction1.name); expect(playSound).toHaveBeenCalledWith(reaction1.name);

View File

@@ -42,7 +42,7 @@ test("shows a reaction when sent", () => {
const reaction = ReactionSet[0]; const reaction = ReactionSet[0];
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
}); });
}); });
const span = getByRole("presentation"); const span = getByRole("presentation");
@@ -60,8 +60,8 @@ test("shows two of the same reaction when sent", () => {
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />); const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
[bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
}); });
}); });
expect(getAllByRole("presentation")).toHaveLength(2); expect(getAllByRole("presentation")).toHaveLength(2);
@@ -77,8 +77,8 @@ test("shows two different reactions when sent", () => {
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />); const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reactionA, expireAfter: 0 },
[bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reactionB, expireAfter: 0 },
}); });
}); });
const [reactionElementA, reactionElementB] = getAllByRole("presentation"); const [reactionElementA, reactionElementB] = getAllByRole("presentation");
@@ -96,7 +96,7 @@ test("hides reactions when reaction animations are disabled", () => {
const { container } = render(<ReactionsOverlay vm={vm} />); const { container } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject$.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
}); });
}); });
expect(container.getElementsByTagName("span")).toHaveLength(0); expect(container.getElementsByTagName("span")).toHaveLength(0);

View File

@@ -15,6 +15,7 @@ import {
import { BehaviorSubject, of } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
import { vitest } from "vitest"; import { vitest } from "vitest";
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container"; import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
import EventEmitter from "events";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { CallViewModel } from "../state/CallViewModel"; import { CallViewModel } from "../state/CallViewModel";
@@ -27,6 +28,80 @@ import {
} from "./test-fixtures"; } from "./test-fixtures";
import { type RaisedHandInfo, type ReactionInfo } from "../reactions"; import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
export function getBasicRTCSession(
members: RoomMember[],
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
): {
rtcSession: MockRTCSession;
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
} {
const matrixRoomId = "!myRoomId:example.com";
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
const roomEmitter = new EventEmitter();
const clientEmitter = new EventEmitter();
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" }),
decryptEventIfNeeded: vitest.fn().mockResolvedValue(undefined),
on: vitest
.fn()
.mockImplementation(
(eventName: string, fn: (...args: unknown[]) => void) => {
clientEmitter.on(eventName, fn);
},
),
emit: (eventName: string, ...args: unknown[]) =>
clientEmitter.emit(eventName, ...args),
off: vitest
.fn()
.mockImplementation(
(eventName: string, fn: (...args: unknown[]) => void) => {
clientEmitter.off(eventName, fn);
},
),
} as Partial<MatrixClient> as MatrixClient,
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
roomId: matrixRoomId,
on: vitest
.fn()
.mockImplementation(
(eventName: string, fn: (...args: unknown[]) => void) => {
roomEmitter.on(eventName, fn);
},
),
emit: (eventName: string, ...args: unknown[]) =>
roomEmitter.emit(eventName, ...args),
off: vitest
.fn()
.mockImplementation(
(eventName: string, fn: (...args: unknown[]) => void) => {
roomEmitter.off(eventName, fn);
},
),
});
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
initialRemoteRtcMemberships,
);
const rtcSession = new MockRTCSession(
matrixRoom,
localRtcMember,
).withMemberships(remoteRtcMemberships$);
return {
rtcSession,
remoteRtcMemberships$,
};
}
/** /**
* Construct a basic CallViewModel to test components that make use of it. * Construct a basic CallViewModel to test components that make use of it.
* @param members * @param members
@@ -43,41 +118,18 @@ export function getBasicCallViewModelEnvironment(
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>; handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>; reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
} { } {
const matrixRoomId = "!myRoomId:example.com"; const { rtcSession, remoteRtcMemberships$ } = getBasicRTCSession(
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); members,
initialRemoteRtcMemberships,
);
const handRaisedSubject$ = new BehaviorSubject({});
const reactionsSubject$ = new BehaviorSubject({});
const remoteParticipants$ = of([aliceParticipant]); const remoteParticipants$ = of([aliceParticipant]);
const liveKitRoom = mockLivekitRoom( const liveKitRoom = mockLivekitRoom(
{ localParticipant }, { localParticipant },
{ remoteParticipants$ }, { 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,
roomId: matrixRoomId,
});
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
initialRemoteRtcMemberships,
);
const handRaisedSubject$ = new BehaviorSubject({});
const reactionsSubject$ = new BehaviorSubject({});
const rtcSession = new MockRTCSession(
matrixRoom,
localRtcMember,
).withMemberships(remoteRtcMemberships$);
const vm = new CallViewModel( const vm = new CallViewModel(
rtcSession as unknown as MatrixRTCSession, rtcSession as unknown as MatrixRTCSession,
liveKitRoom, liveKitRoom,
@@ -90,7 +142,7 @@ export function getBasicCallViewModelEnvironment(
); );
return { return {
vm, vm,
remoteRtcMemberships$: remoteRtcMemberships$, remoteRtcMemberships$,
rtcSession, rtcSession,
handRaisedSubject$: handRaisedSubject$, handRaisedSubject$: handRaisedSubject$,
reactionsSubject$: reactionsSubject$, reactionsSubject$: reactionsSubject$,