diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 483e8a9b3856..a67d43401765 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -14,18 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { mocked } from "jest-mock"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import "focus-visible"; // to fix context menus +import { mocked } from "jest-mock"; +import React from "react"; +import { MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread"; import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../src/components/structures/ThreadPanel"; +import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../src/contexts/RoomContext"; import { _t } from "../../../src/languageHandler"; -import ResizeNotifier from "../../../src/utils/ResizeNotifier"; -import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; -import { createTestClient, mkStubRoom } from "../../test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { shouldShowFeedback } from "../../../src/utils/Feedback"; -import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; +import ResizeNotifier from "../../../src/utils/ResizeNotifier"; +import { createTestClient, getRoomContext, mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils"; +import { mkThread } from "../../test-utils/threads"; jest.mock("../../../src/utils/Feedback"); @@ -122,4 +127,184 @@ describe("ThreadPanel", () => { expect(foundButton).toMatchSnapshot(); }); }); + + describe("Filtering", () => { + const ROOM_ID = "!roomId:example.org"; + const SENDER = "@alice:example.org"; + + let mockClient: MatrixClient; + let room: Room; + + const TestThreadPanel = () => ( + + + + + + ); + + beforeEach(async () => { + jest.clearAllMocks(); + + stubClient(); + mockPlatformPeg(); + mockClient = mocked(MatrixClientPeg.get()); + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + jest.spyOn(room, "fetchRoomThreads").mockReturnValue(Promise.resolve()); + jest.spyOn(mockClient, "getRoom").mockReturnValue(room); + await room.createThreadsTimelineSets(); + const [allThreads, myThreads] = room.threadsTimelineSets; + jest.spyOn(room, "createThreadsTimelineSets") + .mockReturnValue(Promise.resolve([allThreads, myThreads])); + }); + + function toggleThreadFilter(container: HTMLElement, newFilter: ThreadFilterType) { + fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")); + const found = screen.queryAllByRole("menuitemradio"); + expect(found).toHaveLength(2); + + const allThreadsContent = `${_t("All threads")}${_t("Shows all threads from current room")}`; + const myThreadsContent = `${_t("My threads")}${_t("Shows all threads you've participated in")}`; + + const allThreadsOption = found.find(it => it.textContent === allThreadsContent); + const myThreadsOption = found.find(it => it.textContent === myThreadsContent); + expect(allThreadsOption).toBeTruthy(); + expect(myThreadsOption).toBeTruthy(); + + const toSelect = newFilter === ThreadFilterType.My ? myThreadsOption : allThreadsOption; + fireEvent.click(toSelect); + } + + type EventData = { sender: string, content: string }; + + function findEvents(container: HTMLElement): EventData[] { + return Array.from(container.querySelectorAll(".mx_EventTile")).map(el => { + const sender = el.querySelector(".mx_DisambiguatedProfile_displayName").textContent; + const content = el.querySelector(".mx_EventTile_body").textContent; + return { sender, content }; + }); + } + + function toEventData(event: MatrixEvent): EventData { + return { sender: event.event.sender, content: event.event.content.body }; + } + + it("correctly filters Thread List with multiple threads", async () => { + const otherThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [mockClient.getUserId()], + }); + + const mixedThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [SENDER, mockClient.getUserId()], + }); + + const ownThread = mkThread({ + room, + client: mockClient, + authorId: mockClient.getUserId(), + participantUserIds: [mockClient.getUserId()], + }); + + const threadRoots = [otherThread.rootEvent, mixedThread.rootEvent, ownThread.rootEvent]; + jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation( + (_, eventId) => Promise.resolve(threadRoots.find(it => it.getId() === eventId).event), + ); + const [allThreads, myThreads] = room.threadsTimelineSets; + allThreads.addLiveEvent(otherThread.rootEvent); + allThreads.addLiveEvent(mixedThread.rootEvent); + allThreads.addLiveEvent(ownThread.rootEvent); + myThreads.addLiveEvent(mixedThread.rootEvent); + myThreads.addLiveEvent(ownThread.rootEvent); + + let events: EventData[] = []; + const renderResult = render(); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(3); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + expect(events[1]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[2]).toEqual(toEventData(ownThread.rootEvent)); + await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy()); + toggleThreadFilter(renderResult.container, ThreadFilterType.My); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(2); + }); + expect(events[0]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[1]).toEqual(toEventData(ownThread.rootEvent)); + toggleThreadFilter(renderResult.container, ThreadFilterType.All); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(3); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + expect(events[1]).toEqual(toEventData(mixedThread.rootEvent)); + expect(events[2]).toEqual(toEventData(ownThread.rootEvent)); + }); + + it("correctly filters Thread List with a single, unparticipated thread", async () => { + const otherThread = mkThread({ + room, + client: mockClient, + authorId: SENDER, + participantUserIds: [mockClient.getUserId()], + }); + + const threadRoots = [otherThread.rootEvent]; + jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation( + (_, eventId) => Promise.resolve(threadRoots.find(it => it.getId() === eventId).event), + ); + const [allThreads] = room.threadsTimelineSets; + allThreads.addLiveEvent(otherThread.rootEvent); + + let events: EventData[] = []; + const renderResult = render(); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(1); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy()); + toggleThreadFilter(renderResult.container, ThreadFilterType.My); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(0); + }); + toggleThreadFilter(renderResult.container, ThreadFilterType.All); + await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); + await waitFor(() => { + events = findEvents(renderResult.container); + expect(findEvents(renderResult.container)).toHaveLength(1); + }); + expect(events[0]).toEqual(toEventData(otherThread.rootEvent)); + }); + }) });