diff --git a/src/useReactions.test.tsx b/src/useReactions.test.tsx index 79caeb0a..6ea47514 100644 --- a/src/useReactions.test.tsx +++ b/src/useReactions.test.tsx @@ -6,32 +6,18 @@ Please see LICENSE in the repository root for full details. */ import { act, render } from "@testing-library/react"; -import { FC, ReactNode } from "react"; +import { FC } from "react"; import { describe, expect, test } from "vitest"; -import { - MatrixRTCSession, - MatrixRTCSessionEvent, -} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { - EventTimeline, - EventTimelineSet, - EventType, - MatrixClient, - MatrixEvent, - Room, - RoomEvent, -} from "matrix-js-sdk/src/matrix"; -import EventEmitter from "events"; -import { randomUUID } from "crypto"; -import { ReactionsProvider, useReactions } from "./useReactions"; - -/** - * Test explanation. - * This test suite checks that the useReactions hook appropriately reacts - * to new reactions, redactions and membership changesin the room. There is - * a large amount of test structure used to construct a mock environment. - */ +import { useReactions } from "./useReactions"; +import { + createReaction, + createRedaction, + MockRoom, + MockRTCSession, + TestComponentWrapper, +} from "./utils/test-reactions"; +import { RoomEvent } from "matrix-js-sdk/src/matrix"; const memberUserIdAlice = "@alice:example.org"; const memberEventAlice = "$membership-alice:example.org"; @@ -44,6 +30,13 @@ const membership: Record = { "$membership-charlie:example.org": "@charlie:example.org", }; +/** + * Test explanation. + * This test suite checks that the useReactions hook appropriately reacts + * to new reactions, redactions and membership changesin the room. There is + * a large amount of test structure used to construct a mock environment. + */ + const TestComponent: FC = () => { const { raisedHands, myReactionId } = useReactions(); return ( @@ -61,145 +54,53 @@ const TestComponent: FC = () => { ); }; -const TestComponentWrapper = ({ - rtcSession, -}: { - rtcSession: MockRTCSession; -}): ReactNode => { - return ( - - - - ); -}; - -export class MockRTCSession extends EventEmitter { - public memberships = Object.entries(membership).map(([eventId, sender]) => ({ - sender, - eventId, - createdTs: (): Date => new Date(), - })); - - public constructor(public readonly room: MockRoom) { - super(); - } - - 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); - } -} - -function createReaction( - parentMemberEvent: string, - overridenSender?: string, -): MatrixEvent { - return new MatrixEvent({ - sender: overridenSender ?? membership[parentMemberEvent], - type: EventType.Reaction, - origin_server_ts: new Date().getTime(), - content: { - "m.relates_to": { - key: "🖐️", - event_id: parentMemberEvent, - }, - }, - event_id: randomUUID(), - }); -} - -function createRedaction(sender: string, reactionEventId: string): MatrixEvent { - return new MatrixEvent({ - sender, - type: EventType.RoomRedaction, - origin_server_ts: new Date().getTime(), - redacts: reactionEventId, - content: {}, - event_id: randomUUID(), - }); -} - -export class MockRoom extends EventEmitter { - public constructor(private readonly existingRelations: MatrixEvent[] = []) { - super(); - } - - public get client(): MatrixClient { - return { - getUserId: (): string => memberUserIdAlice, - } as unknown as MatrixClient; - } - - public get relations(): Room["relations"] { - return { - getChildEventsForEvent: (membershipEventId: string) => ({ - getRelations: (): MatrixEvent[] => { - return this.existingRelations.filter( - (r) => - r.getContent()["m.relates_to"]?.event_id === membershipEventId, - ); - }, - }), - } as unknown as Room["relations"]; - } - - public testSendReaction( - parentMemberEvent: string, - overridenSender?: string, - ): string { - const evt = createReaction(parentMemberEvent, overridenSender); - this.emit(RoomEvent.Timeline, evt, this, undefined, false, { - timeline: new EventTimeline(new EventTimelineSet(undefined)), - }); - return evt.getId()!; - } -} - describe("useReactions", () => { test("starts with an empty list", () => { - const rtcSession = new MockRTCSession(new MockRoom()); + const rtcSession = new MockRTCSession( + new MockRoom(memberUserIdAlice), + membership, + ); const { queryByRole } = render( - , + + + , ); expect(queryByRole("list")?.children).to.have.lengthOf(0); }); test("handles own raised hand", async () => { - const room = new MockRoom(); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice); + const rtcSession = new MockRTCSession(room, membership); const { queryByText } = render( - , + + + , ); - await act(() => room.testSendReaction(memberEventAlice)); + await act(() => room.testSendReaction(memberEventAlice, membership)); expect(queryByText("Local reaction")).toBeTruthy(); }); test("handles incoming raised hand", async () => { - const room = new MockRoom(); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); - await act(() => room.testSendReaction(memberEventAlice)); + await act(() => room.testSendReaction(memberEventAlice, membership)); expect(queryByRole("list")?.children).to.have.lengthOf(1); - await act(() => room.testSendReaction(memberEventBob)); + await act(() => room.testSendReaction(memberEventBob, membership)); expect(queryByRole("list")?.children).to.have.lengthOf(2); }); test("handles incoming unraised hand", async () => { - const room = new MockRoom(); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); const reactionEventId = await act(() => - room.testSendReaction(memberEventAlice), + room.testSendReaction(memberEventAlice, membership), ); expect(queryByRole("list")?.children).to.have.lengthOf(1); await act(() => @@ -213,30 +114,42 @@ describe("useReactions", () => { expect(queryByRole("list")?.children).to.have.lengthOf(0); }); test("handles loading prior raised hand events", () => { - const room = new MockRoom([createReaction(memberEventAlice)]); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice, [ + createReaction(memberEventAlice, membership), + ]); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); expect(queryByRole("list")?.children).to.have.lengthOf(1); }); // If the membership event changes for a user, we want to remove // the raised hand event. test("will remove reaction when a member leaves the call", () => { - const room = new MockRoom([createReaction(memberEventAlice)]); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice, [ + createReaction(memberEventAlice, membership), + ]); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); expect(queryByRole("list")?.children).to.have.lengthOf(1); act(() => rtcSession.testRemoveMember(memberUserIdAlice)); expect(queryByRole("list")?.children).to.have.lengthOf(0); }); test("will remove reaction when a member joins via a new event", () => { - const room = new MockRoom([createReaction(memberEventAlice)]); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice, [ + createReaction(memberEventAlice, membership), + ]); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); expect(queryByRole("list")?.children).to.have.lengthOf(1); // Simulate leaving and rejoining @@ -247,20 +160,24 @@ describe("useReactions", () => { expect(queryByRole("list")?.children).to.have.lengthOf(0); }); test("ignores invalid sender for historic event", () => { - const room = new MockRoom([ - createReaction(memberEventAlice, memberUserIdBob), + const room = new MockRoom(memberUserIdAlice, [ + createReaction(memberEventAlice, membership), ]); - const rtcSession = new MockRTCSession(room); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); expect(queryByRole("list")?.children).to.have.lengthOf(0); }); test("ignores invalid sender for new event", async () => { - const room = new MockRoom([]); - const rtcSession = new MockRTCSession(room); + const room = new MockRoom(memberUserIdAlice); + const rtcSession = new MockRTCSession(room, membership); const { queryByRole } = render( - , + + + , ); await act(() => room.testSendReaction(memberEventAlice, memberUserIdBob)); expect(queryByRole("list")?.children).to.have.lengthOf(0); diff --git a/src/utils/testReactions.tsx b/src/utils/testReactions.tsx new file mode 100644 index 00000000..0965379e --- /dev/null +++ b/src/utils/testReactions.tsx @@ -0,0 +1,145 @@ +import { PropsWithChildren, ReactNode } from "react"; +import { ReactionsProvider } from "../useReactions"; +import { randomUUID } from "crypto"; +import EventEmitter from "events"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventType, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { + MatrixEvent, + EventTimeline, + EventTimelineSet, + Room, +} from "matrix-js-sdk/src/matrix"; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc"; + +export const TestComponentWrapper = ({ + rtcSession, + children, +}: PropsWithChildren<{ + rtcSession: MockRTCSession; +}>): ReactNode => { + return ( + + {children} + + ); +}; + +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 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); + } +} + +export function createReaction( + parentMemberEvent: string, + membershipOrOverridenSender: Record | string, +): MatrixEvent { + return new MatrixEvent({ + sender: + typeof membershipOrOverridenSender === "string" + ? membershipOrOverridenSender + : membershipOrOverridenSender[parentMemberEvent], + type: EventType.Reaction, + origin_server_ts: new Date().getTime(), + content: { + "m.relates_to": { + key: "🖐️", + event_id: parentMemberEvent, + }, + }, + event_id: randomUUID(), + }); +} + +export function createRedaction( + sender: string, + reactionEventId: string, +): MatrixEvent { + return new MatrixEvent({ + sender, + type: EventType.RoomRedaction, + origin_server_ts: new Date().getTime(), + redacts: reactionEventId, + content: {}, + event_id: randomUUID(), + }); +} + +export class MockRoom extends EventEmitter { + public constructor( + private readonly ownUserId: string, + private readonly existingRelations: MatrixEvent[] = [], + ) { + super(); + } + + public get client(): MatrixClient { + return { + getUserId: (): string => this.ownUserId, + } as unknown as MatrixClient; + } + + public get relations(): Room["relations"] { + return { + getChildEventsForEvent: (membershipEventId: string) => ({ + getRelations: (): MatrixEvent[] => { + return this.existingRelations.filter( + (r) => + r.getContent()["m.relates_to"]?.event_id === membershipEventId, + ); + }, + }), + } as unknown as Room["relations"]; + } + + public testSendReaction( + parentMemberEvent: string, + overridenSender: string, + ): string; + public testSendReaction( + parentMemberEvent: string, + membershipOrOverridenSender: Record, + ): string; + public testSendReaction( + parentMemberEvent: string, + membershipOrOverridenSender: Record | string, + ): string { + const evt = createReaction(parentMemberEvent, membershipOrOverridenSender); + this.emit(RoomEvent.Timeline, evt, this, undefined, false, { + timeline: new EventTimeline(new EventTimelineSet(undefined)), + }); + return evt.getId()!; + } +}