/* Copyright 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import { EventTimeline, EventType, IEvent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { PinnedEventTile } from "../../../../src/components/views/rooms/PinnedEventTile"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { stubClient } from "../../../test-utils"; import dis from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { getForwardableEvent } from "../../../../src/events"; import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog"; jest.mock("../../../../src/components/views/dialogs/ConfirmRedactDialog", () => ({ createRedactEventDialog: jest.fn(), })); describe("", () => { const userId = "@alice:server.org"; const roomId = "!room:server.org"; let mockClient: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; beforeEach(() => { mockClient = stubClient(); room = new Room(roomId, mockClient, userId); permalinkCreator = new RoomPermalinkCreator(room); jest.spyOn(dis, "dispatch").mockReturnValue(undefined); }); /** * Create a pinned event with the given content. * @param content */ function makePinEvent(content?: Partial) { return new MatrixEvent({ type: EventType.RoomMessage, sender: userId, content: { body: "First pinned message", msgtype: "m.text", }, room_id: roomId, origin_server_ts: 0, event_id: "$eventId", ...content, }); } /** * Render the component with the given event. * @param event - pinned event */ function renderComponent(event: MatrixEvent) { return render( , ); } /** * Render the component and open the menu. */ async function renderAndOpenMenu() { const pinEvent = makePinEvent(); const renderResult = renderComponent(pinEvent); await userEvent.click(screen.getByRole("button", { name: "Open menu" })); return { pinEvent, renderResult }; } it("should throw when pinned event has no sender", () => { const pinEventWithoutSender = makePinEvent({ sender: undefined }); expect(() => renderComponent(pinEventWithoutSender)).toThrow("Pinned event unexpectedly has no sender"); }); it("should render pinned event", () => { const { container } = renderComponent(makePinEvent()); expect(container).toMatchSnapshot(); }); it("should render pinned event with thread info", async () => { const event = makePinEvent({ content: { "body": "First pinned message", "msgtype": "m.text", "m.relates_to": { "event_id": "$threadRootEventId", "is_falling_back": true, "m.in_reply_to": { event_id: "$$threadRootEventId", }, "rel_type": "m.thread", }, }, }); const threadRootEvent = makePinEvent({ event_id: "$threadRootEventId" }); jest.spyOn(room, "findEventById").mockReturnValue(threadRootEvent); const { container } = renderComponent(event); expect(container).toMatchSnapshot(); await userEvent.click(screen.getByRole("button", { name: "thread message" })); // Check that the thread is opened expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.ShowThread, rootEvent: threadRootEvent, push: true, }); }); it("should render the menu without unpin and delete", async () => { jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue( false, ); jest.spyOn( room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "maySendRedactionForEvent", ).mockReturnValue(false); await renderAndOpenMenu(); // Unpin and delete should not be present await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); expect(screen.getByRole("menuitem", { name: "View in timeline" })).toBeInTheDocument(); expect(screen.getByRole("menuitem", { name: "Forward" })).toBeInTheDocument(); expect(screen.queryByRole("menuitem", { name: "Unpin" })).toBeNull(); expect(screen.queryByRole("menuitem", { name: "Delete" })).toBeNull(); expect(screen.getByRole("menu")).toMatchSnapshot(); }); it("should render the menu with all the options", async () => { // Enable unpin jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue( true, ); // Enable redaction jest.spyOn( room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "maySendRedactionForEvent", ).mockReturnValue(true); await renderAndOpenMenu(); await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); ["View in timeline", "Forward", "Unpin", "Delete"].forEach((name) => expect(screen.getByRole("menuitem", { name })).toBeInTheDocument(), ); expect(screen.getByRole("menu")).toMatchSnapshot(); }); it("should view in the timeline", async () => { const { pinEvent } = await renderAndOpenMenu(); // Test view in timeline button await userEvent.click(screen.getByRole("menuitem", { name: "View in timeline" })); expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.ViewRoom, event_id: pinEvent.getId(), highlighted: true, room_id: pinEvent.getRoomId(), metricsTrigger: undefined, // room doesn't change }); }); it("should open forward dialog", async () => { const { pinEvent } = await renderAndOpenMenu(); // Test forward button await userEvent.click(screen.getByRole("menuitem", { name: "Forward" })); expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.OpenForwardDialog, event: getForwardableEvent(pinEvent, mockClient), permalinkCreator: permalinkCreator, }); }); it("should unpin the event", async () => { const { pinEvent } = await renderAndOpenMenu(); const pinEvent2 = makePinEvent({ event_id: "$eventId2" }); const stateEvent = { getContent: jest.fn().mockReturnValue({ pinned: [pinEvent.getId(), pinEvent2.getId()] }), } as unknown as MatrixEvent; // Enable unpin jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue( true, ); // Mock the state event jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockReturnValue( stateEvent, ); // Test unpin button await userEvent.click(screen.getByRole("menuitem", { name: "Unpin" })); expect(mockClient.sendStateEvent).toHaveBeenCalledWith( room.roomId, EventType.RoomPinnedEvents, { pinned: [pinEvent2.getId()], }, "", ); }); it("should delete the event", async () => { // Enable redaction jest.spyOn( room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "maySendRedactionForEvent", ).mockReturnValue(true); const { pinEvent } = await renderAndOpenMenu(); await userEvent.click(screen.getByRole("menuitem", { name: "Delete" })); expect(createRedactEventDialog).toHaveBeenCalledWith({ mxEvent: pinEvent, }); }); });