mirror of https://github.com/vector-im/riot-web
541 lines
21 KiB
TypeScript
541 lines
21 KiB
TypeScript
/*
|
|
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 { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
|
import { EventType, JoinRule, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
|
import {
|
|
fireEvent,
|
|
getAllByLabelText,
|
|
getByLabelText,
|
|
getByRole,
|
|
getByText,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from "@testing-library/react";
|
|
|
|
import { filterConsole, mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
|
|
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
|
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";
|
|
import { Call, ElementCall } from "../../../../src/models/Call";
|
|
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
|
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
|
|
|
jest.mock("../../../../src/utils/ShieldUtils");
|
|
|
|
describe("RoomHeader", () => {
|
|
filterConsole("[getType] Room !1:example.org does not have an m.room.create event");
|
|
|
|
let room: Room;
|
|
|
|
const ROOM_ID = "!1:example.org";
|
|
|
|
let setCardSpy: jest.SpyInstance | undefined;
|
|
|
|
beforeEach(async () => {
|
|
stubClient();
|
|
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");
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it("renders the room header", () => {
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
expect(container).toHaveTextContent(ROOM_ID);
|
|
});
|
|
|
|
it("renders the room topic", async () => {
|
|
const TOPIC = "Hello World! http://element.io";
|
|
|
|
const roomTopic = new MatrixEvent({
|
|
type: EventType.RoomTopic,
|
|
event_id: "$00002",
|
|
room_id: room.roomId,
|
|
sender: "@alice:example.com",
|
|
origin_server_ts: 1,
|
|
content: { topic: TOPIC },
|
|
state_key: "",
|
|
});
|
|
await room.addLiveEvents([roomTopic]);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
expect(container).toHaveTextContent(TOPIC);
|
|
expect(getByRole(container, "link")).toHaveTextContent("http://element.io");
|
|
});
|
|
|
|
it("opens the room summary", async () => {
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
fireEvent.click(getByText(container, ROOM_ID));
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
|
});
|
|
|
|
it("does not show the face pile for DMs", () => {
|
|
const client = MatrixClientPeg.get()!;
|
|
|
|
jest.spyOn(client, "getAccountData").mockReturnValue(
|
|
mkEvent({
|
|
event: true,
|
|
type: EventType.Direct,
|
|
user: client.getSafeUserId(),
|
|
content: {
|
|
"user@example.com": [room.roomId],
|
|
},
|
|
}),
|
|
);
|
|
|
|
room.getJoinedMembers = jest.fn().mockReturnValue([
|
|
{
|
|
userId: "@me:example.org",
|
|
name: "Member",
|
|
rawDisplayName: "Member",
|
|
roomId: room.roomId,
|
|
membership: "join",
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
},
|
|
]);
|
|
|
|
const { asFragment } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
expect(asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("shows a face pile for rooms", async () => {
|
|
const members = [
|
|
{
|
|
userId: "@me:example.org",
|
|
name: "Member",
|
|
rawDisplayName: "Member",
|
|
roomId: room.roomId,
|
|
membership: "join",
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
},
|
|
{
|
|
userId: "@you:example.org",
|
|
name: "Member",
|
|
rawDisplayName: "Member",
|
|
roomId: room.roomId,
|
|
membership: "join",
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
},
|
|
{
|
|
userId: "@them:example.org",
|
|
name: "Member",
|
|
rawDisplayName: "Member",
|
|
roomId: room.roomId,
|
|
membership: "join",
|
|
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,
|
|
membership: "join",
|
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
},
|
|
];
|
|
room.currentState.setJoinedMemberCount(members.length);
|
|
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
expect(container).toHaveTextContent("4");
|
|
|
|
const facePile = getByLabelText(container, "4 members");
|
|
expect(facePile).toHaveTextContent("4");
|
|
|
|
fireEvent.click(facePile);
|
|
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
|
});
|
|
|
|
it("opens the thread panel", async () => {
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
fireEvent.click(getByLabelText(container, "Threads"));
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
|
});
|
|
|
|
it("opens the notifications panel", async () => {
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
|
if (name === "feature_notifications") return true;
|
|
});
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
fireEvent.click(getByLabelText(container, "Notifications"));
|
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
|
});
|
|
|
|
describe("groups call disabled", () => {
|
|
it("you can't call if you're alone", () => {
|
|
mockRoomMembers(room, 1);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
for (const button of getAllByLabelText(container, "There's no one here to call")) {
|
|
expect(button).toBeDisabled();
|
|
}
|
|
});
|
|
|
|
it("you can call when you're two in the room", async () => {
|
|
mockRoomMembers(room, 2);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
const voiceButton = getByLabelText(container, "Voice call");
|
|
const videoButton = getByLabelText(container, "Video call");
|
|
expect(voiceButton).not.toBeDisabled();
|
|
expect(videoButton).not.toBeDisabled();
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
|
|
|
fireEvent.click(voiceButton);
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
|
|
fireEvent.click(videoButton);
|
|
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,
|
|
);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
|
expect(button).toBeDisabled();
|
|
}
|
|
});
|
|
|
|
it("can calls in large rooms if able to edit widgets", () => {
|
|
mockRoomMembers(room, 10);
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
expect(getByLabelText(container, "Voice call")).not.toBeDisabled();
|
|
expect(getByLabelText(container, "Video call")).not.toBeDisabled();
|
|
});
|
|
|
|
it("disable calls in large rooms by default", () => {
|
|
mockRoomMembers(room, 10);
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
expect(
|
|
getByLabelText(container, "You do not have permission to start voice calls", { selector: "button" }),
|
|
).toBeDisabled();
|
|
expect(
|
|
getByLabelText(container, "You do not have permission to start video calls", { selector: "button" }),
|
|
).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
describe("group call enabled", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_group_calls");
|
|
});
|
|
|
|
it("renders only the video call element", async () => {
|
|
mockRoomMembers(room, 3);
|
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
|
// allow element calls
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
expect(screen.queryByTitle("Voice call")).toBeNull();
|
|
|
|
const videoCallButton = getByLabelText(container, "Video call");
|
|
expect(videoCallButton).not.toBeDisabled();
|
|
|
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
|
|
|
fireEvent.click(getByLabelText(container, "Video call"));
|
|
|
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
|
});
|
|
|
|
it("can't call if there's an ongoing (pinned) call", () => {
|
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
|
// allow element calls
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
|
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {} } as Call);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
expect(getByLabelText(container, "Ongoing call")).toBeDisabled();
|
|
});
|
|
|
|
it("clicking on ongoing (unpinned) call re-pins it", () => {
|
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
|
// allow element calls
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
|
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
|
|
|
const widget = {};
|
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
expect(getByLabelText(container, "Video call")).not.toBeDisabled();
|
|
fireEvent.click(getByLabelText(container, "Video call"));
|
|
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
|
});
|
|
|
|
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,
|
|
);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
|
expect(button).toBeDisabled();
|
|
}
|
|
});
|
|
|
|
it("can't call if you have no friends", () => {
|
|
mockRoomMembers(room, 1);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
for (const button of getAllByLabelText(container, "There's no one here to call")) {
|
|
expect(button).toBeDisabled();
|
|
}
|
|
});
|
|
|
|
it("calls using legacy or jitsi", async () => {
|
|
mockRoomMembers(room, 2);
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
const voiceButton = getByLabelText(container, "Voice call");
|
|
const videoButton = getByLabelText(container, "Video call");
|
|
expect(voiceButton).not.toBeDisabled();
|
|
expect(videoButton).not.toBeDisabled();
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
|
fireEvent.click(voiceButton);
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
|
|
fireEvent.click(videoButton);
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
|
});
|
|
|
|
it("calls using legacy or jitsi for large rooms", async () => {
|
|
mockRoomMembers(room, 3);
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
if (key === "im.vector.modular.widgets") return true;
|
|
return false;
|
|
});
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
const voiceButton = getByLabelText(container, "Voice call");
|
|
const videoButton = getByLabelText(container, "Video call");
|
|
expect(voiceButton).not.toBeDisabled();
|
|
expect(videoButton).not.toBeDisabled();
|
|
|
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
|
fireEvent.click(voiceButton);
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
|
|
fireEvent.click(videoButton);
|
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
|
});
|
|
|
|
it("calls using element calls for large rooms", async () => {
|
|
mockRoomMembers(room, 3);
|
|
|
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
|
if (key === "im.vector.modular.widgets") return true;
|
|
if (key === ElementCall.CALL_EVENT_TYPE.name) return true;
|
|
return false;
|
|
});
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
const voiceButton = getByLabelText(container, "Voice call");
|
|
const videoButton = getByLabelText(container, "Video call");
|
|
expect(voiceButton).not.toBeDisabled();
|
|
expect(videoButton).not.toBeDisabled();
|
|
|
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
|
fireEvent.click(videoButton);
|
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
room.addLiveEvents([joinRuleEvent]);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
expect(getByLabelText(container, "Public room")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("dm", () => {
|
|
let client: MatrixClient;
|
|
beforeEach(() => {
|
|
client = MatrixClientPeg.get()!;
|
|
|
|
// Make the mocked room a DM
|
|
jest.spyOn(client, "getAccountData").mockImplementation((eventType: string): MatrixEvent | undefined => {
|
|
if (eventType === EventType.Direct) {
|
|
return mkEvent({
|
|
event: true,
|
|
content: {
|
|
[client.getUserId()!]: [room.roomId],
|
|
},
|
|
type: EventType.Direct,
|
|
user: client.getSafeUserId(),
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
jest.spyOn(client, "isCryptoEnabled").mockReturnValue(true);
|
|
});
|
|
|
|
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);
|
|
|
|
const { container } = render(
|
|
<RoomHeader room={room} />,
|
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
|
);
|
|
|
|
await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument());
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
*
|
|
* @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,
|
|
membership: "join",
|
|
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);
|
|
}
|