Absorb the matrix-react-sdk repository (#28192)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
committed by
GitHub
parent
2b99496025
commit
c05c429803
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 { act, fireEvent, render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { RoomMember, EventType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getMockClientWithEventEmitter, makeRoomWithStateEvents, mkEvent } from "../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
AddPrivilegedUsers,
|
||||
getUserIdsFromCompletions,
|
||||
hasLowerOrEqualLevelThanDefaultLevel,
|
||||
} from "../../../../../src/components/views/settings/AddPrivilegedUsers";
|
||||
import UserProvider from "../../../../../src/autocomplete/UserProvider";
|
||||
import { ICompletion } from "../../../../../src/autocomplete/Autocompleter";
|
||||
|
||||
jest.mock("../../../../../src/autocomplete/UserProvider");
|
||||
jest.mock("../../../../../src/stores/WidgetStore");
|
||||
jest.mock("../../../../../src/stores/widgets/WidgetLayoutStore");
|
||||
|
||||
const completions: ICompletion[] = [
|
||||
{
|
||||
component: <div />,
|
||||
type: "user",
|
||||
completion: "user_1",
|
||||
completionId: "@user_1:host.local",
|
||||
range: { start: 1, end: 1 },
|
||||
},
|
||||
{
|
||||
component: <div />,
|
||||
type: "user",
|
||||
completion: "user_2",
|
||||
completionId: "@user_2:host.local",
|
||||
range: { start: 1, end: 1 },
|
||||
},
|
||||
{ component: <div />, type: "user", completion: "user_without_completion_id", range: { start: 1, end: 1 } },
|
||||
];
|
||||
|
||||
describe("<AddPrivilegedUsers />", () => {
|
||||
const provider = mocked(UserProvider, { shallow: true });
|
||||
provider.prototype.getCompletions.mockResolvedValue(completions);
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
// `makeRoomWithStateEvents` only work's if `getRoom` is present.
|
||||
getRoom: jest.fn(),
|
||||
setPowerLevel: jest.fn(),
|
||||
});
|
||||
|
||||
const room = makeRoomWithStateEvents([], { roomId: "room_id", mockClient: mockClient });
|
||||
room.getMember = (userId: string) => {
|
||||
const member = new RoomMember("room_id", userId);
|
||||
member.powerLevel = 0;
|
||||
|
||||
return member;
|
||||
};
|
||||
(room.currentState.getStateEvents as unknown) = (_eventType: string, _stateKey: string) => {
|
||||
return mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {},
|
||||
user: "user_id",
|
||||
});
|
||||
};
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<AddPrivilegedUsers room={room} defaultUserLevel={0} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
it("checks whether form submit works as intended", async () => {
|
||||
const { getByTestId, queryAllByTestId } = render(getComponent());
|
||||
|
||||
// Verify that the submit button is disabled initially.
|
||||
const submitButton = getByTestId("add-privileged-users-submit-button");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Find some suggestions and select them.
|
||||
const autocompleteInput = getByTestId("autocomplete-input");
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(autocompleteInput);
|
||||
fireEvent.change(autocompleteInput, { target: { value: "u" } });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(provider.mock.instances[0].getCompletions).toHaveBeenCalledTimes(1));
|
||||
const matchOne = getByTestId("autocomplete-suggestion-item-@user_1:host.local");
|
||||
const matchTwo = getByTestId("autocomplete-suggestion-item-@user_2:host.local");
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseDown(matchOne);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseDown(matchTwo);
|
||||
});
|
||||
|
||||
// Check that `defaultUserLevel` is initially set and select a higher power level.
|
||||
expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeTruthy();
|
||||
expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy();
|
||||
expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeFalsy();
|
||||
|
||||
const powerLevelSelect = getByTestId("power-level-select-element");
|
||||
await userEvent.selectOptions(powerLevelSelect, "100");
|
||||
|
||||
expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeFalsy();
|
||||
expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy();
|
||||
expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeTruthy();
|
||||
|
||||
// The submit button should be enabled now.
|
||||
expect(submitButton).toBeEnabled();
|
||||
|
||||
// Submit the form.
|
||||
act(() => {
|
||||
fireEvent.submit(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1));
|
||||
|
||||
// Verify that the submit button is disabled again.
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Verify that previously selected items are reset.
|
||||
const selectionItems = queryAllByTestId("autocomplete-selection-item", { exact: false });
|
||||
expect(selectionItems).toHaveLength(0);
|
||||
|
||||
// Verify that power level select is reset to `defaultUserLevel`.
|
||||
expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeTruthy();
|
||||
expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy();
|
||||
expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeFalsy();
|
||||
});
|
||||
|
||||
it("getUserIdsFromCompletions() should map completions to user id's", () => {
|
||||
expect(getUserIdsFromCompletions(completions)).toStrictEqual(["@user_1:host.local", "@user_2:host.local"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ defaultUserLevel: -50, expectation: false },
|
||||
{ defaultUserLevel: 0, expectation: true },
|
||||
{ defaultUserLevel: 50, expectation: true },
|
||||
])(
|
||||
"hasLowerOrEqualLevelThanDefaultLevel() should return $expectation for default level $defaultUserLevel",
|
||||
({ defaultUserLevel, expectation }) => {
|
||||
expect(hasLowerOrEqualLevelThanDefaultLevel(room, completions[0], defaultUserLevel)).toBe(expectation);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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, screen, waitFor } from "jest-matrix-react";
|
||||
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { AddRemoveThreepids } from "../../../../../src/components/views/settings/AddRemoveThreepids";
|
||||
import { clearAllModals, stubClient } from "../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
|
||||
const MOCK_IDENTITY_ACCESS_TOKEN = "mock_identity_access_token";
|
||||
const mockGetAccessToken = jest.fn().mockResolvedValue(MOCK_IDENTITY_ACCESS_TOKEN);
|
||||
jest.mock("../../../../../src/IdentityAuthClient", () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: mockGetAccessToken,
|
||||
})),
|
||||
);
|
||||
|
||||
const EMAIL1 = {
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "alice@nowhere.dummy",
|
||||
};
|
||||
|
||||
const PHONE1 = {
|
||||
medium: ThreepidMedium.Phone,
|
||||
address: "447700900000",
|
||||
};
|
||||
|
||||
const PHONE1_LOCALNUM = "07700900000";
|
||||
|
||||
describe("AddRemoveThreepids", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
clearAllModals();
|
||||
});
|
||||
|
||||
const clientProviderWrapper: React.FC = ({ children }) => (
|
||||
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
it("should render a loader while loading", async () => {
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[]}
|
||||
isLoading={true}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render email addresses", async () => {
|
||||
const { container } = render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[EMAIL1]}
|
||||
isLoading={false}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render phone numbers", async () => {
|
||||
const { container } = render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[PHONE1]}
|
||||
isLoading={false}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle no email addresses", async () => {
|
||||
const { container } = render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should add an email address", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
|
||||
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox", { name: "Email Address" });
|
||||
await userEvent.type(input, EMAIL1.address);
|
||||
const addButton = screen.getByRole("button", { name: "Add" });
|
||||
await userEvent.click(addButton);
|
||||
|
||||
expect(client.requestAdd3pidEmailToken).toHaveBeenCalledWith(EMAIL1.address, client.generateClientSecret(), 1);
|
||||
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||
|
||||
expect(continueButton).toBeEnabled();
|
||||
|
||||
await userEvent.click(continueButton);
|
||||
|
||||
expect(client.addThreePidOnly).toHaveBeenCalledWith({
|
||||
client_secret: client.generateClientSecret(),
|
||||
sid: "1",
|
||||
auth: undefined,
|
||||
});
|
||||
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display an error if the link has not been clicked", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
const createDialogFn = jest.spyOn(Modal, "createDialog");
|
||||
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
|
||||
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox", { name: "Email Address" });
|
||||
await userEvent.type(input, EMAIL1.address);
|
||||
const addButton = screen.getByRole("button", { name: "Add" });
|
||||
await userEvent.click(addButton);
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||
|
||||
expect(continueButton).toBeEnabled();
|
||||
|
||||
mocked(client).addThreePidOnly.mockRejectedValueOnce(new Error("Unauthorized"));
|
||||
|
||||
await userEvent.click(continueButton);
|
||||
|
||||
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
||||
description: "Unauthorized",
|
||||
title: "Unable to verify email address.",
|
||||
});
|
||||
|
||||
expect(onChangeFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should add a phone number", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
|
||||
sid: "1",
|
||||
msisdn: PHONE1.address,
|
||||
intl_fmt: "+" + PHONE1.address,
|
||||
success: true,
|
||||
submit_url: "https://example.dummy",
|
||||
});
|
||||
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
|
||||
await userEvent.click(countryDropdown);
|
||||
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
|
||||
await userEvent.click(gbOption);
|
||||
|
||||
const input = screen.getByRole("textbox", { name: "Phone Number" });
|
||||
await userEvent.type(input, PHONE1_LOCALNUM);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /Add/ });
|
||||
userEvent.click(addButton);
|
||||
|
||||
const continueButton = await screen.findByRole("button", { name: /Continue/ });
|
||||
|
||||
await expect(continueButton).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
await expect(
|
||||
await screen.findByText(
|
||||
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
|
||||
"GB",
|
||||
PHONE1_LOCALNUM,
|
||||
client.generateClientSecret(),
|
||||
1,
|
||||
);
|
||||
|
||||
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||
await userEvent.type(verificationInput, "123456");
|
||||
|
||||
expect(continueButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
userEvent.click(continueButton);
|
||||
|
||||
await waitFor(() => expect(continueButton).toHaveAttribute("aria-disabled", "true"));
|
||||
|
||||
expect(client.addThreePidOnly).toHaveBeenCalledWith({
|
||||
client_secret: client.generateClientSecret(),
|
||||
sid: "1",
|
||||
auth: undefined,
|
||||
});
|
||||
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
}, 10000);
|
||||
|
||||
it("should display an error if the code is incorrect", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
const createDialogFn = jest.spyOn(Modal, "createDialog");
|
||||
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
|
||||
sid: "1",
|
||||
msisdn: PHONE1.address,
|
||||
intl_fmt: "+" + PHONE1.address,
|
||||
success: true,
|
||||
submit_url: "https://example.dummy",
|
||||
});
|
||||
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const input = screen.getByRole("textbox", { name: "Phone Number" });
|
||||
await userEvent.type(input, PHONE1_LOCALNUM);
|
||||
|
||||
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
|
||||
await userEvent.click(countryDropdown);
|
||||
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
|
||||
await userEvent.click(gbOption);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /Add/ });
|
||||
await userEvent.click(addButton);
|
||||
|
||||
mocked(client).addThreePidOnly.mockRejectedValueOnce(new Error("Unauthorized"));
|
||||
|
||||
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||
await userEvent.type(verificationInput, "123457");
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: /Continue/ });
|
||||
await userEvent.click(continueButton);
|
||||
|
||||
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
||||
description: "Unauthorized",
|
||||
title: "Unable to verify phone number.",
|
||||
});
|
||||
|
||||
expect(onChangeFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should remove an email address", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[EMAIL1]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const removeButton = screen.getByRole("button", { name: /Remove/ });
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
|
||||
|
||||
const confirmRemoveButton = screen.getByRole("button", { name: /Remove/ });
|
||||
await userEvent.click(confirmRemoveButton);
|
||||
|
||||
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return to default view if adding is cancelled", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[EMAIL1]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const removeButton = screen.getByRole("button", { name: /Remove/ });
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
|
||||
|
||||
const confirmRemoveButton = screen.getByRole("button", { name: /Cancel/ });
|
||||
await userEvent.click(confirmRemoveButton);
|
||||
|
||||
expect(screen.queryByText(`Remove ${EMAIL1.address}?`)).not.toBeInTheDocument();
|
||||
|
||||
expect(client.deleteThreePid).not.toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||
expect(onChangeFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should remove a phone number", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[PHONE1]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const removeButton = screen.getByRole("button", { name: /Remove/ });
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible();
|
||||
|
||||
const confirmRemoveButton = screen.getByRole("button", { name: /Remove/ });
|
||||
await userEvent.click(confirmRemoveButton);
|
||||
|
||||
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should bind an email address", async () => {
|
||||
mocked(client).requestEmailToken.mockResolvedValue({ sid: "1" });
|
||||
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
|
||||
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="is"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[Object.assign({}, EMAIL1, { bound: false })]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText(EMAIL1.address)).toBeVisible();
|
||||
const shareButton = screen.getByRole("button", { name: /Share/ });
|
||||
await userEvent.click(shareButton);
|
||||
|
||||
expect(screen.getByText("Verify the link in your inbox")).toBeVisible();
|
||||
|
||||
expect(client.requestEmailToken).toHaveBeenCalledWith(
|
||||
EMAIL1.address,
|
||||
client.generateClientSecret(),
|
||||
1,
|
||||
undefined,
|
||||
MOCK_IDENTITY_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
const completeButton = screen.getByRole("button", { name: /Complete/ });
|
||||
await userEvent.click(completeButton);
|
||||
|
||||
expect(client.bindThreePid).toHaveBeenCalledWith({
|
||||
sid: "1",
|
||||
client_secret: client.generateClientSecret(),
|
||||
id_server: "https://the_best_id_server.dummy",
|
||||
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should bind a phone number", async () => {
|
||||
mocked(client).requestMsisdnToken.mockResolvedValue({
|
||||
success: true,
|
||||
sid: "1",
|
||||
msisdn: PHONE1.address,
|
||||
intl_fmt: "+" + PHONE1.address,
|
||||
});
|
||||
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
|
||||
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="is"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[Object.assign({}, PHONE1, { bound: false })]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText(PHONE1.address)).toBeVisible();
|
||||
const shareButton = screen.getByRole("button", { name: /Share/ });
|
||||
await userEvent.click(shareButton);
|
||||
|
||||
expect(screen.getByText("Please enter verification code sent via text.")).toBeVisible();
|
||||
|
||||
expect(client.requestMsisdnToken).toHaveBeenCalledWith(
|
||||
null,
|
||||
"+" + PHONE1.address,
|
||||
client.generateClientSecret(),
|
||||
1,
|
||||
undefined,
|
||||
MOCK_IDENTITY_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
const codeInput = screen.getByRole("textbox", { name: "Verification code" });
|
||||
await userEvent.type(codeInput, "123456");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(client.bindThreePid).toHaveBeenCalledWith({
|
||||
sid: "1",
|
||||
client_secret: client.generateClientSecret(),
|
||||
id_server: "https://the_best_id_server.dummy",
|
||||
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should revoke a bound email address", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="is"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[Object.assign({}, EMAIL1, { bound: true })]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText(EMAIL1.address)).toBeVisible();
|
||||
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
|
||||
await userEvent.click(revokeButton);
|
||||
|
||||
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should revoke a bound phone number", async () => {
|
||||
const onChangeFn = jest.fn();
|
||||
render(
|
||||
<AddRemoveThreepids
|
||||
mode="is"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[Object.assign({}, PHONE1, { bound: true })]}
|
||||
isLoading={false}
|
||||
onChange={onChangeFn}
|
||||
/>,
|
||||
{
|
||||
wrapper: clientProviderWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText(PHONE1.address)).toBeVisible();
|
||||
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
|
||||
await userEvent.click(revokeButton);
|
||||
|
||||
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import AvatarSetting from "../../../../../src/components/views/settings/AvatarSetting";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
|
||||
const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
|
||||
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
|
||||
type: "image/gif",
|
||||
});
|
||||
|
||||
describe("<AvatarSetting />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("renders avatar with specified alt text", async () => {
|
||||
const { queryByAltText } = render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatarAltText="Avatar of Peter Fox"
|
||||
avatar="mxc://example.org/my-avatar"
|
||||
/>,
|
||||
);
|
||||
|
||||
const imgElement = queryByAltText("Avatar of Peter Fox");
|
||||
expect(imgElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a file as the avatar when supplied", async () => {
|
||||
render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatarAltText="Avatar of Peter Fox"
|
||||
avatar={AVATAR_FILE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const imgElement = await screen.findByRole("button", { name: "Avatar of Peter Fox" });
|
||||
expect(imgElement).toBeInTheDocument();
|
||||
expect(imgElement).toHaveAttribute("src", "data:image/gif;base64," + BASE64_GIF);
|
||||
});
|
||||
|
||||
it("calls onChange when a file is uploaded", async () => {
|
||||
const onChange = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatar="mxc://example.org/my-avatar"
|
||||
avatarAltText="Avatar of Peter Fox"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fileInput = screen.getByAltText("Upload");
|
||||
await user.upload(fileInput, AVATAR_FILE);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(AVATAR_FILE);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
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, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import ChangePassword from "../../../../../src/components/views/settings/ChangePassword";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
|
||||
describe("<ChangePassword />", () => {
|
||||
it("renders expected fields", () => {
|
||||
const onFinished = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const { asFragment } = render(<ChangePassword onFinished={onFinished} onError={onError} />);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show validation tooltip if passwords do not match", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const { getByLabelText, getByText } = render(<ChangePassword onFinished={onFinished} onError={onError} />);
|
||||
|
||||
const currentPasswordField = getByLabelText("Current password");
|
||||
await userEvent.type(currentPasswordField, "CurrentPassword1234");
|
||||
|
||||
const newPasswordField = getByLabelText("New Password");
|
||||
await userEvent.type(newPasswordField, "$%newPassword1234");
|
||||
const confirmPasswordField = getByLabelText("Confirm password");
|
||||
await userEvent.type(confirmPasswordField, "$%newPassword1235");
|
||||
|
||||
await userEvent.click(getByText("Change Password"));
|
||||
|
||||
await expect(screen.findByText("Passwords don't match")).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call MatrixClient::setPassword with expected parameters", async () => {
|
||||
const cli = stubClient();
|
||||
mocked(cli.setPassword).mockResolvedValue({});
|
||||
|
||||
const onFinished = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const { getByLabelText, getByText } = render(<ChangePassword onFinished={onFinished} onError={onError} />);
|
||||
|
||||
const currentPasswordField = getByLabelText("Current password");
|
||||
await userEvent.type(currentPasswordField, "CurrentPassword1234");
|
||||
|
||||
const newPasswordField = getByLabelText("New Password");
|
||||
await userEvent.type(newPasswordField, "$%newPassword1234");
|
||||
const confirmPasswordField = getByLabelText("Confirm password");
|
||||
await userEvent.type(confirmPasswordField, "$%newPassword1234");
|
||||
|
||||
await userEvent.click(getByText("Change Password"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(cli.setPassword).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: cli.getUserId(),
|
||||
},
|
||||
password: "CurrentPassword1234",
|
||||
}),
|
||||
"$%newPassword1234",
|
||||
false,
|
||||
);
|
||||
});
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
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, screen } from "jest-matrix-react";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import CrossSigningPanel from "../../../../../src/components/views/settings/CrossSigningPanel";
|
||||
import {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import ConfirmDestroyCrossSigningDialog from "../../../../../src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog";
|
||||
|
||||
describe("<CrossSigningPanel />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
const getComponent = () => render(<CrossSigningPanel />);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsCrypto(),
|
||||
doesServerSupportUnstableFeature: jest.fn(),
|
||||
});
|
||||
|
||||
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
|
||||
mockClient.isCrossSigningReady.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render a spinner while loading", () => {
|
||||
getComponent();
|
||||
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render when homeserver does not support cross-signing", async () => {
|
||||
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
|
||||
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when cross signing is ready", () => {
|
||||
it("should render when keys are not backed up", async () => {
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
|
||||
"⚠️ Cross-signing is ready but keys are not backed up.",
|
||||
);
|
||||
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render when keys are backed up", async () => {
|
||||
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: true,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: true,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
|
||||
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow reset of cross-signing", async () => {
|
||||
mockClient.getCrypto()!.bootstrapCrossSigning = jest.fn().mockResolvedValue(undefined);
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
screen.getByRole("button", { name: "Reset" }).click();
|
||||
expect(modalSpy).toHaveBeenCalledWith(ConfirmDestroyCrossSigningDialog, expect.any(Object));
|
||||
modalSpy.mock.lastCall![1]!.onFinished(true);
|
||||
expect(mockClient.getCrypto()!.bootstrapCrossSigning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ setupNewCrossSigning: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when cross signing is not ready", () => {
|
||||
beforeEach(() => {
|
||||
mocked(mockClient.getCrypto()!.isCrossSigningReady).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("should render when keys are not backed up", async () => {
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up.");
|
||||
});
|
||||
|
||||
it("should render when keys are backed up", async () => {
|
||||
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: true,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: true,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
|
||||
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
|
||||
);
|
||||
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 } from "jest-matrix-react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import * as TestUtils from "../../../../test-utils";
|
||||
import CryptographyPanel from "../../../../../src/components/views/settings/CryptographyPanel";
|
||||
|
||||
describe("CryptographyPanel", () => {
|
||||
it("shows the session ID and key", async () => {
|
||||
const sessionId = "ABCDEFGHIJ";
|
||||
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
|
||||
const sessionKeyFormatted = "<strong>AbCD eFgh IJK7 L/m4 nOPq RSTU VW4x yzaB CDef 6gHI Jkl</strong>";
|
||||
|
||||
TestUtils.stubClient();
|
||||
const client: MatrixClient = MatrixClientPeg.safeGet();
|
||||
client.deviceId = sessionId;
|
||||
|
||||
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
|
||||
|
||||
// When we render the CryptographyPanel
|
||||
const rendered = render(<CryptographyPanel />);
|
||||
|
||||
// Then it displays info about the user's session
|
||||
const codes = rendered.container.querySelectorAll("code");
|
||||
expect(codes.length).toEqual(2);
|
||||
expect(codes[0].innerHTML).toEqual(sessionId);
|
||||
|
||||
// Initially a placeholder
|
||||
expect(codes[1].innerHTML).toEqual("<strong>...</strong>");
|
||||
|
||||
// Then the actual key
|
||||
await TestUtils.flushPromises();
|
||||
expect(codes[1].innerHTML).toEqual(sessionKeyFormatted);
|
||||
});
|
||||
|
||||
it("handles errors fetching session key", async () => {
|
||||
const sessionId = "ABCDEFGHIJ";
|
||||
|
||||
TestUtils.stubClient();
|
||||
const client: MatrixClient = MatrixClientPeg.safeGet();
|
||||
client.deviceId = sessionId;
|
||||
|
||||
mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh"));
|
||||
|
||||
// When we render the CryptographyPanel
|
||||
const rendered = render(<CryptographyPanel />);
|
||||
|
||||
// Then it displays info about the user's session
|
||||
const codes = rendered.container.querySelectorAll("code");
|
||||
|
||||
// Initially a placeholder
|
||||
expect(codes[1].innerHTML).toEqual("<strong>...</strong>");
|
||||
|
||||
// Then "not supported key
|
||||
await TestUtils.flushPromises();
|
||||
expect(codes[1].innerHTML).toEqual("<strong><not supported></strong>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
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, within } from "jest-matrix-react";
|
||||
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import EventIndexPanel from "../../../../../src/components/views/settings/EventIndexPanel";
|
||||
import EventIndexPeg from "../../../../../src/indexing/EventIndexPeg";
|
||||
import EventIndex from "../../../../../src/indexing/EventIndex";
|
||||
import { clearAllModals, flushPromises, getMockClientWithEventEmitter } from "../../../../test-utils";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
|
||||
describe("<EventIndexPanel />", () => {
|
||||
getMockClientWithEventEmitter({
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
});
|
||||
|
||||
const getComponent = () => render(<EventIndexPanel />);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(EventIndexPeg, "get").mockRestore();
|
||||
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
|
||||
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
|
||||
jest.spyOn(EventIndexPeg, "initEventIndex").mockClear().mockResolvedValue(true);
|
||||
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockClear();
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
|
||||
jest.spyOn(SettingsStore, "setValue").mockClear();
|
||||
|
||||
// @ts-ignore private property
|
||||
EventIndexPeg.error = null;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
describe("when event index is initialised", () => {
|
||||
it("renders event index information", () => {
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens event index management dialog", async () => {
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||
getComponent();
|
||||
|
||||
fireEvent.click(screen.getByText("Manage"));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(within(dialog).getByText("Message search")).toBeInTheDocument();
|
||||
|
||||
// close the modal
|
||||
fireEvent.click(within(dialog).getByText("Done"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("when event indexing is fully supported and enabled but not initialised", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
|
||||
|
||||
// @ts-ignore private property
|
||||
EventIndexPeg.error = new Error("Test error message");
|
||||
});
|
||||
|
||||
it("displays an error when no event index is found and enabling not in progress", () => {
|
||||
getComponent();
|
||||
|
||||
expect(screen.getByText("Message search initialisation failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays an error from the event index", () => {
|
||||
getComponent();
|
||||
|
||||
expect(screen.getByText("Test error message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("asks for confirmation when resetting seshat", async () => {
|
||||
getComponent();
|
||||
|
||||
fireEvent.click(screen.getByText("Reset"));
|
||||
|
||||
// wait for reset modal to open
|
||||
await screen.findByText("Reset event store?");
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
expect(within(dialog).getByText("Reset event store?")).toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByText("Cancel"));
|
||||
|
||||
// didn't reset
|
||||
expect(SettingsStore.setValue).not.toHaveBeenCalled();
|
||||
expect(EventIndexPeg.deleteEventIndex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resets seshat", async () => {
|
||||
getComponent();
|
||||
|
||||
fireEvent.click(screen.getByText("Reset"));
|
||||
|
||||
// wait for reset modal to open
|
||||
await screen.findByText("Reset event store?");
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
fireEvent.click(within(dialog).getByText("Reset event store"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||
"enableEventIndexing",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
false,
|
||||
);
|
||||
expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled();
|
||||
|
||||
await clearAllModals();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when event indexing is supported but not enabled", () => {
|
||||
it("renders enable text", () => {
|
||||
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||
|
||||
getComponent();
|
||||
|
||||
expect(
|
||||
screen.getByText("Securely cache encrypted messages locally for them to appear in search results."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it("enables event indexing on enable button click", async () => {
|
||||
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||
let deferredInitEventIndex: IDeferred<boolean> | undefined;
|
||||
jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => {
|
||||
deferredInitEventIndex = defer<boolean>();
|
||||
return deferredInitEventIndex.promise;
|
||||
});
|
||||
|
||||
getComponent();
|
||||
|
||||
fireEvent.click(screen.getByText("Enable"));
|
||||
|
||||
await flushPromises();
|
||||
// spinner shown while enabling
|
||||
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||
|
||||
// add an event indx to the peg and resolve the init promise
|
||||
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||
expect(EventIndexPeg.initEventIndex).toHaveBeenCalled();
|
||||
deferredInitEventIndex!.resolve(true);
|
||||
await flushPromises();
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("enableEventIndexing", null, SettingLevel.DEVICE, true);
|
||||
|
||||
// message for enabled event index
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when event indexing is supported but not installed", () => {
|
||||
it("renders link to install seshat", () => {
|
||||
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
|
||||
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when event indexing is not supported", () => {
|
||||
it("renders link to download a desktop client", () => {
|
||||
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 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 * as TestUtils from "../../../../test-utils";
|
||||
import FontScalingPanel from "../../../../../src/components/views/settings/FontScalingPanel";
|
||||
|
||||
describe("FontScalingPanel", () => {
|
||||
it("renders the font scaling UI", () => {
|
||||
TestUtils.stubClient();
|
||||
const { asFragment } = render(<FontScalingPanel />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
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 { act, fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
|
||||
import {
|
||||
EventType,
|
||||
GuestAccess,
|
||||
HistoryVisibility,
|
||||
JoinRule,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
ClientEvent,
|
||||
RoomMember,
|
||||
MatrixError,
|
||||
Visibility,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import {
|
||||
clearAllModals,
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import { filterBoolean } from "../../../../../src/utils/arrays";
|
||||
import JoinRuleSettings, { JoinRuleSettingsProps } from "../../../../../src/components/views/settings/JoinRuleSettings";
|
||||
import { PreferredRoomVersions } from "../../../../../src/utils/PreferredRoomVersions";
|
||||
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<JoinRuleSettings />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getRoom: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
getLocalAliases: jest.fn().mockReturnValue([]),
|
||||
sendStateEvent: jest.fn(),
|
||||
upgradeRoom: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
invite: jest.fn().mockResolvedValue(undefined),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getRoomDirectoryVisibility: jest.fn(),
|
||||
setRoomDirectoryVisibility: jest.fn(),
|
||||
});
|
||||
const roomId = "!room:server.org";
|
||||
const newRoomId = "!roomUpgraded:server.org";
|
||||
|
||||
const defaultProps = {
|
||||
room: new Room(roomId, client, userId),
|
||||
closeSettingsFn: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
};
|
||||
const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
|
||||
render(<JoinRuleSettings {...defaultProps} {...props} />);
|
||||
|
||||
const setRoomStateEvents = (
|
||||
room: Room,
|
||||
roomVersion: string,
|
||||
joinRule?: JoinRule,
|
||||
guestAccess?: GuestAccess,
|
||||
history?: HistoryVisibility,
|
||||
): void => {
|
||||
const events = filterBoolean<MatrixEvent>([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomCreate,
|
||||
content: { room_version: roomVersion },
|
||||
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(() => {
|
||||
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
|
||||
client.isRoomEncrypted.mockReturnValue(false);
|
||||
client.upgradeRoom.mockResolvedValue({ replacement_room: newRoomId });
|
||||
client.getRoom.mockReturnValue(null);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
|
||||
});
|
||||
|
||||
type TestCase = [string, { label: string; unsupportedRoomVersion: string; preferredRoomVersion: string }];
|
||||
const testCases: TestCase[] = [
|
||||
[
|
||||
JoinRule.Knock,
|
||||
{
|
||||
label: "Ask to join",
|
||||
unsupportedRoomVersion: "6",
|
||||
preferredRoomVersion: PreferredRoomVersions.KnockRooms,
|
||||
},
|
||||
],
|
||||
[
|
||||
JoinRule.Restricted,
|
||||
{
|
||||
label: "Space members",
|
||||
unsupportedRoomVersion: "8",
|
||||
preferredRoomVersion: PreferredRoomVersions.RestrictedRooms,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
describe.each(testCases)("%s rooms", (joinRule, { label, unsupportedRoomVersion, preferredRoomVersion }) => {
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
describe(`when room does not support join rule ${joinRule}`, () => {
|
||||
it(`should not show ${joinRule} room join rule when upgrade is disabled`, () => {
|
||||
// room that doesn't support the join rule
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, unsupportedRoomVersion);
|
||||
|
||||
getComponent({ room: room, promptUpgrade: false });
|
||||
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`should show ${joinRule} room join rule when upgrade is enabled`, () => {
|
||||
// room that doesn't support the join rule
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, unsupportedRoomVersion);
|
||||
|
||||
getComponent({ room: room, promptUpgrade: true });
|
||||
|
||||
expect(within(screen.getByText(label)).getByText("Upgrade required")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`upgrades room when changing join rule to ${joinRule}`, async () => {
|
||||
const deferredInvites: IDeferred<any>[] = [];
|
||||
// room that doesn't support the join rule
|
||||
const room = new Room(roomId, client, userId);
|
||||
const parentSpace = new Room("!parentSpace:server.org", client, userId);
|
||||
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parentSpace.roomId]));
|
||||
setRoomStateEvents(room, unsupportedRoomVersion);
|
||||
const memberAlice = new RoomMember(roomId, "@alice:server.org");
|
||||
const memberBob = new RoomMember(roomId, "@bob:server.org");
|
||||
const memberCharlie = new RoomMember(roomId, "@charlie:server.org");
|
||||
jest.spyOn(room, "getMembersWithMembership").mockImplementation((membership) =>
|
||||
membership === KnownMembership.Join ? [memberAlice, memberBob] : [memberCharlie],
|
||||
);
|
||||
const upgradedRoom = new Room(newRoomId, client, userId);
|
||||
setRoomStateEvents(upgradedRoom, preferredRoomVersion);
|
||||
client.getRoom.mockImplementation((id) => {
|
||||
if (roomId === id) return room;
|
||||
if (parentSpace.roomId === id) return parentSpace;
|
||||
return null;
|
||||
});
|
||||
|
||||
// resolve invites by hand
|
||||
// flushPromises is too blunt to test reliably
|
||||
client.invite.mockImplementation(() => {
|
||||
const p = defer<{}>();
|
||||
deferredInvites.push(p);
|
||||
return p.promise;
|
||||
});
|
||||
|
||||
getComponent({ room: room, promptUpgrade: true });
|
||||
|
||||
fireEvent.click(screen.getByText(label));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
fireEvent.click(within(dialog).getByText("Upgrade"));
|
||||
|
||||
expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, preferredRoomVersion);
|
||||
|
||||
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
|
||||
|
||||
// "create" our new room, have it come thru sync
|
||||
client.getRoom.mockImplementation((id) => {
|
||||
if (roomId === id) return room;
|
||||
if (newRoomId === id) return upgradedRoom;
|
||||
if (parentSpace.roomId === id) return parentSpace;
|
||||
return null;
|
||||
});
|
||||
client.emit(ClientEvent.Room, upgradedRoom);
|
||||
|
||||
// invite users
|
||||
expect(await screen.findByText("Sending invites... (0 out of 2)")).toBeInTheDocument();
|
||||
deferredInvites.pop()!.resolve({});
|
||||
expect(await screen.findByText("Sending invites... (1 out of 2)")).toBeInTheDocument();
|
||||
deferredInvites.pop()!.resolve({});
|
||||
|
||||
// Usually we see "Updating space..." in the UI here, but we
|
||||
// removed the assertion about it, because it sometimes fails,
|
||||
// presumably because it disappeared too quickly to be visible.
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// done, modal closed
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it(`upgrades room with no parent spaces or members when changing join rule to ${joinRule}`, async () => {
|
||||
// room that doesn't support the join rule
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, unsupportedRoomVersion);
|
||||
const upgradedRoom = new Room(newRoomId, client, userId);
|
||||
setRoomStateEvents(upgradedRoom, preferredRoomVersion);
|
||||
|
||||
getComponent({ room: room, promptUpgrade: true });
|
||||
|
||||
fireEvent.click(screen.getByText(label));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
fireEvent.click(within(dialog).getByText("Upgrade"));
|
||||
|
||||
expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, preferredRoomVersion);
|
||||
|
||||
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
|
||||
|
||||
// "create" our new room, have it come thru sync
|
||||
client.getRoom.mockImplementation((id) => {
|
||||
if (roomId === id) return room;
|
||||
if (newRoomId === id) return upgradedRoom;
|
||||
return null;
|
||||
});
|
||||
client.emit(ClientEvent.Room, upgradedRoom);
|
||||
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
// done, modal closed
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("knock rooms directory visibility", () => {
|
||||
const getCheckbox = () => screen.getByRole("checkbox");
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => (room = new Room(roomId, client, userId)));
|
||||
|
||||
describe("when join rule is knock", () => {
|
||||
beforeEach(() => setRoomStateEvents(room, PreferredRoomVersions.KnockRooms, JoinRule.Knock));
|
||||
|
||||
it("should set the visibility to public", async () => {
|
||||
jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Private });
|
||||
jest.spyOn(client, "setRoomDirectoryVisibility").mockResolvedValue({});
|
||||
getComponent({ room });
|
||||
fireEvent.click(getCheckbox());
|
||||
await act(async () => await flushPromises());
|
||||
expect(client.setRoomDirectoryVisibility).toHaveBeenCalledWith(roomId, Visibility.Public);
|
||||
expect(getCheckbox()).toBeChecked();
|
||||
});
|
||||
|
||||
it("should set the visibility to private", async () => {
|
||||
jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Public });
|
||||
jest.spyOn(client, "setRoomDirectoryVisibility").mockResolvedValue({});
|
||||
getComponent({ room });
|
||||
await act(async () => await flushPromises());
|
||||
fireEvent.click(getCheckbox());
|
||||
await act(async () => await flushPromises());
|
||||
expect(client.setRoomDirectoryVisibility).toHaveBeenCalledWith(roomId, Visibility.Private);
|
||||
expect(getCheckbox()).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should call onError if setting visibility fails", async () => {
|
||||
const error = new MatrixError();
|
||||
jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Private });
|
||||
jest.spyOn(client, "setRoomDirectoryVisibility").mockRejectedValue(error);
|
||||
getComponent({ room });
|
||||
fireEvent.click(getCheckbox());
|
||||
await act(async () => await flushPromises());
|
||||
expect(getCheckbox()).not.toBeChecked();
|
||||
expect(defaultProps.onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the room version is unsupported and upgrade is enabled", () => {
|
||||
it("should disable the checkbox", () => {
|
||||
setRoomStateEvents(room, "6", JoinRule.Invite);
|
||||
getComponent({ promptUpgrade: true, room });
|
||||
expect(getCheckbox()).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when join rule is not knock", () => {
|
||||
beforeEach(() => {
|
||||
setRoomStateEvents(room, PreferredRoomVersions.KnockRooms, JoinRule.Invite);
|
||||
getComponent({ room });
|
||||
});
|
||||
|
||||
it("should disable the checkbox", () => {
|
||||
expect(getCheckbox()).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should set the visibility to private by default", () => {
|
||||
expect(getCheckbox()).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show knock room join rule", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
const room = new Room(newRoomId, client, userId);
|
||||
setRoomStateEvents(room, PreferredRoomVersions.KnockRooms);
|
||||
getComponent({ room });
|
||||
expect(screen.queryByText("Ask to join")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { Key } from "../../../../../src/Keyboard";
|
||||
import { mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils/platform";
|
||||
import { KeyboardKey, KeyboardShortcut } from "../../../../../src/components/views/settings/KeyboardShortcut";
|
||||
|
||||
const renderKeyboardShortcut = (Component: React.FunctionComponent<any>, props: Record<string, any>) => {
|
||||
return render(<Component {...props} />).container;
|
||||
};
|
||||
|
||||
describe("KeyboardShortcut", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
it("renders key icon", () => {
|
||||
const body = renderKeyboardShortcut(KeyboardKey, { name: Key.ARROW_DOWN });
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders alternative key name", () => {
|
||||
const body = renderKeyboardShortcut(KeyboardKey, { name: Key.PAGE_DOWN });
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("doesn't render + if last", () => {
|
||||
const body = renderKeyboardShortcut(KeyboardKey, { name: Key.A, last: true });
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("doesn't render same modifier twice", () => {
|
||||
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
|
||||
const body1 = renderKeyboardShortcut(KeyboardShortcut, {
|
||||
value: {
|
||||
key: Key.A,
|
||||
ctrlOrCmdKey: true,
|
||||
metaKey: true,
|
||||
},
|
||||
});
|
||||
expect(body1).toMatchSnapshot();
|
||||
|
||||
const body2 = renderKeyboardShortcut(KeyboardShortcut, {
|
||||
value: {
|
||||
key: Key.A,
|
||||
ctrlOrCmdKey: true,
|
||||
ctrlKey: true,
|
||||
},
|
||||
});
|
||||
expect(body2).toMatchSnapshot();
|
||||
jest.resetModules();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 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 { act, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { LayoutSwitcher } from "../../../../../src/components/views/settings/LayoutSwitcher";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
|
||||
describe("<LayoutSwitcher />", () => {
|
||||
const matrixClient = stubClient();
|
||||
const profileInfo = {
|
||||
displayname: "Alice",
|
||||
};
|
||||
|
||||
async function renderLayoutSwitcher() {
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<LayoutSwitcher />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
// Wait for the profile info to be displayed in the event tile preview
|
||||
// Also avoid act warning
|
||||
await waitFor(() => expect(screen.getAllByText(profileInfo.displayname).length).toBe(3));
|
||||
return renderResult;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
mocked(matrixClient).getProfileInfo.mockResolvedValue(profileInfo);
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
const { asFragment } = await renderLayoutSwitcher();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("layout selection", () => {
|
||||
it("should display the modern layout", async () => {
|
||||
await renderLayoutSwitcher();
|
||||
expect(screen.getByRole("radio", { name: "Modern" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the layout when selected", async () => {
|
||||
await renderLayoutSwitcher();
|
||||
act(() => screen.getByRole("radio", { name: "Message bubbles" }).click());
|
||||
|
||||
expect(screen.getByRole("radio", { name: "Message bubbles" })).toBeChecked();
|
||||
await waitFor(() => expect(SettingsStore.getValue<boolean>("layout")).toBe(Layout.Bubble));
|
||||
});
|
||||
});
|
||||
|
||||
describe("compact layout", () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, false);
|
||||
});
|
||||
|
||||
it("should be enabled", async () => {
|
||||
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
|
||||
await renderLayoutSwitcher();
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Show compact text and messages" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the setting when toggled", async () => {
|
||||
await renderLayoutSwitcher();
|
||||
act(() => screen.getByRole("checkbox", { name: "Show compact text and messages" }).click());
|
||||
|
||||
await waitFor(() => expect(SettingsStore.getValue<boolean>("useCompactLayout")).toBe(true));
|
||||
});
|
||||
|
||||
it("should be disabled when the modern layout is not enabled", async () => {
|
||||
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await renderLayoutSwitcher();
|
||||
expect(screen.getByRole("checkbox", { name: "Show compact text and messages" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
930
test/unit-tests/components/views/settings/Notifications-test.tsx
Normal file
930
test/unit-tests/components/views/settings/Notifications-test.tsx
Normal file
@@ -0,0 +1,930 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 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 {
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
RuleId,
|
||||
IPusher,
|
||||
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
PushRuleActionName,
|
||||
TweakName,
|
||||
ConditionKind,
|
||||
IPushRuleCondition,
|
||||
PushRuleKind,
|
||||
IThreepid,
|
||||
ThreepidMedium,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
getByTestId,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import Notifications from "../../../../../src/components/views/settings/Notifications";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { StandardActions } from "../../../../../src/notifications/StandardActions";
|
||||
import {
|
||||
clearAllModals,
|
||||
getMockClientWithEventEmitter,
|
||||
mkMessage,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
|
||||
// don't pollute test output with error logs from mock rejections
|
||||
jest.mock("matrix-js-sdk/src/logger");
|
||||
|
||||
// Avoid indirectly importing any eagerly created stores that would require extra setup
|
||||
jest.mock("../../../../../src/Notifier");
|
||||
|
||||
const masterRule: IPushRule = {
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
conditions: [],
|
||||
default: true,
|
||||
enabled: false,
|
||||
rule_id: RuleId.Master,
|
||||
};
|
||||
const oneToOneRule: IPushRule = {
|
||||
conditions: [
|
||||
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" },
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
const encryptedOneToOneRule: IPushRule = {
|
||||
conditions: [
|
||||
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" },
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
{ set_tweak: TweakName.Highlight, value: false },
|
||||
],
|
||||
rule_id: ".m.rule.encrypted_room_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
const groupRule = {
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" }],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
{ set_tweak: TweakName.Highlight, value: false },
|
||||
],
|
||||
rule_id: ".m.rule.message",
|
||||
default: true,
|
||||
enabled: true,
|
||||
};
|
||||
const encryptedGroupRule: IPushRule = {
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" }],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".m.rule.encrypted",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const bananaRule = {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||
pattern: "banana",
|
||||
rule_id: "banana",
|
||||
default: false,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const pushRules: IPushRules = {
|
||||
global: {
|
||||
underride: [
|
||||
{
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.call.invite" }],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "ring" },
|
||||
{ set_tweak: TweakName.Highlight, value: false },
|
||||
],
|
||||
rule_id: ".m.rule.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
oneToOneRule,
|
||||
encryptedOneToOneRule,
|
||||
groupRule,
|
||||
encryptedGroupRule,
|
||||
{
|
||||
conditions: [
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "im.vector.modular.widgets" },
|
||||
{ kind: ConditionKind.EventMatch, key: "content.type", pattern: "jitsi" },
|
||||
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "*" },
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||
rule_id: ".im.vector.jitsi",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
sender: [],
|
||||
room: [
|
||||
{
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: "!zJPyWqpMorfCcWObge:matrix.org",
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
content: [
|
||||
bananaRule,
|
||||
{
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
{ set_tweak: TweakName.Highlight },
|
||||
],
|
||||
pattern: "kadev1",
|
||||
rule_id: ".m.rule.contains_user_name",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
conditions: [],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".m.rule.master",
|
||||
default: true,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "content.msgtype", pattern: "m.notice" }],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".m.rule.suppress_notices",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" },
|
||||
{ kind: ConditionKind.EventMatch, key: "content.membership", pattern: "invite" },
|
||||
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "@kadev1:matrix.org" },
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
{ set_tweak: TweakName.Highlight, value: false },
|
||||
],
|
||||
rule_id: ".m.rule.invite_for_me",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" }],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".m.rule.member_event",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [{ kind: "contains_display_name" }],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
{ set_tweak: TweakName.Highlight },
|
||||
],
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{ kind: ConditionKind.EventMatch, key: "content.body", pattern: "@room" },
|
||||
{ kind: "sender_notification_permission", key: "room" },
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||
rule_id: ".m.rule.roomnotif",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.tombstone" },
|
||||
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "" },
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||
rule_id: ".m.rule.tombstone",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.reaction" }],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".m.rule.reaction",
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
device: {},
|
||||
} as IPushRules;
|
||||
|
||||
const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve));
|
||||
|
||||
describe("<Notifications />", () => {
|
||||
const getComponent = () => render(<Notifications />);
|
||||
|
||||
// get component, wait for async data and force a render
|
||||
const getComponentAndWait = async () => {
|
||||
const component = getComponent();
|
||||
await waitForElementToBeRemoved(() => component.queryAllByRole("progressbar"));
|
||||
return component;
|
||||
};
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
getPushRules: jest.fn(),
|
||||
getPushers: jest.fn(),
|
||||
getThreePids: jest.fn(),
|
||||
setPusher: jest.fn(),
|
||||
removePusher: jest.fn(),
|
||||
setPushRuleEnabled: jest.fn(),
|
||||
setPushRuleActions: jest.fn(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getAccountData: jest.fn().mockImplementation((eventType) => {
|
||||
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
||||
return new MatrixEvent({
|
||||
type: eventType,
|
||||
content: {
|
||||
is_silenced: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
setAccountData: jest.fn(),
|
||||
sendReadReceipt: jest.fn(),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
addPushRule: jest.fn().mockResolvedValue({}),
|
||||
deletePushRule: jest.fn().mockResolvedValue({}),
|
||||
});
|
||||
mockClient.getPushRules.mockResolvedValue(pushRules);
|
||||
|
||||
beforeEach(async () => {
|
||||
let i = 0;
|
||||
mocked(randomString).mockImplementation(() => {
|
||||
return "testid_" + i++;
|
||||
});
|
||||
|
||||
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
|
||||
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
|
||||
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
|
||||
mockClient.setPusher.mockReset().mockResolvedValue({});
|
||||
mockClient.removePusher.mockClear().mockResolvedValue({});
|
||||
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
||||
mockClient.pushRules = pushRules;
|
||||
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
|
||||
mockClient.addPushRule.mockClear();
|
||||
mockClient.deletePushRule.mockClear();
|
||||
|
||||
userEvent.setup();
|
||||
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("renders spinner while loading", async () => {
|
||||
getComponent();
|
||||
expect(screen.getByTestId("spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders error message when fetching push rules fails", async () => {
|
||||
mockClient.getPushRules.mockRejectedValue({});
|
||||
await getComponentAndWait();
|
||||
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
||||
});
|
||||
it("renders error message when fetching pushers fails", async () => {
|
||||
mockClient.getPushers.mockRejectedValue({});
|
||||
await getComponentAndWait();
|
||||
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
||||
});
|
||||
it("renders error message when fetching threepids fails", async () => {
|
||||
mockClient.getThreePids.mockRejectedValue({});
|
||||
await getComponentAndWait();
|
||||
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("main notification switches", () => {
|
||||
it("renders only enable notifications switch when notifications are disabled", async () => {
|
||||
const disableNotificationsPushRules = {
|
||||
global: {
|
||||
...pushRules.global,
|
||||
override: [{ ...masterRule, enabled: true }],
|
||||
},
|
||||
} as unknown as IPushRules;
|
||||
mockClient.getPushRules.mockClear().mockResolvedValue(disableNotificationsPushRules);
|
||||
const { container } = await getComponentAndWait();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders switches correctly", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
expect(screen.getByTestId("notif-master-switch")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-device-switch")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-setting-notificationsEnabled")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-setting-notificationBodyEnabled")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-setting-audioNotificationsEnabled")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("email switches", () => {
|
||||
const testEmail = "tester@test.com";
|
||||
beforeEach(() => {
|
||||
mockClient.getThreePids.mockResolvedValue({
|
||||
threepids: [
|
||||
// should render switch bc pushKey and address match
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: testEmail,
|
||||
} as unknown as IThreepid,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders email switches correctly when email 3pids exist", async () => {
|
||||
await getComponentAndWait();
|
||||
expect(screen.getByTestId("notif-email-switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders email switches correctly when notifications are on for email", async () => {
|
||||
mockClient.getPushers.mockResolvedValue({
|
||||
pushers: [{ kind: "email", pushkey: testEmail } as unknown as IPusher],
|
||||
});
|
||||
await getComponentAndWait();
|
||||
|
||||
const emailSwitch = screen.getByTestId("notif-email-switch");
|
||||
expect(emailSwitch.querySelector('[aria-checked="true"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enables email notification when toggling on", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]')!;
|
||||
fireEvent.click(emailToggle);
|
||||
|
||||
expect(mockClient.setPusher).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: "email",
|
||||
app_id: "m.email",
|
||||
pushkey: testEmail,
|
||||
app_display_name: "Email Notifications",
|
||||
device_display_name: testEmail,
|
||||
append: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("displays error when pusher update fails", async () => {
|
||||
mockClient.setPusher.mockRejectedValue({});
|
||||
await getComponentAndWait();
|
||||
|
||||
const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]')!;
|
||||
fireEvent.click(emailToggle);
|
||||
|
||||
// force render
|
||||
await flushPromises();
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
expect(
|
||||
within(dialog).getByText("An error occurred whilst saving your notification preferences."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// dismiss the dialog
|
||||
fireEvent.click(within(dialog).getByText("OK"));
|
||||
expect(screen.getByTestId("error-message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enables email notification when toggling off", async () => {
|
||||
const testPusher = {
|
||||
kind: "email",
|
||||
pushkey: "tester@test.com",
|
||||
app_id: "testtest",
|
||||
} as unknown as IPusher;
|
||||
mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] });
|
||||
await getComponentAndWait();
|
||||
|
||||
const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]')!;
|
||||
fireEvent.click(emailToggle);
|
||||
|
||||
expect(mockClient.removePusher).toHaveBeenCalledWith(testPusher.pushkey, testPusher.app_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles master switch correctly", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
// master switch is on
|
||||
expect(screen.getByLabelText("Enable notifications for this account")).toBeChecked();
|
||||
fireEvent.click(screen.getByLabelText("Enable notifications for this account"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "override", ".m.rule.master", true);
|
||||
});
|
||||
|
||||
it("toggles and sets settings correctly", async () => {
|
||||
await getComponentAndWait();
|
||||
let audioNotifsToggle!: HTMLDivElement;
|
||||
|
||||
const update = () => {
|
||||
audioNotifsToggle = screen
|
||||
.getByTestId("notif-setting-audioNotificationsEnabled")
|
||||
.querySelector('div[role="switch"]')!;
|
||||
};
|
||||
update();
|
||||
|
||||
expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("true");
|
||||
expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true);
|
||||
|
||||
fireEvent.click(audioNotifsToggle);
|
||||
update();
|
||||
|
||||
expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("false");
|
||||
expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("individual notification level settings", () => {
|
||||
it("renders categories correctly", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
expect(screen.getByTestId("notif-section-vector_global")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-section-vector_mentions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("notif-section-vector_other")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders radios correctly", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
|
||||
const globalSection = screen.getByTestId(`notif-section-${section}`);
|
||||
// 4 notification rules with class 'global'
|
||||
expect(globalSection.querySelectorAll("fieldset").length).toEqual(4);
|
||||
// oneToOneRule is set to 'on'
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
expect(oneToOneRuleElement.querySelector("[aria-label='On']")).toBeInTheDocument();
|
||||
// encryptedOneToOneRule is set to 'loud'
|
||||
const encryptedOneToOneElement = screen.getByTestId(section + encryptedOneToOneRule.rule_id);
|
||||
expect(encryptedOneToOneElement.querySelector("[aria-label='Noisy']")).toBeInTheDocument();
|
||||
// encryptedGroupRule is set to 'off'
|
||||
const encryptedGroupElement = screen.getByTestId(section + encryptedGroupRule.rule_id);
|
||||
expect(encryptedGroupElement.querySelector("[aria-label='Off']")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates notification level when changed", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
|
||||
// oneToOneRule is set to 'on'
|
||||
// and is kind: 'underride'
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
|
||||
await act(async () => {
|
||||
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||
fireEvent.click(offToggle);
|
||||
});
|
||||
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
oneToOneRule.rule_id,
|
||||
true,
|
||||
);
|
||||
|
||||
// actions for '.m.rule.room_one_to_one' state is ACTION_DONT_NOTIFY
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
oneToOneRule.rule_id,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds an error message when updating notification level fails", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
|
||||
const error = new Error("oups");
|
||||
mockClient.setPushRuleEnabled.mockRejectedValue(error);
|
||||
|
||||
// oneToOneRule is set to 'on'
|
||||
// and is kind: 'underride'
|
||||
const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
|
||||
await act(() => {
|
||||
fireEvent.click(offToggle);
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// error message attached to oneToOne rule
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
// old value still shown as selected
|
||||
expect(within(oneToOneRuleElement).getByLabelText("On")).toBeChecked();
|
||||
expect(
|
||||
within(oneToOneRuleElement).getByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears error message for notification rule on retry", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
|
||||
const error = new Error("oups");
|
||||
mockClient.setPushRuleEnabled.mockRejectedValueOnce(error).mockResolvedValue({});
|
||||
|
||||
// oneToOneRule is set to 'on'
|
||||
// and is kind: 'underride'
|
||||
const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
|
||||
await act(() => {
|
||||
fireEvent.click(offToggle);
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// error message attached to oneToOne rule
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
expect(
|
||||
within(oneToOneRuleElement).getByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// retry
|
||||
fireEvent.click(offToggle);
|
||||
|
||||
// error removed as soon as we start request
|
||||
expect(
|
||||
within(oneToOneRuleElement).queryByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// no error after successful change
|
||||
expect(
|
||||
within(oneToOneRuleElement).queryByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("synced rules", () => {
|
||||
const pollStartOneToOne = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.RoomMemberCount,
|
||||
is: "2",
|
||||
} as IPushRuleCondition<ConditionKind.RoomMemberCount>,
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.start",
|
||||
} as IPushRuleCondition<ConditionKind.EventMatch>,
|
||||
],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
const pollStartGroup = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.start",
|
||||
},
|
||||
],
|
||||
actions: [PushRuleActionName.Notify],
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_start",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
const pollEndOneToOne = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.RoomMemberCount,
|
||||
is: "2",
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.end",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Highlight, value: false },
|
||||
{ set_tweak: TweakName.Sound, value: "default" },
|
||||
],
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const setPushRuleMock = (rules: IPushRule[] = []): void => {
|
||||
const combinedRules = {
|
||||
...pushRules,
|
||||
global: {
|
||||
...pushRules.global,
|
||||
underride: [...pushRules.global.underride!, ...rules],
|
||||
},
|
||||
};
|
||||
mockClient.getPushRules.mockClear().mockResolvedValue(combinedRules);
|
||||
mockClient.pushRules = combinedRules;
|
||||
};
|
||||
|
||||
// ".m.rule.room_one_to_one" and ".m.rule.message" have synced rules
|
||||
it("succeeds when no synced rules exist for user", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
|
||||
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||
fireEvent.click(offToggle);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// didnt attempt to update any non-existant rules
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||
|
||||
// no error
|
||||
expect(
|
||||
within(oneToOneRuleElement).queryByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates synced rules when they exist for user", async () => {
|
||||
setPushRuleMock([pollStartOneToOne, pollStartGroup]);
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
|
||||
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||
fireEvent.click(offToggle);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// updated synced rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
oneToOneRule.rule_id,
|
||||
[PushRuleActionName.DontNotify],
|
||||
);
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollStartOneToOne.rule_id,
|
||||
[PushRuleActionName.DontNotify],
|
||||
);
|
||||
// only called for parent rule and one existing synced rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||||
|
||||
// no error
|
||||
expect(
|
||||
within(oneToOneRuleElement).queryByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not update synced rules when main rule update fails", async () => {
|
||||
setPushRuleMock([pollStartOneToOne]);
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
// have main rule update fail
|
||||
mockClient.setPushRuleActions.mockRejectedValue("oups");
|
||||
|
||||
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||
await act(() => {
|
||||
fireEvent.click(offToggle);
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
oneToOneRule.rule_id,
|
||||
[PushRuleActionName.DontNotify],
|
||||
);
|
||||
// only called for parent rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
within(oneToOneRuleElement).getByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets the UI toggle to rule value when no synced rule exist for the user", async () => {
|
||||
setPushRuleMock([]);
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
|
||||
// loudest state of synced rules should be the toggle value
|
||||
expect(oneToOneRuleElement.querySelector('input[aria-label="On"]')).toBeChecked();
|
||||
});
|
||||
|
||||
it("sets the UI toggle to the loudest synced rule value", async () => {
|
||||
// oneToOneRule is set to 'On'
|
||||
// pollEndOneToOne is set to 'Loud'
|
||||
setPushRuleMock([pollStartOneToOne, pollEndOneToOne]);
|
||||
await getComponentAndWait();
|
||||
const section = "vector_global";
|
||||
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||
|
||||
// loudest state of synced rules should be the toggle value
|
||||
expect(oneToOneRuleElement.querySelector('input[aria-label="Noisy"]')).toBeChecked();
|
||||
|
||||
const onToggle = oneToOneRuleElement.querySelector('input[aria-label="On"]')!;
|
||||
fireEvent.click(onToggle);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// called for all 3 rules
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(3);
|
||||
const expectedActions = [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }];
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
oneToOneRule.rule_id,
|
||||
expectedActions,
|
||||
);
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollStartOneToOne.rule_id,
|
||||
expectedActions,
|
||||
);
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollEndOneToOne.rule_id,
|
||||
expectedActions,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("keywords", () => {
|
||||
// keywords rule is not a real rule, but controls actions on keywords content rules
|
||||
const keywordsRuleId = "_keywords";
|
||||
it("updates individual keywords content rules when keywords rule is toggled", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_mentions";
|
||||
|
||||
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));
|
||||
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "content", bananaRule.rule_id, false);
|
||||
|
||||
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Noisy"));
|
||||
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"content",
|
||||
bananaRule.rule_id,
|
||||
StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders an error when updating keywords fails", async () => {
|
||||
await getComponentAndWait();
|
||||
const section = "vector_mentions";
|
||||
|
||||
mockClient.setPushRuleEnabled.mockRejectedValueOnce("oups");
|
||||
|
||||
await act(() => {
|
||||
fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
const rule = screen.getByTestId(section + keywordsRuleId);
|
||||
|
||||
expect(
|
||||
within(rule).getByText(
|
||||
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("adds a new keyword", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||
expect(screen.getByLabelText("Keyword")).toHaveValue("jest");
|
||||
|
||||
fireEvent.click(screen.getByText("Add"));
|
||||
|
||||
expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: "highlight", value: false }],
|
||||
pattern: "jest",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds a new keyword with same actions as existing rules when keywords rule is off", async () => {
|
||||
const offContentRule = {
|
||||
...bananaRule,
|
||||
enabled: false,
|
||||
actions: [PushRuleActionName.Notify],
|
||||
};
|
||||
const pushRulesWithContentOff = {
|
||||
global: {
|
||||
...pushRules.global,
|
||||
content: [offContentRule],
|
||||
},
|
||||
};
|
||||
mockClient.pushRules = pushRulesWithContentOff;
|
||||
mockClient.getPushRules.mockClear().mockResolvedValue(pushRulesWithContentOff);
|
||||
|
||||
await getComponentAndWait();
|
||||
|
||||
const keywords = screen.getByTestId("vector_mentions_keywords");
|
||||
|
||||
expect(within(keywords).getByLabelText("Off")).toBeChecked();
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||
expect(screen.getByLabelText("Keyword")).toHaveValue("jest");
|
||||
|
||||
fireEvent.click(screen.getByText("Add"));
|
||||
|
||||
expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||
pattern: "jest",
|
||||
});
|
||||
});
|
||||
|
||||
it("removes keyword", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Keyword"), "jest");
|
||||
|
||||
const keyword = screen.getByText("banana");
|
||||
|
||||
fireEvent.click(within(keyword.parentElement!).getByLabelText("Remove"));
|
||||
|
||||
expect(mockClient.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "banana");
|
||||
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear all notifications", () => {
|
||||
it("clears all notifications", async () => {
|
||||
const room = new Room("room123", mockClient, "@alice:example.org");
|
||||
mockClient.getRooms.mockReset().mockReturnValue([room]);
|
||||
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: "room123",
|
||||
user: "@alice:example.org",
|
||||
ts: 1,
|
||||
});
|
||||
await room.addLiveEvents([message]);
|
||||
|
||||
const { container } = await getComponentAndWait();
|
||||
const clearNotificationEl = getByTestId(container, "clear-notifications");
|
||||
|
||||
fireEvent.click(clearNotificationEl);
|
||||
|
||||
expect(clearNotificationEl.className).toContain("mx_AccessibleButton_disabled");
|
||||
await waitFor(() => expect(clearNotificationEl.className).not.toContain("mx_AccessibleButton_disabled"));
|
||||
expect(mockClient.sendReadReceipt).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 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, screen } from "jest-matrix-react";
|
||||
import React, { ComponentProps } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { PowerLevelSelector } from "../../../../../src/components/views/settings/PowerLevelSelector";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
describe("PowerLevelSelector", () => {
|
||||
const matrixClient = stubClient();
|
||||
|
||||
const currentUser = matrixClient.getUserId()!;
|
||||
const userLevels = {
|
||||
[currentUser]: 100,
|
||||
"@alice:server.org": 50,
|
||||
"@bob:server.org": 0,
|
||||
};
|
||||
|
||||
const renderPLS = (props: Partial<ComponentProps<typeof PowerLevelSelector>>) =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<PowerLevelSelector
|
||||
userLevels={userLevels}
|
||||
canChangeLevels={true}
|
||||
currentUserLevel={userLevels[currentUser]}
|
||||
title="title"
|
||||
// filter nothing by default
|
||||
filter={() => true}
|
||||
onClick={jest.fn()}
|
||||
{...props}
|
||||
>
|
||||
empty label
|
||||
</PowerLevelSelector>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
it("should render", () => {
|
||||
renderPLS({});
|
||||
expect(screen.getByRole("group")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display only the current user", async () => {
|
||||
// Display only the current user
|
||||
renderPLS({ filter: (user) => user === currentUser });
|
||||
|
||||
// Only alice should be displayed
|
||||
const userSelects = screen.getAllByRole("combobox");
|
||||
expect(userSelects).toHaveLength(1);
|
||||
expect(userSelects[0]).toHaveAccessibleName(currentUser);
|
||||
|
||||
expect(screen.getByRole("group")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should be able to change the power level of the current user", async () => {
|
||||
const onClick = jest.fn();
|
||||
renderPLS({ onClick });
|
||||
|
||||
// Until the power level is changed, the apply button should be disabled
|
||||
// compound button is using aria-disabled instead of the disabled attribute, we can't toBeDisabled on it
|
||||
expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const select = screen.getByRole("combobox", { name: currentUser });
|
||||
// Sanity check
|
||||
expect(select).toHaveValue("100");
|
||||
|
||||
// Change current user power level to 50
|
||||
await userEvent.selectOptions(select, "50");
|
||||
expect(select).toHaveValue("50");
|
||||
// After the user level changes, the apply button should be enabled
|
||||
expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "false");
|
||||
|
||||
// Click on Apply should call onClick with the new power level
|
||||
await userEvent.click(screen.getByRole("button", { name: "Apply" }));
|
||||
expect(onClick).toHaveBeenCalledWith(50, currentUser);
|
||||
});
|
||||
|
||||
it("should not be able to change the power level if `canChangeLevels` is false", async () => {
|
||||
renderPLS({ canChangeLevels: false });
|
||||
|
||||
// The selects should be disabled
|
||||
const userSelects = screen.getAllByRole("combobox");
|
||||
userSelects.forEach((select) => expect(select).toBeDisabled());
|
||||
});
|
||||
|
||||
it("should be able to change only the level of someone with a lower level", async () => {
|
||||
const userLevels = {
|
||||
[currentUser]: 50,
|
||||
"@alice:server.org": 100,
|
||||
};
|
||||
renderPLS({ userLevels });
|
||||
|
||||
expect(screen.getByRole("combobox", { name: currentUser })).toBeEnabled();
|
||||
expect(screen.getByRole("combobox", { name: "@alice:server.org" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should display the children if there is no user to display", async () => {
|
||||
// No user to display
|
||||
renderPLS({ filter: () => false });
|
||||
|
||||
expect(screen.getByText("empty label")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
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, within } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import SecureBackupPanel from "../../../../../src/components/views/settings/SecureBackupPanel";
|
||||
import { accessSecretStorage } from "../../../../../src/SecurityManager";
|
||||
|
||||
jest.mock("../../../../../src/SecurityManager", () => ({
|
||||
accessSecretStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<SecureBackupPanel />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
});
|
||||
|
||||
const getComponent = () => render(<SecureBackupPanel />);
|
||||
|
||||
beforeEach(() => {
|
||||
client.getKeyBackupVersion.mockResolvedValue({
|
||||
version: "1",
|
||||
algorithm: "test",
|
||||
auth_data: {
|
||||
public_key: "1234",
|
||||
},
|
||||
});
|
||||
Object.assign(client.getCrypto()!, {
|
||||
isKeyBackupTrusted: jest.fn().mockResolvedValue({
|
||||
trusted: false,
|
||||
matchesDecryptionKey: false,
|
||||
}),
|
||||
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
||||
deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
|
||||
client.getKeyBackupVersion.mockClear();
|
||||
|
||||
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||
});
|
||||
|
||||
it("displays a loader while checking keybackup", async () => {
|
||||
getComponent();
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
await flushPromises();
|
||||
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles error fetching backup", async () => {
|
||||
// getKeyBackupVersion can fail for various reasons
|
||||
client.getKeyBackupVersion.mockImplementation(async () => {
|
||||
throw new Error("beep beep");
|
||||
});
|
||||
const renderResult = getComponent();
|
||||
await renderResult.findByText("Unable to load key backup status");
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("handles absence of backup", async () => {
|
||||
client.getKeyBackupVersion.mockResolvedValue(null);
|
||||
getComponent();
|
||||
// flush getKeyBackupVersion promise
|
||||
await flushPromises();
|
||||
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("suggests connecting session to key backup when backup exists", async () => {
|
||||
const { container } = getComponent();
|
||||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("displays when session is connected to key backup", async () => {
|
||||
mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
getComponent();
|
||||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("✅ This session is backing up your keys.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("asks for confirmation before deleting a backup", async () => {
|
||||
getComponent();
|
||||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(screen.getByText("Delete Backup"));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(dialog).getByText("Cancel"));
|
||||
|
||||
expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes backup after confirmation", async () => {
|
||||
client.getKeyBackupVersion
|
||||
.mockResolvedValueOnce({
|
||||
version: "1",
|
||||
algorithm: "test",
|
||||
auth_data: {
|
||||
public_key: "1234",
|
||||
},
|
||||
})
|
||||
.mockResolvedValue(null);
|
||||
getComponent();
|
||||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(screen.getByText("Delete Backup"));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(dialog).getByTestId("dialog-primary-button"));
|
||||
|
||||
expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1");
|
||||
|
||||
// delete request
|
||||
await flushPromises();
|
||||
// refresh backup info
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("resets secret storage", async () => {
|
||||
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(true);
|
||||
getComponent();
|
||||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
client.getKeyBackupVersion.mockClear();
|
||||
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByText("Reset"));
|
||||
|
||||
// enter loading state
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
await flushPromises();
|
||||
|
||||
// backup status refreshed
|
||||
expect(client.getKeyBackupVersion).toHaveBeenCalled();
|
||||
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
flushPromises,
|
||||
} from "../../../../test-utils";
|
||||
import SetIntegrationManager from "../../../../../src/components/views/settings/SetIntegrationManager";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
|
||||
describe("SetIntegrationManager", () => {
|
||||
const userId = "@alice:server.org";
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
getCapabilities: jest.fn(),
|
||||
getThreePids: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
deleteThreePid: jest.fn(),
|
||||
});
|
||||
|
||||
let stores: SdkContextClass;
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SDKContext.Provider value={stores}>
|
||||
<SetIntegrationManager />
|
||||
</SDKContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
it("should not render manage integrations section when widgets feature is disabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settingName !== UIFeature.Widgets);
|
||||
render(getComponent());
|
||||
|
||||
expect(screen.queryByTestId("mx_SetIntegrationManager")).not.toBeInTheDocument();
|
||||
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Widgets);
|
||||
});
|
||||
it("should render manage integrations sections", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settingName === UIFeature.Widgets);
|
||||
|
||||
render(getComponent());
|
||||
|
||||
expect(screen.getByTestId("mx_SetIntegrationManager")).toMatchSnapshot();
|
||||
});
|
||||
it("should update integrations provisioning on toggle", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settingName === UIFeature.Widgets);
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
|
||||
render(getComponent());
|
||||
|
||||
const integrationSection = screen.getByTestId("mx_SetIntegrationManager");
|
||||
fireEvent.click(within(integrationSection).getByRole("switch"));
|
||||
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||
"integrationProvisioning",
|
||||
null,
|
||||
SettingLevel.ACCOUNT,
|
||||
true,
|
||||
);
|
||||
expect(within(integrationSection).getByRole("switch")).toBeChecked();
|
||||
});
|
||||
it("handles error when updating setting fails", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settingName === UIFeature.Widgets);
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue").mockRejectedValue("oups");
|
||||
|
||||
render(getComponent());
|
||||
|
||||
const integrationSection = screen.getByTestId("mx_SetIntegrationManager");
|
||||
fireEvent.click(within(integrationSection).getByRole("switch"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith("Error changing integration manager provisioning");
|
||||
expect(logger.error).toHaveBeenCalledWith("oups");
|
||||
await waitFor(() => expect(within(integrationSection).getByRole("switch")).not.toBeChecked());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 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 SettingsFieldset from "../../../../../src/components/views/settings/SettingsFieldset";
|
||||
|
||||
describe("<SettingsFieldset />", () => {
|
||||
const defaultProps = {
|
||||
"legend": "Who can read history?",
|
||||
"children": <div>test</div>,
|
||||
"data-testid": "test",
|
||||
};
|
||||
const getComponent = (props = {}) => {
|
||||
return render(<SettingsFieldset {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
it("renders fieldset without description", () => {
|
||||
expect(getComponent().asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders fieldset with plain text description", () => {
|
||||
const description = "Changes to who can read history.";
|
||||
expect(getComponent({ description }).asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders fieldset with react description", () => {
|
||||
const description = (
|
||||
<>
|
||||
<p>Test</p>
|
||||
<a href="#test">a link</a>
|
||||
</>
|
||||
);
|
||||
expect(getComponent({ description }).asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 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 { act, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { ThemeChoicePanel } from "../../../../../src/components/views/settings/ThemeChoicePanel";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import ThemeWatcher from "../../../../../src/settings/watchers/ThemeWatcher";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
|
||||
jest.mock("../../../../../src/settings/watchers/ThemeWatcher");
|
||||
|
||||
describe("<ThemeChoicePanel />", () => {
|
||||
/**
|
||||
* Enable or disable the system theme
|
||||
* @param enable
|
||||
*/
|
||||
async function enableSystemTheme(enable: boolean) {
|
||||
await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme
|
||||
* @param theme
|
||||
*/
|
||||
async function setTheme(theme: string) {
|
||||
await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, theme);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mocked(ThemeWatcher).mockImplementation(() => {
|
||||
return {
|
||||
isSystemThemeSupported: jest.fn().mockReturnValue(true),
|
||||
} as unknown as MockedObject<ThemeWatcher>;
|
||||
});
|
||||
|
||||
await enableSystemTheme(false);
|
||||
await setTheme("light");
|
||||
});
|
||||
|
||||
it("renders the theme choice UI", () => {
|
||||
const { asFragment } = render(<ThemeChoicePanel />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("theme selection", () => {
|
||||
describe("system theme", () => {
|
||||
it("should disable Match system theme", async () => {
|
||||
render(<ThemeChoicePanel />);
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should enable Match system theme", async () => {
|
||||
await enableSystemTheme(true);
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the system theme when clicked", async () => {
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
act(() => screen.getByRole("checkbox", { name: "Match system theme" }).click());
|
||||
|
||||
// The system theme should be enabled
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked();
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("use_system_theme", null, "device", true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("theme selection", () => {
|
||||
it("should disable theme selection when system theme is enabled", async () => {
|
||||
await enableSystemTheme(true);
|
||||
render(<ThemeChoicePanel />);
|
||||
|
||||
// We expect all the themes to be disabled
|
||||
const themes = screen.getAllByRole("radio");
|
||||
themes.forEach((theme) => {
|
||||
expect(theme).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should enable theme selection when system theme is disabled", async () => {
|
||||
render(<ThemeChoicePanel />);
|
||||
|
||||
// We expect all the themes to be disabled
|
||||
const themes = screen.getAllByRole("radio");
|
||||
themes.forEach((theme) => {
|
||||
expect(theme).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have light theme selected", async () => {
|
||||
render(<ThemeChoicePanel />);
|
||||
|
||||
// We expect the light theme to be selected
|
||||
const lightTheme = screen.getByRole("radio", { name: "Light" });
|
||||
expect(lightTheme).toBeChecked();
|
||||
|
||||
// And the dark theme shouldn't be selected
|
||||
const darkTheme = screen.getByRole("radio", { name: "Dark" });
|
||||
expect(darkTheme).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should switch to dark theme", async () => {
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
|
||||
const darkTheme = screen.getByRole("radio", { name: "Dark" });
|
||||
const lightTheme = screen.getByRole("radio", { name: "Light" });
|
||||
expect(darkTheme).not.toBeChecked();
|
||||
|
||||
// Switch to the dark theme
|
||||
act(() => darkTheme.click());
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("theme", null, "device", "dark");
|
||||
|
||||
// Dark theme is now selected
|
||||
await waitFor(() => expect(darkTheme).toBeChecked());
|
||||
// Light theme is not selected anymore
|
||||
expect(lightTheme).not.toBeChecked();
|
||||
// The setting should be updated
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("theme", null, "device", "dark");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom theme", () => {
|
||||
const aliceTheme = { name: "Alice theme", is_dark: true, colors: {} };
|
||||
const bobTheme = { name: "Bob theme", is_dark: false, colors: {} };
|
||||
|
||||
beforeEach(async () => {
|
||||
await SettingsStore.setValue("feature_custom_themes", null, SettingLevel.DEVICE, true);
|
||||
await SettingsStore.setValue("custom_themes", null, SettingLevel.DEVICE, [aliceTheme]);
|
||||
});
|
||||
|
||||
it("should render the custom theme section", () => {
|
||||
const { asFragment } = render(<ThemeChoicePanel />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should add a custom theme", async () => {
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
// Respond to the theme request
|
||||
fetchMock.get("http://bob.theme", {
|
||||
body: bobTheme,
|
||||
});
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
|
||||
// Add the new custom theme
|
||||
const customThemeInput = screen.getByRole("textbox", { name: "Add custom theme" });
|
||||
await userEvent.type(customThemeInput, "http://bob.theme");
|
||||
screen.getByRole("button", { name: "Add custom theme" }).click();
|
||||
|
||||
// The new custom theme is added to the user's themes
|
||||
await waitFor(() =>
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("custom_themes", null, "account", [
|
||||
aliceTheme,
|
||||
bobTheme,
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should display custom theme", () => {
|
||||
const { asFragment } = render(<ThemeChoicePanel />);
|
||||
|
||||
expect(screen.getByRole("radio", { name: aliceTheme.name })).toBeInTheDocument();
|
||||
expect(screen.getByRole("listitem", { name: aliceTheme.name })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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, { ChangeEvent } from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { MatrixClient, UploadResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
import UserProfileSettings from "../../../../../src/components/views/settings/UserProfileSettings";
|
||||
import { mkStubRoom, stubClient } from "../../../../test-utils";
|
||||
import { ToastContext, ToastRack } from "../../../../../src/contexts/ToastContext";
|
||||
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
|
||||
interface MockedAvatarSettingProps {
|
||||
removeAvatar: () => void;
|
||||
onChange: (file: File) => void;
|
||||
}
|
||||
|
||||
let removeAvatarFn: () => void;
|
||||
let changeAvatarFn: (file: File) => void;
|
||||
|
||||
jest.mock(
|
||||
"../../../../../src/components/views/settings/AvatarSetting",
|
||||
() =>
|
||||
(({ removeAvatar, onChange }) => {
|
||||
removeAvatarFn = removeAvatar;
|
||||
changeAvatarFn = onChange;
|
||||
return <div>Mocked AvatarSetting</div>;
|
||||
}) as React.FC<MockedAvatarSettingProps>,
|
||||
);
|
||||
|
||||
jest.mock("../../../../../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
register: jest.fn(),
|
||||
}));
|
||||
|
||||
let editInPlaceOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
let editInPlaceOnSave: () => void;
|
||||
let editInPlaceOnCancel: () => void;
|
||||
|
||||
interface MockedEditInPlaceProps {
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
jest.mock("@vector-im/compound-web", () => {
|
||||
const compound = jest.requireActual("@vector-im/compound-web");
|
||||
return {
|
||||
__esModule: true,
|
||||
...compound,
|
||||
EditInPlace: (({ onChange, onSave, onCancel, value }) => {
|
||||
editInPlaceOnChange = onChange;
|
||||
editInPlaceOnSave = onSave;
|
||||
editInPlaceOnCancel = onCancel;
|
||||
return <div>Mocked EditInPlace: {value}</div>;
|
||||
}) as React.FC<MockedEditInPlaceProps>,
|
||||
};
|
||||
});
|
||||
|
||||
const renderProfileSettings = (toastRack: Partial<ToastRack>, client: MatrixClient) => {
|
||||
return render(
|
||||
<TooltipProvider>
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings canSetAvatar={true} canSetDisplayName={true} />
|
||||
</ToastContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
</TooltipProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("ProfileSettings", () => {
|
||||
let client: MatrixClient;
|
||||
let toastRack: Partial<ToastRack>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
toastRack = {
|
||||
displayToast: jest.fn().mockReturnValue(jest.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
it("removes avatar", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "avatarMxc", "get").mockReturnValue("mxc://example.org/my-avatar");
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(removeAvatarFn).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
removeAvatarFn();
|
||||
});
|
||||
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("changes avatar", async () => {
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
const returnedMxcUri = "mxc://example.org/my-avatar";
|
||||
mocked(client).uploadContent.mockResolvedValue({ content_uri: returnedMxcUri });
|
||||
|
||||
const fileSentinel = {};
|
||||
await act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(client.uploadContent).toHaveBeenCalledWith(fileSentinel);
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith(returnedMxcUri);
|
||||
});
|
||||
|
||||
it("displays toast while uploading avatar", async () => {
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
const clearToastFn = jest.fn();
|
||||
mocked(toastRack.displayToast!).mockReturnValue(clearToastFn);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
let resolveUploadPromise = (r: UploadResponse) => {};
|
||||
const uploadPromise = new Promise<UploadResponse>((r) => {
|
||||
resolveUploadPromise = r;
|
||||
});
|
||||
mocked(client).uploadContent.mockReturnValue(uploadPromise);
|
||||
|
||||
const fileSentinel = {};
|
||||
const changeAvatarActPromise = act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(toastRack.displayToast).toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
resolveUploadPromise({ content_uri: "bloop" });
|
||||
});
|
||||
await changeAvatarActPromise;
|
||||
|
||||
expect(clearToastFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("changes display name", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnSave).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "The Value" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await editInPlaceOnSave();
|
||||
});
|
||||
|
||||
expect(client.setDisplayName).toHaveBeenCalledWith("The Value");
|
||||
});
|
||||
|
||||
it("displays error if changing display name fails", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
mocked(client).setDisplayName.mockRejectedValue(new Error("Failed to set display name"));
|
||||
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
expect(editInPlaceOnSave).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "Not Alice any more" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await expect(editInPlaceOnSave()).rejects.toEqual(expect.any(Error));
|
||||
});
|
||||
});
|
||||
|
||||
it("resets on cancel", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnChange).toBeDefined();
|
||||
expect(editInPlaceOnCancel).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "Alicia Zattic" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alicia Zattic")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnCancel();
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("signs out directly if no rooms are encrypted", async () => {
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
const signOutButton = await screen.findByText("Sign out");
|
||||
await userEvent.click(signOutButton);
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({ action: "logout" });
|
||||
});
|
||||
|
||||
it("displays confirmation dialog if rooms are encrypted", async () => {
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
const mockRoom = mkStubRoom("!test:room", "Test Room", client);
|
||||
client.getRooms = jest.fn().mockReturnValue([mockRoom]);
|
||||
client.getCrypto = jest.fn().mockReturnValue({
|
||||
isEncryptionEnabledInRoom: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
|
||||
renderProfileSettings(toastRack, client);
|
||||
|
||||
const signOutButton = await screen.findByText("Sign out");
|
||||
await userEvent.click(signOutButton);
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddRemoveThreepids should handle no email addresses 1`] = `
|
||||
<div>
|
||||
<form
|
||||
autocomplete="off"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="email"
|
||||
id="mx_Field_3"
|
||||
label="Email Address"
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_3"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddRemoveThreepids should render email addresses 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AddRemoveThreepids_existing"
|
||||
>
|
||||
<span
|
||||
class="mx_AddRemoveThreepids_existing_address"
|
||||
>
|
||||
alice@nowhere.dummy
|
||||
</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_1"
|
||||
label="Email Address"
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddRemoveThreepids should render phone numbers 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AddRemoveThreepids_existing"
|
||||
>
|
||||
<span
|
||||
class="mx_AddRemoveThreepids_existing_address"
|
||||
>
|
||||
447700900000
|
||||
</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_2"
|
||||
label="Phone Number"
|
||||
placeholder="Phone Number"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ChangePassword /> renders expected fields 1`] = `
|
||||
<DocumentFragment>
|
||||
<form>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Current password"
|
||||
placeholder="Current password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Current password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_PassphraseField"
|
||||
>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
id="mx_Field_2"
|
||||
label="New Password"
|
||||
placeholder="New Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
id="mx_Field_3"
|
||||
label="Confirm password"
|
||||
placeholder="Confirm password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_3"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Change Password
|
||||
</div>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<CrossSigningPanel /> when cross signing is not ready should render when keys are backed up 1`] = `
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Cross-signing private keys:
|
||||
</th>
|
||||
<td>
|
||||
in secret storage
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are backed up 1`] = `
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Cross-signing private keys:
|
||||
</th>
|
||||
<td>
|
||||
in secret storage
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are not backed up 1`] = `
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Cross-signing private keys:
|
||||
</th>
|
||||
<td>
|
||||
not found in storage
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EventIndexPanel /> when event index is initialised renders event index information 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Manage
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<EventIndexPanel /> when event indexing is not supported renders link to download a desktop client 1`] = `
|
||||
<div>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`<EventIndexPanel /> when event indexing is supported but not installed renders link to install seshat 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<span>
|
||||
Element is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Element Desktop with
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md#adding-seshat-for-search-in-e2e-encrypted-rooms"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
search components added
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,150 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FontScalingPanel renders the font scaling UI 1`] = `
|
||||
<DocumentFragment>
|
||||
<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>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`KeyboardShortcut doesn't render + if last 1`] = `
|
||||
<div>
|
||||
<kbd>
|
||||
|
||||
a
|
||||
|
||||
</kbd>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KeyboardShortcut doesn't render same modifier twice 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_KeyboardShortcut"
|
||||
>
|
||||
<kbd>
|
||||
|
||||
Ctrl
|
||||
|
||||
</kbd>
|
||||
+
|
||||
<kbd>
|
||||
|
||||
a
|
||||
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KeyboardShortcut doesn't render same modifier twice 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_KeyboardShortcut"
|
||||
>
|
||||
<kbd>
|
||||
|
||||
Ctrl
|
||||
|
||||
</kbd>
|
||||
+
|
||||
<kbd>
|
||||
|
||||
a
|
||||
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KeyboardShortcut renders alternative key name 1`] = `
|
||||
<div>
|
||||
<kbd>
|
||||
|
||||
Page Down
|
||||
|
||||
</kbd>
|
||||
+
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KeyboardShortcut renders key icon 1`] = `
|
||||
<div>
|
||||
<kbd>
|
||||
|
||||
↓
|
||||
|
||||
</kbd>
|
||||
+
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,456 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<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-0"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-0"
|
||||
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"
|
||||
>
|
||||
Alice
|
||||
</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"
|
||||
>
|
||||
A
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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-1"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-1"
|
||||
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"
|
||||
>
|
||||
Alice
|
||||
</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"
|
||||
>
|
||||
A
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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-2"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-2"
|
||||
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"
|
||||
>
|
||||
Alice
|
||||
</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"
|
||||
>
|
||||
A
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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-3"
|
||||
class="_input_qnvru_32"
|
||||
id="radix-4"
|
||||
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-4"
|
||||
>
|
||||
Show compact text and messages
|
||||
</label>
|
||||
<span
|
||||
class="_message_dgy0u_98 _help-message_dgy0u_104"
|
||||
id="radix-3"
|
||||
>
|
||||
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>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,95 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Notifications /> main notification switches renders only enable notifications switch when notifications are disabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
data-testid="notif-master-switch"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_label"
|
||||
>
|
||||
<div
|
||||
id="mx_LabelledToggleSwitch_testid_0"
|
||||
>
|
||||
Enable notifications for this account
|
||||
</div>
|
||||
<span
|
||||
class="mx_Caption"
|
||||
id="mx_LabelledToggleSwitch_testid_0_caption"
|
||||
>
|
||||
Turn off to disable notifications on all your devices and sessions
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-describedby="mx_LabelledToggleSwitch_testid_0_caption"
|
||||
aria-disabled="false"
|
||||
aria-labelledby="mx_LabelledToggleSwitch_testid_0"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_testid_1"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Show all activity in the room list (dots or number of unread messages)
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-disabled="false"
|
||||
aria-label="Show all activity in the room list (dots or number of unread messages)"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_testid_1"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_testid_2"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Only show notifications in the thread activity centre
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-disabled="false"
|
||||
aria-label="Only show notifications in the thread activity centre"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_testid_2"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,235 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PowerLevelSelector should display only the current user 1`] = `
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
title
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PowerSelector"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
data-testid="power-level-select-element"
|
||||
id="mx_Field_4"
|
||||
label="@userId:matrix.org"
|
||||
placeholder="@userId:matrix.org"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
data-testid="power-level-option-0"
|
||||
value="0"
|
||||
>
|
||||
Default
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-50"
|
||||
value="50"
|
||||
>
|
||||
Moderator
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-100"
|
||||
value="100"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
|
||||
value="SELECT_VALUE_CUSTOM"
|
||||
>
|
||||
Custom level
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_4"
|
||||
>
|
||||
@userId:matrix.org
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Apply"
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton mx_PowerLevelSelector_Button"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`PowerLevelSelector should render 1`] = `
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
title
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PowerSelector"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
data-testid="power-level-select-element"
|
||||
id="mx_Field_1"
|
||||
label="@bob:server.org"
|
||||
placeholder="@bob:server.org"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
data-testid="power-level-option-0"
|
||||
value="0"
|
||||
>
|
||||
Default
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-50"
|
||||
value="50"
|
||||
>
|
||||
Moderator
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-100"
|
||||
value="100"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
|
||||
value="SELECT_VALUE_CUSTOM"
|
||||
>
|
||||
Custom level
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
@bob:server.org
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PowerSelector"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
data-testid="power-level-select-element"
|
||||
id="mx_Field_2"
|
||||
label="@alice:server.org"
|
||||
placeholder="@alice:server.org"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
data-testid="power-level-option-0"
|
||||
value="0"
|
||||
>
|
||||
Default
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-50"
|
||||
value="50"
|
||||
>
|
||||
Moderator
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-100"
|
||||
value="100"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
|
||||
value="SELECT_VALUE_CUSTOM"
|
||||
>
|
||||
Custom level
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
@alice:server.org
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PowerSelector"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
data-testid="power-level-select-element"
|
||||
id="mx_Field_3"
|
||||
label="@userId:matrix.org"
|
||||
placeholder="@userId:matrix.org"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
data-testid="power-level-option-0"
|
||||
value="0"
|
||||
>
|
||||
Default
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-50"
|
||||
value="50"
|
||||
>
|
||||
Moderator
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-100"
|
||||
value="100"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
|
||||
value="SELECT_VALUE_CUSTOM"
|
||||
>
|
||||
Custom level
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_3"
|
||||
>
|
||||
@userId:matrix.org
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Apply"
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton mx_PowerLevelSelector_Button"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
@@ -0,0 +1,193 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SecureBackupPanel /> handles error fetching backup 1`] = `
|
||||
<div>
|
||||
<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_SettingsSubsection_text"
|
||||
>
|
||||
Unable to load key backup status
|
||||
</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>
|
||||
`;
|
||||
|
||||
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
|
||||
<div>
|
||||
<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_SettingsSubsection_text"
|
||||
>
|
||||
<span>
|
||||
This session is
|
||||
<strong>
|
||||
not backing up your keys
|
||||
</strong>
|
||||
, but you do have an existing backup you can restore from and add to going forward.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
|
||||
</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>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Latest backup version on server:
|
||||
</th>
|
||||
<td>
|
||||
1
|
||||
(
|
||||
Algorithm:
|
||||
|
||||
<code>
|
||||
test
|
||||
</code>
|
||||
)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Active backup version:
|
||||
</th>
|
||||
<td>
|
||||
None
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div />
|
||||
</details>
|
||||
<div
|
||||
class="mx_SecureBackupPanel_buttonRow"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Connect this session to Key Backup
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Delete Backup
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,56 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SetIntegrationManager should render manage integrations sections 1`] = `
|
||||
<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="false"
|
||||
aria-disabled="false"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch 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>
|
||||
`;
|
||||
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SettingsFieldset /> renders fieldset with plain text description 1`] = `
|
||||
<DocumentFragment>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
data-testid="test"
|
||||
>
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SettingsFieldset /> renders fieldset with react description 1`] = `
|
||||
<DocumentFragment>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
data-testid="test"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
Who can read history?
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
<a
|
||||
href="#test"
|
||||
>
|
||||
a link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SettingsFieldset /> renders fieldset without description 1`] = `
|
||||
<DocumentFragment>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
data-testid="test"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
Who can read history?
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,724 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
|
||||
<DocumentFragment>
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_qnvru_18"
|
||||
>
|
||||
<input
|
||||
class="_input_qnvru_32"
|
||||
id="radix-48"
|
||||
name="systemTheme"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_qnvru_42"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_dgy0u_46"
|
||||
>
|
||||
<label
|
||||
class="_label_dgy0u_67"
|
||||
for="radix-48"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-49"
|
||||
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-49"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-50"
|
||||
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-50"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-51"
|
||||
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-51"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-52"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="custom-Alice theme"
|
||||
/>
|
||||
<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-52"
|
||||
>
|
||||
Alice theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="mx_ThemeChoicePanel_CustomTheme"
|
||||
>
|
||||
<form
|
||||
class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
|
||||
>
|
||||
<div
|
||||
class="_field_dgy0u_34"
|
||||
>
|
||||
<label
|
||||
class="_label_dgy0u_67"
|
||||
for="radix-54"
|
||||
>
|
||||
Add custom theme
|
||||
</label>
|
||||
<div
|
||||
class="_controls_1h4nb_17"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-53"
|
||||
class="_control_9gon8_18"
|
||||
id="radix-54"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="_message_dgy0u_98 _help-message_dgy0u_104"
|
||||
id="radix-53"
|
||||
>
|
||||
Enter the URL of a custom theme you want to apply.
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul
|
||||
class="mx_ThemeChoicePanel_CustomThemeList"
|
||||
>
|
||||
<li
|
||||
aria-label="Alice theme"
|
||||
class="mx_ThemeChoicePanel_CustomThemeList_theme"
|
||||
>
|
||||
<span
|
||||
class="mx_ThemeChoicePanel_CustomThemeList_name"
|
||||
>
|
||||
Alice theme
|
||||
</span>
|
||||
<button
|
||||
aria-label="Delete"
|
||||
aria-labelledby="floating-ui-24"
|
||||
class="_icon-button_bh2qc_17 _destructive_bh2qc_83"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 5 19V6a.968.968 0 0 1-.713-.287A.968.968 0 0 1 4 5c0-.283.096-.52.287-.713A.968.968 0 0 1 5 4h4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 10 3h4a.97.97 0 0 1 .713.288A.968.968 0 0 1 15 4h4a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 19 6v13c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 17 21H7ZM7 6v13h10V6H7Zm2 10c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 11 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 10 8a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 9v7Zm4 0c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 15 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 14 8a.968.968 0 0 0-.713.287A.967.967 0 0 0 13 9v7Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_144s5_17"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ThemeChoicePanel /> custom theme should render the custom theme section 1`] = `
|
||||
<DocumentFragment>
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_qnvru_18"
|
||||
>
|
||||
<input
|
||||
class="_input_qnvru_32"
|
||||
id="radix-32"
|
||||
name="systemTheme"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_qnvru_42"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_dgy0u_46"
|
||||
>
|
||||
<label
|
||||
class="_label_dgy0u_67"
|
||||
for="radix-32"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-33"
|
||||
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-33"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-34"
|
||||
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-34"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-35"
|
||||
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-35"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-36"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="custom-Alice theme"
|
||||
/>
|
||||
<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-36"
|
||||
>
|
||||
Alice theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="mx_ThemeChoicePanel_CustomTheme"
|
||||
>
|
||||
<form
|
||||
class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
|
||||
>
|
||||
<div
|
||||
class="_field_dgy0u_34"
|
||||
>
|
||||
<label
|
||||
class="_label_dgy0u_67"
|
||||
for="radix-38"
|
||||
>
|
||||
Add custom theme
|
||||
</label>
|
||||
<div
|
||||
class="_controls_1h4nb_17"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-37"
|
||||
class="_control_9gon8_18"
|
||||
id="radix-38"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="_message_dgy0u_98 _help-message_dgy0u_104"
|
||||
id="radix-37"
|
||||
>
|
||||
Enter the URL of a custom theme you want to apply.
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul
|
||||
class="mx_ThemeChoicePanel_CustomThemeList"
|
||||
>
|
||||
<li
|
||||
aria-label="Alice theme"
|
||||
class="mx_ThemeChoicePanel_CustomThemeList_theme"
|
||||
>
|
||||
<span
|
||||
class="mx_ThemeChoicePanel_CustomThemeList_name"
|
||||
>
|
||||
Alice theme
|
||||
</span>
|
||||
<button
|
||||
aria-label="Delete"
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="_icon-button_bh2qc_17 _destructive_bh2qc_83"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 5 19V6a.968.968 0 0 1-.713-.287A.968.968 0 0 1 4 5c0-.283.096-.52.287-.713A.968.968 0 0 1 5 4h4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 10 3h4a.97.97 0 0 1 .713.288A.968.968 0 0 1 15 4h4a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 19 6v13c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 17 21H7ZM7 6v13h10V6H7Zm2 10c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 11 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 10 8a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 9v7Zm4 0c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 15 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 14 8a.968.968 0 0 0-.713.287A.967.967 0 0 0 13 9v7Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_144s5_17"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
|
||||
<DocumentFragment>
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_qnvru_18"
|
||||
>
|
||||
<input
|
||||
class="_input_qnvru_32"
|
||||
id="radix-0"
|
||||
name="systemTheme"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_qnvru_42"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_dgy0u_46"
|
||||
>
|
||||
<label
|
||||
class="_label_dgy0u_67"
|
||||
for="radix-0"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-1"
|
||||
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-1"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-2"
|
||||
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-2"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_dgy0u_52"
|
||||
>
|
||||
<div
|
||||
class="_container_1vw5h_18"
|
||||
>
|
||||
<input
|
||||
class="_input_1vw5h_26"
|
||||
id="radix-3"
|
||||
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-3"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_144s5_17"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 { act, fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import CurrentDeviceSection from "../../../../../../src/components/views/settings/devices/CurrentDeviceSection";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<CurrentDeviceSection />", () => {
|
||||
const deviceId = "alices_device";
|
||||
|
||||
const alicesVerifiedDevice = {
|
||||
device_id: deviceId,
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const alicesUnverifiedDevice = {
|
||||
device_id: deviceId,
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
device: alicesVerifiedDevice,
|
||||
onVerifyCurrentDevice: jest.fn(),
|
||||
onSignOutCurrentDevice: jest.fn(),
|
||||
saveDeviceName: jest.fn(),
|
||||
isLoading: false,
|
||||
isSigningOut: false,
|
||||
otherSessionsCount: 1,
|
||||
setPushNotifications: jest.fn(),
|
||||
};
|
||||
|
||||
const getComponent = (props = {}): React.ReactElement => <CurrentDeviceSection {...defaultProps} {...props} />;
|
||||
|
||||
it("renders spinner while device is loading", () => {
|
||||
const { container } = render(getComponent({ device: undefined, isLoading: true }));
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
});
|
||||
|
||||
it("handles when device is falsy", async () => {
|
||||
const { container } = render(getComponent({ device: undefined }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders device and correct security card when device is verified", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders device and correct security card when device is unverified", () => {
|
||||
const { container } = render(getComponent({ device: alicesUnverifiedDevice }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("displays device details on main tile click", () => {
|
||||
const { getByTestId, container } = render(getComponent({ device: alicesUnverifiedDevice }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId(`device-tile-${alicesUnverifiedDevice.device_id}`));
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId(`device-tile-${alicesUnverifiedDevice.device_id}`));
|
||||
});
|
||||
|
||||
// device details are hidden
|
||||
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeFalsy();
|
||||
});
|
||||
|
||||
it("displays device details on toggle click", () => {
|
||||
const { container, getByTestId } = render(getComponent({ device: alicesUnverifiedDevice }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("current-session-toggle-details"));
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_DeviceDetails")).toMatchSnapshot();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("current-session-toggle-details"));
|
||||
});
|
||||
|
||||
// device details are hidden
|
||||
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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 } from "jest-matrix-react";
|
||||
|
||||
import { DeviceDetailHeading } from "../../../../../../src/components/views/settings/devices/DeviceDetailHeading";
|
||||
import { flushPromisesWithFakeTimers } from "../../../../../test-utils";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("<DeviceDetailHeading />", () => {
|
||||
const device = {
|
||||
device_id: "123",
|
||||
display_name: "My device",
|
||||
isVerified: true,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const defaultProps = {
|
||||
device,
|
||||
saveDeviceName: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => <DeviceDetailHeading {...defaultProps} {...props} />;
|
||||
|
||||
const setInputValue = (getByTestId: RenderResult["getByTestId"], value: string) => {
|
||||
const input = getByTestId("device-rename-input");
|
||||
|
||||
fireEvent.change(input, { target: { value } });
|
||||
};
|
||||
|
||||
it("renders device name", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders device id as fallback when device has no display name", () => {
|
||||
const { getByText } = render(
|
||||
getComponent({
|
||||
device: { ...device, display_name: undefined },
|
||||
}),
|
||||
);
|
||||
expect(getByText(device.device_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays name edit form on rename button click", () => {
|
||||
const { getByTestId, container } = render(getComponent());
|
||||
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("cancelling edit switches back to original display", () => {
|
||||
const { getByTestId, container } = render(getComponent());
|
||||
|
||||
// start editing
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
|
||||
// stop editing
|
||||
fireEvent.click(getByTestId("device-rename-cancel-cta"));
|
||||
|
||||
expect(container.getElementsByClassName("mx_DeviceDetailHeading").length).toBe(1);
|
||||
});
|
||||
|
||||
it("clicking submit updates device name with edited value", () => {
|
||||
const saveDeviceName = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ saveDeviceName }));
|
||||
|
||||
// start editing
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
|
||||
setInputValue(getByTestId, "new device name");
|
||||
|
||||
fireEvent.click(getByTestId("device-rename-submit-cta"));
|
||||
|
||||
expect(saveDeviceName).toHaveBeenCalledWith("new device name");
|
||||
});
|
||||
|
||||
it("disables form while device name is saving", () => {
|
||||
const { getByTestId, container } = render(getComponent());
|
||||
|
||||
// start editing
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
|
||||
setInputValue(getByTestId, "new device name");
|
||||
|
||||
fireEvent.click(getByTestId("device-rename-submit-cta"));
|
||||
|
||||
// buttons disabled
|
||||
expect(getByTestId("device-rename-cancel-cta").getAttribute("aria-disabled")).toEqual("true");
|
||||
expect(getByTestId("device-rename-submit-cta").getAttribute("aria-disabled")).toEqual("true");
|
||||
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
});
|
||||
|
||||
it("toggles out of editing mode when device name is saved successfully", async () => {
|
||||
const { getByTestId, findByTestId } = render(getComponent());
|
||||
|
||||
// start editing
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
setInputValue(getByTestId, "new device name");
|
||||
fireEvent.click(getByTestId("device-rename-submit-cta"));
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// read mode displayed
|
||||
await expect(findByTestId("device-detail-heading")).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays error when device name fails to save", async () => {
|
||||
const saveDeviceName = jest.fn().mockRejectedValueOnce("oups").mockResolvedValue({});
|
||||
const { getByTestId, queryByText, findByText, container } = render(getComponent({ saveDeviceName }));
|
||||
|
||||
// start editing
|
||||
fireEvent.click(getByTestId("device-heading-rename-cta"));
|
||||
setInputValue(getByTestId, "new device name");
|
||||
fireEvent.click(getByTestId("device-rename-submit-cta"));
|
||||
|
||||
// flush promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
// then tick for render
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// error message displayed
|
||||
await expect(findByText("Failed to set session name")).resolves.toBeTruthy();
|
||||
// spinner removed
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy();
|
||||
|
||||
// try again
|
||||
fireEvent.click(getByTestId("device-rename-submit-cta"));
|
||||
|
||||
// error message cleared
|
||||
expect(queryByText("Failed to set display name")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
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, { ComponentProps } from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
import { PUSHER_ENABLED } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DeviceDetails from "../../../../../../src/components/views/settings/devices/DeviceDetails";
|
||||
import { mkPusher } from "../../../../../test-utils/test-utils";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<DeviceDetails />", () => {
|
||||
const baseDevice = {
|
||||
device_id: "my-device",
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const defaultProps: ComponentProps<typeof DeviceDetails> = {
|
||||
device: baseDevice,
|
||||
isSigningOut: false,
|
||||
onSignOutDevice: jest.fn(),
|
||||
saveDeviceName: jest.fn(),
|
||||
setPushNotifications: jest.fn(),
|
||||
supportsMSC3881: true,
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
|
||||
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
jest.useFakeTimers();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.setSystemTime(now);
|
||||
});
|
||||
|
||||
it("renders device without metadata", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders device with metadata", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
display_name: "My Device",
|
||||
last_seen_ip: "123.456.789",
|
||||
last_seen_ts: now - 60000000,
|
||||
appName: "Element Web",
|
||||
client: "Firefox 100",
|
||||
deviceModel: "Iphone X",
|
||||
deviceOperatingSystem: "Windows 95",
|
||||
};
|
||||
const { container } = render(getComponent({ device }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders a verified device", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
isVerified: true,
|
||||
};
|
||||
const { container } = render(getComponent({ device }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("disables sign out button while sign out is pending", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device, isSigningOut: true }));
|
||||
expect(getByTestId("device-detail-sign-out-cta").getAttribute("aria-disabled")).toEqual("true");
|
||||
});
|
||||
|
||||
it("renders the push notification section when a pusher exists", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
const pusher = mkPusher({
|
||||
device_id: device.device_id,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
device,
|
||||
pusher,
|
||||
isSigningOut: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getByTestId("device-detail-push-notification")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides the push notification section when no pusher", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
device,
|
||||
pusher: null,
|
||||
isSigningOut: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => getByTestId("device-detail-push-notification")).toThrow();
|
||||
});
|
||||
|
||||
it("disables the checkbox when there is no server support", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
const pusher = mkPusher({
|
||||
device_id: device.device_id,
|
||||
[PUSHER_ENABLED.name]: false,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
device,
|
||||
pusher,
|
||||
isSigningOut: true,
|
||||
supportsMSC3881: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const checkbox = getByTestId("device-detail-push-notification-checkbox");
|
||||
|
||||
expect(checkbox.getAttribute("aria-disabled")).toEqual("true");
|
||||
expect(checkbox.getAttribute("aria-checked")).toEqual("false");
|
||||
});
|
||||
|
||||
it("changes the pusher status when clicked", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
|
||||
const enabled = false;
|
||||
|
||||
const pusher = mkPusher({
|
||||
device_id: device.device_id,
|
||||
[PUSHER_ENABLED.name]: enabled,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
device,
|
||||
pusher,
|
||||
isSigningOut: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const checkbox = getByTestId("device-detail-push-notification-checkbox");
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled);
|
||||
});
|
||||
|
||||
it("changes the local notifications settings status when clicked", () => {
|
||||
const device = {
|
||||
...baseDevice,
|
||||
};
|
||||
|
||||
const enabled = false;
|
||||
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
device,
|
||||
localNotificationSettings: {
|
||||
is_silenced: !enabled,
|
||||
},
|
||||
isSigningOut: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const checkbox = getByTestId("device-detail-push-notification-checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 } from "jest-matrix-react";
|
||||
|
||||
import { DeviceExpandDetailsButton } from "../../../../../../src/components/views/settings/devices/DeviceExpandDetailsButton";
|
||||
|
||||
describe("<DeviceExpandDetailsButton />", () => {
|
||||
const defaultProps = {
|
||||
isExpanded: false,
|
||||
onClick: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => <DeviceExpandDetailsButton {...defaultProps} {...props} />;
|
||||
|
||||
it("renders when not expanded", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders when expanded", () => {
|
||||
const { container } = render(getComponent({ isExpanded: true }));
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onClick", () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ "data-testid": "test", onClick }));
|
||||
fireEvent.click(getByTestId("test"));
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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 DeviceSecurityCard from "../../../../../../src/components/views/settings/devices/DeviceSecurityCard";
|
||||
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
|
||||
|
||||
describe("<DeviceSecurityCard />", () => {
|
||||
const defaultProps = {
|
||||
variation: DeviceSecurityVariation.Verified,
|
||||
heading: "Verified session",
|
||||
description: "nice",
|
||||
};
|
||||
const getComponent = (props = {}): React.ReactElement => <DeviceSecurityCard {...defaultProps} {...props} />;
|
||||
|
||||
it("renders basic card", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with children", () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
children: <div>hey</div>,
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 } from "jest-matrix-react";
|
||||
import { IMyDevice } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DeviceTile from "../../../../../../src/components/views/settings/devices/DeviceTile";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<DeviceTile />", () => {
|
||||
const defaultProps = {
|
||||
device: {
|
||||
device_id: "123",
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
},
|
||||
};
|
||||
const getComponent = (props = {}) => <DeviceTile {...defaultProps} {...props} />;
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.setSystemTime(now);
|
||||
});
|
||||
|
||||
it("renders a device with no metadata", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("applies interactive class when tile has click handler", () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ onClick }));
|
||||
expect(getByTestId("device-tile-123").className.includes("mx_DeviceTile_interactive")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders a verified device with no metadata", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders display name with a tooltip", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
display_name: "My device",
|
||||
};
|
||||
const { container } = render(getComponent({ device }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders last seen ip metadata", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
display_name: "My device",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId("device-metadata-lastSeenIp").textContent).toEqual(device.last_seen_ip);
|
||||
});
|
||||
|
||||
it("separates metadata with a dot", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
last_seen_ts: now - 60000,
|
||||
};
|
||||
const { container } = render(getComponent({ device }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("Last activity", () => {
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
it("renders with day of week and time when last activity is less than 6 days ago", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
last_seen_ts: now - MS_DAY * 3,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Fri 15:14");
|
||||
});
|
||||
|
||||
it("renders with month and date when last activity is more than 6 days ago", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
last_seen_ts: now - MS_DAY * 8,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Mar 6");
|
||||
});
|
||||
|
||||
it("renders with month, date, year when activity is in a different calendar year", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
last_seen_ts: new Date("2021-12-29").getTime(),
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId("device-metadata-lastActivity").textContent).toEqual("Last activity Dec 29, 2021");
|
||||
});
|
||||
|
||||
it("renders with inactive notice when last activity was more than 90 days ago", () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: "123",
|
||||
last_seen_ip: "1.2.3.4",
|
||||
last_seen_ts: now - MS_DAY * 100,
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId("device-metadata-inactive").textContent).toEqual("Inactive for 90+ days (Dec 4, 2021)");
|
||||
// last activity and verification not shown when inactive
|
||||
expect(queryByTestId("device-metadata-lastActivity")).toBeFalsy();
|
||||
expect(queryByTestId("device-metadata-verificationStatus")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 { DeviceTypeIcon } from "../../../../../../src/components/views/settings/devices/DeviceTypeIcon";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<DeviceTypeIcon />", () => {
|
||||
const defaultProps = {
|
||||
isVerified: false,
|
||||
isSelected: false,
|
||||
};
|
||||
const getComponent = (props = {}) => <DeviceTypeIcon {...defaultProps} {...props} />;
|
||||
|
||||
it("renders an unverified device", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders a verified device", () => {
|
||||
const { container } = render(getComponent({ isVerified: true }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when selected", () => {
|
||||
const { container } = render(getComponent({ isSelected: true }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders an unknown device icon when no device type given", () => {
|
||||
const { getByLabelText } = render(getComponent());
|
||||
expect(getByLabelText("Unknown session type")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders a desktop device type", () => {
|
||||
const deviceType = DeviceType.Desktop;
|
||||
const { getByLabelText } = render(getComponent({ deviceType }));
|
||||
expect(getByLabelText("Desktop session")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders a web device type", () => {
|
||||
const deviceType = DeviceType.Web;
|
||||
const { getByLabelText } = render(getComponent({ deviceType }));
|
||||
expect(getByLabelText("Web session")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders a mobile device type", () => {
|
||||
const deviceType = DeviceType.Mobile;
|
||||
const { getByLabelText } = render(getComponent({ deviceType }));
|
||||
expect(getByLabelText("Mobile session")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders an unknown device type", () => {
|
||||
const deviceType = DeviceType.Unknown;
|
||||
const { getByLabelText } = render(getComponent({ deviceType }));
|
||||
expect(getByLabelText("Unknown session type")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 {
|
||||
DeviceVerificationStatusCard,
|
||||
DeviceVerificationStatusCardProps,
|
||||
} from "../../../../../../src/components/views/settings/devices/DeviceVerificationStatusCard";
|
||||
import { ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<DeviceVerificationStatusCard />", () => {
|
||||
const deviceId = "test-device";
|
||||
const unverifiedDevice: ExtendedDevice = {
|
||||
device_id: deviceId,
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const verifiedDevice: ExtendedDevice = {
|
||||
...unverifiedDevice,
|
||||
isVerified: true,
|
||||
};
|
||||
const unverifiableDevice: ExtendedDevice = {
|
||||
...unverifiedDevice,
|
||||
isVerified: null,
|
||||
};
|
||||
const defaultProps = {
|
||||
device: unverifiedDevice,
|
||||
onVerifyDevice: jest.fn(),
|
||||
};
|
||||
const getComponent = (props: Partial<DeviceVerificationStatusCardProps> = {}) => (
|
||||
<DeviceVerificationStatusCard {...defaultProps} {...props} />
|
||||
);
|
||||
|
||||
const verifyButtonTestId = `verification-status-button-${deviceId}`;
|
||||
|
||||
describe("for the current device", () => {
|
||||
// current device uses different copy
|
||||
it("renders an unverified device", () => {
|
||||
const { getByText } = render(getComponent({ isCurrentDevice: true }));
|
||||
expect(getByText("Verify your current session for enhanced secure messaging.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders an unverifiable device", () => {
|
||||
const { getByText } = render(
|
||||
getComponent({
|
||||
device: unverifiableDevice,
|
||||
isCurrentDevice: true,
|
||||
}),
|
||||
);
|
||||
expect(getByText("This session doesn't support encryption and thus can't be verified.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders a verified device", () => {
|
||||
const { getByText } = render(
|
||||
getComponent({
|
||||
device: verifiedDevice,
|
||||
isCurrentDevice: true,
|
||||
}),
|
||||
);
|
||||
expect(getByText("Your current session is ready for secure messaging.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders an unverified device", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders an unverifiable device", () => {
|
||||
const { container, queryByTestId } = render(getComponent({ device: unverifiableDevice }));
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(queryByTestId(verifyButtonTestId)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders a verified device", () => {
|
||||
const { container, queryByTestId } = render(getComponent({ device: verifiedDevice }));
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(queryByTestId(verifyButtonTestId)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
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, { ComponentProps } from "react";
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import { FilteredDeviceList } from "../../../../../../src/components/views/settings/devices/FilteredDeviceList";
|
||||
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
|
||||
import { flushPromises, mockPlatformPeg } from "../../../../../test-utils";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
mockPlatformPeg();
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
describe("<FilteredDeviceList />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
const newDevice = {
|
||||
device_id: "new",
|
||||
last_seen_ts: Date.now() - 500,
|
||||
last_seen_ip: "123.456.789",
|
||||
display_name: "My Device",
|
||||
isVerified: true,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const unverifiedNoMetadata = {
|
||||
device_id: "unverified-no-metadata",
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const verifiedNoMetadata = {
|
||||
device_id: "verified-no-metadata",
|
||||
isVerified: true,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const hundredDaysOld = {
|
||||
device_id: "100-days-old",
|
||||
isVerified: true,
|
||||
last_seen_ts: Date.now() - MS_DAY * 100,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const hundredDaysOldUnverified = {
|
||||
device_id: "unverified-100-days-old",
|
||||
isVerified: false,
|
||||
last_seen_ts: Date.now() - MS_DAY * 100,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const defaultProps: ComponentProps<typeof FilteredDeviceList> = {
|
||||
onFilterChange: jest.fn(),
|
||||
onDeviceExpandToggle: jest.fn(),
|
||||
onSignOutDevices: jest.fn(),
|
||||
saveDeviceName: jest.fn(),
|
||||
setPushNotifications: jest.fn(),
|
||||
setSelectedDeviceIds: jest.fn(),
|
||||
localNotificationSettings: new Map(),
|
||||
expandedDeviceIds: [],
|
||||
signingOutDeviceIds: [],
|
||||
selectedDeviceIds: [],
|
||||
devices: {
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[newDevice.device_id]: newDevice,
|
||||
[hundredDaysOld.device_id]: hundredDaysOld,
|
||||
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
|
||||
},
|
||||
pushers: [],
|
||||
supportsMSC3881: true,
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => <FilteredDeviceList {...defaultProps} {...props} />;
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("renders devices in correct order", () => {
|
||||
const { container } = render(getComponent());
|
||||
const tiles = container.querySelectorAll(".mx_DeviceTile");
|
||||
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
|
||||
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
|
||||
expect(tiles[2].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOldUnverified.device_id}`);
|
||||
expect(tiles[3].getAttribute("data-testid")).toEqual(`device-tile-${unverifiedNoMetadata.device_id}`);
|
||||
expect(tiles[4].getAttribute("data-testid")).toEqual(`device-tile-${verifiedNoMetadata.device_id}`);
|
||||
});
|
||||
|
||||
it("updates list order when devices change", () => {
|
||||
const updatedOldDevice = { ...hundredDaysOld, last_seen_ts: new Date().getTime() };
|
||||
const updatedDevices = {
|
||||
[hundredDaysOld.device_id]: updatedOldDevice,
|
||||
[newDevice.device_id]: newDevice,
|
||||
};
|
||||
const { container, rerender } = render(getComponent());
|
||||
|
||||
rerender(getComponent({ devices: updatedDevices }));
|
||||
|
||||
const tiles = container.querySelectorAll(".mx_DeviceTile");
|
||||
expect(tiles.length).toBe(2);
|
||||
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
|
||||
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
|
||||
});
|
||||
|
||||
it("displays no results message when there are no devices", () => {
|
||||
const { container } = render(getComponent({ devices: {} }));
|
||||
|
||||
expect(container.getElementsByClassName("mx_FilteredDeviceList_noResults")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("filtering", () => {
|
||||
const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => {
|
||||
const dropdown = container.querySelector('[aria-label="Filter devices"]');
|
||||
|
||||
fireEvent.click(dropdown as Element);
|
||||
// tick to let dropdown render
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element);
|
||||
};
|
||||
|
||||
it("does not display filter description when filter is falsy", () => {
|
||||
const { container } = render(getComponent({ filter: undefined }));
|
||||
const tiles = container.querySelectorAll(".mx_DeviceTile");
|
||||
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard").length).toBeFalsy();
|
||||
expect(tiles.length).toEqual(5);
|
||||
});
|
||||
|
||||
it("updates filter when prop changes", () => {
|
||||
const { container, rerender } = render(getComponent({ filter: DeviceSecurityVariation.Verified }));
|
||||
const tiles = container.querySelectorAll(".mx_DeviceTile");
|
||||
expect(tiles.length).toEqual(3);
|
||||
expect(tiles[0].getAttribute("data-testid")).toEqual(`device-tile-${newDevice.device_id}`);
|
||||
expect(tiles[1].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
|
||||
expect(tiles[2].getAttribute("data-testid")).toEqual(`device-tile-${verifiedNoMetadata.device_id}`);
|
||||
|
||||
rerender(getComponent({ filter: DeviceSecurityVariation.Inactive }));
|
||||
|
||||
const rerenderedTiles = container.querySelectorAll(".mx_DeviceTile");
|
||||
expect(rerenderedTiles.length).toEqual(2);
|
||||
expect(rerenderedTiles[0].getAttribute("data-testid")).toEqual(`device-tile-${hundredDaysOld.device_id}`);
|
||||
expect(rerenderedTiles[1].getAttribute("data-testid")).toEqual(
|
||||
`device-tile-${hundredDaysOldUnverified.device_id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onFilterChange handler", async () => {
|
||||
const onFilterChange = jest.fn();
|
||||
const { container } = render(getComponent({ onFilterChange }));
|
||||
await setFilter(container, DeviceSecurityVariation.Verified);
|
||||
|
||||
expect(onFilterChange).toHaveBeenCalledWith(DeviceSecurityVariation.Verified);
|
||||
});
|
||||
|
||||
it("calls onFilterChange handler correctly when setting filter to All", async () => {
|
||||
const onFilterChange = jest.fn();
|
||||
const { container } = render(getComponent({ onFilterChange, filter: DeviceSecurityVariation.Verified }));
|
||||
await setFilter(container, "ALL");
|
||||
|
||||
// filter is cleared
|
||||
expect(onFilterChange).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[DeviceSecurityVariation.Verified, [newDevice, hundredDaysOld, verifiedNoMetadata]],
|
||||
[DeviceSecurityVariation.Unverified, [hundredDaysOldUnverified, unverifiedNoMetadata]],
|
||||
[DeviceSecurityVariation.Inactive, [hundredDaysOld, hundredDaysOldUnverified]],
|
||||
])("filters correctly for %s", (filter, expectedDevices) => {
|
||||
const { container } = render(getComponent({ filter }));
|
||||
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard")).toMatchSnapshot();
|
||||
const tileDeviceIds = [...container.querySelectorAll(".mx_DeviceTile")].map((tile) =>
|
||||
tile.getAttribute("data-testid"),
|
||||
);
|
||||
expect(tileDeviceIds).toEqual(expectedDevices.map((device) => `device-tile-${device.device_id}`));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[DeviceSecurityVariation.Verified],
|
||||
[DeviceSecurityVariation.Unverified],
|
||||
[DeviceSecurityVariation.Inactive],
|
||||
])("renders no results correctly for %s", (filter) => {
|
||||
const { container } = render(getComponent({ filter, devices: {} }));
|
||||
expect(container.getElementsByClassName("mx_FilteredDeviceList_securityCard").length).toBeFalsy();
|
||||
expect(container.getElementsByClassName("mx_FilteredDeviceList_noResults")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("clears filter from no results message", () => {
|
||||
const onFilterChange = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
getComponent({
|
||||
onFilterChange,
|
||||
filter: DeviceSecurityVariation.Verified,
|
||||
devices: {
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
},
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("devices-clear-filter-btn"));
|
||||
});
|
||||
|
||||
expect(onFilterChange).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device details", () => {
|
||||
it("renders expanded devices with device details", () => {
|
||||
const expandedDeviceIds = [newDevice.device_id, hundredDaysOld.device_id];
|
||||
const { container, getByTestId } = render(getComponent({ expandedDeviceIds }));
|
||||
expect(container.getElementsByClassName("mx_DeviceDetails").length).toBeTruthy();
|
||||
expect(getByTestId(`device-detail-${newDevice.device_id}`)).toBeTruthy();
|
||||
expect(getByTestId(`device-detail-${hundredDaysOld.device_id}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking toggle calls onDeviceExpandToggle", () => {
|
||||
const onDeviceExpandToggle = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ onDeviceExpandToggle }));
|
||||
|
||||
act(() => {
|
||||
const tile = getByTestId(`device-tile-${hundredDaysOld.device_id}`);
|
||||
const toggle = tile.querySelector('[aria-label="Show details"]');
|
||||
fireEvent.click(toggle as Element);
|
||||
});
|
||||
|
||||
expect(onDeviceExpandToggle).toHaveBeenCalledWith(hundredDaysOld.device_id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 { fireEvent, render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import FilteredDeviceListHeader from "../../../../../../src/components/views/settings/devices/FilteredDeviceListHeader";
|
||||
|
||||
describe("<FilteredDeviceListHeader />", () => {
|
||||
const defaultProps = {
|
||||
selectedDeviceCount: 0,
|
||||
isAllSelected: false,
|
||||
toggleSelectAll: jest.fn(),
|
||||
children: <div>test</div>,
|
||||
["data-testid"]: "test123",
|
||||
};
|
||||
const getComponent = (props = {}) => <FilteredDeviceListHeader {...defaultProps} {...props} />;
|
||||
|
||||
it("renders correctly when no devices are selected", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when all devices are selected", () => {
|
||||
const { container } = render(getComponent({ isAllSelected: true }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when some devices are selected", () => {
|
||||
const { getByText } = render(getComponent({ selectedDeviceCount: 2 }));
|
||||
expect(getByText("2 sessions selected")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking checkbox toggles selection", () => {
|
||||
const toggleSelectAll = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ toggleSelectAll }));
|
||||
fireEvent.click(getByTestId("device-select-all-checkbox"));
|
||||
|
||||
expect(toggleSelectAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
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 { cleanup, render, waitFor } from "jest-matrix-react";
|
||||
import { MockedObject, mocked } from "jest-mock";
|
||||
import React from "react";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
LegacyRendezvousFailureReason,
|
||||
ClientRendezvousFailureReason,
|
||||
MSC4108SignInWithQR,
|
||||
MSC4108FailureReason,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LoginWithQR from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||
import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/rendezvous");
|
||||
jest.mock("matrix-js-sdk/src/rendezvous/transports");
|
||||
jest.mock("matrix-js-sdk/src/rendezvous/channels");
|
||||
|
||||
const mockedFlow = jest.fn();
|
||||
|
||||
jest.mock("../../../../../../src/components/views/auth/LoginWithQRFlow", () => (props: Record<string, any>) => {
|
||||
mockedFlow(props);
|
||||
return <div />;
|
||||
});
|
||||
|
||||
function makeClient() {
|
||||
return mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
on: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(true),
|
||||
removeListener: jest.fn(),
|
||||
requestLoginToken: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||
getCrypto: jest.fn().mockReturnValue({}),
|
||||
crypto: {},
|
||||
} as unknown as MatrixClient);
|
||||
}
|
||||
|
||||
function unresolvedPromise<T>(): Promise<T> {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
describe("<LoginWithQR />", () => {
|
||||
let client!: MockedObject<MatrixClient>;
|
||||
const defaultProps = {
|
||||
legacy: true,
|
||||
mode: Mode.Show,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
const mockConfirmationDigits = "mock-confirmation-digits";
|
||||
const mockRendezvousCode = "mock-rendezvous-code";
|
||||
const newDeviceId = "new-device-id";
|
||||
|
||||
beforeEach(() => {
|
||||
mockedFlow.mockReset();
|
||||
jest.resetAllMocks();
|
||||
client = makeClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client = makeClient();
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("MSC3906", () => {
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<React.StrictMode>
|
||||
<LoginWithQR {...defaultProps} {...props} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue();
|
||||
// @ts-ignore
|
||||
// workaround for https://github.com/facebook/jest/issues/9675
|
||||
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined);
|
||||
client.requestLoginToken.mockResolvedValue({
|
||||
login_token: "token",
|
||||
expires_in_ms: 1000 * 1000,
|
||||
} as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet
|
||||
});
|
||||
|
||||
test("no homeserver support", async () => {
|
||||
// simulate no support
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockRejectedValue("");
|
||||
render(getComponent({ client }));
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Error,
|
||||
failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue("");
|
||||
render(getComponent({ client }));
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Error,
|
||||
failureReason: ClientRendezvousFailureReason.Unknown,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.ShowingQR,
|
||||
}),
|
||||
),
|
||||
);
|
||||
// display QR code
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
code: mockRendezvousCode,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("render QR then decline", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
|
||||
// decline
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Decline);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("approve - no crypto", async () => {
|
||||
(client as any).crypto = undefined;
|
||||
(client as any).getCrypto = () => undefined;
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.WaitingForDevice,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verifying", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() =>
|
||||
unresolvedPromise(),
|
||||
);
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Verifying,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
// expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve + verify", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token");
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
expect(rendezvous.close).toHaveBeenCalled();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test("approve - rate limited", async () => {
|
||||
mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429));
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.LegacyConnected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.LegacyConnected,
|
||||
confirmationDigits: mockConfirmationDigits,
|
||||
onClick: expect.any(Function),
|
||||
});
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
// the 429 error should be handled and mapped
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Error,
|
||||
failureReason: "rate_limited",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MSC4108", () => {
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<React.StrictMode>
|
||||
<LoginWithQR {...defaultProps} {...props} legacy={false} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
test("render QR then back", async () => {
|
||||
const onFinished = jest.fn();
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise());
|
||||
render(getComponent({ client, onFinished }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.ShowingQR,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
expect(rendezvous.negotiateProtocols).toHaveBeenCalled();
|
||||
|
||||
// back
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Back);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled);
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockRejectedValue(
|
||||
new HTTPError("Internal Server Error", 500),
|
||||
);
|
||||
const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel");
|
||||
await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown));
|
||||
});
|
||||
|
||||
test("reciprocates login", async () => {
|
||||
jest.spyOn(global.window, "open");
|
||||
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({
|
||||
verificationUri: "mock-verification-uri",
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.WaitingForDevice,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank");
|
||||
});
|
||||
|
||||
test("handles errors during reciprocation", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockRejectedValue(
|
||||
new HTTPError("Internal Server Error", 500),
|
||||
);
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.Error,
|
||||
failureReason: ClientRendezvousFailureReason.Unknown,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("handles user cancelling during reciprocation", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({});
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Cancel);
|
||||
|
||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
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 { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import {
|
||||
ClientRendezvousFailureReason,
|
||||
LegacyRendezvousFailureReason,
|
||||
MSC4108FailureReason,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
|
||||
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
|
||||
import { LoginWithQRFailureReason, FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR";
|
||||
import { Click, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types";
|
||||
|
||||
describe("<LoginWithQRFlow />", () => {
|
||||
const onClick = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
onClick,
|
||||
};
|
||||
|
||||
const getComponent = (props: {
|
||||
phase: Phase;
|
||||
onClick?: () => Promise<void>;
|
||||
failureReason?: FailureReason;
|
||||
code?: string;
|
||||
confirmationDigits?: string;
|
||||
}) => <LoginWithQRFlow {...defaultProps} {...props} />;
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
onClick.mockReset();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders spinner while loading", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.Loading }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders spinner whilst QR generating", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.ShowingQR }));
|
||||
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByTestId("cancel-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
|
||||
});
|
||||
|
||||
it("renders QR code", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.ShowingQR, code: "mock-code" }));
|
||||
// QR code is rendered async so we wait for it:
|
||||
await waitFor(() => screen.getAllByAltText("QR Code").length === 1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders code when connected", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" }));
|
||||
expect(screen.getAllByText("mock-digits")).toHaveLength(1);
|
||||
expect(screen.getAllByTestId("decline-login-button")).toHaveLength(1);
|
||||
expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByTestId("decline-login-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Decline, undefined);
|
||||
fireEvent.click(screen.getByTestId("approve-login-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Approve, undefined);
|
||||
});
|
||||
|
||||
it("renders spinner while signing in", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
|
||||
expect(screen.getAllByTestId("cancel-button")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByTestId("cancel-button"));
|
||||
expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined);
|
||||
});
|
||||
|
||||
it("renders spinner while verifying", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.Verifying }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders check code confirmation", async () => {
|
||||
const { container } = render(getComponent({ phase: Phase.OutOfBandConfirmation }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
for (const failureReason of [
|
||||
...Object.values(LegacyRendezvousFailureReason),
|
||||
...Object.values(MSC4108FailureReason),
|
||||
...Object.values(LoginWithQRFailureReason),
|
||||
...Object.values(ClientRendezvousFailureReason),
|
||||
]) {
|
||||
it(`renders ${failureReason}`, async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
phase: Phase.Error,
|
||||
failureReason,
|
||||
}),
|
||||
);
|
||||
expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
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 { mocked } from "jest-mock";
|
||||
import { IClientWellKnown, IServerVersions, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import LoginWithQRSection from "../../../../../../src/components/views/settings/devices/LoginWithQRSection";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
|
||||
function makeClient(wellKnown: IClientWellKnown) {
|
||||
const crypto = mocked({
|
||||
supportsSecretsForQrLogin: jest.fn().mockReturnValue(true),
|
||||
isCrossSigningReady: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
|
||||
return mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
on: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getClientWellKnown: jest.fn().mockReturnValue(wellKnown),
|
||||
getCrypto: jest.fn().mockReturnValue(crypto),
|
||||
} as unknown as MatrixClient);
|
||||
}
|
||||
|
||||
function makeVersions(unstableFeatures: Record<string, boolean>): IServerVersions {
|
||||
return {
|
||||
versions: [],
|
||||
unstable_features: unstableFeatures,
|
||||
};
|
||||
}
|
||||
|
||||
describe("<LoginWithQRSection />", () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({}));
|
||||
});
|
||||
|
||||
describe("MSC3906", () => {
|
||||
const defaultProps = {
|
||||
onShowQr: () => {},
|
||||
versions: makeVersions({}),
|
||||
wellKnown: {},
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;
|
||||
|
||||
describe("should not render", () => {
|
||||
it("no support at all", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("only get_login_token enabled", async () => {
|
||||
const { container } = render(
|
||||
getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("MSC3886 + get_login_token disabled", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
versions: makeVersions({ "org.matrix.msc3886": true }),
|
||||
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } },
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should render panel", () => {
|
||||
it("get_login_token + MSC3886", async () => {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
versions: makeVersions({
|
||||
"org.matrix.msc3886": true,
|
||||
}),
|
||||
capabilities: {
|
||||
[GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("get_login_token + .well-known", async () => {
|
||||
const wellKnown = {
|
||||
"io.element.rendezvous": {
|
||||
server: "https://rz.local",
|
||||
},
|
||||
};
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown));
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
versions: makeVersions({}),
|
||||
capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } },
|
||||
wellKnown,
|
||||
}),
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MSC4108", () => {
|
||||
describe("MSC4108", () => {
|
||||
const defaultProps = {
|
||||
onShowQr: () => {},
|
||||
versions: makeVersions({ "org.matrix.msc4108": true }),
|
||||
wellKnown: {},
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;
|
||||
|
||||
let client: MatrixClient;
|
||||
beforeEach(() => {
|
||||
client = makeClient({});
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
});
|
||||
|
||||
test("no homeserver support", async () => {
|
||||
const { container } = render(getComponent({ versions: makeVersions({ "org.matrix.msc4108": false }) }));
|
||||
expect(container.textContent).toContain("Not supported by your account provider");
|
||||
});
|
||||
|
||||
test("no support in crypto", async () => {
|
||||
client.getCrypto()!.exportSecretsBundle = undefined;
|
||||
const { container } = render(getComponent({ client }));
|
||||
expect(container.textContent).toContain("Not supported by your account provider");
|
||||
});
|
||||
|
||||
test("failed to connect", async () => {
|
||||
fetchMock.catch(500);
|
||||
const { container } = render(getComponent({ client }));
|
||||
expect(container.textContent).toContain("Not supported by your account provider");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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 { act, fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import SecurityRecommendations from "../../../../../../src/components/views/settings/devices/SecurityRecommendations";
|
||||
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
describe("<SecurityRecommendations />", () => {
|
||||
const unverifiedNoMetadata = { device_id: "unverified-no-metadata", isVerified: false };
|
||||
const verifiedNoMetadata = { device_id: "verified-no-metadata", isVerified: true };
|
||||
const hundredDaysOld = { device_id: "100-days-old", isVerified: true, last_seen_ts: Date.now() - MS_DAY * 100 };
|
||||
const hundredDaysOldUnverified = {
|
||||
device_id: "unverified-100-days-old",
|
||||
isVerified: false,
|
||||
last_seen_ts: Date.now() - MS_DAY * 100,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
devices: {},
|
||||
goToFilteredList: jest.fn(),
|
||||
currentDeviceId: "abc123",
|
||||
};
|
||||
const getComponent = (props = {}) => <SecurityRecommendations {...defaultProps} {...props} />;
|
||||
|
||||
it("renders null when no devices", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders unverified devices section when user has unverified devices", () => {
|
||||
const devices = {
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
|
||||
};
|
||||
const { container } = render(getComponent({ devices }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not render unverified devices section when only the current device is unverified", () => {
|
||||
const devices = {
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
};
|
||||
const { container } = render(getComponent({ devices, currentDeviceId: unverifiedNoMetadata.device_id }));
|
||||
// nothing to render
|
||||
expect(container.firstChild).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders inactive devices section when user has inactive devices", () => {
|
||||
const devices = {
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
|
||||
};
|
||||
const { container } = render(getComponent({ devices }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders both cards when user has both unverified and inactive devices", () => {
|
||||
const devices = {
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[hundredDaysOld.device_id]: hundredDaysOld,
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
};
|
||||
const { container } = render(getComponent({ devices }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("clicking view all unverified devices button works", () => {
|
||||
const goToFilteredList = jest.fn();
|
||||
const devices = {
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[hundredDaysOld.device_id]: hundredDaysOld,
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("unverified-devices-cta"));
|
||||
});
|
||||
|
||||
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Unverified);
|
||||
});
|
||||
|
||||
it("clicking view all inactive devices button works", () => {
|
||||
const goToFilteredList = jest.fn();
|
||||
const devices = {
|
||||
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
|
||||
[hundredDaysOld.device_id]: hundredDaysOld,
|
||||
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
|
||||
};
|
||||
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("inactive-devices-cta"));
|
||||
});
|
||||
|
||||
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Inactive);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 { act, fireEvent, render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import SelectableDeviceTile from "../../../../../../src/components/views/settings/devices/SelectableDeviceTile";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
describe("<SelectableDeviceTile />", () => {
|
||||
const device = {
|
||||
display_name: "My Device",
|
||||
device_id: "my-device",
|
||||
last_seen_ip: "123.456.789",
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const defaultProps = {
|
||||
onSelect: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
device,
|
||||
children: <div>test</div>,
|
||||
isSelected: false,
|
||||
};
|
||||
const getComponent = (props = {}) => <SelectableDeviceTile {...defaultProps} {...props} />;
|
||||
|
||||
it("renders unselected device tile with checkbox", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders selected tile", () => {
|
||||
const { container } = render(getComponent({ isSelected: true }));
|
||||
expect(container.querySelector(`#device-tile-checkbox-${device.device_id}`)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onSelect on checkbox click", () => {
|
||||
const onSelect = jest.fn();
|
||||
const { container } = render(getComponent({ onSelect }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(container.querySelector(`#device-tile-checkbox-${device.device_id}`)!);
|
||||
});
|
||||
|
||||
expect(onSelect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onClick on device tile info click", () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByText } = render(getComponent({ onClick }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByText(device.display_name));
|
||||
});
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call onClick when clicking device tiles actions", () => {
|
||||
const onClick = jest.fn();
|
||||
const onDeviceActionClick = jest.fn();
|
||||
const children = (
|
||||
<button onClick={onDeviceActionClick} data-testid="device-action-button">
|
||||
test
|
||||
</button>
|
||||
);
|
||||
const { getByTestId } = render(getComponent({ onClick, children }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("device-action-button"));
|
||||
});
|
||||
|
||||
// action click handler called
|
||||
expect(onDeviceActionClick).toHaveBeenCalled();
|
||||
// main click handler not called
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,474 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<CurrentDeviceSection /> displays device details on toggle click 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_DeviceDetails mx_CurrentDeviceSection_deviceDetails"
|
||||
data-testid="device-detail-alices_device"
|
||||
>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading"
|
||||
data-testid="device-detail-heading"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
alices_device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="device-heading-rename-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceDetails_sectionHeading"
|
||||
>
|
||||
Session details
|
||||
</p>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-session"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Session ID
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
alices_device
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<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>
|
||||
</section>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<CurrentDeviceSection /> handles when device is falsy 1`] = `
|
||||
<div>
|
||||
<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-disabled="true"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Options"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_disabled"
|
||||
data-testid="current-session-menu"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_KebabContextMenu_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<CurrentDeviceSection /> renders device and correct security card when device is unverified 1`] = `
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
class="mx_KebabContextMenu_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<CurrentDeviceSection /> renders device and correct security card when device is verified 1`] = `
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
class="mx_KebabContextMenu_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,97 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceDetailHeading /> displays name edit form on rename button click 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<form
|
||||
aria-disabled="false"
|
||||
class="mx_DeviceDetailHeading_renameForm"
|
||||
method="post"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceDetailHeading_renameFormHeading"
|
||||
id="device-rename-123"
|
||||
>
|
||||
Rename session
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_DeviceDetailHeading_renameFormInput"
|
||||
>
|
||||
<input
|
||||
aria-describedby="device-rename-description-123"
|
||||
aria-labelledby="device-rename-123"
|
||||
autocomplete="off"
|
||||
data-testid="device-rename-input"
|
||||
id="mx_Field_1"
|
||||
maxlength="100"
|
||||
type="text"
|
||||
value="My device"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Caption"
|
||||
id="device-rename-description-123"
|
||||
>
|
||||
Please be aware that session names are also visible to people you communicate with.
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Learn more
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading_renameFormButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
data-testid="device-rename-submit-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Save
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
|
||||
data-testid="device-rename-cancel-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<DeviceDetailHeading /> renders device name 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading"
|
||||
data-testid="device-detail-heading"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
My device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="device-heading-rename-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,428 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceDetails /> renders a verified device 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceDetails"
|
||||
data-testid="device-detail-my-device"
|
||||
>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading"
|
||||
data-testid="device-detail-heading"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
my-device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="device-heading-rename-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceDetails_sectionHeading"
|
||||
>
|
||||
Session details
|
||||
</p>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-session"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Session ID
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
my-device
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceDetails /> renders device with metadata 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceDetails"
|
||||
data-testid="device-detail-my-device"
|
||||
>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading"
|
||||
data-testid="device-detail-heading"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
My Device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="device-heading-rename-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceDetails_sectionHeading"
|
||||
>
|
||||
Session details
|
||||
</p>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-session"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Session ID
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
my-device
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Last activity
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
Sun 22:34
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-application"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Application
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Name
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
Element Web
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-device"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Device
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Model
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
Iphone X
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Operating system
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
Windows 95
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Browser
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
Firefox 100
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
IP address
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
123.456.789
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceDetails /> renders device without metadata 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceDetails"
|
||||
data-testid="device-detail-my-device"
|
||||
>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceDetailHeading"
|
||||
data-testid="device-detail-heading"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
my-device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DeviceDetailHeading_renameCta mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="device-heading-rename-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceDetails_sectionHeading"
|
||||
>
|
||||
Session details
|
||||
</p>
|
||||
<table
|
||||
class="mx_DeviceDetails_metadataTable"
|
||||
data-testid="device-detail-metadata-session"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataLabel"
|
||||
>
|
||||
Session ID
|
||||
</td>
|
||||
<td
|
||||
class="mxDeviceDetails_metadataValue"
|
||||
>
|
||||
my-device
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section
|
||||
class="mx_DeviceDetails_section"
|
||||
>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceExpandDetailsButton /> renders when expanded 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<div
|
||||
aria-label="Hide details"
|
||||
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_DeviceExpandDetailsButton_expanded mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<DeviceExpandDetailsButton /> renders when not expanded 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<div
|
||||
aria-label="Show details"
|
||||
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceSecurityCard /> renders basic card 1`] = `
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
nice
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceSecurityCard /> renders with children 1`] = `
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
Verified session
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
nice
|
||||
</p>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_actions"
|
||||
>
|
||||
<div>
|
||||
hey
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,233 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceTile /> renders a device with no metadata 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-123"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
123
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTile /> renders a verified device with no metadata 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-123"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
123
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTile /> renders display name with a tooltip 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-123"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
My device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTile /> separates metadata with a dot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-123"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
123
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-lastActivity"
|
||||
>
|
||||
Last activity 15:13
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-lastSeenIp"
|
||||
>
|
||||
1.2.3.4
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders a verified device 1`] = `
|
||||
<div>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders an unverified device 1`] = `
|
||||
<div>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders correctly when selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon mx_DeviceTypeIcon_selected"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
@@ -0,0 +1,127 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeviceVerificationStatusCard /> renders a verified device 1`] = `
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
This 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>
|
||||
`;
|
||||
|
||||
exports[`<DeviceVerificationStatusCard /> renders an unverifiable device 1`] = `
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceVerificationStatusCard /> renders an unverified device 1`] = `
|
||||
<div>
|
||||
<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 or sign out from this session for best security and reliability.
|
||||
<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-test-device"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify session
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,200 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilteredDeviceList /> displays no results message when there are no devices 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_noResults"
|
||||
>
|
||||
No sessions found.
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering filters correctly for Inactive 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_securityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Inactive"
|
||||
>
|
||||
<div
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_content"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_heading"
|
||||
>
|
||||
Inactive sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
<span>
|
||||
Consider signing out from old sessions (90 days or older) you don't use anymore.
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Learn more
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering filters correctly for Unverified 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_securityCard"
|
||||
>
|
||||
<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 sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
<span>
|
||||
Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Learn more
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering filters correctly for Verified 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_securityCard"
|
||||
>
|
||||
<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 sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
<span>
|
||||
For best security, sign out from any session that you don't recognize or use anymore.
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Learn more
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering renders no results correctly for Inactive 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_noResults"
|
||||
>
|
||||
No inactive sessions found.
|
||||
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="devices-clear-filter-btn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show all
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering renders no results correctly for Unverified 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_noResults"
|
||||
>
|
||||
No unverified sessions found.
|
||||
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="devices-clear-filter-btn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show all
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceList /> filtering renders no results correctly for Verified 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_FilteredDeviceList_noResults"
|
||||
>
|
||||
No verified sessions found.
|
||||
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="devices-clear-filter-btn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show all
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,90 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when all devices are selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
data-testid="test123"
|
||||
>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
aria-label="Deselect all"
|
||||
aria-labelledby="floating-ui-6"
|
||||
checked=""
|
||||
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>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when no devices are selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
data-testid="test123"
|
||||
>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all"
|
||||
aria-labelledby="floating-ui-1"
|
||||
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>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,307 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LoginWithQRSection /> MSC3906 should not render MSC3886 + get_login_token disabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Link new device
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQRSection"
|
||||
>
|
||||
<p
|
||||
class="mx_SettingsTab_subsectionText"
|
||||
>
|
||||
Use a QR code to sign in to another device and set up secure messaging.
|
||||
</p>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M15 16v-3h-2v3h2Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
|
||||
/>
|
||||
</svg>
|
||||
Show QR code
|
||||
</div>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||
>
|
||||
Not supported by your account provider
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<LoginWithQRSection /> MSC3906 should not render no support at all 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Link new device
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQRSection"
|
||||
>
|
||||
<p
|
||||
class="mx_SettingsTab_subsectionText"
|
||||
>
|
||||
Use a QR code to sign in to another device and set up secure messaging.
|
||||
</p>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M15 16v-3h-2v3h2Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
|
||||
/>
|
||||
</svg>
|
||||
Show QR code
|
||||
</div>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||
>
|
||||
Not supported by your account provider
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<LoginWithQRSection /> MSC3906 should not render only get_login_token enabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Link new device
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQRSection"
|
||||
>
|
||||
<p
|
||||
class="mx_SettingsTab_subsectionText"
|
||||
>
|
||||
Use a QR code to sign in to another device and set up secure messaging.
|
||||
</p>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M15 16v-3h-2v3h2Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
|
||||
/>
|
||||
</svg>
|
||||
Show QR code
|
||||
</div>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||
>
|
||||
Not supported by your account provider
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + .well-known 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Link new device
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQRSection"
|
||||
>
|
||||
<p
|
||||
class="mx_SettingsTab_subsectionText"
|
||||
>
|
||||
Use a QR code to sign in to another device and set up secure messaging.
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M15 16v-3h-2v3h2Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
|
||||
/>
|
||||
</svg>
|
||||
Show QR code
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<LoginWithQRSection /> MSC3906 should render panel get_login_token + MSC3886 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Link new device
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQRSection"
|
||||
>
|
||||
<p
|
||||
class="mx_SettingsTab_subsectionText"
|
||||
>
|
||||
Use a QR code to sign in to another device and set up secure messaging.
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 5V5h4v4H5Zm-2 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 5v-4h4v4H5Zm9-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-6Zm1 2v4h4V5h-4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M15 16v-3h-2v3h2Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 16h-2v2h-2v3h2v-3h2v2h4v-2h-2v-5h-2v3Z"
|
||||
/>
|
||||
</svg>
|
||||
Show QR code
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,376 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SecurityRecommendations /> renders both cards when user has both unverified and inactive devices 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
data-testid="security-recommendations-section"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Security recommendations
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Improve your account security by following these recommendations.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<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 sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="unverified-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SecurityRecommendations_spacing"
|
||||
/>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Inactive"
|
||||
>
|
||||
<div
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_content"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_heading"
|
||||
>
|
||||
Inactive sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Consider signing out from old sessions (90 days or older) you don't use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="inactive-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SecurityRecommendations /> renders inactive devices section when user has inactive devices 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
data-testid="security-recommendations-section"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Security recommendations
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Improve your account security by following these recommendations.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<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 sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="unverified-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SecurityRecommendations_spacing"
|
||||
/>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Inactive"
|
||||
>
|
||||
<div
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_content"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_heading"
|
||||
>
|
||||
Inactive sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Consider signing out from old sessions (90 days or older) you don't use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="inactive-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SecurityRecommendations /> renders unverified devices section when user has unverified devices 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
data-testid="security-recommendations-section"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Security recommendations
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Improve your account security by following these recommendations.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<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 sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="unverified-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (2)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SecurityRecommendations_spacing"
|
||||
/>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Inactive"
|
||||
>
|
||||
<div
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_content"
|
||||
>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_heading"
|
||||
>
|
||||
Inactive sessions
|
||||
</p>
|
||||
<p
|
||||
class="mx_DeviceSecurityCard_description"
|
||||
>
|
||||
Consider signing out from old sessions (90 days or older) you don't use anymore.
|
||||
<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_link_inline"
|
||||
data-testid="inactive-devices-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View all (1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,101 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SelectableDeviceTile /> renders selected tile 1`] = `
|
||||
<input
|
||||
checked=""
|
||||
data-testid="device-tile-checkbox-my-device"
|
||||
id="device-tile-checkbox-my-device"
|
||||
type="checkbox"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<SelectableDeviceTile /> renders unselected device tile with checkbox 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SelectableDeviceTile"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_SelectableDeviceTile_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
data-testid="device-tile-checkbox-my-device"
|
||||
id="device-tile-checkbox-my-device"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-tile-checkbox-my-device"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile mx_DeviceTile_interactive"
|
||||
data-testid="device-tile-my-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"
|
||||
>
|
||||
My Device
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-lastSeenIp"
|
||||
>
|
||||
123.456.789
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
my-device
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`deleteDevices() opens interactive auth dialog when delete fails with 401 1`] = `
|
||||
{
|
||||
"m.login.sso": {
|
||||
"1": {
|
||||
"body": "Confirm logging out these devices by using Single Sign On to prove your identity.",
|
||||
"continueKind": "primary",
|
||||
"continueText": "Single Sign On",
|
||||
"title": "Use Single Sign On to continue",
|
||||
},
|
||||
"2": {
|
||||
"body": "Click the button below to confirm signing out these devices.",
|
||||
"continueKind": "danger",
|
||||
"continueText": "Sign out devices",
|
||||
"title": "Confirm signing out these devices",
|
||||
},
|
||||
},
|
||||
"org.matrix.login.sso": {
|
||||
"1": {
|
||||
"body": "Confirm logging out these devices by using Single Sign On to prove your identity.",
|
||||
"continueKind": "primary",
|
||||
"continueText": "Single Sign On",
|
||||
"title": "Use Single Sign On to continue",
|
||||
},
|
||||
"2": {
|
||||
"body": "Click the button below to confirm signing out these devices.",
|
||||
"continueKind": "danger",
|
||||
"continueText": "Sign out devices",
|
||||
"title": "Confirm signing out these devices",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { MatrixError, UIAFlow } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { deleteDevicesWithInteractiveAuth } from "../../../../../../src/components/views/settings/devices/deleteDevices";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
|
||||
|
||||
describe("deleteDevices()", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const deviceIds = ["device_1", "device_2"];
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
deleteMultipleDevices: jest.fn(),
|
||||
});
|
||||
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog") as jest.SpyInstance;
|
||||
|
||||
const interactiveAuthError = new MatrixError({ flows: [] as UIAFlow[] }, 401);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("deletes devices and calls onFinished when interactive auth is not required", async () => {
|
||||
mockClient.deleteMultipleDevices.mockResolvedValue({});
|
||||
const onFinished = jest.fn();
|
||||
|
||||
await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished);
|
||||
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(deviceIds, undefined);
|
||||
expect(onFinished).toHaveBeenCalledWith(true, undefined);
|
||||
|
||||
// didnt open modal
|
||||
expect(modalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws without opening auth dialog when delete fails with a non-401 status code", async () => {
|
||||
const error = new Error("");
|
||||
// @ts-ignore
|
||||
error.httpStatus = 404;
|
||||
mockClient.deleteMultipleDevices.mockRejectedValue(error);
|
||||
const onFinished = jest.fn();
|
||||
|
||||
await expect(deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished)).rejects.toThrow(error);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
|
||||
// didnt open modal
|
||||
expect(modalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws without opening auth dialog when delete fails without data.flows", async () => {
|
||||
const error = new Error("");
|
||||
// @ts-ignore
|
||||
error.httpStatus = 401;
|
||||
// @ts-ignore
|
||||
error.data = {};
|
||||
mockClient.deleteMultipleDevices.mockRejectedValue(error);
|
||||
const onFinished = jest.fn();
|
||||
|
||||
await expect(deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished)).rejects.toThrow(error);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
|
||||
// didnt open modal
|
||||
expect(modalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens interactive auth dialog when delete fails with 401", async () => {
|
||||
mockClient.deleteMultipleDevices.mockRejectedValue(interactiveAuthError);
|
||||
const onFinished = jest.fn();
|
||||
|
||||
await deleteDevicesWithInteractiveAuth(mockClient, deviceIds, onFinished);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
|
||||
// opened modal
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
|
||||
const { title, authData, aestheticsForStagePhases } = modalSpy.mock.calls[0][1]!;
|
||||
|
||||
// modal opened as expected
|
||||
expect(title).toEqual("Authentication");
|
||||
expect(authData).toEqual(interactiveAuthError.data);
|
||||
expect(aestheticsForStagePhases).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 { filterDevicesBySecurityRecommendation } from "../../../../../../src/components/views/settings/devices/filter";
|
||||
import { DeviceSecurityVariation } from "../../../../../../src/components/views/settings/devices/types";
|
||||
import { DeviceType } from "../../../../../../src/utils/device/parseUserAgent";
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
describe("filterDevicesBySecurityRecommendation()", () => {
|
||||
const unverifiedNoMetadata = {
|
||||
device_id: "unverified-no-metadata",
|
||||
isVerified: false,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const verifiedNoMetadata = {
|
||||
device_id: "verified-no-metadata",
|
||||
isVerified: true,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const hundredDaysOld = {
|
||||
device_id: "100-days-old",
|
||||
isVerified: true,
|
||||
last_seen_ts: Date.now() - MS_DAY * 100,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const hundredDaysOldUnverified = {
|
||||
device_id: "unverified-100-days-old",
|
||||
isVerified: false,
|
||||
last_seen_ts: Date.now() - MS_DAY * 100,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
const fiftyDaysOld = {
|
||||
device_id: "50-days-old",
|
||||
isVerified: true,
|
||||
last_seen_ts: Date.now() - MS_DAY * 50,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
|
||||
const devices = [unverifiedNoMetadata, verifiedNoMetadata, hundredDaysOld, hundredDaysOldUnverified, fiftyDaysOld];
|
||||
|
||||
it("returns all devices when no securityRecommendations are passed", () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [])).toBe(devices);
|
||||
});
|
||||
|
||||
it("returns devices older than 90 days as inactive", () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Inactive])).toEqual([
|
||||
// devices without ts metadata are not filtered as inactive
|
||||
hundredDaysOld,
|
||||
hundredDaysOldUnverified,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns correct devices for verified filter", () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Verified])).toEqual([
|
||||
verifiedNoMetadata,
|
||||
hundredDaysOld,
|
||||
fiftyDaysOld,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns correct devices for unverified filter", () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Unverified])).toEqual([
|
||||
unverifiedNoMetadata,
|
||||
hundredDaysOldUnverified,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns correct devices for combined verified and inactive filters", () => {
|
||||
expect(
|
||||
filterDevicesBySecurityRecommendation(devices, [
|
||||
DeviceSecurityVariation.Unverified,
|
||||
DeviceSecurityVariation.Inactive,
|
||||
]),
|
||||
).toEqual([hundredDaysOldUnverified]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { act, render, screen } from "jest-matrix-react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import DiscoverySettings from "../../../../../../src/components/views/settings/discovery/DiscoverySettings";
|
||||
import { stubClient } from "../../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
|
||||
const mockGetAccessToken = jest.fn().mockResolvedValue("$$getAccessToken");
|
||||
jest.mock("../../../../../../src/IdentityAuthClient", () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: mockGetAccessToken,
|
||||
})),
|
||||
);
|
||||
|
||||
describe("DiscoverySettings", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const DiscoveryWrapper = (props = {}) => <MatrixClientContext.Provider value={client} {...props} />;
|
||||
|
||||
it("is empty if 3pid features are disabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => {
|
||||
if (key === UIFeature.ThirdPartyID) return false;
|
||||
});
|
||||
|
||||
const { container } = render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("displays alert if an identity server needs terms accepting", async () => {
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue({
|
||||
["policies"]: { en: "No ball games" },
|
||||
});
|
||||
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
await expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("button to accept terms is disabled if checkbox not checked", async () => {
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue({
|
||||
["policies"]: { en: "No ball games" },
|
||||
});
|
||||
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
const acceptCheckbox = await screen.findByRole("checkbox", { name: "Accept" });
|
||||
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||
expect(acceptCheckbox).toBeInTheDocument();
|
||||
expect(continueButton).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
await userEvent.click(acceptCheckbox);
|
||||
expect(continueButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("updates if ID server is changed", async () => {
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
mocked(client).getThreePids.mockClear();
|
||||
|
||||
act(() => {
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: "id_server_changed",
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
expect(client.getThreePids).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 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 { act, findByRole, getByRole, queryByRole, render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import {
|
||||
ThreepidMedium,
|
||||
IPushRules,
|
||||
MatrixClient,
|
||||
NotificationCountType,
|
||||
PushRuleKind,
|
||||
Room,
|
||||
RuleId,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import NotificationSettings2 from "../../../../../../src/components/views/settings/notifications/NotificationSettings2";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { StandardActions } from "../../../../../../src/notifications/StandardActions";
|
||||
import { mkMessage, stubClient } from "../../../../../test-utils";
|
||||
import Mock = jest.Mock;
|
||||
|
||||
const waitForUpdate = (): Promise<void> => new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
const labelGlobalMute = "Enable notifications for this account";
|
||||
const labelLevelAllMessage = "All messages";
|
||||
const labelLevelMentionsOnly = "Mentions and Keywords only";
|
||||
const labelSoundPeople = "People";
|
||||
const labelSoundMentions = "Mentions and Keywords";
|
||||
const labelSoundCalls = "Audio and Video calls";
|
||||
const labelActivityInvites = "Invited to a room";
|
||||
const labelActivityStatus = "New room activity, upgrades and status messages occur";
|
||||
const labelActivityBots = "Messages sent by bots";
|
||||
const labelMentionUser = "Notify when someone mentions using @displayname or @mxid";
|
||||
const labelMentionRoom = "Notify when someone mentions using @room";
|
||||
const labelMentionKeyword =
|
||||
"Notify when someone uses a keyword" + "Enter keywords here, or use for spelling variations or nicknames";
|
||||
const labelResetDefault = "Reset to default settings";
|
||||
|
||||
const keywords = ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"];
|
||||
|
||||
describe("<Notifications />", () => {
|
||||
let cli: MatrixClient;
|
||||
let pushRules: IPushRules;
|
||||
|
||||
beforeAll(async () => {
|
||||
pushRules = (await import("../../../../models/notificationsettings/pushrules_sample.json")) as IPushRules;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules);
|
||||
cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false);
|
||||
cli.setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled);
|
||||
cli.setPushRuleActions = jest.fn(cli.setPushRuleActions);
|
||||
cli.addPushRule = jest.fn(cli.addPushRule).mockResolvedValue({});
|
||||
cli.deletePushRule = jest.fn(cli.deletePushRule).mockResolvedValue({});
|
||||
cli.removePusher = jest.fn(cli.removePusher).mockResolvedValue({});
|
||||
cli.setPusher = jest.fn(cli.setPusher).mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("matches the snapshot", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("correctly handles the loading/disabled state", async () => {
|
||||
(cli.getPushRules as Mock).mockReturnValue(new Promise<IPushRules>(() => {}));
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await waitForUpdate();
|
||||
expect(screen.container).toMatchSnapshot();
|
||||
|
||||
const globalMute = screen.getByLabelText(labelGlobalMute);
|
||||
expect(globalMute).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const levelAllMessages = screen.getByLabelText(labelLevelAllMessage);
|
||||
expect(levelAllMessages).toBeDisabled();
|
||||
|
||||
const soundPeople = screen.getByLabelText(labelSoundPeople);
|
||||
expect(soundPeople).toBeDisabled();
|
||||
const soundMentions = screen.getByLabelText(labelSoundMentions);
|
||||
expect(soundMentions).toBeDisabled();
|
||||
const soundCalls = screen.getByLabelText(labelSoundCalls);
|
||||
expect(soundCalls).toBeDisabled();
|
||||
|
||||
const activityInvites = screen.getByLabelText(labelActivityInvites);
|
||||
expect(activityInvites).toBeDisabled();
|
||||
const activityStatus = screen.getByLabelText(labelActivityStatus);
|
||||
expect(activityStatus).toBeDisabled();
|
||||
const activityBots = screen.getByLabelText(labelActivityBots);
|
||||
expect(activityBots).toBeDisabled();
|
||||
|
||||
const mentionUser = screen.getByLabelText(labelMentionUser.replace("@mxid", cli.getUserId()!));
|
||||
expect(mentionUser).toBeDisabled();
|
||||
const mentionRoom = screen.getByLabelText(labelMentionRoom);
|
||||
expect(mentionRoom).toBeDisabled();
|
||||
const mentionKeyword = screen.getByLabelText(labelMentionKeyword);
|
||||
expect(mentionKeyword).toBeDisabled();
|
||||
await Promise.all([
|
||||
user.click(globalMute),
|
||||
user.click(levelAllMessages),
|
||||
user.click(soundPeople),
|
||||
user.click(soundMentions),
|
||||
user.click(soundCalls),
|
||||
user.click(activityInvites),
|
||||
user.click(activityStatus),
|
||||
user.click(activityBots),
|
||||
user.click(mentionUser),
|
||||
user.click(mentionRoom),
|
||||
user.click(mentionKeyword),
|
||||
]);
|
||||
});
|
||||
|
||||
expect(cli.setPushRuleActions).not.toHaveBeenCalled();
|
||||
expect(cli.setPushRuleEnabled).not.toHaveBeenCalled();
|
||||
expect(cli.addPushRule).not.toHaveBeenCalled();
|
||||
expect(cli.deletePushRule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("form elements actually toggle the model value", () => {
|
||||
it("global mute", async () => {
|
||||
const label = labelGlobalMute;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.Master, true);
|
||||
});
|
||||
|
||||
it("notification level", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(labelLevelAllMessage)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(labelLevelAllMessage));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true);
|
||||
(cli.setPushRuleEnabled as Mock).mockClear();
|
||||
expect(screen.getByLabelText(labelLevelMentionsOnly)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(labelLevelMentionsOnly));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true);
|
||||
});
|
||||
|
||||
describe("play a sound for", () => {
|
||||
it("people", async () => {
|
||||
const label = labelSoundPeople;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.DM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it("mentions", async () => {
|
||||
const label = labelSoundMentions;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.ContainsDisplayName,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
RuleId.ContainsUserName,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls", async () => {
|
||||
const label = labelSoundCalls;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.IncomingCall,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activity", () => {
|
||||
it("invite", async () => {
|
||||
const label = labelActivityInvites;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("status messages", async () => {
|
||||
const label = labelActivityStatus;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.MemberEvent,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.Tombstone,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
});
|
||||
it("notices", async () => {
|
||||
const label = labelActivityBots;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("mentions", () => {
|
||||
it("room mentions", async () => {
|
||||
const label = labelMentionRoom;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.AtRoomNotification,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("user mentions", async () => {
|
||||
const label = labelMentionUser.replace("@mxid", cli.getUserId()!);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.ContainsDisplayName,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
RuleId.ContainsUserName,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("keywords", async () => {
|
||||
const label = labelMentionKeyword;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
for (const pattern of keywords) {
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
pattern,
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("keywords", () => {
|
||||
it("allows adding keywords", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const inputField = screen.getByRole("textbox", { name: "Keyword" });
|
||||
const addButton = screen.getByRole("button", { name: "Add" });
|
||||
expect(inputField).not.toBeDisabled();
|
||||
expect(addButton).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.type(inputField, "testkeyword");
|
||||
await user.click(addButton);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "testkeyword", {
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
rule_id: "testkeyword",
|
||||
enabled: true,
|
||||
default: false,
|
||||
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
pattern: "testkeyword",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows deleting keywords", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const tag = screen.getByText("justj4nn3");
|
||||
const deleteButton = getByRole(tag, "button", { name: "Remove" });
|
||||
expect(deleteButton).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(deleteButton);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3");
|
||||
});
|
||||
});
|
||||
|
||||
it("resets the model correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const button = screen.getByText(labelResetDefault);
|
||||
expect(button).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(button);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
false,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.Message,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.DM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
|
||||
for (const pattern of keywords) {
|
||||
expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, pattern);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("pusher settings", () => {
|
||||
it("can create email pushers", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const label = "test@example.tld";
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPusher).toHaveBeenCalledWith({
|
||||
app_display_name: "Email Notifications",
|
||||
app_id: "m.email",
|
||||
append: true,
|
||||
data: { brand: "Element" },
|
||||
device_display_name: "test@example.tld",
|
||||
kind: "email",
|
||||
lang: "en-US",
|
||||
pushkey: "test@example.tld",
|
||||
});
|
||||
});
|
||||
|
||||
it("can remove email pushers", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "abctest",
|
||||
},
|
||||
{
|
||||
app_display_name: "Email Notifications",
|
||||
app_id: "m.email",
|
||||
data: { brand: "Element" },
|
||||
device_display_name: "test@example.tld",
|
||||
kind: "email",
|
||||
lang: "en-US",
|
||||
pushkey: "test@example.tld",
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const label = "test@example.tld";
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.removePusher).toHaveBeenCalledWith("test@example.tld", "m.email");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear all notifications", () => {
|
||||
it("is hidden when no notifications exist", async () => {
|
||||
const room = new Room("room123", cli, "@alice:example.org");
|
||||
cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]);
|
||||
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await waitForUpdate();
|
||||
expect(
|
||||
queryByRole(container, "button", {
|
||||
name: "Mark all messages as read",
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears all notifications", async () => {
|
||||
const room = new Room("room123", cli, "@alice:example.org");
|
||||
cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]);
|
||||
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: "room123",
|
||||
user: "@alice:example.org",
|
||||
ts: 1,
|
||||
});
|
||||
room.addLiveEvents([message]);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await waitForUpdate();
|
||||
const clearNotificationEl = await findByRole(container, "button", {
|
||||
name: "Mark all messages as read",
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await user.click(clearNotificationEl);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.sendReadReceipt).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clearNotificationEl).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 } from "jest-matrix-react";
|
||||
|
||||
import SettingsSubsection from "../../../../../../src/components/views/settings/shared/SettingsSubsection";
|
||||
|
||||
describe("<SettingsSubsection />", () => {
|
||||
const defaultProps = {
|
||||
heading: "Test",
|
||||
children: <div>test settings content</div>,
|
||||
};
|
||||
const getComponent = (props = {}): React.ReactElement => <SettingsSubsection {...defaultProps} {...props} />;
|
||||
|
||||
it("renders with plain text heading", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with react element heading", () => {
|
||||
const heading = <h3>This is the heading</h3>;
|
||||
const { container } = render(getComponent({ heading }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without description", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with plain text description", () => {
|
||||
const { container } = render(getComponent({ description: "This describes the subsection" }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with react element description", () => {
|
||||
const description = (
|
||||
<p>
|
||||
This describes the section <a href="/#">link</a>
|
||||
</p>
|
||||
);
|
||||
const { container } = render(getComponent({ description }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 { SettingsSubsectionHeading } from "../../../../../../src/components/views/settings/shared/SettingsSubsectionHeading";
|
||||
|
||||
describe("<SettingsSubsectionHeading />", () => {
|
||||
const defaultProps = {
|
||||
heading: "test",
|
||||
};
|
||||
const getComponent = (props = {}) => render(<SettingsSubsectionHeading {...defaultProps} {...props} />);
|
||||
|
||||
it("renders without children", () => {
|
||||
const { container } = getComponent();
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with children", () => {
|
||||
const children = <a href="/#">test</a>;
|
||||
const { container } = getComponent({ children });
|
||||
expect({ container }).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SettingsSubsection /> renders with plain text description 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Test
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
This describes the subsection
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div>
|
||||
test settings content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubsection /> renders with plain text heading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Test
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div>
|
||||
test settings content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubsection /> renders with react element description 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Test
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<p>
|
||||
This describes the section
|
||||
<a
|
||||
href="/#"
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div>
|
||||
test settings content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubsection /> renders with react element heading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<h3>
|
||||
This is the heading
|
||||
</h3>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div>
|
||||
test settings content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubsection /> renders without description 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Test
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div>
|
||||
test settings content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,38 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SettingsSubsectionHeading /> renders with children 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
test
|
||||
</h3>
|
||||
<a
|
||||
href="/#"
|
||||
>
|
||||
test
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubsectionHeading /> renders without children 1`] = `
|
||||
{
|
||||
"container": <div>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
test
|
||||
</h3>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 } 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 = () => document.querySelector("p");
|
||||
|
||||
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(getGroup().querySelector("p")).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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -0,0 +1,835 @@
|
||||
// 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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 4 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 6 14Zm6 0c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 12 14Zm6 0c-.55 0-1.02-.196-1.413-.588A1.926 1.926 0 0 1 16 12c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 18 10c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412 0 .55-.196 1.02-.587 1.412A1.926 1.926 0 0 1 18 14Z"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user