/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, fireEvent, screen, waitFor } from "@testing-library/react";
import { EventType, MatrixEvent, Room, MatrixClient, JoinRule } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked, MockedObject } from "jest-mock";
import userEvent from "@testing-library/user-event";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import RoomSummaryCard from "../../../../src/components/views/right_panel/RoomSummaryCard";
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
import ExportDialog from "../../../../src/components/views/dialogs/ExportDialog";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import * as settingsHooks from "../../../../src/hooks/useSettings";
import Modal from "../../../../src/Modal";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/PollHistoryDialog";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { _t } from "../../../../src/languageHandler";
import { tagRoom } from "../../../../src/utils/room/tagRoom";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import { Action } from "../../../../src/dispatcher/actions";
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
jest.mock("../../../../src/utils/room/tagRoom");
describe("", () => {
const userId = "@alice:domain.org";
const roomId = "!room:domain.org";
let mockClient!: MockedObject;
let room!: Room;
const getComponent = (props = {}) => {
const defaultProps = {
room,
onClose: jest.fn(),
permalinkCreator: new RoomPermalinkCreator(room),
};
return render(, {
wrapper: ({ children }) => (
{children}
),
});
};
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getAccountData: jest.fn(),
isRoomEncrypted: jest.fn(),
getOrCreateFilter: jest.fn().mockResolvedValue({ filterId: 1 }),
getRoom: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
deleteRoomTag: jest.fn().mockResolvedValue({}),
setRoomTag: jest.fn().mockResolvedValue({}),
});
room = new Room(roomId, mockClient, userId);
const roomCreateEvent = new MatrixEvent({
type: "m.room.create",
room_id: roomId,
sender: userId,
content: {
creator: userId,
room_version: "5",
},
state_key: "",
});
room.currentState.setStateEvents([roomCreateEvent]);
room.updateMyMembership(KnownMembership.Join);
jest.spyOn(Modal, "createDialog");
jest.spyOn(RightPanelStore.instance, "pushCard");
jest.spyOn(settingsHooks, "useFeatureEnabled").mockReturnValue(false);
jest.spyOn(defaultDispatcher, "dispatch");
jest.clearAllMocks();
DMRoomMap.makeShared(mockClient);
mockClient.getRoom.mockReturnValue(room);
jest.spyOn(room, "isElementVideoRoom").mockRestore();
jest.spyOn(room, "isCallRoom").mockRestore();
});
afterEach(() => {
jest.restoreAllMocks();
});
it("renders the room summary", () => {
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("renders the room topic in the summary", () => {
room.currentState.setStateEvents([
new MatrixEvent({
type: "m.room.topic",
room_id: roomId,
sender: userId,
content: {
topic: "This is the room's topic.",
},
state_key: "",
}),
]);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("has button to edit topic", () => {
room.currentState.setStateEvents([
new MatrixEvent({
type: "m.room.topic",
room_id: roomId,
sender: userId,
content: {
topic: "This is the room's topic.",
},
state_key: "",
}),
]);
const { container, getByText } = getComponent();
expect(getByText("Edit")).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
describe("search", () => {
it("has the search field", async () => {
const onSearchChange = jest.fn();
const { getByPlaceholderText } = getComponent({
onSearchChange,
});
expect(getByPlaceholderText("Search messages…")).toBeVisible();
});
it("should focus the search field if Action.FocusMessageSearch is fired", async () => {
const onSearchChange = jest.fn();
const { getByPlaceholderText } = getComponent({
onSearchChange,
});
expect(getByPlaceholderText("Search messages…")).not.toHaveFocus();
defaultDispatcher.fire(Action.FocusMessageSearch);
await waitFor(() => {
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
});
});
it("should focus the search field if focusRoomSearch=true", () => {
const onSearchChange = jest.fn();
const { getByPlaceholderText } = getComponent({
onSearchChange,
focusRoomSearch: true,
});
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
});
it("should cancel search on escape", () => {
const onSearchChange = jest.fn();
const onSearchCancel = jest.fn();
const { getByPlaceholderText } = getComponent({
onSearchChange,
onSearchCancel,
focusRoomSearch: true,
});
expect(getByPlaceholderText("Search messages…")).toHaveFocus();
fireEvent.keyDown(getByPlaceholderText("Search messages…"), { key: "Escape" });
expect(onSearchCancel).toHaveBeenCalled();
});
it("should empty search field when the timeline rendering type changes away", async () => {
const onSearchChange = jest.fn();
const { rerender } = render(
,
);
await userEvent.type(screen.getByPlaceholderText("Search messages…"), "test");
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("test");
rerender(
,
);
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("");
});
});
it("opens room file panel on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText("Files"));
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith({ phase: RightPanelPhases.FilePanel }, true);
});
it("opens room export dialog on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText(_t("export_chat|title")));
expect(Modal.createDialog).toHaveBeenCalledWith(ExportDialog, { room });
});
it("opens share room dialog on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText(_t("action|copy_link")));
expect(Modal.createDialog).toHaveBeenCalledWith(ShareDialog, { target: room });
});
it("opens invite dialog on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText(_t("action|invite")));
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "view_invite", roomId: room.roomId });
});
it("fires favourite dispatch on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText(_t("room|context_menu|favourite")));
expect(tagRoom).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
});
it("opens room settings on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText(_t("common|settings")));
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
});
describe("pinning", () => {
it("renders pins options", () => {
const { getByText } = getComponent();
expect(getByText("Pinned messages")).toBeInTheDocument();
});
});
describe("poll history", () => {
it("renders poll history option", () => {
const { getByText } = getComponent();
expect(getByText("Polls")).toBeInTheDocument();
});
it("opens poll history dialog on button click", () => {
const permalinkCreator = new RoomPermalinkCreator(room);
const { getByText } = getComponent({ permalinkCreator });
fireEvent.click(getByText("Polls"));
expect(Modal.createDialog).toHaveBeenCalledWith(PollHistoryDialog, {
room,
matrixClient: mockClient,
permalinkCreator: permalinkCreator,
});
});
});
describe("video rooms", () => {
it("does not render irrelevant options for element video room", () => {
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(true);
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_video_rooms");
const { queryByText } = getComponent();
// options not rendered
expect(queryByText("Files")).not.toBeInTheDocument();
expect(queryByText("Pinned")).not.toBeInTheDocument();
expect(queryByText("Export chat")).not.toBeInTheDocument();
});
it("does not render irrelevant options for element call room", () => {
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
mocked(settingsHooks.useFeatureEnabled).mockImplementation(
(feature) => feature === "feature_element_call_video_rooms" || feature === "feature_video_rooms",
);
const { queryByText } = getComponent();
// options not rendered
expect(queryByText("Files")).not.toBeInTheDocument();
expect(queryByText("Pinned")).not.toBeInTheDocument();
expect(queryByText("Export chat")).not.toBeInTheDocument();
});
});
describe("public room label", () => {
beforeEach(() => {
jest.spyOn(room.currentState, "getJoinRule").mockReturnValue(JoinRule.Public);
});
it("does not show public room label for a DM", async () => {
mockClient.getAccountData.mockImplementation(
(eventType) =>
({
[EventType.Direct]: new MatrixEvent({
type: EventType.Direct,
content: {
"@bob:sesame.st": ["some-room-id"],
// this room is a DM with ernie
"@ernie:sesame.st": ["some-other-room-id", room.roomId],
},
}),
})[eventType],
);
getComponent();
await flushPromises();
expect(screen.queryByText("Public room")).not.toBeInTheDocument();
});
it("does not show public room label for non public room", async () => {
jest.spyOn(room.currentState, "getJoinRule").mockReturnValue(JoinRule.Invite);
getComponent();
await flushPromises();
expect(screen.queryByText("Public room")).not.toBeInTheDocument();
});
it("shows a public room label for a public room", async () => {
jest.spyOn(room.currentState, "getJoinRule").mockReturnValue(JoinRule.Public);
getComponent();
await flushPromises();
expect(screen.queryByText("Public room")).toBeInTheDocument();
});
});
});