Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 14:57:26 +01:00
commit f0ee7f7905
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
3265 changed files with 484599 additions and 699 deletions

View file

@ -0,0 +1,21 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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, { ReactElement } from "react";
import { render } from "jest-matrix-react";
import SettingsTab, { SettingsTabProps } from "../../../../../../src/components/views/settings/tabs/SettingsTab";
describe("<SettingsTab />", () => {
const getComponent = (props: SettingsTabProps): ReactElement => <SettingsTab {...props} />;
it("renders tab", () => {
const { container } = render(getComponent({ children: <div>test</div> }));
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SettingsTab /> renders tab 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div>
test
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,164 @@
/*
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 { fireEvent, render, RenderResult, screen } from "jest-matrix-react";
import { MatrixClient, Room, EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import AdvancedRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/AdvancedRoomSettingsTab";
import { mkEvent, mkStubRoom, stubClient } from "../../../../../../test-utils";
import dis from "../../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
jest.mock("../../../../../../../src/dispatcher/dispatcher");
describe("AdvancedRoomSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<AdvancedRoomSettingsTab room={room} closeSettingsFn={jest.fn()} />);
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = mkStubRoom(roomId, "test room", cli);
mocked(cli.getRoom).mockReturnValue(room);
mocked(dis.dispatch).mockReset();
mocked(room.findPredecessor).mockImplementation((msc3946: boolean) =>
msc3946
? { roomId: "old_room_id_via_predecessor", viaServers: ["one.example.com", "two.example.com"] }
: { roomId: "old_room_id", eventId: "tombstone_event_id" },
);
});
it("should render as expected", () => {
const tab = renderTab();
expect(tab.asFragment()).toMatchSnapshot();
});
it("should display room ID", () => {
const tab = renderTab();
tab.getByText(roomId);
});
it("should display room version", () => {
mocked(room.getVersion).mockReturnValue("custom_room_version_1");
const tab = renderTab();
tab.getByText("custom_room_version_1");
});
it("displays message when room cannot federate", () => {
const createEvent = new MatrixEvent({
sender: "@a:b.com",
type: EventType.RoomCreate,
content: { "m.federate": false },
room_id: room.roomId,
state_key: "",
});
jest.spyOn(room.currentState, "getStateEvents").mockImplementation((type) =>
type === EventType.RoomCreate ? createEvent : null,
);
renderTab();
expect(screen.getByText("This room is not accessible by remote Matrix servers")).toBeInTheDocument();
});
function mockStateEvents(room: Room) {
const createEvent = mkEvent({
event: true,
user: "@a:b.com",
type: EventType.RoomCreate,
content: { predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" } },
room: room.roomId,
});
// Because we're mocking Room.findPredecessor, it may not be necessary
// to provide the actual event here, but we do need the create event,
// and in future this may be needed, so included for symmetry.
const predecessorEvent = mkEvent({
event: true,
user: "@a:b.com",
type: EventType.RoomPredecessor,
content: { predecessor_room_id: "old_room_id_via_predecessor" },
room: room.roomId,
});
type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null;
const getStateEvents = jest.spyOn(
room.currentState,
"getStateEvents",
) as unknown as jest.MockedFunction<GetStateEvents2Args>;
getStateEvents.mockImplementation((eventType: string | null, _key: string) => {
switch (eventType) {
case EventType.RoomCreate:
return createEvent;
case EventType.RoomPredecessor:
return predecessorEvent;
default:
return null;
}
});
}
it("should link to predecessor room", async () => {
mockStateEvents(room);
const tab = renderTab();
const link = await tab.findByText("View older messages in test room.");
fireEvent.click(link);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewRoom,
event_id: "tombstone_event_id",
room_id: "old_room_id",
metricsTrigger: "WebPredecessorSettings",
metricsViaKeyboard: false,
});
});
describe("When MSC3946 support is enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue")
.mockReset()
.mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors");
});
it("should link to predecessor room via MSC3946 if enabled", async () => {
mockStateEvents(room);
const tab = renderTab();
const link = await tab.findByText("View older messages in test room.");
fireEvent.click(link);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewRoom,
event_id: undefined,
room_id: "old_room_id_via_predecessor",
via_servers: ["one.example.com", "two.example.com"],
metricsTrigger: "WebPredecessorSettings",
metricsViaKeyboard: false,
});
});
it("handles when room is a space", async () => {
mockStateEvents(room);
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
mockStateEvents(room);
const tab = renderTab();
const link = await tab.findByText("View older version of test room.");
expect(link).toBeInTheDocument();
expect(screen.getByText("Space information")).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,53 @@
/*
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 } from "jest-matrix-react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import BridgeSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/BridgeSettingsTab";
import { getMockClientWithEventEmitter, withClientContextRenderOptions } from "../../../../../../test-utils";
describe("<BridgeSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
getRoom: jest.fn(),
});
const roomId = "!room:server.org";
const getComponent = (room: Room) =>
render(<BridgeSettingsTab room={room} />, withClientContextRenderOptions(client));
it("renders when room is not bridging messages to any platform", () => {
const room = new Room(roomId, client, userId);
const { container } = getComponent(room);
expect(container).toMatchSnapshot();
});
it("renders when room is bridging messages", () => {
const bridgeEvent = new MatrixEvent({
type: "uk.half-shot.bridge",
content: {
channel: { id: "channel-test" },
protocol: { id: "protocol-test" },
bridgebot: "test",
},
room_id: roomId,
state_key: "1",
});
const room = new Room(roomId, client, userId);
room.currentState.setStateEvents([bridgeEvent]);
client.getRoom.mockReturnValue(room);
const { container } = getComponent(room);
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,72 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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, RenderResult, screen } from "jest-matrix-react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import NotificationSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/NotificationSettingsTab";
import { mkStubRoom, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { EchoChamber } from "../../../../../../../src/stores/local-echo/EchoChamber";
import { RoomEchoChamber } from "../../../../../../../src/stores/local-echo/RoomEchoChamber";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
describe("NotificatinSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let roomProps: RoomEchoChamber;
const renderTab = (): RenderResult => {
return render(<NotificationSettingsTab roomId={roomId} closeSettingsFn={() => {}} />);
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
const room = mkStubRoom(roomId, "test room", cli);
roomProps = EchoChamber.forRoom(room);
NotificationSettingsTab.contextType = React.createContext<MatrixClient>(cli);
});
it("should prevent »Settings« link click from bubbling up to radio buttons", async () => {
const tab = renderTab();
// settings link of mentions_only volume
const settingsLink = tab.container.querySelector(
"label.mx_NotificationSettingsTab_mentionsKeywordsEntry div.mx_AccessibleButton",
);
if (!settingsLink) throw new Error("settings link does not exist.");
await userEvent.click(settingsLink);
expect(roomProps.notificationVolume).not.toBe("mentions_only");
});
it("should show the currently chosen custom notification sound", async () => {
SettingsStore.setValue("notificationSound", roomId, SettingLevel.ACCOUNT, {
url: "mxc://server/custom-sound-123",
name: "custom-sound-123",
});
renderTab();
await screen.findByText("custom-sound-123");
});
it("should show the currently chosen custom notification sound url if no name", async () => {
SettingsStore.setValue("notificationSound", roomId, SettingLevel.ACCOUNT, {
url: "mxc://server/custom-sound-123",
});
renderTab();
await screen.findByText("http://this.is.a.url/server/custom-sound-123");
});
});

View file

@ -0,0 +1,210 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 Nordeck IT + Consulting GmbH
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { act, fireEvent, render, screen, within } from "jest-matrix-react";
import {
EventTimeline,
EventType,
MatrixError,
MatrixEvent,
Room,
RoomMember,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import React from "react";
import ErrorDialog from "../../../../../../../src/components/views/dialogs/ErrorDialog";
import { PeopleRoomSettingsTab } from "../../../../../../../src/components/views/settings/tabs/room/PeopleRoomSettingsTab";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../../../src/Modal";
import { flushPromises, getMockClientWithEventEmitter } from "../../../../../../test-utils";
describe("PeopleRoomSettingsTab", () => {
const client = getMockClientWithEventEmitter({
getUserId: jest.fn(),
invite: jest.fn(),
kick: jest.fn(),
mxcUrlToHttp: (mxcUrl: string) => mxcUrl,
});
const roomId = "#ask-to-join:example.org";
const userId = "@alice:example.org";
const member = new RoomMember(roomId, userId);
const room = new Room(roomId, client, userId);
const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const getButton = (name: "Approve" | "Deny" | "See less" | "See more") => screen.getByRole("button", { name });
const getComponent = (room: Room) =>
render(
<MatrixClientContext.Provider value={client}>
<PeopleRoomSettingsTab room={room} />
</MatrixClientContext.Provider>,
);
const getGroup = () => screen.getByRole("group", { name: "Asking to join" });
const getParagraph = () => screen.getByRole("paragraph");
it("renders a heading", () => {
getComponent(room);
expect(screen.getByRole("heading")).toHaveTextContent("People");
});
it('renders a group "asking to join"', () => {
getComponent(room);
expect(getGroup()).toBeInTheDocument();
});
describe("without requests to join", () => {
it('renders a paragraph "no requests"', () => {
getComponent(room);
expect(getParagraph()).toHaveTextContent("No requests");
});
});
describe("with requests to join", () => {
const error = new MatrixError();
const knockUserId = "@albert.einstein:example.org";
const knockMember = new RoomMember(roomId, knockUserId);
const reason =
"There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.";
beforeEach(() => {
jest.spyOn(Modal, "createDialog");
jest.spyOn(room, "canInvite").mockReturnValue(true);
jest.spyOn(room, "getMember").mockReturnValue(member);
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([knockMember]);
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true);
knockMember.setMembershipEvent(
new MatrixEvent({
content: {
avatar_url: "mxc://example.org/albert-einstein.png",
displayname: "Albert Einstein",
membership: KnownMembership.Knock,
reason,
},
origin_server_ts: -464140800000,
type: EventType.RoomMember,
}),
);
});
it("renders requests fully", () => {
getComponent(room);
expect(getGroup()).toMatchSnapshot();
});
it("renders requests reduced", () => {
knockMember.setMembershipEvent(
new MatrixEvent({
content: {
displayname: "albert.einstein",
membership: KnownMembership.Knock,
},
type: EventType.RoomMember,
}),
);
getComponent(room);
expect(getGroup()).toMatchSnapshot();
});
it("allows to expand a reason", () => {
getComponent(room);
fireEvent.click(getButton("See more"));
expect(within(getGroup()).getByRole("paragraph")).toHaveTextContent(reason);
});
it("allows to collapse a reason", () => {
getComponent(room);
fireEvent.click(getButton("See more"));
fireEvent.click(getButton("See less"));
expect(getParagraph()).toHaveTextContent(`${reason.substring(0, 120)}`);
});
it("does not truncate a reason unnecessarily", () => {
const reason = "I have no special talents. I am only passionately curious.";
knockMember.setMembershipEvent(
new MatrixEvent({
content: {
displayname: "albert.einstein",
membership: KnownMembership.Knock,
reason,
},
type: EventType.RoomMember,
}),
);
getComponent(room);
expect(getParagraph()).toHaveTextContent(reason);
});
it("disables the deny button if the power level is insufficient", () => {
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(false);
getComponent(room);
expect(getButton("Deny")).toHaveAttribute("disabled");
});
it("calls kick on deny", () => {
jest.spyOn(client, "kick").mockResolvedValue({});
getComponent(room);
fireEvent.click(getButton("Deny"));
expect(client.kick).toHaveBeenCalledWith(roomId, knockUserId);
});
it("fails to deny a request", async () => {
jest.spyOn(client, "kick").mockRejectedValue(error);
getComponent(room);
fireEvent.click(getButton("Deny"));
await act(() => flushPromises());
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
title: error.name,
description: error.message,
});
});
it("succeeds to deny a request", () => {
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]);
getComponent(room);
act(() => {
room.emit(RoomStateEvent.Update, state);
});
expect(getParagraph()).toHaveTextContent("No requests");
});
it("disables the approve button if the power level is insufficient", () => {
jest.spyOn(room, "canInvite").mockReturnValue(false);
getComponent(room);
expect(getButton("Approve")).toHaveAttribute("disabled");
});
it("calls invite on approve", () => {
jest.spyOn(client, "invite").mockResolvedValue({});
getComponent(room);
fireEvent.click(getButton("Approve"));
expect(client.invite).toHaveBeenCalledWith(roomId, knockUserId);
});
it("fails to approve a request", async () => {
jest.spyOn(client, "invite").mockRejectedValue(error);
getComponent(room);
fireEvent.click(getButton("Approve"));
await act(() => flushPromises());
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
title: error.name,
description: error.message,
});
});
it("succeeds to approve a request", () => {
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]);
getComponent(room);
act(() => {
room.emit(RoomStateEvent.Update, state);
});
expect(getParagraph()).toHaveTextContent("No requests");
});
});
});

View file

@ -0,0 +1,267 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { fireEvent, getByRole, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import { MatrixClient, EventType, MatrixEvent, Room, RoomMember, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked } from "jest-mock";
import { defer } from "matrix-js-sdk/src/utils";
import userEvent from "@testing-library/user-event";
import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => {
const userId = "@alice:server.org";
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (propRoom: Room = room): RenderResult => {
return render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
};
const getVoiceBroadcastsSelect = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts']")!;
};
const getVoiceBroadcastsSelectedOption = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked")!;
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = mkStubRoom(roomId, "test room", cli);
});
it("should allow an Admin to demote themselves but not others", () => {
mocked(cli.getRoom).mockReturnValue(room);
// @ts-ignore - mocked doesn't support overloads properly
mocked(room.currentState.getStateEvents).mockImplementation((type, key) => {
if (key === undefined) return [] as MatrixEvent[];
if (type === "m.room.power_levels") {
return new MatrixEvent({
sender: "@sender:server",
room_id: roomId,
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[cli.getUserId()!]: 100,
"@admin:server": 100,
},
},
});
}
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled();
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", () => {
expect(getVoiceBroadcastsSelectedOption().textContent).toBe("Moderator");
});
describe("when setting »Default« permission for »Voice broadcasts«", () => {
beforeEach(() => {
fireEvent.change(getVoiceBroadcastsSelect(), {
target: { value: 0 },
});
});
it("should update the power levels", () => {
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[VoiceBroadcastInfoEventType]: 0,
},
});
});
});
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "feature_group_calls") return val;
});
};
const getStartCallSelect = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Start Element Call calls']")!;
};
const getStartCallSelectedOption = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Start Element Call calls'] option:checked")!;
};
const getJoinCallSelect = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Join Element Call calls']")!;
};
const getJoinCallSelectedOption = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Join Element Call calls'] option:checked")!;
};
describe("Element Call enabled", () => {
beforeEach(() => {
setGroupCallsEnabled(true);
});
describe("Join Element calls", () => {
it("defaults to moderator for joining calls", () => {
expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
});
it("can change joining calls power level", () => {
const tab = renderTab();
fireEvent.change(getJoinCallSelect(tab), {
target: { value: 0 },
});
expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default");
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
});
});
});
describe("Start Element calls", () => {
it("defaults to moderator for starting calls", () => {
expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
});
it("can change starting calls power level", () => {
const tab = renderTab();
fireEvent.change(getStartCallSelect(tab), {
target: { value: 0 },
});
expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default");
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 0,
},
});
});
});
});
it("hides when group calls disabled", () => {
setGroupCallsEnabled(false);
const tab = renderTab();
expect(getStartCallSelect(tab)).toBeFalsy();
expect(getStartCallSelectedOption(tab)).toBeFalsy();
expect(getJoinCallSelect(tab)).toBeFalsy();
expect(getJoinCallSelectedOption(tab)).toBeFalsy();
});
});
describe("Banned users", () => {
it("should not render banned section when no banned users", () => {
const room = new Room(roomId, cli, userId);
renderTab(room);
expect(screen.queryByText("Banned users")).not.toBeInTheDocument();
});
it("renders banned users", () => {
const bannedMember = new RoomMember(roomId, "@bob:server.org");
bannedMember.setMembershipEvent(
new MatrixEvent({
type: EventType.RoomMember,
content: {
membership: KnownMembership.Ban,
reason: "just testing",
},
sender: userId,
}),
);
const room = new Room(roomId, cli, userId);
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bannedMember]);
renderTab(room);
expect(screen.getByText("Banned users").parentElement).toMatchSnapshot();
});
it("uses banners display name when available", () => {
const bannedMember = new RoomMember(roomId, "@bob:server.org");
const senderMember = new RoomMember(roomId, "@alice:server.org");
senderMember.name = "Alice";
bannedMember.setMembershipEvent(
new MatrixEvent({
type: EventType.RoomMember,
content: {
membership: KnownMembership.Ban,
reason: "just testing",
},
sender: userId,
}),
);
const room = new Room(roomId, cli, userId);
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bannedMember]);
jest.spyOn(room, "getMember").mockReturnValue(senderMember);
renderTab(room);
expect(screen.getByTitle("Banned by Alice")).toBeInTheDocument();
});
});
it("should roll back power level change on error", async () => {
const deferred = defer<ISendEventResponse>();
mocked(cli.sendStateEvent).mockReturnValue(deferred.promise);
mocked(cli.getRoom).mockReturnValue(room);
// @ts-ignore - mocked doesn't support overloads properly
mocked(room.currentState.getStateEvents).mockImplementation((type, key) => {
if (key === undefined) return [] as MatrixEvent[];
if (type === "m.room.power_levels") {
return new MatrixEvent({
sender: "@sender:server",
room_id: roomId,
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[cli.getUserId()!]: 100,
},
},
});
}
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
const selector = container.querySelector(`[placeholder="${cli.getUserId()}"]`)!;
fireEvent.change(selector, { target: { value: "50" } });
expect(selector).toHaveValue("50");
// Get the apply button of the privileged user section and click on it
const privilegedUsersSection = screen.getByRole("group", { name: "Privileged Users" });
const applyButton = getByRole(privilegedUsersSection, "button", { name: "Apply" });
await userEvent.click(applyButton);
deferred.reject("Error");
await waitFor(() => expect(selector).toHaveValue("100"));
});
});

View file

@ -0,0 +1,440 @@
/*
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 { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
import { EventType, GuestAccess, HistoryVisibility, JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import SecurityRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/SecurityRoomSettingsTab";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import {
clearAllModals,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsUser,
} from "../../../../../../test-utils";
import { filterBoolean } from "../../../../../../../src/utils/arrays";
describe("<SecurityRoomSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
isRoomEncrypted: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
sendStateEvent: jest.fn(),
getClientWellKnown: jest.fn(),
});
const roomId = "!room:server.org";
const getComponent = (room: Room, closeSettingsFn = jest.fn()) =>
render(<SecurityRoomSettingsTab room={room} closeSettingsFn={closeSettingsFn} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
),
});
const setRoomStateEvents = (
room: Room,
joinRule?: JoinRule,
guestAccess?: GuestAccess,
history?: HistoryVisibility,
): void => {
const events = filterBoolean<MatrixEvent>([
new MatrixEvent({
type: EventType.RoomCreate,
content: { version: "test" },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
guestAccess &&
new MatrixEvent({
type: EventType.RoomGuestAccess,
content: { guest_access: guestAccess },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
history &&
new MatrixEvent({
type: EventType.RoomHistoryVisibility,
content: { history_visibility: history },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
joinRule &&
new MatrixEvent({
type: EventType.RoomJoinRules,
content: { join_rule: joinRule },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
]);
room.currentState.setStateEvents(events);
};
beforeEach(async () => {
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
client.isRoomEncrypted.mockReturnValue(false);
client.getClientWellKnown.mockReturnValue(undefined);
jest.spyOn(SettingsStore, "getValue").mockRestore();
await clearAllModals();
});
describe("join rule", () => {
it("warns when trying to make an encrypted room public", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
const modal = await screen.findByRole("dialog");
expect(modal).toMatchSnapshot();
fireEvent.click(screen.getByText("Cancel"));
// join rule not updated
expect(screen.getByLabelText("Private (invite only)").hasAttribute("checked")).toBeTruthy();
});
it("updates join rule", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomJoinRules,
{
join_rule: JoinRule.Public,
},
"",
);
});
it("handles error when updating join rule fails", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
const dialog = await screen.findByRole("dialog");
expect(dialog).toMatchSnapshot();
fireEvent.click(within(dialog).getByText("OK"));
});
it("displays advanced section toggle when join rule is public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
expect(screen.getByText("Show advanced")).toBeInTheDocument();
});
it("does not display advanced section toggle when join rule is not public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
expect(screen.queryByText("Show advanced")).not.toBeInTheDocument();
});
});
describe("guest access", () => {
it("uses forbidden by default when room has no guest access event", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
});
it("updates guest access on toggle", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomGuestAccess,
{ guest_access: GuestAccess.CanJoin },
"",
);
});
it("logs error and resets state when updating guest access fails", async () => {
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public, GuestAccess.CanJoin);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith("oups");
// toggle reset to old value
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
});
});
describe("history visibility", () => {
it("does not render section when RoomHistorySettings feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByText("Who can read history")).not.toBeInTheDocument();
});
it("uses shared as default history visibility when no state event found", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByText("Who can read history?").parentElement).toMatchSnapshot();
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
});
it("does not render world readable option when room is encrypted", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument();
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
});
describe("encryption", () => {
it("displays encryption as enabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
// can't disable encryption once enabled
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
});
it("asks users to confirm when setting room to encrypted", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("Cancel"));
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
});
it("enables encryption after confirmation", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Enable encryption?")).toBeInTheDocument();
fireEvent.click(within(dialog).getByText("OK"));
await waitFor(() =>
expect(client.sendStateEvent).toHaveBeenCalledWith(room.roomId, EventType.RoomEncryption, {
algorithm: "m.megolm.v1.aes-sha2",
}),
);
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
describe("when encryption is force disabled by e2ee well-known config", () => {
beforeEach(() => {
client.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: true,
},
});
});
it("displays encrypted rooms as encrypted", () => {
// rooms that are already encrypted still show encrypted
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
});
it("displays unencrypted rooms with toggle disabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(false);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,135 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import { MatrixClient, Room, MatrixEvent, EventType, JoinRule } from "matrix-js-sdk/src/matrix";
import { mkStubRoom, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { VoipRoomSettingsTab } from "../../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab";
import { ElementCall } from "../../../../../../../src/models/Call";
describe("VoipRoomSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<VoipRoomSettingsTab room={room} />);
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = mkStubRoom(roomId, "test room", cli);
jest.spyOn(cli, "sendStateEvent");
jest.spyOn(cli, "getRoom").mockReturnValue(room);
});
describe("Element Call", () => {
const mockPowerLevels = (events: Record<string, number>): void => {
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue({
getContent: () => ({
events,
}),
} as unknown as MatrixEvent);
};
const getElementCallSwitch = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("[data-testid='element-call-switch']")!;
};
describe("correct state", () => {
it("shows enabled when call member power level is 0", () => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
const tab = renderTab();
expect(getElementCallSwitch(tab).querySelector("[aria-checked='true']")).toBeTruthy();
});
it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: level });
const tab = renderTab();
expect(getElementCallSwitch(tab).querySelector("[aria-checked='false']")).toBeTruthy();
});
});
describe("enabling/disabling", () => {
describe("enabling Element calls", () => {
beforeEach(() => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 100 });
});
it("enables Element calls in public room", async () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")!);
await waitFor(() =>
expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 50,
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
}),
),
);
});
it("enables Element calls in private room", async () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")!);
await waitFor(() =>
expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 0,
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
}),
),
);
});
});
it("disables Element calls", async () => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")!);
await waitFor(() =>
expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 100,
[ElementCall.MEMBER_EVENT_TYPE.name]: 100,
},
}),
),
);
});
});
});
});

View file

@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdvancedRoomSettingsTab should render as expected 1`] = `
<DocumentFragment>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Advanced
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Room information
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div>
<span>
Internal room ID
</span>
<div
class="mx_CopyableText mx_CopyableText_border"
>
!room:example.com
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
</div>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Room version
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div>
<span>
Room version:
</span>
 1
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BridgeSettingsTab /> renders when room is bridging messages 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Bridges
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div>
<p>
<span>
This room is bridging messages to the following platforms.
<a
href="https://matrix.org/bridges/"
rel="noreferrer noopener"
target="_blank"
>
Learn more.
</a>
</span>
</p>
<ul
class="mx_RoomSettingsDialog_BridgeList"
>
<li
class="mx_RoomSettingsDialog_BridgeList_listItem"
>
<div
class="mx_RoomSettingsDialog_column_icon"
>
<div
class="mx_RoomSettingsDialog_noProtocolIcon"
/>
</div>
<div
class="mx_RoomSettingsDialog_column_data"
>
<h3
class="mx_RoomSettingsDialog_column_data_protocolName"
>
protocol-test
</h3>
<p
class="mx_RoomSettingsDialog_column_data_details mx_RoomSettingsDialog_workspace_channel_details"
>
<span
class="mx_RoomSettingsDialog_channel"
>
<span>
Channel:
<span>
channel-test
</span>
</span>
</span>
</p>
<ul
class="mx_RoomSettingsDialog_column_data_metadata mx_RoomSettingsDialog_metadata"
>
<li>
<span>
This bridge is managed by
.
</span>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<BridgeSettingsTab /> renders when room is not bridging messages to any platform 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Bridges
</h2>
<div
class="mx_SettingsSection_subSections"
>
<p>
<span>
This room isn't bridging messages to any platforms.
<a
href="https://matrix.org/bridges/"
rel="noreferrer noopener"
target="_blank"
>
Learn more.
</a>
</span>
</p>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PeopleRoomSettingsTab with requests to join renders requests fully 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Asking to join
</legend>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_PeopleRoomSettingsTab_knock"
>
<span
aria-label="Profile picture"
class="_avatar_mcap2_17 mx_BaseAvatar mx_PeopleRoomSettingsTab_avatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 42px;"
title="@albert.einstein:example.org"
>
<img
alt=""
class="_image_mcap2_50"
data-type="round"
height="42px"
loading="lazy"
referrerpolicy="no-referrer"
src="mxc://example.org/albert-einstein.png"
width="42px"
/>
</span>
<div
class="mx_PeopleRoomSettingsTab_content"
>
<span
class="mx_PeopleRoomSettingsTab_name"
>
Albert Einstein
</span>
<time
class="mx_PeopleRoomSettingsTab_timestamp"
>
Apr 18, 1955
</time>
<span
class="mx_PeopleRoomSettingsTab_userId"
>
@albert.einstein:example.org
</span>
<p
class="mx_PeopleRoomSettingsTab_seeMoreOrLess"
>
There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a…
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
See more
</div>
</div>
<div
aria-label="Deny"
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary_outline"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
/>
</svg>
</div>
<div
aria-label="Approve"
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary"
role="button"
tabindex="0"
>
<div
height="18"
width="18"
/>
</div>
</div>
</div>
</fieldset>
`;
exports[`PeopleRoomSettingsTab with requests to join renders requests reduced 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Asking to join
</legend>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_PeopleRoomSettingsTab_knock"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_PeopleRoomSettingsTab_avatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 42px;"
title="@albert.einstein:example.org"
>
a
</span>
<div
class="mx_PeopleRoomSettingsTab_content"
>
<span
class="mx_PeopleRoomSettingsTab_name"
>
albert.einstein
</span>
<span
class="mx_PeopleRoomSettingsTab_userId"
>
@albert.einstein:example.org
</span>
</div>
<div
aria-label="Deny"
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary_outline"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
/>
</svg>
</div>
<div
aria-label="Approve"
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary"
role="button"
tabindex="0"
>
<div
height="18"
width="18"
/>
</div>
</div>
</div>
</fieldset>
`;

View file

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RolesRoomSettingsTab Banned users renders banned users 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Banned users
</legend>
<div
class="mx_SettingsFieldset_content"
>
<ul
class="mx_RolesRoomSettingsTab_bannedList"
>
<li>
<span
title="Banned by @alice:server.org"
>
<strong>
@bob:server.org
</strong>
Reason: just testing
</span>
</li>
</ul>
</div>
</fieldset>
`;

View file

@ -0,0 +1,235 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default history visibility when no state event found 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Who can read history?
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-world_readable"
name="historyVis"
type="radio"
value="world_readable"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Anyone
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
<input
checked=""
id="historyVis-shared"
name="historyVis"
type="radio"
value="shared"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since the point in time of selecting this option)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-invited"
name="historyVis"
type="radio"
value="invited"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they were invited)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-joined"
name="historyVis"
type="radio"
value="joined"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they joined)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
</div>
</fieldset>
`;
exports[`<SecurityRoomSettingsTab /> join rule handles error when updating join rule fails 1`] = `
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ErrorDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Failed to update the join rules
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
Unknown failure
</div>
<div
class="mx_Dialog_buttons"
>
<button
class="mx_Dialog_primary"
>
OK
</button>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
`;
exports[`<SecurityRoomSettingsTab /> join rule warns when trying to make an encrypted room public 1`] = `
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Are you sure you want to make this encrypted room public?
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
<span>
<strong>
It's not recommended to make encrypted rooms public.
</strong>
It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.
</span>
</p>
<p>
<span>
To avoid these issues, create a
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
new public room
</div>
for the conversation you plan to have.
</span>
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,417 @@
/*
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 { fireEvent, render, screen, within } from "jest-matrix-react";
import React from "react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import userEvent from "@testing-library/user-event";
import { MockedObject } from "jest-mock";
import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab";
import { SdkContextClass, SDKContext } from "../../../../../../../src/contexts/SDKContext";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
mockPlatformPeg,
flushPromises,
} from "../../../../../../test-utils";
import { UIFeature } from "../../../../../../../src/settings/UIFeature";
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../../../src/Modal";
let changePasswordOnError: (e: Error) => void;
let changePasswordOnFinished: () => void;
jest.mock(
"../../../../../../../src/components/views/settings/ChangePassword",
() =>
({ onError, onFinished }: { onError: (e: Error) => void; onFinished: () => void }) => {
changePasswordOnError = onError;
changePasswordOnFinished = onFinished;
return <button>Mock change password</button>;
},
);
describe("<AccountUserSettingsTab />", () => {
const defaultProps = {
closeSettingsFn: jest.fn(),
};
const userId = "@alice:server.org";
let mockClient: MockedObject<MatrixClient>;
let stores: SdkContextClass;
const getComponent = () => (
<MatrixClientContext.Provider value={mockClient}>
<SDKContext.Provider value={stores}>
<AccountUserSettingsTab {...defaultProps} />
</SDKContext.Provider>
</MatrixClientContext.Provider>
);
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
mockPlatformPeg();
jest.clearAllMocks();
jest.spyOn(SettingsStore, "getValue").mockRestore();
jest.spyOn(logger, "error").mockRestore();
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
getCapabilities: jest.fn(),
getThreePids: jest.fn(),
getIdentityServerUrl: jest.fn(),
deleteThreePid: jest.fn(),
});
mockClient.getCapabilities.mockResolvedValue({});
mockClient.getThreePids.mockResolvedValue({
threepids: [],
});
mockClient.deleteThreePid.mockResolvedValue({
id_server_unbind_result: "success",
});
stores = new SdkContextClass();
stores.client = mockClient;
// stub out this store completely to avoid mocking initialisation
const mockOidcClientStore = {} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("does not show account management link when not available", () => {
const { queryByTestId } = render(getComponent());
expect(queryByTestId("external-account-management-outer")).toBeFalsy();
expect(queryByTestId("external-account-management-link")).toBeFalsy();
});
it("show account management link in expected format", async () => {
const accountManagementLink = "https://id.server.org/my-account";
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
render(getComponent());
const manageAccountLink = await screen.findByRole("button", { name: "Manage account" });
expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink);
});
describe("deactive account", () => {
it("should not render section when account deactivation feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName !== UIFeature.Deactivate,
);
render(getComponent());
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Deactivate);
});
it("should not render section when account is managed externally", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
// account is managed externally when we have delegated auth configured
const accountManagementLink = "https://id.server.org/my-account";
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
render(getComponent());
await flushPromises();
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
});
it("should render section when account deactivation feature is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
render(getComponent());
expect(screen.getByText("Deactivate Account", { selector: "h2" }).parentElement!).toMatchSnapshot();
});
it("should display the deactivate account dialog when clicked", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
const createDialogFn = jest.fn();
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent());
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
expect(createDialogFn).toHaveBeenCalled();
});
it("should close settings if account deactivated", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
const createDialogFn = jest.fn();
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent());
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
createDialogFn.mock.calls[0][1].onFinished(true);
expect(defaultProps.closeSettingsFn).toHaveBeenCalled();
});
it("should not close settings if account not deactivated", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
const createDialogFn = jest.fn();
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent());
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
createDialogFn.mock.calls[0][1].onFinished(false);
expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled();
});
});
describe("3pids", () => {
beforeEach(() => {
mockClient.getCapabilities.mockResolvedValue({
"m.3pid_changes": {
enabled: true,
},
});
mockClient.getThreePids.mockResolvedValue({
threepids: [
{
medium: ThreepidMedium.Email,
address: "test@test.io",
validated_at: 1685067124552,
added_at: 1685067124552,
},
{
medium: ThreepidMedium.Phone,
address: "123456789",
validated_at: 1685067124552,
added_at: 1685067124552,
},
],
});
mockClient.getIdentityServerUrl.mockReturnValue(undefined);
});
it("should show loaders while 3pids load", () => {
render(getComponent());
expect(
within(screen.getByTestId("mx_AccountEmailAddresses")).getByLabelText("Loading…"),
).toBeInTheDocument();
expect(within(screen.getByTestId("mx_AccountPhoneNumbers")).getByLabelText("Loading…")).toBeInTheDocument();
});
it("should display 3pid email addresses and phone numbers", async () => {
render(getComponent());
await flushPromises();
expect(screen.getByTestId("mx_AccountEmailAddresses")).toMatchSnapshot();
expect(screen.getByTestId("mx_AccountPhoneNumbers")).toMatchSnapshot();
});
it("should allow removing an existing email addresses", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountEmailAddresses");
fireEvent.click(within(section).getByText("Remove"));
// confirm removal
expect(screen.getByText("Remove test@test.io?")).toBeInTheDocument();
fireEvent.click(within(section).getByText("Remove"));
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, "test@test.io");
});
it("should allow adding a new email address", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountEmailAddresses");
// just check the fields are enabled
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
});
it("should allow removing an existing phone number", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountPhoneNumbers");
fireEvent.click(within(section).getByText("Remove"));
// confirm removal
expect(screen.getByText("Remove 123456789?")).toBeInTheDocument();
fireEvent.click(within(section).getByText("Remove"));
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, "123456789");
});
it("should allow adding a new phone number", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountPhoneNumbers");
// just check the fields are enabled
expect(within(section).getByLabelText("Phone Number")).not.toBeDisabled();
});
it("should allow 3pid changes when capabilities does not have 3pid_changes", async () => {
// We support as far back as v1.1 which doesn't have m.3pid_changes
// so the behaviour for when it is missing has to be assume true
mockClient.getCapabilities.mockResolvedValue({});
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountEmailAddresses");
// just check the fields are enabled
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
});
describe("when 3pid changes capability is disabled", () => {
beforeEach(() => {
mockClient.getCapabilities.mockResolvedValue({
"m.3pid_changes": {
enabled: false,
},
});
});
it("should not allow removing email addresses", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountEmailAddresses");
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
});
it("should not allow adding a new email addresses", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountEmailAddresses");
// fields are not enabled
expect(within(section).getByLabelText("Email Address")).toBeDisabled();
expect(within(section).getByText("Add")).toHaveAttribute("aria-disabled");
});
it("should not allow removing phone numbers", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountPhoneNumbers");
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
});
it("should not allow adding a new phone number", async () => {
render(getComponent());
await flushPromises();
const section = screen.getByTestId("mx_AccountPhoneNumbers");
expect(within(section).getByLabelText("Phone Number")).toBeDisabled();
});
});
});
describe("Password change", () => {
beforeEach(() => {
mockClient.getCapabilities.mockResolvedValue({
"m.change_password": {
enabled: true,
},
});
});
it("should display a dialog if password change succeeded", async () => {
const createDialogFn = jest.fn();
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent());
const changeButton = await screen.findByRole("button", { name: "Mock change password" });
userEvent.click(changeButton);
expect(changePasswordOnFinished).toBeDefined();
changePasswordOnFinished();
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
title: "Success",
description: "Your password was successfully changed.",
});
});
it("should display an error if password change failed", async () => {
const ERROR_STRING =
"Your password must contain exactly 5 lowercase letters, a box drawing character and the badger emoji.";
const createDialogFn = jest.fn();
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
render(getComponent());
const changeButton = await screen.findByRole("button", { name: "Mock change password" });
userEvent.click(changeButton);
expect(changePasswordOnError).toBeDefined();
changePasswordOnError(new Error(ERROR_STRING));
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
title: "Error changing password",
description: ERROR_STRING,
});
});
});
});

View file

@ -0,0 +1,27 @@
/*
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 { render } from "jest-matrix-react";
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import AppearanceUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AppearanceUserSettingsTab";
import { withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
describe("AppearanceUserSettingsTab", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
});
it("should render", () => {
const { asFragment } = render(<AppearanceUserSettingsTab />, withClientContextRenderOptions(client));
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,99 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import KeyboardUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/KeyboardUserSettingsTab";
import { Key } from "../../../../../../../src/Keyboard";
import { mockPlatformPeg } from "../../../../../../test-utils/platform";
const PATH_TO_KEYBOARD_SHORTCUTS = "../../../../../../../src/accessibility/KeyboardShortcuts";
const PATH_TO_KEYBOARD_SHORTCUT_UTILS = "../../../../../../../src/accessibility/KeyboardShortcutUtils";
const mockKeyboardShortcuts = (override: Record<string, any>) => {
jest.doMock(PATH_TO_KEYBOARD_SHORTCUTS, () => {
const original = jest.requireActual(PATH_TO_KEYBOARD_SHORTCUTS);
return {
...original,
...override,
};
});
};
const mockKeyboardShortcutUtils = (override: Record<string, any>) => {
jest.doMock(PATH_TO_KEYBOARD_SHORTCUT_UTILS, () => {
const original = jest.requireActual(PATH_TO_KEYBOARD_SHORTCUT_UTILS);
return {
...original,
...override,
};
});
};
const renderKeyboardUserSettingsTab = () => {
return render(<KeyboardUserSettingsTab />).container;
};
describe("KeyboardUserSettingsTab", () => {
beforeEach(() => {
jest.resetModules();
mockPlatformPeg();
});
it("renders list of keyboard shortcuts", () => {
mockKeyboardShortcuts({
CATEGORIES: {
Composer: {
settingNames: ["keybind1", "keybind2"],
categoryLabel: "Composer",
},
Navigation: {
settingNames: ["keybind3"],
categoryLabel: "Navigation",
},
},
});
mockKeyboardShortcutUtils({
getKeyboardShortcutValue: (name: string) => {
switch (name) {
case "keybind1":
return {
key: Key.A,
ctrlKey: true,
};
case "keybind2": {
return {
key: Key.B,
ctrlKey: true,
};
}
case "keybind3": {
return {
key: Key.ENTER,
};
}
}
},
getKeyboardShortcutDisplayName: (name: string) => {
switch (name) {
case "keybind1":
return "Cancel replying to a message";
case "keybind2":
return "Toggle Bold";
case "keybind3":
return "Select room from the room list";
}
},
});
const body = renderKeyboardUserSettingsTab();
expect(body).toMatchSnapshot();
});
});

View file

@ -0,0 +1,56 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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, screen } from "jest-matrix-react";
import LabsUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import SdkConfig from "../../../../../../../src/SdkConfig";
describe("<LabsUserSettingsTab />", () => {
const defaultProps = {
closeSettingsFn: jest.fn(),
};
const getComponent = () => <LabsUserSettingsTab {...defaultProps} />;
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
beforeEach(() => {
jest.clearAllMocks();
settingsValueSpy.mockReturnValue(false);
SdkConfig.reset();
SdkConfig.add({ brand: "BrandedClient" });
localStorage.clear();
});
it("renders settings marked as beta as beta cards", () => {
render(getComponent());
expect(screen.getByText("Upcoming features").parentElement!).toMatchSnapshot();
});
it("does not render non-beta labs settings when disabled in config", () => {
const sdkConfigSpy = jest.spyOn(SdkConfig, "get");
render(getComponent());
expect(sdkConfigSpy).toHaveBeenCalledWith("show_labs_settings");
// only section is beta section
expect(screen.queryByText("Early previews")).not.toBeInTheDocument();
});
it("renders non-beta labs settings when enabled in config", () => {
// enable labs
SdkConfig.add({ show_labs_settings: true });
const { container } = render(getComponent());
// non-beta labs section
expect(screen.getByText("Early previews")).toBeInTheDocument();
const labsSections = container.getElementsByClassName("mx_SettingsSubsection");
expect(labsSections).toHaveLength(10);
});
});

View file

@ -0,0 +1,37 @@
/*
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 } from "jest-matrix-react";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../../test-utils";
import MjolnirUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/MjolnirUserSettingsTab";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
describe("<MjolnirUserSettingsTab />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
});
const getComponent = () =>
render(<MjolnirUserSettingsTab />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
),
});
it("renders correctly when user has no ignored users", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,189 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { fireEvent, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import PreferencesUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { mockPlatformPeg, stubClient } from "../../../../../../test-utils";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
import PlatformPeg from "../../../../../../../src/PlatformPeg";
describe("PreferencesUserSettingsTab", () => {
beforeEach(() => {
mockPlatformPeg();
});
const renderTab = (): RenderResult => {
return render(<PreferencesUserSettingsTab closeSettingsFn={() => {}} />);
};
it("should render", () => {
const { asFragment } = renderTab();
expect(asFragment()).toMatchSnapshot();
});
it("should reload when changing language", async () => {
const reloadStub = jest.fn();
PlatformPeg.get()!.reload = reloadStub;
renderTab();
const languageDropdown = await screen.findByRole("button", { name: "Language Dropdown" });
expect(languageDropdown).toBeInTheDocument();
await userEvent.click(languageDropdown);
const germanOption = await screen.findByText("Deutsch");
await userEvent.click(germanOption);
expect(reloadStub).toHaveBeenCalled();
});
it("should search and select a user timezone", async () => {
renderTab();
expect(await screen.findByText(/Browser default/)).toBeInTheDocument();
const timezoneDropdown = await screen.findByRole("button", { name: "Set timezone" });
await userEvent.click(timezoneDropdown);
// Without filtering `expect(screen.queryByRole("option" ...` take over 1s.
await fireEvent.change(screen.getByRole("combobox", { name: "Set timezone" }), {
target: { value: "Africa/Abidjan" },
});
expect(screen.queryByRole("option", { name: "Africa/Abidjan" })).toBeInTheDocument();
expect(screen.queryByRole("option", { name: "Europe/Paris" })).not.toBeInTheDocument();
await fireEvent.change(screen.getByRole("combobox", { name: "Set timezone" }), {
target: { value: "Europe/Paris" },
});
expect(screen.queryByRole("option", { name: "Africa/Abidjan" })).not.toBeInTheDocument();
const option = await screen.getByRole("option", { name: "Europe/Paris" });
await userEvent.click(option);
expect(await screen.findByText("Europe/Paris")).toBeInTheDocument();
});
it("should not show spell check setting if unsupported", async () => {
PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(false);
renderTab();
expect(screen.queryByRole("switch", { name: "Allow spell check" })).not.toBeInTheDocument();
});
it("should enable spell check", async () => {
const spellCheckEnableFn = jest.fn();
PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(true);
PlatformPeg.get()!.getSpellCheckEnabled = jest.fn().mockReturnValue(false);
PlatformPeg.get()!.setSpellCheckEnabled = spellCheckEnableFn;
renderTab();
const toggle = await screen.findByRole("switch", { name: "Allow spell check" });
expect(toggle).toHaveAttribute("aria-checked", "false");
await userEvent.click(toggle);
expect(spellCheckEnableFn).toHaveBeenCalledWith(true);
});
describe("send read receipts", () => {
beforeEach(() => {
stubClient();
jest.spyOn(SettingsStore, "setValue");
jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as MediaQueryList);
});
afterEach(() => {
jest.resetAllMocks();
});
const getToggle = () => renderTab().getByRole("switch", { name: "Send read receipts" });
const mockIsVersionSupported = (val: boolean) => {
const client = MatrixClientPeg.safeGet();
jest.spyOn(client, "doesServerSupportUnstableFeature").mockResolvedValue(false);
jest.spyOn(client, "isVersionSupported").mockImplementation(async (version: string) => {
if (version === "v1.4") return val;
return false;
});
MatrixClientBackedController.matrixClient = client;
};
const mockGetValue = (val: boolean) => {
const copyOfGetValueAt = SettingsStore.getValueAt;
SettingsStore.getValueAt = <T,>(
level: SettingLevel,
name: string,
roomId?: string,
isExplicit?: boolean,
): T => {
if (name === "sendReadReceipts") return val as T;
return copyOfGetValueAt(level, name, roomId, isExplicit);
};
};
const expectSetValueToHaveBeenCalled = (
name: string,
roomId: string | null,
level: SettingLevel,
value: boolean,
) => expect(SettingsStore.setValue).toHaveBeenCalledWith(name, roomId, level, value);
describe("with server support", () => {
beforeEach(() => {
mockIsVersionSupported(true);
});
it("can be enabled", async () => {
mockGetValue(false);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true);
});
it("can be disabled", async () => {
mockGetValue(true);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, false);
});
});
describe("without server support", () => {
beforeEach(() => {
mockIsVersionSupported(false);
});
it("is forcibly enabled", async () => {
const toggle = getToggle();
await waitFor(() => {
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("aria-disabled", "true");
});
});
it("cannot be disabled", async () => {
mockGetValue(true);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "true"));
fireEvent.click(toggle);
expect(SettingsStore.setValue).not.toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,61 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { render } from "jest-matrix-react";
import React from "react";
import SecurityUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
mockClientMethodsCrypto,
mockClientMethodsDevice,
mockPlatformPeg,
} from "../../../../../../test-utils";
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
describe("<SecurityUserSettingsTab />", () => {
const defaultProps = {
closeSettingsFn: jest.fn(),
};
const userId = "@alice:server.org";
const deviceId = "alices-device";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getKeyBackupVersion: jest.fn(),
});
const sdkContext = new SdkContextClass();
sdkContext.client = mockClient;
const getComponent = () => (
<MatrixClientContext.Provider value={mockClient}>
<SDKContext.Provider value={sdkContext}>
<SecurityUserSettingsTab {...defaultProps} />
</SDKContext.Provider>
</MatrixClientContext.Provider>
);
beforeEach(() => {
mockPlatformPeg();
jest.clearAllMocks();
});
it("renders security section", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,91 @@
/*
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 { fireEvent, render, screen } from "jest-matrix-react";
import SidebarUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/SidebarUserSettingsTab";
import PosthogTrackers from "../../../../../../../src/PosthogTrackers";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { MetaSpace } from "../../../../../../../src/stores/spaces";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
import { flushPromises } from "../../../../../../test-utils";
import SdkConfig from "../../../../../../../src/SdkConfig";
describe("<SidebarUserSettingsTab />", () => {
beforeEach(() => {
jest.spyOn(PosthogTrackers, "trackInteraction").mockClear();
jest.spyOn(SettingsStore, "getValue").mockRestore();
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
});
it("renders sidebar settings with guest spa url", () => {
const spy = jest.spyOn(SdkConfig, "get").mockReturnValue({ guest_spa_url: "https://somewhere.org" });
const originalGetValue = SettingsStore.getValue;
const spySettingsStore = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
return setting === "feature_video_rooms" ? true : originalGetValue(setting);
});
const { container } = render(<SidebarUserSettingsTab />);
expect(container).toMatchSnapshot();
spySettingsStore.mockRestore();
spy.mockRestore();
});
it("renders sidebar settings without guest spa url", () => {
const originalGetValue = SettingsStore.getValue;
const spySettingsStore = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
return setting === "feature_video_rooms" ? true : originalGetValue(setting);
});
const { container } = render(<SidebarUserSettingsTab />);
expect(container).toMatchSnapshot();
spySettingsStore.mockRestore();
});
it("toggles all rooms in home setting", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
if (settingName === "Spaces.enabledMetaSpaces") {
return {
[MetaSpace.Home]: true,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: true,
[MetaSpace.Orphans]: true,
};
}
return false;
});
render(<SidebarUserSettingsTab />);
fireEvent.click(screen.getByTestId("mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"));
await flushPromises();
expect(SettingsStore.setValue).toHaveBeenCalledWith("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, true);
expect(PosthogTrackers.trackInteraction).toHaveBeenCalledWith(
"WebSettingsSidebarTabSpacesCheckbox",
// synthetic event from checkbox
expect.objectContaining({ type: "change" }),
1,
);
});
it("disables all rooms in home setting when home space is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
if (settingName === "Spaces.enabledMetaSpaces") {
return {
[MetaSpace.Home]: false,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: true,
[MetaSpace.Orphans]: true,
};
}
return false;
});
render(<SidebarUserSettingsTab />);
expect(screen.getByTestId("mx_SidebarUserSettingsTab_homeAllRoomsCheckbox")).toBeDisabled();
});
});

View file

@ -0,0 +1,135 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 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 { mocked } from "jest-mock";
import { fireEvent, render, screen } from "jest-matrix-react";
import { logger } from "matrix-js-sdk/src/logger";
import VoiceUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../../../src/MediaDeviceHandler";
import { flushPromises } from "../../../../../../test-utils";
jest.mock("../../../../../../../src/MediaDeviceHandler");
const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);
describe("<VoiceUserSettingsTab />", () => {
const getComponent = (): React.ReactElement => <VoiceUserSettingsTab />;
const audioIn1 = {
deviceId: "1",
groupId: "g1",
kind: MediaDeviceKindEnum.AudioInput,
label: "Audio input test 1",
};
const videoIn1 = {
deviceId: "2",
groupId: "g1",
kind: MediaDeviceKindEnum.VideoInput,
label: "Video input test 1",
};
const videoIn2 = {
deviceId: "3",
groupId: "g1",
kind: MediaDeviceKindEnum.VideoInput,
label: "Video input test 2",
};
const defaultMediaDevices = {
[MediaDeviceKindEnum.AudioOutput]: [],
[MediaDeviceKindEnum.AudioInput]: [audioIn1],
[MediaDeviceKindEnum.VideoInput]: [videoIn1, videoIn2],
} as unknown as IMediaDevices;
beforeEach(() => {
jest.clearAllMocks();
MediaDeviceHandlerMock.hasAnyLabeledDevices.mockResolvedValue(true);
MediaDeviceHandlerMock.getDevices.mockResolvedValue(defaultMediaDevices);
MediaDeviceHandlerMock.getVideoInput.mockReturnValue(videoIn1.deviceId);
// @ts-ignore bad mocking
MediaDeviceHandlerMock.instance = { setDevice: jest.fn().mockResolvedValue(undefined) };
});
describe("devices", () => {
it("renders dropdowns for input devices", async () => {
render(getComponent());
await flushPromises();
expect(screen.getByLabelText("Microphone")).toHaveDisplayValue(audioIn1.label);
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn1.label);
});
it("updates device", async () => {
render(getComponent());
await flushPromises();
fireEvent.change(screen.getByLabelText("Camera"), { target: { value: videoIn2.deviceId } });
expect(MediaDeviceHandlerMock.instance.setDevice).toHaveBeenCalledWith(
videoIn2.deviceId,
MediaDeviceKindEnum.VideoInput,
);
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label);
});
it("logs and resets device when update fails", async () => {
// stub to avoid littering console with expected error
jest.spyOn(logger, "error").mockImplementation(() => {});
MediaDeviceHandlerMock.instance.setDevice.mockRejectedValue("oups!");
render(getComponent());
await flushPromises();
fireEvent.change(screen.getByLabelText("Camera"), { target: { value: videoIn2.deviceId } });
expect(MediaDeviceHandlerMock.instance.setDevice).toHaveBeenCalledWith(
videoIn2.deviceId,
MediaDeviceKindEnum.VideoInput,
);
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label);
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Failed to set device videoinput: 3");
// reset to original
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn1.label);
});
it("does not render dropdown when no devices exist for type", async () => {
render(getComponent());
await flushPromises();
expect(screen.getByText("No Audio Outputs detected")).toBeInTheDocument();
expect(screen.queryByLabelText("Audio Output")).not.toBeInTheDocument();
});
});
it("renders audio processing settings", () => {
const { getByTestId } = render(getComponent());
expect(getByTestId("voice-auto-gain")).toBeTruthy();
expect(getByTestId("voice-noise-suppression")).toBeTruthy();
expect(getByTestId("voice-echo-cancellation")).toBeTruthy();
});
it("sets and displays audio processing settings", () => {
MediaDeviceHandlerMock.getAudioAutoGainControl.mockReturnValue(false);
MediaDeviceHandlerMock.getAudioEchoCancellation.mockReturnValue(true);
MediaDeviceHandlerMock.getAudioNoiseSuppression.mockReturnValue(false);
const { getByRole } = render(getComponent());
getByRole("switch", { name: "Automatically adjust the microphone volume" }).click();
getByRole("switch", { name: "Noise suppression" }).click();
getByRole("switch", { name: "Echo cancellation" }).click();
expect(MediaDeviceHandler.setAudioAutoGainControl).toHaveBeenCalledWith(true);
expect(MediaDeviceHandler.setAudioEchoCancellation).toHaveBeenCalledWith(false);
expect(MediaDeviceHandler.setAudioNoiseSuppression).toHaveBeenCalledWith(true);
});
});

View file

@ -0,0 +1,220 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses and phone numbers 1`] = `
<div
class="mx_SettingsSubsection"
data-testid="mx_AccountEmailAddresses"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Email addresses
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
>
<div
class="mx_AddRemoveThreepids_existing"
>
<span
class="mx_AddRemoveThreepids_existing_address"
>
test@test.io
</span>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_sm"
role="button"
tabindex="0"
>
Remove
</div>
</div>
<form
autocomplete="off"
novalidate=""
>
<div
class="mx_Field mx_Field_input"
>
<input
autocomplete="email"
id="mx_Field_9"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_9"
>
Email Address
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Add
</div>
</form>
</div>
</div>
`;
exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses and phone numbers 2`] = `
<div
class="mx_SettingsSubsection"
data-testid="mx_AccountPhoneNumbers"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Phone numbers
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
>
<div
class="mx_AddRemoveThreepids_existing"
>
<span
class="mx_AddRemoveThreepids_existing_address"
>
123456789
</span>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_sm"
role="button"
tabindex="0"
>
Remove
</div>
</div>
<form
autocomplete="off"
novalidate=""
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft"
>
<span
class="mx_Field_prefix"
>
<div
class="mx_Dropdown mx_PhoneNumbers_country mx_CountryDropdown"
>
<div
aria-describedby="mx_CountryDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Country Dropdown"
aria-owns="mx_CountryDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_CountryDropdown_value"
>
<span
class="mx_CountryDropdown_shortOption"
>
<div
class="mx_Dropdown_option_emoji"
>
🇺🇸
</div>
+1
</span>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</span>
<input
autocomplete="tel-national"
id="mx_Field_10"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_10"
>
Phone Number
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Add
</div>
</form>
</div>
</div>
`;
exports[`<AccountUserSettingsTab /> deactive account should render section when account deactivation feature is enabled 1`] = `
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Deactivate Account
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
data-testid="account-management-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Account management
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Deactivating your account is a permanent action — be careful!
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Deactivate Account
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,805 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppearanceUserSettingsTab should render 1`] = `
<DocumentFragment>
<div
class="mx_SettingsTab"
data-testid="mx_AppearanceUserSettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection mx_SettingsSubsection_newUi"
data-testid="themePanel"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
>
Theme
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
>
<div
class="_container_1vw5h_18"
>
<input
class="_input_1vw5h_26"
disabled=""
id="radix-0"
name="themeSelector"
title=""
type="radio"
value="light"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-0"
>
Light
</label>
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
>
<div
class="_container_1vw5h_18"
>
<input
class="_input_1vw5h_26"
disabled=""
id="radix-1"
name="themeSelector"
title=""
type="radio"
value="dark"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-1"
>
Dark
</label>
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
>
<div
class="_container_1vw5h_18"
>
<input
class="_input_1vw5h_26"
disabled=""
id="radix-2"
name="themeSelector"
title=""
type="radio"
value="light-high-contrast"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-2"
>
High contrast
</label>
</div>
</div>
</form>
</div>
<div
class="_separator_144s5_17"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
</div>
<div
class="mx_SettingsSubsection mx_SettingsSubsection_newUi"
data-testid="layoutPanel"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
>
Message layout
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24 mx_LayoutSwitcher_LayoutSelector"
>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Modern"
class="_label_dgy0u_67"
for="radix-3"
>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
>
<div
class="_container_1vw5h_18"
>
<input
checked=""
class="_input_1vw5h_26"
id="radix-3"
name="layout"
title=""
type="radio"
value="group"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
<span>
Modern
</span>
</div>
<hr
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator"
/>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview"
role="presentation"
>
<div
aria-atomic="true"
aria-live="off"
class="mx_EventTile"
data-event-id="$9999999999999999999999999999999999999999999"
data-has-reply="false"
data-layout="group"
data-scroll-tokens="$9999999999999999999999999999999999999999999"
data-self="true"
tabindex="-1"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class="mx_Username_color2 mx_DisambiguatedProfile_displayName"
dir="auto"
>
@userId:matrix.org
</span>
</div>
<div
class="mx_EventTile_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 30px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_EventTile_line"
>
<div
class="mx_MTextBody mx_EventTile_content"
>
<div
class="mx_EventTile_body translate"
dir="auto"
>
Hey you. You're the best!
</div>
</div>
<div
aria-label="Message Actions"
aria-live="off"
class="mx_MessageActionBar"
role="toolbar"
>
<div
aria-label="Edit"
class="mx_AccessibleButton mx_MessageActionBar_iconButton"
role="button"
tabindex="0"
>
<div />
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
role="button"
tabindex="-1"
>
<div />
</div>
</div>
</div>
</div>
</div>
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Message bubbles"
class="_label_dgy0u_67"
for="radix-4"
>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
>
<div
class="_container_1vw5h_18"
>
<input
class="_input_1vw5h_26"
id="radix-4"
name="layout"
title=""
type="radio"
value="bubble"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
<span>
Message bubbles
</span>
</div>
<hr
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator"
/>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview"
role="presentation"
>
<div
aria-atomic="true"
aria-live="off"
class="mx_EventTile"
data-event-id="$9999999999999999999999999999999999999999999"
data-has-reply="false"
data-layout="bubble"
data-scroll-tokens="$9999999999999999999999999999999999999999999"
data-self="true"
tabindex="-1"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class="mx_Username_color2 mx_DisambiguatedProfile_displayName"
dir="auto"
>
@userId:matrix.org
</span>
</div>
<div
class="mx_EventTile_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 30px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_EventTile_line"
>
<div
class="mx_MTextBody mx_EventTile_content"
>
<div
class="mx_EventTile_body translate"
dir="auto"
>
Hey you. You're the best!
</div>
</div>
<div
aria-label="Message Actions"
aria-live="off"
class="mx_MessageActionBar"
role="toolbar"
>
<div
aria-label="Edit"
class="mx_AccessibleButton mx_MessageActionBar_iconButton"
role="button"
tabindex="0"
>
<div />
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
role="button"
tabindex="-1"
>
<div />
</div>
</div>
</div>
</div>
</div>
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="IRC (experimental)"
class="_label_dgy0u_67"
for="radix-5"
>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
>
<div
class="_container_1vw5h_18"
>
<input
class="_input_1vw5h_26"
id="radix-5"
name="layout"
title=""
type="radio"
value="irc"
/>
<div
class="_ui_1vw5h_27"
/>
</div>
<span>
IRC (experimental)
</span>
</div>
<hr
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator"
/>
<div
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview mx_IRCLayout"
role="presentation"
>
<div
aria-atomic="true"
aria-live="off"
class="mx_EventTile"
data-event-id="$9999999999999999999999999999999999999999999"
data-has-reply="false"
data-layout="irc"
data-scroll-tokens="$9999999999999999999999999999999999999999999"
data-self="true"
tabindex="-1"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class="mx_Username_color2 mx_DisambiguatedProfile_displayName"
dir="auto"
>
@userId:matrix.org
</span>
</div>
<div
class="mx_EventTile_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 14px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_EventTile_line"
>
<div
class="mx_MTextBody mx_EventTile_content"
>
<div
class="mx_EventTile_body translate"
dir="auto"
>
Hey you. You're the best!
</div>
</div>
<div
aria-label="Message Actions"
aria-live="off"
class="mx_MessageActionBar"
role="toolbar"
>
<div
aria-label="Edit"
class="mx_AccessibleButton mx_MessageActionBar_iconButton"
role="button"
tabindex="0"
>
<div />
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
role="button"
tabindex="-1"
>
<div />
</div>
</div>
</div>
</div>
</div>
</label>
</div>
</form>
<form
class="_root_dgy0u_24"
>
<div
class="_inline-field_dgy0u_40"
>
<div
class="_inline-field-control_dgy0u_52"
>
<div
class="_container_qnvru_18"
>
<input
aria-describedby="radix-6"
class="_input_qnvru_32"
id="radix-7"
name="compactLayout"
title=""
type="checkbox"
/>
<div
class="_ui_qnvru_42"
/>
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
>
<label
class="_label_dgy0u_67"
for="radix-7"
>
Show compact text and messages
</label>
<span
class="_message_dgy0u_98 _help-message_dgy0u_104"
id="radix-6"
>
Modern layout must be selected to use this feature.
</span>
</div>
</div>
</form>
</div>
<div
class="_separator_144s5_17"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
</div>
<div
class="mx_SettingsSubsection"
data-testid="mx_FontScalingPanel"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Font size
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
>
<div
class="mx_Field mx_Field_select mx_FontScalingPanel_Dropdown"
>
<select
id="mx_Field_1"
label="Font size"
placeholder="Font size"
type="text"
>
<option
value="-7"
>
9
</option>
<option
value="-6"
>
10
</option>
<option
value="-5"
>
11
</option>
<option
value="-4"
>
12
</option>
<option
value="-3"
>
13
</option>
<option
value="-2"
>
14
</option>
<option
value="-1"
>
15
</option>
<option
value="0"
>
16 (default)
</option>
<option
value="1"
>
17
</option>
<option
value="2"
>
18
</option>
<option
value="4"
>
20
</option>
<option
value="6"
>
22
</option>
<option
value="8"
>
24
</option>
<option
value="10"
>
26
</option>
<option
value="12"
>
28
</option>
<option
value="14"
>
30
</option>
<option
value="16"
>
32
</option>
<option
value="18"
>
34
</option>
<option
value="20"
>
36
</option>
</select>
<label
for="mx_Field_1"
>
Font size
</label>
</div>
<div
class="mx_FontScalingPanel_preview mx_EventTilePreview_loader"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_noHeading"
>
<div
aria-expanded="false"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Show advanced
</div>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Image size in the timeline
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_ImageSizePanel_radios"
>
<label>
<div
class="mx_ImageSizePanel_size mx_ImageSizePanel_sizeDefault"
/>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
<input
checked=""
name="image_size"
type="radio"
value="normal"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Default
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
</label>
<label>
<div
class="mx_ImageSizePanel_size mx_ImageSizePanel_sizeLarge"
/>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
name="image_size"
type="radio"
value="large"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Large
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,134 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1`] = `
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Upcoming features
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection_text"
>
What's next for BrandedClient? Labs are the best way to get things early, test out new features and help shape them before they actually launch.
</div>
<div
class="mx_BetaCard"
>
<div
class="mx_BetaCard_columns"
>
<div
class="mx_BetaCard_columns_description"
>
<h3
class="mx_BetaCard_title"
>
<span>
Video rooms
</span>
<span
class="mx_BetaCard_betaPill"
>
Beta
</span>
</h3>
<div
class="mx_BetaCard_caption"
>
<p>
A new way to chat over voice and video in BrandedClient.
</p>
<p>
Video rooms are always-on VoIP channels embedded within a room in BrandedClient.
</p>
</div>
<div
class="mx_BetaCard_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Join the beta
</div>
</div>
<div
class="mx_BetaCard_refreshWarning"
>
Joining the beta will reload BrandedClient.
</div>
<div
class="mx_BetaCard_faq"
/>
</div>
<div
class="mx_BetaCard_columns_image_wrapper"
>
<img
alt=""
class="mx_BetaCard_columns_image"
src="image-file-stub"
/>
</div>
</div>
</div>
<div
class="mx_BetaCard"
>
<div
class="mx_BetaCard_columns"
>
<div
class="mx_BetaCard_columns_description"
>
<h3
class="mx_BetaCard_title"
>
<span>
Notification Settings
</span>
<span
class="mx_BetaCard_betaPill"
>
Beta
</span>
</h3>
<div
class="mx_BetaCard_caption"
>
<p>
Introducing a simpler way to change your notification settings. Customize your BrandedClient, just the way you like.
</p>
</div>
<div
class="mx_BetaCard_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Join the beta
</div>
</div>
</div>
<div
class="mx_BetaCard_columns_image_wrapper"
>
<img
alt=""
class="mx_BetaCard_columns_image"
/>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored users 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection_text"
>
<strong
class="warning"
>
⚠ These settings are meant for advanced users.
</strong>
<p>
<span>
Add users and servers you want to ignore here. Use asterisks to have Element match any characters. For example,
<code>
@bot:*
</code>
would ignore all users that have the name 'bot' on any server.
</span>
</p>
<p>
Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.
</p>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Personal ban list
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<i>
You have not ignored anyone.
</i>
<form
autocomplete="off"
>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Server or user ID to ignore"
placeholder="eg: @bot:* or example.org"
type="text"
value=""
/>
<label
for="mx_Field_1"
>
Server or user ID to ignore
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Ignore
</div>
</form>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Subscribed lists
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
<strong
class="warning"
>
Subscribing to a ban list will cause you to join it!
</strong>
 
<span>
If this isn't what you want, please use a different tool to ignore users.
</span>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<i>
You are not subscribed to any lists
</i>
<form
autocomplete="off"
>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_2"
label="Room ID or address of ban list"
placeholder="Room ID or address of ban list"
type="text"
value=""
/>
<label
for="mx_Field_2"
>
Room ID or address of ban list
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Subscribe
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,432 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<label
class="mx_SetIntegrationManager"
data-testid="mx_SetIntegrationManager"
for="toggle_integration"
>
<div
class="mx_SettingsFlag"
>
<div
class="mx_SetIntegrationManager_heading_manager"
>
<h3
class="mx_Heading_h3"
>
Manage integrations
</h3>
<h4
class="mx_Heading_h4"
>
(scalar.vector.im)
</h4>
</div>
<div
aria-checked="true"
aria-disabled="false"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="toggle_integration"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Use an integration manager
<strong>
(scalar.vector.im)
</strong>
to manage bots, widgets, and sticker packs.
</span>
</div>
<div
class="mx_SettingsSubsection_text"
>
Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.
</div>
</label>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Encryption
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Secure Backup
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
</div>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
<details>
<summary
class="mx_SecureBackupPanel_advanced"
>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
</table>
</details>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Message search
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
<span>
Element can't securely cache encrypted messages locally while running in a web browser. Use
<a
class="mx_ExternalLink"
href="https://element.io/get-started"
rel="noreferrer noopener"
target="_blank"
>
Element Desktop
<i
class="mx_ExternalLink_icon"
/>
</a>
for encrypted messages to appear in search results.
</span>
</div>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Cross-signing
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
<details>
<summary
class="mx_CrossSigningPanel_advanced"
>
Advanced
</summary>
<table
class="mx_CrossSigningPanel_statusList"
>
<tbody>
<tr>
<th
scope="row"
>
Cross-signing public keys:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
not found in storage
</td>
</tr>
<tr>
<th
scope="row"
>
Master private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Self signing private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
User signing private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Homeserver feature support:
</th>
<td>
not found
</td>
</tr>
</tbody>
</table>
</details>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Cryptography
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
<table
class="mx_CryptographyPanel_sessionInfo"
>
<tbody>
<tr>
<th
scope="row"
>
Session ID:
</th>
<td>
<code />
</td>
</tr>
<tr>
<th
scope="row"
>
Session key:
</th>
<td>
<code>
<strong>
...
</strong>
</code>
</td>
</tr>
</tbody>
</table>
</div>
<div
class="mx_CryptographyPanel_importExportButtons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Export E2E room keys
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Import E2E room keys
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Never send encrypted messages to unverified sessions from this session
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Never send encrypted messages to unverified sessions from this session"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Advanced
</h2>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Ignored users
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
You have no ignored users.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,403 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SessionManagerTab /> Device verification does not allow device verification on session that do not support encryption 1`] = `
HTMLCollection [
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
This session doesn't support encryption and thus can't be verified.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</div>
</p>
</div>
</div>,
]
`;
exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Sign out of this session
</span>
</div>
`;
exports[`<SessionManagerTab /> Sign out for an OIDC-aware server Signs out of current device 1`] = `
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
data-testid="device-detail-sign-out-cta"
role="button"
tabindex="0"
>
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Sign out of this session
</span>
</div>
`;
exports[`<SessionManagerTab /> current session section renders current session section with a verified session 1`] = `
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Current session
</h3>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton"
data-testid="current-session-menu"
role="button"
tabindex="0"
>
<div
class="mx_KebabContextMenu_icon"
/>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceTile mx_DeviceTile_interactive"
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Verified"
class="mx_DeviceTypeIcon_verificationIcon verified"
role="img"
/>
</div>
<div
class="mx_DeviceTile_info"
>
<h4
class="mx_Heading_h4"
>
Alices device
</h4>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Verified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
alices_device
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
>
<div
aria-label="Show details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
data-testid="current-session-toggle-details"
role="button"
tabindex="0"
>
<div
class="mx_DeviceExpandDetailsButton_icon"
/>
</div>
</div>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Your current session is ready for secure messaging.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</div>
</p>
</div>
</div>
</div>
</div>
`;
exports[`<SessionManagerTab /> current session section renders current session section with an unverified session 1`] = `
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Current session
</h3>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton"
data-testid="current-session-menu"
role="button"
tabindex="0"
>
<div
class="mx_KebabContextMenu_icon"
/>
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceTile mx_DeviceTile_interactive"
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
<div
class="mx_DeviceTile_info"
>
<h4
class="mx_Heading_h4"
>
Alices device
</h4>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Unverified
</span>
·
<span
data-testid="device-metadata-deviceId"
>
alices_device
</span>
</div>
</div>
<div
class="mx_DeviceTile_actions"
>
<div
aria-label="Show details"
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
data-testid="current-session-toggle-details"
role="button"
tabindex="0"
>
<div
class="mx_DeviceExpandDetailsButton_icon"
/>
</div>
</div>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your current session for enhanced secure messaging.
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
Learn more
</div>
</p>
<div
class="mx_DeviceSecurityCard_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="verification-status-button-alices_device"
role="button"
tabindex="0"
>
Verify session
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<SessionManagerTab /> goes to filtered list from security recommendations 1`] = `
<div
class="mx_FilteredDeviceListHeader"
>
<span
tabindex="0"
>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
aria-label="Select all"
aria-labelledby="floating-ui-142"
data-testid="device-select-all-checkbox"
id="device-select-all-checkbox"
type="checkbox"
/>
<label
for="device-select-all-checkbox"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
</label>
</span>
</span>
<span
class="mx_FilteredDeviceListHeader_label"
>
Sessions
</span>
<div
class="mx_Dropdown mx_FilterDropdown"
>
<div
aria-describedby="device-list-filter_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Filter devices"
aria-owns="device-list-filter_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="device-list-filter_value"
>
Show: Unverified
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,503 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SidebarUserSettingsTab /> renders sidebar settings with guest spa url 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Spaces to show
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked=""
disabled=""
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<label
for="checkbox_vY7Q4uEh9K"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Home
</div>
<div
class="mx_SettingsSubsection_text"
>
Home is useful for getting an overview of everything.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_SidebarUserSettingsTab_homeAllRoomsCheckbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
data-testid="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
id="checkbox_38QgU2Pomx"
type="checkbox"
/>
<label
for="checkbox_38QgU2Pomx"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
Show all rooms
</div>
<div
class="mx_SettingsSubsection_text"
>
Show all your rooms in Home, even if they're in a space.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_wKpa6hpi3Y"
type="checkbox"
/>
<label
for="checkbox_wKpa6hpi3Y"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Favourites
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your favourite rooms and people in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_EetmBG4yVC"
type="checkbox"
/>
<label
for="checkbox_EetmBG4yVC"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
People
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your people in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_eEefiPqpMR"
type="checkbox"
/>
<label
for="checkbox_eEefiPqpMR"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Rooms outside of a space
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your rooms that aren't part of a space in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_MwbPDmfGtm"
type="checkbox"
/>
<label
for="checkbox_MwbPDmfGtm"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
Video rooms and conferences
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all private video rooms and conferences. In conferences you can invite people outside of matrix.
</div>
</div>
</label>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<SidebarUserSettingsTab /> renders sidebar settings without guest spa url 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Spaces to show
</h3>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked=""
disabled=""
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<label
for="checkbox_vY7Q4uEh9K"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Home
</div>
<div
class="mx_SettingsSubsection_text"
>
Home is useful for getting an overview of everything.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_SidebarUserSettingsTab_homeAllRoomsCheckbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
data-testid="mx_SidebarUserSettingsTab_homeAllRoomsCheckbox"
id="checkbox_38QgU2Pomx"
type="checkbox"
/>
<label
for="checkbox_38QgU2Pomx"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
Show all rooms
</div>
<div
class="mx_SettingsSubsection_text"
>
Show all your rooms in Home, even if they're in a space.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_wKpa6hpi3Y"
type="checkbox"
/>
<label
for="checkbox_wKpa6hpi3Y"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Favourites
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your favourite rooms and people in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_EetmBG4yVC"
type="checkbox"
/>
<label
for="checkbox_EetmBG4yVC"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
People
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your people in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_eEefiPqpMR"
type="checkbox"
/>
<label
for="checkbox_eEefiPqpMR"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<div />
Rooms outside of a space
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all your rooms that aren't part of a space in one place.
</div>
</div>
</label>
</span>
<span
class="mx_Checkbox mx_SidebarUserSettingsTab_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_MwbPDmfGtm"
type="checkbox"
/>
<label
for="checkbox_MwbPDmfGtm"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
<div
class="mx_SettingsSubsection_text"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
Video rooms and conferences
</div>
<div
class="mx_SettingsSubsection_text"
>
Group all private video rooms and conferences.
</div>
</div>
</label>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;