2023-05-21 23:53:23 +00:00
|
|
|
/*
|
2024-09-09 13:57:16 +00:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2023-05-21 23:53:23 +00:00
|
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
|
2024-09-09 13:57:16 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2023-05-21 23:53:23 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
import React from "react";
|
2024-08-16 12:16:06 +00:00
|
|
|
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";
|
2023-05-21 23:53:23 +00:00
|
|
|
|
|
|
|
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
2024-08-16 12:16:06 +00:00
|
|
|
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(),
|
|
|
|
}));
|
2023-05-21 23:53:23 +00:00
|
|
|
|
|
|
|
describe("<PinnedEventTile />", () => {
|
|
|
|
const userId = "@alice:server.org";
|
|
|
|
const roomId = "!room:server.org";
|
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
let mockClient: MatrixClient;
|
|
|
|
let room: Room;
|
|
|
|
let permalinkCreator: RoomPermalinkCreator;
|
2023-05-21 23:53:23 +00:00
|
|
|
beforeEach(() => {
|
2024-08-16 12:16:06 +00:00
|
|
|
mockClient = stubClient();
|
|
|
|
room = new Room(roomId, mockClient, userId);
|
|
|
|
permalinkCreator = new RoomPermalinkCreator(room);
|
2024-09-05 14:37:24 +00:00
|
|
|
mockClient.getRoom = jest.fn().mockReturnValue(room);
|
2024-08-16 12:16:06 +00:00
|
|
|
jest.spyOn(dis, "dispatch").mockReturnValue(undefined);
|
2023-05-21 23:53:23 +00:00
|
|
|
});
|
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
/**
|
|
|
|
* Create a pinned event with the given content.
|
|
|
|
* @param content
|
|
|
|
*/
|
|
|
|
function makePinEvent(content?: Partial<IEvent>) {
|
|
|
|
return new MatrixEvent({
|
|
|
|
type: EventType.RoomMessage,
|
2023-05-21 23:53:23 +00:00
|
|
|
sender: userId,
|
|
|
|
content: {
|
|
|
|
body: "First pinned message",
|
|
|
|
msgtype: "m.text",
|
|
|
|
},
|
|
|
|
room_id: roomId,
|
|
|
|
origin_server_ts: 0,
|
2024-08-16 12:16:06 +00:00
|
|
|
event_id: "$eventId",
|
|
|
|
...content,
|
2023-05-21 23:53:23 +00:00
|
|
|
});
|
2024-08-16 12:16:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render the component with the given event.
|
|
|
|
* @param event - pinned event
|
|
|
|
*/
|
|
|
|
function renderComponent(event: MatrixEvent) {
|
|
|
|
return render(
|
|
|
|
<MatrixClientContext.Provider value={mockClient}>
|
|
|
|
<PinnedEventTile permalinkCreator={permalinkCreator} event={event} room={room} />
|
|
|
|
</MatrixClientContext.Provider>,
|
|
|
|
);
|
|
|
|
}
|
2023-05-21 23:53:23 +00:00
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
/**
|
|
|
|
* 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 };
|
|
|
|
}
|
2023-05-21 23:53:23 +00:00
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
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());
|
2023-05-21 23:53:23 +00:00
|
|
|
expect(container).toMatchSnapshot();
|
|
|
|
});
|
|
|
|
|
2024-08-21 09:02:35 +00:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
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
|
2023-05-21 23:53:23 +00:00
|
|
|
});
|
2024-08-16 12:16:06 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
});
|
2023-05-21 23:53:23 +00:00
|
|
|
|
2024-08-16 12:16:06 +00:00
|
|
|
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,
|
|
|
|
});
|
2023-05-21 23:53:23 +00:00
|
|
|
});
|
|
|
|
});
|