2022-05-06 11:09:28 +02:00
|
|
|
/*
|
2025-12-03 15:21:15 +00:00
|
|
|
Copyright (C) 2025 Element Creations Ltd
|
2025-04-15 09:01:35 +01:00
|
|
|
Copyright 2024, 2025 New Vector Ltd.
|
2023-08-01 08:32:53 +01:00
|
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
2022-05-06 11:09:28 +02:00
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-09 14:57:16 +01:00
|
|
|
Please see LICENSE files in the repository root for full details.
|
2022-05-06 11:09:28 +02:00
|
|
|
*/
|
|
|
|
|
|
2021-10-01 14:54:26 +01:00
|
|
|
import React from "react";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { CallType, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
2023-09-12 08:35:07 +01:00
|
|
|
import {
|
2024-11-18 18:27:34 -05:00
|
|
|
EventType,
|
|
|
|
|
JoinRule,
|
|
|
|
|
MatrixEvent,
|
|
|
|
|
PendingEventOrdering,
|
|
|
|
|
Room,
|
|
|
|
|
RoomStateEvent,
|
|
|
|
|
RoomMember,
|
|
|
|
|
} from "matrix-js-sdk/src/matrix";
|
|
|
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
|
|
|
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
|
|
|
|
import {
|
|
|
|
|
act,
|
2023-11-01 13:03:10 +01:00
|
|
|
createEvent,
|
2023-09-12 08:35:07 +01:00
|
|
|
fireEvent,
|
|
|
|
|
getAllByLabelText,
|
|
|
|
|
getByLabelText,
|
|
|
|
|
getByText,
|
2024-06-19 16:49:09 +01:00
|
|
|
queryByLabelText,
|
2023-09-12 08:35:07 +01:00
|
|
|
render,
|
2025-02-05 13:25:06 +00:00
|
|
|
type RenderOptions,
|
2023-09-12 08:35:07 +01:00
|
|
|
screen,
|
|
|
|
|
waitFor,
|
2024-10-14 21:41:58 +05:30
|
|
|
} from "jest-matrix-react";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
2024-08-06 15:51:25 +01:00
|
|
|
import { mocked } from "jest-mock";
|
2025-03-05 15:41:26 -05:00
|
|
|
import userEvent from "@testing-library/user-event";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
2025-01-27 20:35:46 +05:30
|
|
|
import { filterConsole, stubClient } from "../../../../../test-utils";
|
|
|
|
|
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
|
|
|
|
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
|
|
|
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
2025-10-29 09:40:21 +00:00
|
|
|
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext";
|
2025-11-05 07:24:26 +00:00
|
|
|
import RoomContext, { type RoomContextType } from "../../../../../../src/contexts/RoomContext";
|
2025-01-27 20:35:46 +05:30
|
|
|
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
|
|
|
|
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
|
|
|
|
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
|
|
|
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
|
|
|
|
import SdkConfig from "../../../../../../src/SdkConfig";
|
|
|
|
|
import dispatcher from "../../../../../../src/dispatcher/dispatcher";
|
|
|
|
|
import { CallStore } from "../../../../../../src/stores/CallStore";
|
2025-08-07 11:27:53 +02:00
|
|
|
import { type Call } from "../../../../../../src/models/Call";
|
2025-01-27 20:35:46 +05:30
|
|
|
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
|
|
|
|
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
|
|
|
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
|
|
|
|
import { _t } from "../../../../../../src/languageHandler";
|
2025-02-05 13:25:06 +00:00
|
|
|
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
2025-01-27 20:35:46 +05:30
|
|
|
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
2025-04-15 09:01:35 +01:00
|
|
|
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
2025-08-07 11:27:53 +02:00
|
|
|
import { ElementCallMemberEventType } from "../../../../../../src/call-types";
|
2024-04-10 16:46:27 +02:00
|
|
|
|
2025-01-27 20:35:46 +05:30
|
|
|
jest.mock("../../../../../../src/utils/ShieldUtils");
|
|
|
|
|
jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
|
|
|
|
|
useCurrentPhase: () => {
|
|
|
|
|
return { currentPhase: "foo", isOpen: false };
|
|
|
|
|
},
|
|
|
|
|
}));
|
2023-06-09 15:33:54 +03:00
|
|
|
|
2023-08-30 18:55:02 +01:00
|
|
|
describe("RoomHeader", () => {
|
2023-11-16 03:25:34 +00:00
|
|
|
filterConsole(
|
|
|
|
|
"[getType] Room !1:example.org does not have an m.room.create event",
|
|
|
|
|
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.",
|
|
|
|
|
);
|
2023-09-12 08:35:07 +01:00
|
|
|
|
2022-09-25 10:57:25 -04:00
|
|
|
let room: Room;
|
2023-08-01 08:32:53 +01:00
|
|
|
const ROOM_ID = "!1:example.org";
|
2022-09-25 10:57:25 -04:00
|
|
|
|
2023-08-02 11:54:06 +01:00
|
|
|
let setCardSpy: jest.SpyInstance | undefined;
|
|
|
|
|
|
2025-10-29 09:40:21 +00:00
|
|
|
const mockRoomViewStore = {
|
|
|
|
|
isViewingCall: jest.fn().mockReturnValue(false),
|
|
|
|
|
on: jest.fn(),
|
|
|
|
|
off: jest.fn(),
|
|
|
|
|
emit: jest.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-05 07:24:26 +00:00
|
|
|
let roomContext: RoomContextType;
|
2025-10-29 09:40:21 +00:00
|
|
|
|
|
|
|
|
function getWrapper(): RenderOptions {
|
|
|
|
|
return {
|
|
|
|
|
wrapper: ({ children }) => (
|
|
|
|
|
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
|
|
|
|
|
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
|
|
|
|
|
</MatrixClientContext.Provider>
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-25 10:57:25 -04:00
|
|
|
beforeEach(async () => {
|
|
|
|
|
stubClient();
|
2023-08-02 11:54:06 +01:00
|
|
|
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org", {
|
|
|
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
|
|
|
});
|
|
|
|
|
DMRoomMap.setShared({
|
|
|
|
|
getUserIdForRoomId: jest.fn(),
|
|
|
|
|
} as unknown as DMRoomMap);
|
|
|
|
|
|
|
|
|
|
setCardSpy = jest.spyOn(RightPanelStore.instance, "setCard");
|
2024-10-15 16:24:58 +02:00
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Normal);
|
2025-08-28 09:43:00 +01:00
|
|
|
|
|
|
|
|
// Mock CallStore.instance.getCall to return null by default
|
|
|
|
|
// Individual tests can override this when they need a specific Call object
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
2025-10-29 09:40:21 +00:00
|
|
|
|
|
|
|
|
// Reset the mock RoomViewStore
|
|
|
|
|
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
|
|
|
|
|
|
|
|
|
// Create a stable room context for this test
|
|
|
|
|
roomContext = {
|
|
|
|
|
...RoomContext,
|
|
|
|
|
roomId: ROOM_ID,
|
|
|
|
|
roomViewStore: mockRoomViewStore,
|
2025-11-05 07:24:26 +00:00
|
|
|
} as unknown as RoomContextType;
|
2022-09-25 10:57:25 -04:00
|
|
|
});
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
afterEach(() => {
|
2023-09-07 16:34:52 +00:00
|
|
|
jest.restoreAllMocks();
|
2025-04-15 09:01:35 +01:00
|
|
|
SettingsStore.reset();
|
2023-08-23 15:13:40 +01:00
|
|
|
});
|
|
|
|
|
|
2023-08-01 08:32:53 +01:00
|
|
|
it("renders the room header", () => {
|
2024-01-11 19:56:36 +00:00
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
2023-08-01 08:32:53 +01:00
|
|
|
expect(container).toHaveTextContent(ROOM_ID);
|
2022-09-25 10:57:25 -04:00
|
|
|
});
|
2023-08-01 14:47:09 +01:00
|
|
|
|
2023-08-02 11:54:06 +01:00
|
|
|
it("opens the room summary", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2024-01-11 19:56:36 +00:00
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
2023-08-02 11:54:06 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(getByText(container, ROOM_ID));
|
2023-08-02 11:54:06 +01:00
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
|
|
|
|
});
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2023-08-30 18:55:02 +01:00
|
|
|
it("shows a face pile for rooms", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-08-30 18:55:02 +01:00
|
|
|
const members = [
|
|
|
|
|
{
|
|
|
|
|
userId: "@me:example.org",
|
|
|
|
|
name: "Member",
|
|
|
|
|
rawDisplayName: "Member",
|
|
|
|
|
roomId: room.roomId,
|
2024-03-12 14:52:54 +00:00
|
|
|
membership: KnownMembership.Join,
|
2023-08-30 18:55:02 +01:00
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
userId: "@you:example.org",
|
|
|
|
|
name: "Member",
|
|
|
|
|
rawDisplayName: "Member",
|
|
|
|
|
roomId: room.roomId,
|
2024-03-12 14:52:54 +00:00
|
|
|
membership: KnownMembership.Join,
|
2023-08-30 18:55:02 +01:00
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
userId: "@them:example.org",
|
|
|
|
|
name: "Member",
|
|
|
|
|
rawDisplayName: "Member",
|
|
|
|
|
roomId: room.roomId,
|
2024-03-12 14:52:54 +00:00
|
|
|
membership: KnownMembership.Join,
|
2023-08-30 18:55:02 +01:00
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
userId: "@bot:example.org",
|
|
|
|
|
name: "Bot user",
|
|
|
|
|
rawDisplayName: "Bot user",
|
|
|
|
|
roomId: room.roomId,
|
2024-03-12 14:52:54 +00:00
|
|
|
membership: KnownMembership.Join,
|
2023-08-30 18:55:02 +01:00
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
room.currentState.setJoinedMemberCount(members.length);
|
|
|
|
|
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
|
|
|
|
|
2024-01-11 19:56:36 +00:00
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
2023-08-30 18:55:02 +01:00
|
|
|
|
|
|
|
|
expect(container).toHaveTextContent("4");
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const facePile = getByLabelText(document.body, "4 members");
|
2023-08-30 18:55:02 +01:00
|
|
|
expect(facePile).toHaveTextContent("4");
|
|
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(facePile);
|
2023-08-30 18:55:02 +01:00
|
|
|
|
2024-11-25 17:29:32 +00:00
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
|
2023-08-30 18:55:02 +01:00
|
|
|
});
|
|
|
|
|
|
2024-07-09 17:06:50 +05:30
|
|
|
it("has room info icon that opens the room info panel", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2024-07-09 17:06:50 +05:30
|
|
|
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(infoButton);
|
2024-07-09 17:06:50 +05:30
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
it("opens the thread panel", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(getByLabelText(document.body, "Threads"));
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("opens the notifications panel", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2025-04-15 09:01:35 +01:00
|
|
|
SettingsStore.setValue("feature_notifications", null, SettingLevel.DEVICE, true);
|
2023-08-31 21:57:45 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(getByLabelText(document.body, "Notifications"));
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-19 16:49:09 +01:00
|
|
|
it("should show both call buttons in rooms smaller than 3 members", async () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const voiceButton = screen.getByRole("button", { name: "Voice call" });
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
|
|
|
|
expect(voiceButton).toBeInTheDocument();
|
2024-06-19 16:49:09 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show voice call button in managed hybrid environments", async () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ widget_build_url: "https://widget.build.url" });
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Voice call" })).not.toBeInTheDocument();
|
2024-06-19 16:49:09 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show voice call button in rooms larger than 2 members", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Voice call" })).not.toBeInTheDocument();
|
2024-06-19 16:49:09 +01:00
|
|
|
});
|
|
|
|
|
|
2025-05-02 16:05:30 +01:00
|
|
|
describe("UIFeature.Voip disabled", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
SdkConfig.put({
|
|
|
|
|
setting_defaults: {
|
|
|
|
|
[UIFeature.Voip]: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
|
|
|
|
jest.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show call buttons in rooms smaller than 3 members", async () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Video call" })).not.toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Voice call" })).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show call button in rooms larger than 2 members", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Video call" })).not.toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByRole("button", { name: "Voice call" })).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-21 09:42:50 +01:00
|
|
|
describe("UIFeature.Widgets enabled (default)", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.put({
|
|
|
|
|
setting_defaults: {
|
|
|
|
|
[UIFeature.Widgets]: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show call buttons in a room with 2 members", () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show call buttons in a room with more than 2 members", () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("UIFeature.Widgets disabled", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.put({
|
|
|
|
|
setting_defaults: {
|
|
|
|
|
[UIFeature.Widgets]: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show call buttons in a room with 2 members", () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).toBeInTheDocument();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show call buttons in a room with more than 2 members", () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
expect(queryByLabelText(container, "Video call")).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
describe("groups call disabled", () => {
|
2024-06-21 09:42:50 +01:00
|
|
|
beforeEach(() => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.put({
|
|
|
|
|
setting_defaults: {
|
|
|
|
|
[UIFeature.Widgets]: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
2024-06-21 09:42:50 +01:00
|
|
|
});
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
it("you can call when you're two in the room", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-08-23 15:13:40 +01:00
|
|
|
mockRoomMembers(room, 2);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const voiceButton = screen.getByRole("button", { name: "Voice call" });
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
|
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
|
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(voiceButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(videoButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("you can't call if there's already a call", () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
|
|
|
|
|
// The JS-SDK does not export the class `MatrixCall` only the type
|
|
|
|
|
{} as MatrixCall,
|
|
|
|
|
);
|
2024-01-11 19:56:36 +00:00
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
2023-09-11 17:55:32 +01:00
|
|
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(button).toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-19 16:49:09 +01:00
|
|
|
it("can call in large rooms if able to edit widgets", () => {
|
2023-08-23 15:13:40 +01:00
|
|
|
mockRoomMembers(room, 10);
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoCallButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("disable calls in large rooms by default", () => {
|
|
|
|
|
mockRoomMembers(room, 10);
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-09-12 08:35:07 +01:00
|
|
|
expect(
|
2024-10-14 21:41:58 +05:30
|
|
|
getByLabelText(document.body, "You do not have permission to start video calls", {
|
|
|
|
|
selector: "button",
|
|
|
|
|
}),
|
2023-12-14 11:10:01 +00:00
|
|
|
).toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("group call enabled", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.put({
|
|
|
|
|
features: {
|
|
|
|
|
feature_group_calls: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
|
|
|
|
jest.restoreAllMocks();
|
2023-08-23 15:13:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders only the video call element", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-09-12 08:35:07 +01:00
|
|
|
mockRoomMembers(room, 3);
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.add({
|
|
|
|
|
element_call: {
|
|
|
|
|
use_exclusively: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2023-08-23 15:13:40 +01:00
|
|
|
// allow element calls
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
|
|
|
|
expect(screen.queryByTitle("Voice call")).toBeNull();
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(videoCallButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(videoCallButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
|
|
|
|
});
|
|
|
|
|
|
2023-09-12 08:35:07 +01:00
|
|
|
it("can't call if there's an ongoing (pinned) call", () => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.add({
|
|
|
|
|
element_call: {
|
|
|
|
|
use_exclusively: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2023-08-23 15:13:40 +01:00
|
|
|
// allow element calls
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
2023-09-12 08:35:07 +01:00
|
|
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
2024-02-08 17:48:50 +01:00
|
|
|
const widget = { type: "m.jitsi" } as IApp;
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
|
|
|
|
widget,
|
|
|
|
|
on: () => {},
|
2024-08-06 18:22:02 +01:00
|
|
|
off: () => {},
|
2024-02-08 17:48:50 +01:00
|
|
|
} as unknown as Call);
|
|
|
|
|
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2025-12-03 15:21:15 +00:00
|
|
|
// Voice and video
|
|
|
|
|
for (const button of screen.getAllByRole("button", { name: "Ongoing call" })) {
|
|
|
|
|
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
}
|
2023-08-23 15:13:40 +01:00
|
|
|
});
|
|
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
it("clicking on ongoing (unpinned) call re-pins it", async () => {
|
|
|
|
|
const user = userEvent.setup();
|
2024-02-08 17:48:50 +01:00
|
|
|
mockRoomMembers(room, 3);
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.add({
|
|
|
|
|
setting_defaults: {
|
|
|
|
|
[UIFeature.Widgets]: true,
|
|
|
|
|
},
|
|
|
|
|
features: {
|
|
|
|
|
feature_group_calls: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
2024-01-29 17:06:12 +01:00
|
|
|
// allow calls
|
2023-09-12 08:35:07 +01:00
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
|
|
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
|
|
|
|
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
|
|
|
|
|
2024-02-08 17:48:50 +01:00
|
|
|
const widget = { type: "m.jitsi" } as IApp;
|
2024-01-31 16:18:52 +01:00
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
|
|
|
|
widget,
|
|
|
|
|
on: () => {},
|
2024-08-06 18:22:02 +01:00
|
|
|
off: () => {},
|
2024-01-31 16:18:52 +01:00
|
|
|
} as unknown as Call);
|
2024-02-08 17:48:50 +01:00
|
|
|
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
2023-09-12 08:35:07 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(videoButton);
|
2023-09-12 08:35:07 +01:00
|
|
|
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
it("disables calling if there's a jitsi call", () => {
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
|
|
|
|
|
// The JS-SDK does not export the class `MatrixCall` only the type
|
|
|
|
|
{} as MatrixCall,
|
|
|
|
|
);
|
2024-01-11 19:56:36 +00:00
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
2023-09-11 17:55:32 +01:00
|
|
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(button).toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("calls using legacy or jitsi", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-08-23 15:13:40 +01:00
|
|
|
mockRoomMembers(room, 2);
|
2024-01-31 16:18:52 +01:00
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
|
|
|
if (key === "im.vector.modular.widgets") return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const voiceButton = screen.getByRole("button", { name: "Voice call" });
|
|
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
|
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(voiceButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(videoButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("calls using legacy or jitsi for large rooms", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-08-23 15:13:40 +01:00
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
|
|
|
if (key === "im.vector.modular.widgets") return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
|
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(videoButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
|
|
|
|
});
|
|
|
|
|
|
2024-01-29 17:06:12 +01:00
|
|
|
it("calls using element call for large rooms", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2023-08-23 15:13:40 +01:00
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
2025-08-07 11:27:53 +02:00
|
|
|
if (key === ElementCallMemberEventType.name) return true;
|
2023-08-23 15:13:40 +01:00
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
2023-12-14 11:10:01 +00:00
|
|
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-03-05 15:41:26 -05:00
|
|
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
|
|
|
|
await user.click(videoButton);
|
2023-08-23 15:13:40 +01:00
|
|
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
|
|
|
|
});
|
2024-02-08 17:48:50 +01:00
|
|
|
|
|
|
|
|
it("buttons are disabled if there is an ongoing call", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
|
2024-06-17 13:00:41 +02:00
|
|
|
jest.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
2024-02-08 17:48:50 +01:00
|
|
|
new Set([{ roomId: "some_other_room" } as Call]),
|
|
|
|
|
);
|
|
|
|
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
2024-06-19 16:49:09 +01:00
|
|
|
const [videoButton] = getAllByLabelText(container, "Ongoing call");
|
2024-02-08 17:48:50 +01:00
|
|
|
|
|
|
|
|
expect(videoButton).toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-02 17:51:15 +00:00
|
|
|
it("join video call button is shown if there is an ongoing call", async () => {
|
2024-02-08 17:48:50 +01:00
|
|
|
mockRoomMembers(room, 3);
|
2025-08-28 09:43:00 +01:00
|
|
|
// Mock CallStore to return a call with 3 participants
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2026-01-02 17:51:15 +00:00
|
|
|
const joinButton = getByLabelText(document.body, "Join video call");
|
|
|
|
|
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("join voice call button is shown if there is an ongoing call", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
// Mock CallStore to return a call with 3 participants
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice));
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
const joinButton = getByLabelText(document.body, "Join voice call");
|
2024-02-08 17:48:50 +01:00
|
|
|
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("join button is disabled if there is an other ongoing call", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
2025-08-28 09:43:00 +01:00
|
|
|
// Mock CallStore to return a call with 3 participants
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
2024-06-17 13:00:41 +02:00
|
|
|
jest.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
2024-02-08 17:48:50 +01:00
|
|
|
new Set([{ roomId: "some_other_room" } as Call]),
|
|
|
|
|
);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
const joinButton = getByLabelText(document.body, "Ongoing call");
|
2024-02-08 17:48:50 +01:00
|
|
|
|
|
|
|
|
expect(joinButton).toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("close lobby button is shown", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
|
|
|
|
|
2025-10-29 09:40:21 +00:00
|
|
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
getByLabelText(document.body, "Close lobby");
|
2024-02-08 17:48:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("close lobby button is shown if there is an ongoing call but we are viewing the lobby", async () => {
|
|
|
|
|
mockRoomMembers(room, 3);
|
2025-08-28 09:43:00 +01:00
|
|
|
// Mock CallStore to return a call with 3 participants
|
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
2025-10-29 09:40:21 +00:00
|
|
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
2024-02-08 17:48:50 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
getByLabelText(document.body, "Close lobby");
|
2024-02-08 17:48:50 +01:00
|
|
|
});
|
2024-03-07 16:38:53 +01:00
|
|
|
|
|
|
|
|
it("don't show external conference button if the call is not shown", () => {
|
2025-10-29 09:40:21 +00:00
|
|
|
mockRoomViewStore.isViewingCall.mockReturnValue(false);
|
2024-04-10 16:46:27 +02:00
|
|
|
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
|
|
|
|
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
|
|
|
|
});
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2024-03-07 16:38:53 +01:00
|
|
|
expect(screen.queryByLabelText(_t("voip|get_call_link"))).not.toBeInTheDocument();
|
|
|
|
|
|
2025-10-29 09:40:21 +00:00
|
|
|
mockRoomViewStore.isViewingCall.mockReturnValue(true);
|
2024-03-07 16:38:53 +01:00
|
|
|
|
2024-10-16 16:43:07 +01:00
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2024-03-07 16:38:53 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
expect(getByLabelText(document.body, _t("voip|get_call_link"))).toBeInTheDocument();
|
2024-03-07 16:38:53 +01:00
|
|
|
});
|
2025-12-03 15:21:15 +00:00
|
|
|
|
|
|
|
|
it("gives the option of element call or legacy calling for video", async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
|
|
|
if (key === ElementCallMemberEventType.name) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const button = screen.getByRole("button", { name: "Video call" });
|
|
|
|
|
expect(button).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
await user.click(button);
|
|
|
|
|
const elementCallButton = screen.getByRole("menuitem", { name: "Element Call" });
|
|
|
|
|
const legacyCallButton = screen.getByRole("menuitem", { name: "Legacy Call" });
|
|
|
|
|
expect(elementCallButton).toBeInTheDocument();
|
|
|
|
|
expect(legacyCallButton).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
it("gives the option of element call or legacy calling for voice in DM rooms", async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
mockRoomMembers(room, 2);
|
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
|
|
|
if (key === ElementCallMemberEventType.name) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
const button = screen.getByRole("button", { name: "Voice call" });
|
|
|
|
|
expect(button).not.toHaveAttribute("aria-disabled", "true");
|
|
|
|
|
await user.click(button);
|
|
|
|
|
const elementCallButton = screen.getByRole("menuitem", { name: "Element Call" });
|
|
|
|
|
const legacyCallButton = screen.getByRole("menuitem", { name: "Legacy Call" });
|
|
|
|
|
expect(elementCallButton).toBeInTheDocument();
|
|
|
|
|
expect(legacyCallButton).toBeInTheDocument();
|
|
|
|
|
});
|
2024-03-07 16:38:53 +01:00
|
|
|
});
|
|
|
|
|
|
2023-09-01 10:45:50 +01:00
|
|
|
describe("public room", () => {
|
|
|
|
|
it("shows a globe", () => {
|
|
|
|
|
const joinRuleEvent = new MatrixEvent({
|
|
|
|
|
type: EventType.RoomJoinRules,
|
|
|
|
|
content: { join_rule: JoinRule.Public },
|
|
|
|
|
sender: MatrixClientPeg.get()!.getSafeUserId(),
|
|
|
|
|
state_key: "",
|
|
|
|
|
room_id: room.roomId,
|
|
|
|
|
});
|
2024-11-06 14:55:21 +00:00
|
|
|
room.addLiveEvents([joinRuleEvent], { addToState: true });
|
2023-09-01 10:45:50 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-09-01 10:45:50 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
expect(getByLabelText(document.body, "Public room")).toBeInTheDocument();
|
2023-09-01 10:45:50 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-31 12:06:14 +01:00
|
|
|
describe("dm", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
// Make the mocked room a DM
|
2024-08-06 15:51:25 +01:00
|
|
|
mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId) => {
|
|
|
|
|
if (roomId === room.roomId) return "@user:example.com";
|
2023-08-31 12:06:14 +01:00
|
|
|
});
|
2024-08-06 15:51:25 +01:00
|
|
|
room.getMember = jest.fn((userId) => new RoomMember(room.roomId, userId));
|
|
|
|
|
room.getJoinedMembers = jest.fn().mockReturnValue([
|
|
|
|
|
{
|
|
|
|
|
userId: "@me:example.org",
|
|
|
|
|
name: "Member",
|
|
|
|
|
rawDisplayName: "Member",
|
|
|
|
|
roomId: room.roomId,
|
|
|
|
|
membership: KnownMembership.Join,
|
|
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
userId: "@bob:example.org",
|
|
|
|
|
name: "Other Member",
|
|
|
|
|
rawDisplayName: "Other Member",
|
|
|
|
|
roomId: room.roomId,
|
|
|
|
|
membership: KnownMembership.Join,
|
|
|
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
|
|
|
},
|
|
|
|
|
]);
|
2023-08-31 12:06:14 +01:00
|
|
|
});
|
|
|
|
|
|
2025-04-15 09:01:35 +01:00
|
|
|
afterEach(() => {
|
|
|
|
|
SdkConfig.reset();
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-31 12:06:14 +01:00
|
|
|
it.each([
|
|
|
|
|
[ShieldUtils.E2EStatus.Verified, "Verified"],
|
|
|
|
|
[ShieldUtils.E2EStatus.Warning, "Untrusted"],
|
|
|
|
|
])("shows the %s icon", async (value: ShieldUtils.E2EStatus, expectedLabel: string) => {
|
|
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(value);
|
|
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2023-08-31 12:06:14 +01:00
|
|
|
|
2024-10-14 21:41:58 +05:30
|
|
|
await waitFor(() => expect(getByLabelText(document.body, expectedLabel)).toBeInTheDocument());
|
2023-08-31 12:06:14 +01:00
|
|
|
});
|
2024-08-06 15:51:25 +01:00
|
|
|
|
|
|
|
|
it("does not show the face pile for DMs", () => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SdkConfig.put({
|
|
|
|
|
features: {
|
|
|
|
|
feature_notifications: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
2024-08-06 15:51:25 +01:00
|
|
|
const { asFragment } = render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
|
|
|
|
|
expect(asFragment()).toMatchSnapshot();
|
|
|
|
|
});
|
2024-11-18 18:27:34 -05:00
|
|
|
|
|
|
|
|
it("updates the icon when the encryption status changes", async () => {
|
|
|
|
|
// The room starts verified
|
|
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
|
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
|
|
|
|
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
|
|
|
|
|
|
|
|
|
|
// A new member joins, and the room becomes unverified
|
|
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
|
|
|
|
act(() => {
|
|
|
|
|
room.emit(
|
|
|
|
|
RoomStateEvent.Members,
|
|
|
|
|
new MatrixEvent({
|
|
|
|
|
event_id: "$event_id",
|
|
|
|
|
type: EventType.RoomMember,
|
|
|
|
|
state_key: "@alice:example.org",
|
|
|
|
|
content: {
|
|
|
|
|
membership: "join",
|
|
|
|
|
},
|
|
|
|
|
room_id: ROOM_ID,
|
|
|
|
|
sender: "@alice:example.org",
|
|
|
|
|
}),
|
|
|
|
|
room.currentState,
|
|
|
|
|
new RoomMember(room.roomId, "@alice:example.org"),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
|
|
|
|
|
|
|
|
|
|
// The user becomes verified
|
|
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
|
|
|
|
|
act(() => {
|
|
|
|
|
MatrixClientPeg.get()!.emit(
|
|
|
|
|
CryptoEvent.UserTrustStatusChanged,
|
|
|
|
|
"@alice:example.org",
|
|
|
|
|
new UserVerificationStatus(true, true, true, false),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
|
|
|
|
|
|
|
|
|
|
// An unverified device is added
|
|
|
|
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
|
|
|
|
|
act(() => {
|
|
|
|
|
MatrixClientPeg.get()!.emit(CryptoEvent.DevicesUpdated, ["@alice:example.org"], false);
|
|
|
|
|
});
|
|
|
|
|
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
|
|
|
|
|
});
|
2023-08-31 12:06:14 +01:00
|
|
|
});
|
2023-11-01 13:03:10 +01:00
|
|
|
|
|
|
|
|
it("renders additionalButtons", async () => {
|
|
|
|
|
const additionalButtons: ViewRoomOpts["buttons"] = [
|
|
|
|
|
{
|
2023-11-09 12:23:30 +01:00
|
|
|
icon: () => <>test-icon</>,
|
2023-11-01 13:03:10 +01:00
|
|
|
id: "test-id",
|
|
|
|
|
label: () => "test-label",
|
|
|
|
|
onClick: () => {},
|
|
|
|
|
},
|
|
|
|
|
];
|
2024-01-11 19:56:36 +00:00
|
|
|
render(<RoomHeader room={room} additionalButtons={additionalButtons} />, getWrapper());
|
2023-11-01 13:03:10 +01:00
|
|
|
expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("calls onClick-callback on additionalButtons", () => {
|
|
|
|
|
const callback = jest.fn();
|
|
|
|
|
const additionalButtons: ViewRoomOpts["buttons"] = [
|
|
|
|
|
{
|
2023-11-09 12:23:30 +01:00
|
|
|
icon: () => <>test-icon</>,
|
2023-11-01 13:03:10 +01:00
|
|
|
id: "test-id",
|
|
|
|
|
label: () => "test-label",
|
|
|
|
|
onClick: callback,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2024-01-11 19:56:36 +00:00
|
|
|
render(<RoomHeader room={room} additionalButtons={additionalButtons} />, getWrapper());
|
2023-11-01 13:03:10 +01:00
|
|
|
|
|
|
|
|
const button = screen.getByRole("button", { name: "test-label" });
|
|
|
|
|
const event = createEvent.click(button);
|
|
|
|
|
event.stopPropagation = jest.fn();
|
|
|
|
|
fireEvent(button, event);
|
|
|
|
|
|
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
|
|
|
expect(event.stopPropagation).toHaveBeenCalled();
|
|
|
|
|
});
|
2024-01-03 13:25:44 +01:00
|
|
|
|
|
|
|
|
describe("ask to join disabled", () => {
|
|
|
|
|
it("does not render the RoomKnocksBar", () => {
|
2024-01-11 19:56:36 +00:00
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2024-01-03 13:25:44 +01:00
|
|
|
expect(screen.queryByRole("heading", { name: "Asking to join" })).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("ask to join enabled", () => {
|
|
|
|
|
it("does render the RoomKnocksBar", () => {
|
2025-04-15 09:01:35 +01:00
|
|
|
SettingsStore.setValue("feature_ask_to_join", null, SettingLevel.DEVICE, true);
|
2024-01-03 13:25:44 +01:00
|
|
|
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
|
|
|
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
|
|
|
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([new RoomMember(room.roomId, "@foo")]);
|
|
|
|
|
|
2024-01-11 19:56:36 +00:00
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2024-01-03 13:25:44 +01:00
|
|
|
expect(screen.getByRole("heading", { name: "Asking to join" })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-09-26 11:14:53 +01:00
|
|
|
|
|
|
|
|
it("should open room settings when clicking the room avatar", async () => {
|
2025-03-05 15:41:26 -05:00
|
|
|
const user = userEvent.setup();
|
2024-10-14 21:41:58 +05:30
|
|
|
render(<RoomHeader room={room} />, getWrapper());
|
2024-09-26 11:14:53 +01:00
|
|
|
|
|
|
|
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
2025-03-05 15:41:26 -05:00
|
|
|
await user.click(getByLabelText(document.body, "Open room settings"));
|
2024-09-26 11:14:53 +01:00
|
|
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ action: "open_room_settings" }));
|
|
|
|
|
});
|
2022-09-25 10:57:25 -04:00
|
|
|
});
|
2023-08-23 15:13:40 +01:00
|
|
|
|
2025-08-28 09:43:00 +01:00
|
|
|
/**
|
|
|
|
|
* Creates a mock Call object with stable participants to prevent React dependency errors
|
|
|
|
|
*/
|
2026-01-02 17:51:15 +00:00
|
|
|
function createMockCall(
|
|
|
|
|
roomId: string = "!1:example.org",
|
|
|
|
|
participantCount: number = 0,
|
|
|
|
|
callType: CallType = CallType.Video,
|
|
|
|
|
): Call {
|
2025-08-28 09:43:00 +01:00
|
|
|
const participants = new Map();
|
|
|
|
|
|
|
|
|
|
// Create mock participants with devices
|
|
|
|
|
for (let i = 0; i < participantCount; i++) {
|
|
|
|
|
const mockMember = {
|
|
|
|
|
userId: `@user-${i}:example.org`,
|
|
|
|
|
name: `Member ${i}`,
|
|
|
|
|
} as RoomMember;
|
|
|
|
|
|
|
|
|
|
const deviceSet = new Set([`device-${i}`]);
|
|
|
|
|
participants.set(mockMember, deviceSet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
roomId,
|
|
|
|
|
participants,
|
|
|
|
|
widget: { id: "test-widget" },
|
|
|
|
|
connectionState: "disconnected",
|
2026-01-02 17:51:15 +00:00
|
|
|
callType,
|
2025-08-28 09:43:00 +01:00
|
|
|
on: jest.fn(),
|
|
|
|
|
off: jest.fn(),
|
|
|
|
|
emit: jest.fn(),
|
|
|
|
|
} as unknown as Call;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 15:13:40 +01:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param count the number of users to create
|
|
|
|
|
*/
|
|
|
|
|
function mockRoomMembers(room: Room, count: number) {
|
|
|
|
|
const members = Array(count)
|
|
|
|
|
.fill(0)
|
|
|
|
|
.map((_, index) => ({
|
|
|
|
|
userId: `@user-${index}:example.org`,
|
|
|
|
|
name: `Member ${index}`,
|
|
|
|
|
rawDisplayName: `Member ${index}`,
|
|
|
|
|
roomId: room.roomId,
|
2024-03-12 14:52:54 +00:00
|
|
|
membership: KnownMembership.Join,
|
2023-08-23 15:13:40 +01:00
|
|
|
getAvatarUrl: () => `mxc://avatar.url/user-${index}.png`,
|
|
|
|
|
getMxcAvatarUrl: () => `mxc://avatar.url/user-${index}.png`,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
room.currentState.setJoinedMemberCount(members.length);
|
|
|
|
|
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
|
|
|
|
}
|