mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-07 15:08:24 +00:00
mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/
mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts
And a couple of gitignore tweaks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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 { type 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");
|
||||
|
||||
await act(async () => {
|
||||
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,603 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor, cleanup } from "jest-matrix-react";
|
||||
import { type MatrixClient, MatrixError, 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";
|
||||
import InteractiveAuthDialog from "../../../../../src/components/views/dialogs/InteractiveAuthDialog.tsx";
|
||||
|
||||
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(async () => {
|
||||
jest.restoreAllMocks();
|
||||
await clearAllModals();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const clientProviderWrapper: React.FC = ({ children }: React.PropsWithChildren) => (
|
||||
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
it("should handle no email addresses", async () => {
|
||||
const { container } = render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Email}
|
||||
threepids={[]}
|
||||
isLoading={false}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText("Email Address")).resolves.toBeVisible();
|
||||
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 = await screen.findByRole("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 = await screen.findByRole("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 = await screen.findByRole("button", { name: /Country Dropdown/ });
|
||||
await userEvent.click(countryDropdown);
|
||||
const gbOption = screen.getByText("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/ });
|
||||
|
||||
expect(continueButton).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
|
||||
),
|
||||
).resolves.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 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 = await screen.findByRole("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 = await screen.findByRole("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 = await screen.findByRole("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,
|
||||
},
|
||||
);
|
||||
|
||||
await expect(screen.findByText(EMAIL1.address)).resolves.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,
|
||||
},
|
||||
);
|
||||
|
||||
await expect(screen.findByText(PHONE1.address)).resolves.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,
|
||||
},
|
||||
);
|
||||
|
||||
await expect(screen.findByText(EMAIL1.address)).resolves.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,
|
||||
},
|
||||
);
|
||||
|
||||
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
|
||||
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
|
||||
await userEvent.click(revokeButton);
|
||||
|
||||
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
|
||||
expect(onChangeFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show UIA dialog when necessary for adding email", 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 MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
|
||||
);
|
||||
|
||||
await userEvent.click(continueButton);
|
||||
|
||||
expect(createDialogFn).toHaveBeenCalledWith(
|
||||
InteractiveAuthDialog,
|
||||
expect.objectContaining({
|
||||
title: "Add Email Address",
|
||||
makeRequest: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show UIA dialog when necessary for adding msisdn", 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://some-url",
|
||||
});
|
||||
|
||||
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.getByText("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" });
|
||||
await userEvent.click(addButton);
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||
|
||||
expect(continueButton).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
|
||||
),
|
||||
).resolves.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");
|
||||
|
||||
mocked(client).addThreePidOnly.mockRejectedValueOnce(
|
||||
new MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
|
||||
);
|
||||
|
||||
await userEvent.click(continueButton);
|
||||
|
||||
expect(createDialogFn).toHaveBeenCalledWith(
|
||||
InteractiveAuthDialog,
|
||||
expect.objectContaining({
|
||||
title: "Add Phone Number",
|
||||
makeRequest: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render phone numbers", async () => {
|
||||
const { container } = render(
|
||||
<AddRemoveThreepids
|
||||
mode="hs"
|
||||
medium={ThreepidMedium.Phone}
|
||||
threepids={[PHONE1]}
|
||||
isLoading={false}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } 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",
|
||||
});
|
||||
const GENERIC_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "not-avatar.doc", {
|
||||
type: "application/msword",
|
||||
});
|
||||
|
||||
describe("<AvatarSetting />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("renders avatar with specified accessible name", async () => {
|
||||
const { getByRole } = render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatarAccessibleName="Avatar of Peter Fox"
|
||||
avatar="mxc://example.org/my-avatar"
|
||||
/>,
|
||||
);
|
||||
|
||||
const avatarButton = getByRole("button", { name: "Avatar of Peter Fox" });
|
||||
expect(avatarButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a file as the avatar when supplied", async () => {
|
||||
render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatarAccessibleName="Avatar of Peter Fox"
|
||||
avatar={AVATAR_FILE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const avatarButton = await screen.findByRole("button", { name: "Avatar of Peter Fox" });
|
||||
const imgElement = avatarButton.querySelector("img");
|
||||
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"
|
||||
avatarAccessibleName="Avatar of Peter Fox"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fileInput = screen.getByAltText("Upload");
|
||||
await user.upload(fileInput, AVATAR_FILE);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(AVATAR_FILE);
|
||||
});
|
||||
|
||||
it("should noop when selecting no file", async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatar="mxc://example.org/my-avatar"
|
||||
avatarAccessibleName="Avatar of Peter Fox"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fileInput = screen.getByAltText("Upload");
|
||||
// Can't use userEvent.upload here as it doesn't support uploading invalid files
|
||||
fireEvent.change(fileInput, { target: { files: [] } });
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show error if user tries to use non-image file", async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(
|
||||
<AvatarSetting
|
||||
placeholderId="blee"
|
||||
placeholderName="boo"
|
||||
avatar="mxc://example.org/my-avatar"
|
||||
avatarAccessibleName="Avatar of Peter Fox"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fileInput = screen.getByAltText("Upload");
|
||||
// Can't use userEvent.upload here as it doesn't support uploading invalid files
|
||||
fireEvent.change(fileInput, { target: { files: [GENERIC_FILE] } });
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
await expect(screen.findByRole("heading", { name: "Upload Failed" })).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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,192 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 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: PromiseWithResolvers<boolean> | undefined;
|
||||
jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => {
|
||||
deferredInitEventIndex = Promise.withResolvers<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 OR LicenseRef-Element-Commercial
|
||||
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,346 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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,
|
||||
type GuestAccess,
|
||||
type HistoryVisibility,
|
||||
JoinRule,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
ClientEvent,
|
||||
RoomMember,
|
||||
MatrixError,
|
||||
Visibility,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import {
|
||||
clearAllModals,
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import { filterBoolean } from "../../../../../src/utils/arrays";
|
||||
import JoinRuleSettings, {
|
||||
type 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: PromiseWithResolvers<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 = Promise.withResolvers<{}>();
|
||||
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, undefined);
|
||||
|
||||
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
await expect(within(dialog).findByText("Loading new room")).resolves.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, undefined);
|
||||
|
||||
expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
await expect(within(dialog).findByText("Loading new room")).resolves.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 OR LicenseRef-Element-Commercial
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
* 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("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("switch", { name: "Show compact text and messages" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the setting when toggled", async () => {
|
||||
await renderLayoutSwitcher();
|
||||
act(() => screen.getByRole("switch", { name: "Show compact text and messages" }).click());
|
||||
|
||||
await waitFor(() => expect(SettingsStore.getValue("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("switch", { name: "Show compact text and messages" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,937 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
type IPushRule,
|
||||
type IPushRules,
|
||||
RuleId,
|
||||
type IPusher,
|
||||
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
PushRuleActionName,
|
||||
TweakName,
|
||||
ConditionKind,
|
||||
type IPushRuleCondition,
|
||||
PushRuleKind,
|
||||
type IThreepid,
|
||||
ThreepidMedium,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } 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 { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||
import { Form } from "@vector-im/compound-web";
|
||||
|
||||
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(
|
||||
<Form.Root>
|
||||
<Notifications />
|
||||
</Form.Root>,
|
||||
);
|
||||
|
||||
// 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(secureRandomString).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();
|
||||
// @ts-expect-error
|
||||
mockClient.pushProcessor = new PushProcessor(mockClient);
|
||||
|
||||
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.getByLabelText("Enable notifications for this account")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Enable notifications for this device")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Enable desktop notifications for this session")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Show message in desktop notification")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Enable audible notifications for this session")).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.getByLabelText(`Enable email notifications for ${testEmail}`)).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.getByLabelText(`Enable email notifications for ${testEmail}`);
|
||||
expect(emailSwitch).toBeChecked();
|
||||
});
|
||||
|
||||
it("enables email notification when toggling on", async () => {
|
||||
await getComponentAndWait();
|
||||
|
||||
const emailToggle = screen.getByLabelText(`Enable email notifications for ${testEmail}`);
|
||||
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.getByLabelText(`Enable email notifications for ${testEmail}`);
|
||||
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.getByLabelText(`Enable email notifications for ${testEmail}`);
|
||||
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!: HTMLInputElement;
|
||||
|
||||
const update = () => {
|
||||
audioNotifsToggle = screen.getByLabelText("Enable audible notifications for this session");
|
||||
};
|
||||
update();
|
||||
|
||||
expect(audioNotifsToggle).toBeChecked();
|
||||
expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true);
|
||||
|
||||
fireEvent.click(audioNotifsToggle);
|
||||
update();
|
||||
|
||||
expect(audioNotifsToggle).not.toBeChecked();
|
||||
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], { addToState: true });
|
||||
|
||||
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,169 @@
|
||||
/*
|
||||
* 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 OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import React, { type 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();
|
||||
const userLevels = {
|
||||
[currentUser]: 100,
|
||||
"@alice:server.org": 100,
|
||||
"@bob:server.org": 0,
|
||||
};
|
||||
renderPLS({ userLevels, 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();
|
||||
});
|
||||
|
||||
it("should display modal warning if user is last admin", 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");
|
||||
|
||||
// modal should appear because only admin in the room
|
||||
expect(screen.findByText("WARNING")).toBeTruthy();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
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 display modal warning if user is last admin and return to initial value if user cancel", 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");
|
||||
|
||||
// modal should appear because only admin in the room
|
||||
expect(screen.findByText("WARNING")).toBeTruthy();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
// the power level should be back to initial value
|
||||
expect(select).toHaveValue("100");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import SetIdServer from "../../../../../src/components/views/settings/SetIdServer";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser, mockClientMethodsServer } from "../../../../test-utils";
|
||||
|
||||
describe("<SetIdServer />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
getOpenIdToken: jest.fn().mockResolvedValue("a_token"),
|
||||
getTerms: jest.fn(),
|
||||
setAccountData: jest.fn(),
|
||||
});
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SetIdServer missingTerms={false} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("renders expected fields", () => {
|
||||
const { asFragment } = render(getComponent());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow setting an identity server", async () => {
|
||||
const { getByLabelText, getByRole, findByRole } = render(getComponent());
|
||||
|
||||
fetchMock.get("https://identity.example.org/_matrix/identity/v2", {
|
||||
body: {},
|
||||
});
|
||||
fetchMock.get("https://identity.example.org/_matrix/identity/v2/account", {
|
||||
body: { user_id: userId },
|
||||
});
|
||||
fetchMock.post("https://identity.example.org/_matrix/identity/v2/account/register", {
|
||||
body: { token: "foobar" },
|
||||
});
|
||||
|
||||
const identServerField = getByLabelText("Enter a new identity server");
|
||||
await userEvent.type(identServerField, "https://identity.example.org");
|
||||
await userEvent.click(getByRole("button", { name: "Change" }));
|
||||
await userEvent.click(await findByRole("button", { name: "Continue" }));
|
||||
});
|
||||
|
||||
it("should clear input on cancel", async () => {
|
||||
const { getByLabelText, findByRole } = render(getComponent());
|
||||
const identServerField = getByLabelText("Enter a new identity server");
|
||||
await userEvent.type(identServerField, "https://identity.example.org");
|
||||
await userEvent.click(await findByRole("button", { name: "Reset" }));
|
||||
expect((identServerField as HTMLInputElement).value).toEqual("");
|
||||
});
|
||||
|
||||
it("should show error when an error occurs", async () => {
|
||||
const { getByLabelText, getByRole, getByText } = render(getComponent());
|
||||
|
||||
fetchMock.get("https://invalid.example.org/_matrix/identity/v2", {
|
||||
body: {},
|
||||
status: 404,
|
||||
});
|
||||
fetchMock.get("https://invalid.example.org/_matrix/identity/v2/account", {
|
||||
body: {},
|
||||
status: 404,
|
||||
});
|
||||
fetchMock.post("https://invalid.example.org/_matrix/identity/v2/account/register", {
|
||||
body: {},
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const identServerField = getByLabelText("Enter a new identity server");
|
||||
await userEvent.type(identServerField, "https://invalid.example.org");
|
||||
await userEvent.click(getByRole("button", { name: "Change" }));
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getByText("Not a valid identity server (status code 404)")).toBeVisible();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
// Check the error vanishes when the input is edited.
|
||||
await userEvent.type(identServerField, "https://identity2.example.org");
|
||||
expect(() => getByText("Not a valid identity server (status code 404)")).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 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(),
|
||||
});
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SetIntegrationManager />
|
||||
</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 OR LicenseRef-Element-Commercial
|
||||
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,23 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { SettingsHeader } from "../../../../../src/components/views/settings/SettingsHeader";
|
||||
|
||||
describe("<SettingsHeader />", () => {
|
||||
it("should render the component", () => {
|
||||
const { asFragment } = render(<SettingsHeader label="Settings Header" />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the component with the recommended tag", () => {
|
||||
const { asFragment } = render(<SettingsHeader label="Settings Header" hasRecommendedTag={true} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { SettingsSubheader } from "../../../../../src/components/views/settings/SettingsSubheader";
|
||||
|
||||
describe("<SettingsSubheader />", () => {
|
||||
it("should display a check icon when in success", () => {
|
||||
const { asFragment } = render(<SettingsSubheader state="success" stateMessage="Success!" />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error icon when in error", () => {
|
||||
const { asFragment } = render(<SettingsSubheader state="error" stateMessage="Error!" />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display a label", () => {
|
||||
const { asFragment } = render(<SettingsSubheader label="My label" state="success" stateMessage="Success!" />);
|
||||
expect(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 OR LicenseRef-Element-Commercial
|
||||
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, type 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("switch", { name: "Match system theme" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should enable Match system theme", async () => {
|
||||
await enableSystemTheme(true);
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
expect(screen.getByRole("switch", { name: "Match system theme" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the system theme when clicked", async () => {
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
act(() => screen.getByRole("switch", { name: "Match system theme" }).click());
|
||||
|
||||
// The system theme should be enabled
|
||||
expect(screen.getByRole("switch", { 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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ChangeEvent } from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { type MatrixClient, type 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, type 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();
|
||||
});
|
||||
});
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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_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 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_15"
|
||||
label="Email Address"
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_15"
|
||||
>
|
||||
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>
|
||||
<svg
|
||||
class="mx_Dropdown_arrow"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<input
|
||||
autocomplete="tel-national"
|
||||
id="mx_Field_16"
|
||||
label="Phone Number"
|
||||
placeholder="Phone Number"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_16"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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/download"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Element Desktop
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</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
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
<svg
|
||||
class="mx_Field_select_chevron"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_FontScalingPanel_preview mx_EventTilePreview_loader"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_11k6c_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSubsection mx_SettingsSubsection_newUi"
|
||||
data-testid="layoutPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Message layout
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16 mx_LayoutSwitcher_LayoutSelector"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
|
||||
>
|
||||
<label
|
||||
aria-label="Modern"
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_0_"
|
||||
name="layout"
|
||||
title=""
|
||||
type="radio"
|
||||
value="group"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</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 _disambiguatedProfile_oa3at_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Username_color2 _disambiguatedProfile_displayName_oa3at_14 mx_DisambiguatedProfile_displayName"
|
||||
dir="auto"
|
||||
>
|
||||
Alice
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EventTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
|
||||
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"
|
||||
id="1"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="_field_19upo_26 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
|
||||
>
|
||||
<label
|
||||
aria-label="Message bubbles"
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_9_"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_9_"
|
||||
name="layout"
|
||||
title=""
|
||||
type="radio"
|
||||
value="bubble"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</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 _disambiguatedProfile_oa3at_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Username_color2 _disambiguatedProfile_displayName_oa3at_14 mx_DisambiguatedProfile_displayName"
|
||||
dir="auto"
|
||||
>
|
||||
Alice
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EventTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
|
||||
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"
|
||||
id="2"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="_field_19upo_26 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
|
||||
>
|
||||
<label
|
||||
aria-label="IRC (experimental)"
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_i_"
|
||||
>
|
||||
<div
|
||||
class="mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_i_"
|
||||
name="layout"
|
||||
title=""
|
||||
type="radio"
|
||||
value="irc"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
/>
|
||||
<div
|
||||
class="mx_DisambiguatedProfile _disambiguatedProfile_oa3at_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Username_color2 _disambiguatedProfile_displayName_oa3at_14 mx_DisambiguatedProfile_displayName"
|
||||
dir="auto"
|
||||
>
|
||||
Alice
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EventTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
|
||||
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"
|
||||
id="3"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.706 2.637a2 2 0 0 1 2.829 0l2.828 2.828a2 2 0 0 1 0 2.829L9.605 20.052a1 1 0 0 1-.465.263L3.483 21.73a1 1 0 0 1-1.212-1.213l1.414-5.657a1 1 0 0 1 .263-.465zm1.224 7.262L14.102 7.07l-8.544 8.544-.943 3.771 3.771-.943z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_s_"
|
||||
class="_input_udcm8_24"
|
||||
id="radix-_r_r_"
|
||||
name="compactLayout"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_r_"
|
||||
>
|
||||
Show compact text and messages
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_s_"
|
||||
>
|
||||
Modern layout must be selected to use this feature.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_cqpyv_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<Notifications /> main notification switches renders only enable notifications switch when notifications are disabled 1`] = `
|
||||
<div>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="_r_0_"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="_r_0_"
|
||||
>
|
||||
Enable notifications for this account
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_2_"
|
||||
>
|
||||
Turn off to disable notifications on all your devices and sessions
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="mx_SettingsFlag_testid_0"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_testid_0"
|
||||
>
|
||||
Show all activity in the room list (dots or number of unread messages)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="mx_SettingsFlag_testid_1"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_testid_1"
|
||||
>
|
||||
Only show notifications in the thread activity centre
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
<svg
|
||||
class="mx_Field_select_chevron"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Apply"
|
||||
class="_button_13vu4_8 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>
|
||||
<svg
|
||||
class="mx_Field_select_chevron"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
<svg
|
||||
class="mx_Field_select_chevron"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
<svg
|
||||
class="mx_Field_select_chevron"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="Apply"
|
||||
class="_button_13vu4_8 mx_Dialog_nonDialogButton mx_PowerLevelSelector_Button"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<SetIdServer /> renders expected fields 1`] = `
|
||||
<DocumentFragment>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
Identity server
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16 mx_IdentityServerPicker"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
Enter a new identity server
|
||||
</label>
|
||||
<div
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-_r_0_"
|
||||
name="input"
|
||||
placeholder=""
|
||||
title=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</fieldset>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`SetIntegrationManager should render manage integrations sections 1`] = `
|
||||
<form
|
||||
class="_root_19upo_16 mx_SetIntegrationManager"
|
||||
data-testid="mx_SetIntegrationManager"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<div
|
||||
class="mx_SetIntegrationManager_heading_manager"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Manage integrations
|
||||
</h3>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
id="mx_SetIntegrationManager_ManagerName"
|
||||
>
|
||||
(scalar.vector.im)
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
id="mx_SetIntegrationManager_BodyText"
|
||||
>
|
||||
<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>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="_r_0_"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="_r_0_"
|
||||
>
|
||||
Enable the integration manager
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<SettingsHeader /> should render the component 1`] = `
|
||||
<DocumentFragment>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Settings Header
|
||||
</h2>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SettingsHeader /> should render the component with the recommended tag 1`] = `
|
||||
<DocumentFragment>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader mx_SettingsHeader_recommended"
|
||||
>
|
||||
Settings Header
|
||||
</h2>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<SettingsSubheader /> should display a check icon when in success 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsSubheader_success"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m10.6 13.8-2.15-2.15a.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275.95.95 0 0 0-.275.7q0 .425.275.7L9.9 15.9q.3.3.7.3t.7-.3l5.65-5.65a.95.95 0 0 0 .275-.7.95.95 0 0 0-.275-.7.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
Success!
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubheader /> should display a label 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
>
|
||||
My label
|
||||
<span
|
||||
class="mx_SettingsSubheader_success"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m10.6 13.8-2.15-2.15a.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275.95.95 0 0 0-.275.7q0 .425.275.7L9.9 15.9q.3.3.7.3t.7-.3l5.65-5.65a.95.95 0 0 0 .275-.7.95.95 0 0 0-.275-.7.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
Success!
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SettingsSubheader /> should display an error icon when in error 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsSubheader_error"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
Error!
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+729
@@ -0,0 +1,729 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Theme
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="radix-_r_28_"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_28_"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_19upo_16 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_29_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_29_"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_2a_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="dark"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_2a_"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_2b_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light-high-contrast"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_2b_"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_2c_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="custom-Alice theme"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_2c_"
|
||||
>
|
||||
Alice theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="mx_ThemeChoicePanel_CustomTheme"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_2d_"
|
||||
>
|
||||
Add custom theme
|
||||
</label>
|
||||
<div
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_2e_"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-_r_2d_"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_2e_"
|
||||
>
|
||||
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="_r_2f_"
|
||||
class="_icon-button_1215g_8 _destructive_1215g_95"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_cqpyv_8"
|
||||
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_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Theme
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="radix-_r_10_"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_10_"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_19upo_16 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_11_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_11_"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_12_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="dark"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_12_"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_13_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light-high-contrast"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_13_"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_14_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="custom-Alice theme"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_14_"
|
||||
>
|
||||
Alice theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="mx_ThemeChoicePanel_CustomTheme"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_15_"
|
||||
>
|
||||
Add custom theme
|
||||
</label>
|
||||
<div
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_16_"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-_r_15_"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_16_"
|
||||
>
|
||||
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="_r_17_"
|
||||
class="_icon-button_1215g_8 _destructive_1215g_95"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_cqpyv_8"
|
||||
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_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Theme
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="radix-_r_0_"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
Match system theme
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_19upo_16 mx_ThemeChoicePanel_ThemeSelectors"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_1_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_1_"
|
||||
>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_2_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="dark"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_2_"
|
||||
>
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_3_"
|
||||
name="themeSelector"
|
||||
title=""
|
||||
type="radio"
|
||||
value="light-high-contrast"
|
||||
/>
|
||||
<div
|
||||
class="_ui_1ug7n_19"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59 mx_ThemeChoicePanel_themeSelector_Label"
|
||||
for="radix-_r_3_"
|
||||
>
|
||||
High contrast
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_cqpyv_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+95
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+141
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, type 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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type 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);
|
||||
});
|
||||
});
|
||||
+38
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+37
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { type 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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+88
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DeviceVerificationStatusCard,
|
||||
type DeviceVerificationStatusCardProps,
|
||||
} from "../../../../../../src/components/views/settings/devices/DeviceVerificationStatusCard";
|
||||
import { type 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();
|
||||
});
|
||||
});
|
||||
+231
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+46
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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,214 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { cleanup, render, waitFor } from "jest-matrix-react";
|
||||
import { mocked, type MockedObject } from "jest-mock";
|
||||
import React from "react";
|
||||
import {
|
||||
ClientRendezvousFailureReason,
|
||||
MSC4108FailureReason,
|
||||
MSC4108SignInWithQR,
|
||||
RendezvousError,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { HTTPError, type MatrixClient } 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";
|
||||
|
||||
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({}),
|
||||
} 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(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedFlow.mockReset();
|
||||
jest.resetAllMocks();
|
||||
client = makeClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client = makeClient();
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("MSC4108", () => {
|
||||
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
|
||||
<LoginWithQR {...defaultProps} {...props} />
|
||||
);
|
||||
|
||||
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(MSC4108FailureReason.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 protocol negotiation", async () => {
|
||||
render(getComponent({ client }));
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue();
|
||||
const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol);
|
||||
// @ts-ignore work-around for lazy mocks
|
||||
err.code = MSC4108FailureReason.UnsupportedProtocol;
|
||||
jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockRejectedValue(err);
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
phase: Phase.ShowingQR,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0];
|
||||
expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol);
|
||||
});
|
||||
});
|
||||
|
||||
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,96 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
|
||||
import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow";
|
||||
import { LoginWithQRFailureReason, type 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?: Uint8Array;
|
||||
}) => <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: new TextEncoder().encode("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 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(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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type IClientWellKnown, type IServerVersions, type MatrixClient } 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("MSC4108", () => {
|
||||
describe("MSC4108", () => {
|
||||
const defaultProps = {
|
||||
onShowQr: () => {},
|
||||
versions: makeVersions({ "org.matrix.msc4108": true }),
|
||||
};
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+108
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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);
|
||||
});
|
||||
});
|
||||
+83
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Unverified"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Unverified"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>,
|
||||
}
|
||||
`;
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</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>
|
||||
`;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
class="mx_DeviceExpandDetailsButton_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>,
|
||||
}
|
||||
`;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<DeviceSecurityCard /> renders basic card 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Verified"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders a verified device 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Verified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon verified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders an unverified device 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DeviceTypeIcon /> renders correctly when selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon mx_DeviceTypeIcon_selected"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<DeviceVerificationStatusCard /> renders a verified device 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceSecurityCard_icon Verified"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M11.106 2.447a2 2 0 0 1 1.789 0l6 3A2 2 0 0 1 20 7.237V12c0 6.742-5.773 9.246-7.51 9.846-.32.111-.66.111-.98 0C9.774 21.246 4 18.743 4 12V7.236a2 2 0 0 1 1.105-1.789zm4.601 6.846a1 1 0 0 0-1.414 0L11 12.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 0-1.414"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</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>,
|
||||
]
|
||||
`;
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when all devices are selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
data-testid="test123"
|
||||
>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
aria-label="Deselect all"
|
||||
aria-labelledby="_r_9_"
|
||||
checked=""
|
||||
class="_input_153f2_18"
|
||||
data-testid="device-select-all-checkbox"
|
||||
id="device-select-all-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</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"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_input_153f2_18"
|
||||
data-testid="device-select-all-checkbox"
|
||||
id="device-select-all-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</span>
|
||||
<span
|
||||
class="mx_FilteredDeviceListHeader_label"
|
||||
>
|
||||
Sessions
|
||||
</span>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+1249
File diff suppressed because it is too large
Load Diff
+397
@@ -0,0 +1,397 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
`;
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<SelectableDeviceTile /> renders selected tile 1`] = `
|
||||
<input
|
||||
checked=""
|
||||
class="_input_153f2_18"
|
||||
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"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32 mx_SelectableDeviceTile_checkbox"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
data-testid="device-tile-checkbox-my-device"
|
||||
id="device-tile-checkbox-my-device"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="device-tile-checkbox-my-device"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTile mx_DeviceTile_interactive"
|
||||
data-testid="device-tile-my-device"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixError, type 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);
|
||||
|
||||
// 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 OR LicenseRef-Element-Commercial
|
||||
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]);
|
||||
});
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { type MatrixClient, MatrixEvent, type Terms, ThreepidMedium } 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 defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
|
||||
const mockGetAccessToken = jest.fn().mockResolvedValue("$$getAccessToken");
|
||||
jest.mock("../../../../../../src/IdentityAuthClient", () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: mockGetAccessToken,
|
||||
})),
|
||||
);
|
||||
|
||||
const sampleTerms = {
|
||||
policies: {
|
||||
terms: { version: "alpha", en: { name: "No ball games", url: "https://foobar" } },
|
||||
},
|
||||
} satisfies Terms;
|
||||
|
||||
const invalidTerms = {
|
||||
policies: {
|
||||
terms: { version: "invalid" },
|
||||
},
|
||||
} satisfies Terms;
|
||||
|
||||
describe("DiscoverySettings", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const DiscoveryWrapper = (props = {}) => <MatrixClientContext.Provider value={client} {...props} />;
|
||||
|
||||
it("displays alert if an identity server needs terms accepting", async () => {
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "https://foobar");
|
||||
});
|
||||
|
||||
it("button to accept terms is disabled if checkbox not checked", async () => {
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("should not disable share button if terms accepted", async () => {
|
||||
mocked(client).getThreePids.mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@email.com",
|
||||
bound: false,
|
||||
added_at: 123,
|
||||
validated_at: 234,
|
||||
},
|
||||
],
|
||||
});
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue(sampleTerms);
|
||||
mocked(client).getAccountData.mockReturnValue(
|
||||
new MatrixEvent({
|
||||
content: { accepted: [sampleTerms.policies["terms"]["en"].url] },
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
const shareButton = await screen.findByRole("button", { name: "Share" });
|
||||
expect(shareButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("should not show invalid terms", async () => {
|
||||
mocked(client).getIdentityServerUrl.mockReturnValue("https://example.com");
|
||||
mocked(client).getTerms.mockResolvedValue(invalidTerms);
|
||||
|
||||
render(<DiscoverySettings />, { wrapper: DiscoveryWrapper });
|
||||
|
||||
expect(await screen.findByText("Let people find you")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { AdvancedPanel } from "../../../../../../src/components/views/settings/encryption/AdvancedPanel";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
||||
|
||||
describe("<AdvancedPanel />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
async function renderAdvancedPanel(onResetIdentityClick = jest.fn()) {
|
||||
const renderResult = render(
|
||||
<AdvancedPanel onResetIdentityClick={onResetIdentityClick} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
// Wait for the device keys to be displayed
|
||||
await waitFor(() => expect(screen.getByText("ed25519")).toBeInTheDocument());
|
||||
return renderResult;
|
||||
}
|
||||
|
||||
describe("<EncryptionDetails />", () => {
|
||||
it("should display a spinner when loading the device keys", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getOwnDeviceKeys").mockImplementation(() => new Promise(() => {}));
|
||||
render(<AdvancedPanel onResetIdentityClick={jest.fn()} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
expect(screen.getByTestId("encryptionDetails")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the device keys", async () => {
|
||||
await renderAdvancedPanel();
|
||||
|
||||
// session id
|
||||
expect(screen.getByText("ABCDEFGHI")).toBeInTheDocument();
|
||||
// session key
|
||||
expect(screen.getByText("ed25519")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("encryptionDetails")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call the onResetIdentityClick callback when the reset cryptographic identity button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onResetIdentityClick = jest.fn();
|
||||
await renderAdvancedPanel(onResetIdentityClick);
|
||||
|
||||
const resetIdentityButton = screen.getByRole("button", { name: "Reset cryptographic identity" });
|
||||
await user.click(resetIdentityButton);
|
||||
|
||||
expect(onResetIdentityClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<OtherSettings />", () => {
|
||||
it("should display the blacklist of unverified devices settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
|
||||
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(true);
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
await renderAdvancedPanel();
|
||||
|
||||
expect(screen.getByTestId("otherSettings")).toMatchSnapshot();
|
||||
const checkbox = screen.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
await user.click(checkbox);
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||
"blacklistUnverifiedDevices",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not display the section when the user can not set the value", async () => {
|
||||
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(false);
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
await renderAdvancedPanel();
|
||||
expect(screen.queryByTestId("otherSettings")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ChangeRecoveryKey } from "../../../../../../src/components/views/settings/encryption/ChangeRecoveryKey";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { copyPlaintext } from "../../../../../../src/utils/strings";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import ErrorDialog from "../../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
import { DeviceListener } from "../../../../../../src/device-listener";
|
||||
|
||||
jest.mock("../../../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("<ChangeRecoveryKey />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
function renderComponent(userHasRecoveryKey = true, onFinish = jest.fn(), onCancelClick = jest.fn()) {
|
||||
return render(
|
||||
<ChangeRecoveryKey
|
||||
userHasRecoveryKey={userHasRecoveryKey}
|
||||
onFinish={onFinish}
|
||||
onCancelClick={onCancelClick}
|
||||
/>,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
}
|
||||
|
||||
describe("flow to set up a recovery key", () => {
|
||||
it("should display information about the recovery key", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onCancelClick = jest.fn();
|
||||
const { asFragment } = renderComponent(false, jest.fn(), onCancelClick);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’.",
|
||||
),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onCancelClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display the recovery key", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onCancelClick = jest.fn();
|
||||
const { asFragment } = renderComponent(false, jest.fn(), onCancelClick);
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
|
||||
expect(screen.getByText("Save your recovery key somewhere safe")).toBeInTheDocument();
|
||||
expect(screen.getByText("encoded private key")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// Test copy button
|
||||
await user.click(screen.getByRole("button", { name: "Copy" }));
|
||||
expect(copyPlaintext).toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onCancelClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should ask the user to enter the recovery key", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(false);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
const { asFragment } = renderComponent(false, onFinish);
|
||||
// Display the recovery key to save
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
// Display the form to confirm the recovery key
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// The finish button should be disabled by default
|
||||
const finishButton = screen.getByRole("button", { name: "Finish set up" });
|
||||
expect(finishButton).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const input = screen.getByTitle("Enter recovery key");
|
||||
// If the user enters an incorrect recovery key, the finish button should be disabled
|
||||
// and we display an error message
|
||||
await userEvent.type(input, "wrong recovery key");
|
||||
expect(finishButton).toHaveAttribute("aria-disabled", "true");
|
||||
expect(screen.getByText("The recovery key you entered is not correct.")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
const setAccountDataSpy = jest.spyOn(matrixClient, "setAccountData");
|
||||
await userEvent.clear(input);
|
||||
// If the user enters the correct recovery key, the finish button should be enabled
|
||||
await userEvent.type(input, "encoded private key");
|
||||
await waitFor(() => expect(finishButton).not.toHaveAttribute("aria-disabled", "true"));
|
||||
|
||||
await user.click(finishButton);
|
||||
expect(setAccountDataSpy).toHaveBeenCalledWith("io.element.recovery", { enabled: true });
|
||||
expect(onFinish).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should reset key backup if needed", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(false, onFinish);
|
||||
// Display the recovery key to save
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
// Display the form to confirm the recovery key
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||
|
||||
const finishButton = screen.getByRole("button", { name: "Finish set up" });
|
||||
|
||||
const input = screen.getByTitle("Enter recovery key");
|
||||
|
||||
// If the user enters the correct recovery key, the finish button should be enabled
|
||||
await userEvent.type(input, "encoded private key");
|
||||
|
||||
await user.click(finishButton);
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not reset key backup if not needed", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(false);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(false, onFinish);
|
||||
// Display the recovery key to save
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
// Display the form to confirm the recovery key
|
||||
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||
|
||||
const finishButton = screen.getByRole("button", { name: "Finish set up" });
|
||||
|
||||
const input = screen.getByTitle("Enter recovery key");
|
||||
|
||||
// If the user enters the correct recovery key, the finish button should be enabled
|
||||
await userEvent.type(input, "encoded private key");
|
||||
|
||||
await user.click(finishButton);
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display errors from bootstrapSecretStorage", async () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockReturnValue(undefined);
|
||||
mocked(matrixClient.getCrypto()!).bootstrapSecretStorage.mockRejectedValue(new Error("can't bootstrap"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { getByRole, getByText, getByTitle } = renderComponent(false);
|
||||
|
||||
// Display the recovery key to save
|
||||
await waitFor(() => user.click(getByRole("button", { name: "Continue" })));
|
||||
// Display the form to confirm the recovery key
|
||||
await waitFor(() => user.click(getByRole("button", { name: "Continue" })));
|
||||
|
||||
await waitFor(() => expect(getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||
|
||||
const finishButton = getByRole("button", { name: "Finish set up" });
|
||||
const input = getByTitle("Enter recovery key");
|
||||
await userEvent.type(input, "encoded private key");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(finishButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
await user.click(finishButton);
|
||||
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||
title: "Failed to set up secret storage",
|
||||
description: "Error: can't bootstrap",
|
||||
});
|
||||
await waitFor(() => user.click(getByRole("button", { name: "OK" })));
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to set up secret storage:",
|
||||
new Error("can't bootstrap"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should disallow repeated attempts to change the recovery key", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(false);
|
||||
|
||||
const mockFn = mocked(matrixClient.getCrypto()!).bootstrapSecretStorage.mockImplementation(() => {
|
||||
// Pretend to do some work.
|
||||
return new Promise((r) => setTimeout(r, 200));
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { getByRole, getByText, getByTitle } = renderComponent(false);
|
||||
|
||||
// Display the recovery key to save
|
||||
await waitFor(() => user.click(getByRole("button", { name: "Continue" })));
|
||||
// Display the form to confirm the recovery key
|
||||
await waitFor(() => user.click(getByRole("button", { name: "Continue" })));
|
||||
|
||||
await waitFor(() => expect(getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||
|
||||
const finishButton = getByRole("button", { name: "Finish set up" });
|
||||
const input = getByTitle("Enter recovery key");
|
||||
await userEvent.type(input, "encoded private key");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(finishButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(finishButton);
|
||||
await user.click(finishButton);
|
||||
await user.click(finishButton);
|
||||
|
||||
await waitFor(() => expect(mockFn).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe("flow to change the recovery key", () => {
|
||||
it("should display the recovery key", async () => {
|
||||
const { asFragment } = renderComponent();
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Change recovery key?")).toBeInTheDocument());
|
||||
expect(screen.getByText("encoded private key")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* 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 type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { DeleteKeyStoragePanel } from "../../../../../../src/components/views/settings/encryption/DeleteKeyStoragePanel";
|
||||
import { useKeyStoragePanelViewModel } from "../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel", () => ({
|
||||
useKeyStoragePanelViewModel: jest
|
||||
.fn()
|
||||
.mockReturnValue({ setEnabled: jest.fn(), isEnabled: true, loading: false, busy: false }),
|
||||
}));
|
||||
|
||||
describe("<DeleteKeyStoragePanel />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
const { asFragment } = render(
|
||||
<DeleteKeyStoragePanel onFinish={() => {}} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onFinished when cancel pressed", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call disable key storage when confirm pressed", async () => {
|
||||
const setEnabled = jest.fn();
|
||||
|
||||
mocked(useKeyStoragePanelViewModel).mockReturnValue({
|
||||
setEnabled,
|
||||
isEnabled: true,
|
||||
loading: false,
|
||||
busy: false,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Delete key storage" }));
|
||||
|
||||
expect(setEnabled).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should wait with button disabled while setEnabled runs", async () => {
|
||||
const setEnabledDefer = Promise.withResolvers<void>();
|
||||
|
||||
mocked(useKeyStoragePanelViewModel).mockReturnValue({
|
||||
setEnabled: jest.fn().mockReturnValue(setEnabledDefer.promise),
|
||||
isEnabled: true,
|
||||
loading: false,
|
||||
busy: false,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Delete key storage" }));
|
||||
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("button", { name: "Delete key storage" })).toHaveAttribute("aria-disabled", "true");
|
||||
setEnabledDefer.resolve();
|
||||
await waitFor(() => expect(onFinish).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
|
||||
|
||||
import { EncryptionCard } from "../../../../../../src/components/views/settings/encryption/EncryptionCard";
|
||||
|
||||
describe("<EncryptionCard />", () => {
|
||||
it("should render", () => {
|
||||
const { asFragment } = render(
|
||||
<EncryptionCard Icon={KeyIcon} title="My title" description="My description">
|
||||
Encryption card children
|
||||
</EncryptionCard>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { RecoveryPanel } from "../../../../../../src/components/views/settings/encryption/RecoveryPanel";
|
||||
|
||||
describe("<RecoveryPanel />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
function renderRecoverPanel(onChangeRecoveryKeyClick = jest.fn()) {
|
||||
return render(
|
||||
<RecoveryPanel onChangeRecoveryKeyClick={onChangeRecoveryKeyClick} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
}
|
||||
|
||||
it("should be in loading state when checking the recovery key and the cached keys", () => {
|
||||
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const { asFragment } = renderRecoverPanel();
|
||||
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should ask to set up a recovery key when there is no recovery key", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onChangeRecoveryKeyClick = jest.fn();
|
||||
const { asFragment } = renderRecoverPanel(onChangeRecoveryKeyClick);
|
||||
|
||||
await waitFor(() => screen.getByRole("button", { name: "Set up recovery" }));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Set up recovery" }));
|
||||
expect(onChangeRecoveryKeyClick).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should allow to change the recovery key when everything is good", async () => {
|
||||
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||
privateKeysInSecretStorage: true,
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: true,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onChangeRecoveryKeyClick = jest.fn();
|
||||
const { asFragment } = renderRecoverPanel(onChangeRecoveryKeyClick);
|
||||
await waitFor(() => screen.getByRole("button", { name: "Change recovery key" }));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Change recovery key" }));
|
||||
expect(onChangeRecoveryKeyClick).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* 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 { mocked } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RecoveryPanelOutOfSync } from "../../../../../../src/components/views/settings/encryption/RecoveryPanelOutOfSync";
|
||||
import { AccessCancelledError, accessSecretStorage } from "../../../../../../src/SecurityManager";
|
||||
import { DeviceListener } from "../../../../../../src/device-listener";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
jest.mock("../../../../../../src/SecurityManager", () => {
|
||||
const originalModule = jest.requireActual("../../../../../../src/SecurityManager");
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
accessSecretStorage: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<RecoveyPanelOutOfSync />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
function renderComponent(
|
||||
onFinish = jest.fn(),
|
||||
onForgotRecoveryKey = jest.fn(),
|
||||
onAccessSecretStorageFailed = jest.fn(),
|
||||
) {
|
||||
matrixClient = createTestClient();
|
||||
return render(
|
||||
<RecoveryPanelOutOfSync
|
||||
onFinish={onFinish}
|
||||
onForgotRecoveryKey={onForgotRecoveryKey}
|
||||
onAccessSecretStorageFailed={onAccessSecretStorageFailed}
|
||||
/>,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const { asFragment } = renderComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onForgotRecoveryKey when the 'Forgot recovery key?' is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onForgotRecoveryKey = jest.fn();
|
||||
renderComponent(jest.fn(), onForgotRecoveryKey);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Forgot recovery key?" }));
|
||||
expect(onForgotRecoveryKey).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should access to 4S and call onFinish when 'Enter recovery key' is clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(false);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
return await func();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(onFinish);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reset key backup if needed", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
return await func();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(onFinish);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onAccessSecretStorageFailed on failure", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
throw new Error("Error");
|
||||
});
|
||||
|
||||
const onAccessSecretStorageFailed = jest.fn();
|
||||
renderComponent(jest.fn(), jest.fn(), onAccessSecretStorageFailed);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onAccessSecretStorageFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call onAccessSecretStorageFailed when cancelled", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
throw new AccessCancelledError();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
const onAccessSecretStorageFailed = jest.fn();
|
||||
renderComponent(onFinish, jest.fn(), onAccessSecretStorageFailed);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
expect(onAccessSecretStorageFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { ResetIdentityPanel } from "../../../../../../src/components/views/settings/encryption/ResetIdentityPanel";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
describe("<ResetIdentityPanel />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
it("should reset the encryption when the continue button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onReset = jest.fn();
|
||||
const { asFragment } = render(
|
||||
<ResetIdentityPanel variant="compromised" onReset={onReset} onCancelClick={jest.fn()} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// We need to pause the reset so that we can check that it's providing
|
||||
// feedback to the user that something is happening.
|
||||
const { promise: resetEncryptionPromise, resolve: resolveResetEncryption } = Promise.withResolvers<void>();
|
||||
jest.spyOn(matrixClient.getCrypto()!, "resetEncryption").mockReturnValue(resetEncryptionPromise);
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: "Continue" });
|
||||
await user.click(continueButton);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
resolveResetEncryption!();
|
||||
await sleep(0);
|
||||
|
||||
expect(matrixClient.getCrypto()!.resetEncryption).toHaveBeenCalled();
|
||||
expect(onReset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display the 'forgot recovery key' variant correctly", async () => {
|
||||
const onReset = jest.fn();
|
||||
const { asFragment } = render(
|
||||
<ResetIdentityPanel variant="forgot" onReset={onReset} onCancelClick={jest.fn()} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the 'sync failed' variant correctly", async () => {
|
||||
const onReset = jest.fn();
|
||||
const { asFragment } = render(
|
||||
<ResetIdentityPanel variant="sync_failed" onReset={onReset} onCancelClick={jest.fn()} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<AdvancedPanel /> <EncryptionDetails /> should display a spinner when loading the device keys 1`] = `
|
||||
<div
|
||||
class="mx_EncryptionDetails"
|
||||
data-testid="encryptionDetails"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionDetails_session"
|
||||
>
|
||||
<h3
|
||||
class="mx_EncryptionDetails_session_title"
|
||||
>
|
||||
Encryption details
|
||||
</h3>
|
||||
<div>
|
||||
<span>
|
||||
Session ID:
|
||||
</span>
|
||||
<span
|
||||
data-testid="deviceId"
|
||||
>
|
||||
ABCDEFGHI
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Session key:
|
||||
</span>
|
||||
<span
|
||||
data-testid="sessionKey"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionDetails_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 15.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-3.6-3.6a.95.95 0 0 1-.275-.7q0-.425.275-.7.274-.275.712-.288t.713.263L11 12.15V5q0-.424.287-.713A.97.97 0 0 1 12 4q.424 0 .713.287Q13 4.576 13 5v7.15l1.875-1.875q.274-.274.713-.263.437.014.712.288a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-3.6 3.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063M6 20q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
Export keys
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 16a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 15V7.85L9.125 9.725q-.3.3-.7.3T7.7 9.7a.93.93 0 0 1-.288-.713A.98.98 0 0 1 7.7 8.3l3.6-3.6q.15-.15.325-.213.175-.062.375-.062t.375.062a.9.9 0 0 1 .325.213l3.6 3.6q.3.3.287.712a.98.98 0 0 1-.287.688q-.3.3-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15q0 .424-.287.713A.97.97 0 0 1 12 16m-6 4q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
Import keys
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Reset cryptographic identity
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<AdvancedPanel /> <EncryptionDetails /> should display the device keys 1`] = `
|
||||
<div
|
||||
class="mx_EncryptionDetails"
|
||||
data-testid="encryptionDetails"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionDetails_session"
|
||||
>
|
||||
<h3
|
||||
class="mx_EncryptionDetails_session_title"
|
||||
>
|
||||
Encryption details
|
||||
</h3>
|
||||
<div>
|
||||
<span>
|
||||
Session ID:
|
||||
</span>
|
||||
<span
|
||||
data-testid="deviceId"
|
||||
>
|
||||
ABCDEFGHI
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Session key:
|
||||
</span>
|
||||
<span
|
||||
data-testid="sessionKey"
|
||||
>
|
||||
ed25519
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionDetails_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 15.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-3.6-3.6a.95.95 0 0 1-.275-.7q0-.425.275-.7.274-.275.712-.288t.713.263L11 12.15V5q0-.424.287-.713A.97.97 0 0 1 12 4q.424 0 .713.287Q13 4.576 13 5v7.15l1.875-1.875q.274-.274.713-.263.437.014.712.288a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-3.6 3.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063M6 20q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
Export keys
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 16a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 15V7.85L9.125 9.725q-.3.3-.7.3T7.7 9.7a.93.93 0 0 1-.288-.713A.98.98 0 0 1 7.7 8.3l3.6-3.6q.15-.15.325-.213.175-.062.375-.062t.375.062a.9.9 0 0 1 .325.213l3.6 3.6q.3.3.287.712a.98.98 0 0 1-.287.688q-.3.3-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15q0 .424-.287.713A.97.97 0 0 1 12 16m-6 4q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
Import keys
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Reset cryptographic identity
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<AdvancedPanel /> <OtherSettings /> should display the blacklist of unverified devices settings 1`] = `
|
||||
<form
|
||||
class="_root_19upo_16 mx_OtherSettings"
|
||||
data-testid="otherSettings"
|
||||
>
|
||||
<h3
|
||||
class="mx_OtherSettings_title"
|
||||
>
|
||||
Other people’s devices
|
||||
</h3>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_7_"
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="radix-_r_6_"
|
||||
name="neverSendEncrypted"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_6_"
|
||||
>
|
||||
In encrypted rooms, only send messages to verified users
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_7_"
|
||||
>
|
||||
Warning: users who have not explicitly verified with you (e.g. using emoji) will not receive your encrypted messages. Also, unverified devices of verified users will not receive your encrypted messages. Changes require an application restart to take effect.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
+782
@@ -0,0 +1,782 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<ChangeRecoveryKey /> flow to change the recovery key should display the recovery key 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Change recovery key
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Change recovery key?
|
||||
</h2>
|
||||
<span>
|
||||
Write down this new recovery key somewhere safe. Then click Continue to confirm the change.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_KeyPanel"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60"
|
||||
>
|
||||
Recovery key
|
||||
</span>
|
||||
<div>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 mx_KeyPanel_key"
|
||||
data-testid="recoveryKey"
|
||||
>
|
||||
encoded private key
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
Do not share this with anyone!
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Copy"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="M14 5H5v9h1a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1a1 1 0 1 1-2 0z"
|
||||
/>
|
||||
<path
|
||||
d="M8 10a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2zm2 0v9h9v-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ChangeRecoveryKey /> flow to set up a recovery key should ask the user to enter the recovery key 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Set up recovery
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Enter your recovery key to confirm
|
||||
</h2>
|
||||
<span>
|
||||
Enter the recovery key shown on the previous screen to finish setting up recovery.
|
||||
</span>
|
||||
</div>
|
||||
<form
|
||||
class="_root_19upo_16 mx_KeyForm"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
Enter recovery key
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8 mx_KeyForm_password mx_no_textinput"
|
||||
id="_r_1_"
|
||||
>
|
||||
<input
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
id="radix-_r_0_"
|
||||
name="recoveryKey"
|
||||
required=""
|
||||
title="Enter recovery key"
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
aria-controls="_r_1_"
|
||||
aria-labelledby="_r_2_"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m16.1 13.3-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.424-.2.863-.3A4.2 4.2 0 0 1 12 7q1.875 0 3.188 1.312Q16.5 9.625 16.5 11.5q0 .5-.1.938t-.3.862m3.2 3.15-1.45-1.4a11 11 0 0 0 1.688-1.588A9 9 0 0 0 20.8 11.5q-1.25-2.524-3.588-4.013Q14.875 6 12 6q-.724 0-1.425.1a10 10 0 0 0-1.375.3L7.65 4.85A11.1 11.1 0 0 1 12 4q3.575 0 6.425 1.887T22.7 10.8a.8.8 0 0 1 .1.313q.025.188.025.387a2 2 0 0 1-.125.7 10.9 10.9 0 0 1-3.4 4.25m-.2 5.45-3.5-3.45q-.874.274-1.762.413Q12.95 19 12 19q-3.575 0-6.425-1.887T1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.763.8.8 0 0 1 .1-.3Q1.825 9.7 2.55 8.75A13.3 13.3 0 0 1 4.15 7L2.075 4.9a.93.93 0 0 1-.275-.688q0-.412.3-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275l17 17q.275.275.288.688a.93.93 0 0 1-.288.712.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275M5.55 8.4q-.725.65-1.325 1.425A9 9 0 0 0 3.2 11.5q1.25 2.524 3.588 4.012T12 17q.5 0 .975-.062.475-.063.975-.138l-.9-.95q-.274.075-.525.113A3.5 3.5 0 0 1 12 16q-1.875 0-3.187-1.312Q7.5 13.375 7.5 11.5q0-.274.038-.525.037-.25.112-.525z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Finish set up
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ChangeRecoveryKey /> flow to set up a recovery key should ask the user to enter the recovery key 2`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Set up recovery
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Enter your recovery key to confirm
|
||||
</h2>
|
||||
<span>
|
||||
Enter the recovery key shown on the previous screen to finish setting up recovery.
|
||||
</span>
|
||||
</div>
|
||||
<form
|
||||
class="_root_19upo_16 mx_KeyForm"
|
||||
>
|
||||
<div
|
||||
class="_field_19upo_26"
|
||||
data-invalid="true"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
data-invalid="true"
|
||||
for="radix-_r_0_"
|
||||
>
|
||||
Enter recovery key
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8 mx_KeyForm_password mx_no_textinput"
|
||||
id="_r_1_"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_8_"
|
||||
aria-invalid="true"
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
data-invalid="true"
|
||||
id="radix-_r_0_"
|
||||
name="recoveryKey"
|
||||
required=""
|
||||
title="Enter recovery key"
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
aria-controls="_r_1_"
|
||||
aria-labelledby="_r_2_"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m16.1 13.3-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.424-.2.863-.3A4.2 4.2 0 0 1 12 7q1.875 0 3.188 1.312Q16.5 9.625 16.5 11.5q0 .5-.1.938t-.3.862m3.2 3.15-1.45-1.4a11 11 0 0 0 1.688-1.588A9 9 0 0 0 20.8 11.5q-1.25-2.524-3.588-4.013Q14.875 6 12 6q-.724 0-1.425.1a10 10 0 0 0-1.375.3L7.65 4.85A11.1 11.1 0 0 1 12 4q3.575 0 6.425 1.887T22.7 10.8a.8.8 0 0 1 .1.313q.025.188.025.387a2 2 0 0 1-.125.7 10.9 10.9 0 0 1-3.4 4.25m-.2 5.45-3.5-3.45q-.874.274-1.762.413Q12.95 19 12 19q-3.575 0-6.425-1.887T1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.763.8.8 0 0 1 .1-.3Q1.825 9.7 2.55 8.75A13.3 13.3 0 0 1 4.15 7L2.075 4.9a.93.93 0 0 1-.275-.688q0-.412.3-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275l17 17q.275.275.288.688a.93.93 0 0 1-.288.712.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275M5.55 8.4q-.725.65-1.325 1.425A9 9 0 0 0 3.2 11.5q1.25 2.524 3.588 4.012T12 17q.5 0 .975-.062.475-.063.975-.138l-.9-.95q-.274.075-.525.113A3.5 3.5 0 0 1 12 16q-1.875 0-3.187-1.312Q7.5 13.375 7.5 11.5q0-.274.038-.525.037-.25.112-.525z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _error-message_19upo_95"
|
||||
id="radix-_r_8_"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
The recovery key you entered is not correct.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Finish set up
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ChangeRecoveryKey /> flow to set up a recovery key should display information about the recovery key 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Set up recovery
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Set up recovery
|
||||
</h2>
|
||||
<span>
|
||||
Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’.
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 mx_InformationPanel_description"
|
||||
>
|
||||
After clicking continue, we’ll generate a recovery key for you.
|
||||
</span>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ChangeRecoveryKey /> flow to set up a recovery key should display the recovery key 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Set up recovery
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Save your recovery key somewhere safe
|
||||
</h2>
|
||||
<span>
|
||||
Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_KeyPanel"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60"
|
||||
>
|
||||
Recovery key
|
||||
</span>
|
||||
<div>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 mx_KeyPanel_key"
|
||||
data-testid="recoveryKey"
|
||||
>
|
||||
encoded private key
|
||||
</span>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
Do not share this with anyone!
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Copy"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="M14 5H5v9h1a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1a1 1 0 1 1-2 0z"
|
||||
/>
|
||||
<path
|
||||
d="M8 10a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2zm2 0v9h9v-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<DeleteKeyStoragePanel /> should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Delete key storage
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8 _destructive_1r8kr_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Are you sure you want to turn off key storage and delete it?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:
|
||||
<ul
|
||||
class="_visual-list_1mgc1_8"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-destructive_1nc1y_26"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
You will not have encrypted message history on new devices
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-destructive_1nc1y_26"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
You will lose access to your encrypted messages if you are signed out of Element everywhere
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Delete key storage
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<EncryptionCard /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.475 16.9A5.86 5.86 0 0 1 7 18q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.026 0 3.537 1.15Q12.05 8.3 12.65 10h7.95q.2 0 .387.075.189.075.338.225l1.025 1.025a1 1 0 0 1 .2.288q.075.162.075.362t-.062.375a1.1 1.1 0 0 1-.188.325l-2.25 2.575a.97.97 0 0 1-1.038.313 1 1 0 0 1-.337-.188L17 14l-1.3 1.3q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L13 14h-.35a5.8 5.8 0 0 1-2.175 2.9m-4.887-3.487Q6.175 14 7 14q.824 0 1.412-.588Q9 12.826 9 12t-.588-1.412A1.93 1.93 0 0 0 7 10q-.824 0-1.412.588A1.93 1.93 0 0 0 5 12q0 .825.588 1.412"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
My title
|
||||
</h2>
|
||||
<span>
|
||||
My description
|
||||
</span>
|
||||
</div>
|
||||
Encryption card children
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<RecoveryPanel /> should allow to change the recovery key when everything is good 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||
data-testid="recoveryPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
|
||||
/>
|
||||
</svg>
|
||||
Change recovery key
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no recovery key 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||
data-testid="recoveryPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader mx_SettingsHeader_recommended"
|
||||
>
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
|
||||
/>
|
||||
</svg>
|
||||
Set up recovery
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RecoveryPanel /> should be in loading state when checking the recovery key and the cached keys 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||
data-testid="recoveryPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
</h2>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
</div>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<RecoveyPanelOutOfSync /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||
data-testid="recoveryPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Recovery
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
>
|
||||
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||
<span
|
||||
class="mx_SettingsSubheader_error"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
Your key storage is out of sync. Click one of the buttons below to fix the problem.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RecoveryPanelOutOfSync"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Forgot recovery key?
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 5 12q0-.825.588-1.412A1.93 1.93 0 0 1 7 10q.824 0 1.412.588Q9 11.175 9 12t-.588 1.412A1.93 1.93 0 0 1 7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.676 0 3.037.825A6.2 6.2 0 0 1 12.2 9h8.375q.2 0 .387.075.188.075.338.225l2 2q.15.15.212.325.063.175.063.375t-.062.375a.9.9 0 0 1-.213.325l-3.175 3.175a1 1 0 0 1-.3.2q-.175.075-.35.1a.8.8 0 0 1-.35-.025.9.9 0 0 1-.325-.175L17.5 15l-1.425 1.075a.95.95 0 0 1-.887.15.9.9 0 0 1-.288-.15L13.375 15H12.2a6.2 6.2 0 0 1-2.162 2.175Q8.675 18 7 18m0-2q1.4 0 2.463-.85A4.03 4.03 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.03 4.03 0 0 0-1.412-2.15Q8.4 8 7 8 5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
|
||||
/>
|
||||
</svg>
|
||||
Enter recovery key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+755
@@ -0,0 +1,755 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Reset encryption
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8 _destructive_1r8kr_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Forgot your recovery key? You’ll need to reset your identity.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_1mgc1_8"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-success_1nc1y_22"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
Your account details, contacts, preferences, and chat list will be kept
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will lose any message history that’s stored only on the server
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will need to verify all your existing devices and contacts again
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ResetIdentityPanel /> should display the 'sync failed' variant correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Reset encryption
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8 _destructive_1r8kr_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Failed to sync key storage. You need to reset your identity.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_1mgc1_8"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-success_1nc1y_22"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
Your account details, contacts, preferences, and chat list will be kept
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will lose any message history that’s stored only on the server
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will need to verify all your existing devices and contacts again
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ResetIdentityPanel /> should reset the encryption when the continue button is clicked 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Reset encryption
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8 _destructive_1r8kr_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Are you sure you want to reset your identity?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_1mgc1_8"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-success_1nc1y_22"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
Your account details, contacts, preferences, and chat list will be kept
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will lose any message history that’s stored only on the server
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will need to verify all your existing devices and contacts again
|
||||
</li>
|
||||
</ul>
|
||||
<span>
|
||||
Only do this if you believe your account has been compromised.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ResetIdentityPanel /> should reset the encryption when the continue button is clicked 2`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_1xygz_8"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
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="m13.3 17.3-4.6-4.6a.9.9 0 0 1-.213-.325A1.1 1.1 0 0 1 8.425 12q0-.2.062-.375A.9.9 0 0 1 8.7 11.3l4.6-4.6a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7L10.8 12l3.9 3.9a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_1xygz_17"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_1xygz_30"
|
||||
>
|
||||
Reset encryption
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_1r8kr_8 _destructive_1r8kr_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Are you sure you want to reset your identity?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_1mgc1_8"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17 _visual-list-item-icon-success_1nc1y_22"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
Your account details, contacts, preferences, and chat list will be kept
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will lose any message history that’s stored only on the server
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_1nc1y_8"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_1nc1y_17"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
You will need to verify all your existing devices and contacts again
|
||||
</li>
|
||||
</ul>
|
||||
<span>
|
||||
Only do this if you believe your account has been compromised.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="_button_13vu4_8 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Reset in progress...
|
||||
</button>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_ResetIdentityPanel_warning"
|
||||
>
|
||||
Do not close this window until the reset is finished
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
+784
@@ -0,0 +1,784 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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,
|
||||
type IPushRules,
|
||||
type MatrixClient,
|
||||
NotificationCountType,
|
||||
PushRuleKind,
|
||||
Room,
|
||||
RuleId,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import { Form } from "@vector-im/compound-web";
|
||||
|
||||
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";
|
||||
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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await waitForUpdate();
|
||||
expect(screen.container).toMatchSnapshot();
|
||||
|
||||
const globalMute = screen.getByLabelText(labelGlobalMute);
|
||||
expect(globalMute).toBeDisabled();
|
||||
|
||||
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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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], { addToState: true });
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<Form.Root>
|
||||
<NotificationSettings2 />
|
||||
</Form.Root>
|
||||
</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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+2334
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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+30
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactElement } from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import SettingsTab, { type 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();
|
||||
});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<SettingsTab /> renders tab 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsTab"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsTab_sections"
|
||||
>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+164
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, type RenderResult, screen } from "jest-matrix-react";
|
||||
import { type MatrixClient, type 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+53
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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();
|
||||
});
|
||||
});
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, type RenderResult, screen, within } from "jest-matrix-react";
|
||||
import { type 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 { type RoomEchoChamber } from "../../../../../../../src/stores/local-echo/RoomEchoChamber";
|
||||
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
|
||||
|
||||
describe("NotificationSettingsTab", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SettingsStore.reset();
|
||||
});
|
||||
|
||||
it("should prevent »Settings« link click from bubbling up to radio buttons", async () => {
|
||||
const tab = renderTab();
|
||||
|
||||
// settings link of mentions_only volume
|
||||
const settingsLink = within(tab.getByText("@mentions & keywords")).getByRole("button", { name: "settings" });
|
||||
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");
|
||||
});
|
||||
});
|
||||
+210
@@ -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 OR LicenseRef-Element-Commercial
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, getByRole, render, type RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
type MatrixClient,
|
||||
EventType,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
type ISendEventResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked } from "jest-mock";
|
||||
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 SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
||||
import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types";
|
||||
|
||||
describe("RolesRoomSettingsTab", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:example.com";
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderTab = async (propRoom: Room = room): Promise<RenderResult> => {
|
||||
const renderResult = render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
|
||||
// Wait for the tab to be ready
|
||||
await waitFor(() => expect(screen.getByText("Permissions")).toBeInTheDocument());
|
||||
return renderResult;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
room = mkStubRoom(roomId, "test room", cli);
|
||||
});
|
||||
|
||||
it("should allow an Admin to demote themselves but not others", async () => {
|
||||
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 } = await renderTab();
|
||||
|
||||
expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled();
|
||||
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
|
||||
});
|
||||
|
||||
describe("Element Call", () => {
|
||||
const setGroupCallsEnabled = (val: boolean): void => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
||||
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", async () => {
|
||||
expect(getJoinCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
|
||||
});
|
||||
|
||||
it("can change joining calls power level", async () => {
|
||||
const tab = await renderTab();
|
||||
|
||||
fireEvent.change(getJoinCallSelect(tab), {
|
||||
target: { value: 0 },
|
||||
});
|
||||
|
||||
expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default");
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
|
||||
events: {
|
||||
[ElementCallMemberEventType.name]: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Start Element calls", () => {
|
||||
it("defaults to moderator for starting calls", async () => {
|
||||
expect(getStartCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
|
||||
});
|
||||
|
||||
it("can change starting calls power level", async () => {
|
||||
const tab = await renderTab();
|
||||
|
||||
fireEvent.change(getStartCallSelect(tab), {
|
||||
target: { value: 0 },
|
||||
});
|
||||
|
||||
expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default");
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
|
||||
events: {
|
||||
[ElementCallEventType.name]: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("hides when group calls disabled", async () => {
|
||||
setGroupCallsEnabled(false);
|
||||
|
||||
const tab = await 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 = Promise.withResolvers<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,
|
||||
// needs at least one remaning admin in the room if we want to demote our user
|
||||
// otherwise another modal will be displayed
|
||||
["@admin:server"]: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
|
||||
mocked(room.getMember).mockReturnValue({ powerLevel: 100 } as any);
|
||||
const { container } = await 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"));
|
||||
});
|
||||
|
||||
it("should allow changing events power levels", async () => {
|
||||
mocked(cli.sendStateEvent).mockResolvedValue({ event_id: "$eventId" });
|
||||
mocked(cli.getRoom).mockReturnValue(room);
|
||||
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
|
||||
const { container } = await renderTab();
|
||||
|
||||
const selector = container.querySelector(`[placeholder="Change topic"]`)!;
|
||||
fireEvent.change(selector, { target: { value: "0" } });
|
||||
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
"m.room.power_levels",
|
||||
expect.objectContaining({
|
||||
events: expect.objectContaining({
|
||||
"m.room.topic": 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow changing top level power levels", async () => {
|
||||
mocked(cli.sendStateEvent).mockResolvedValue({ event_id: "$eventId" });
|
||||
mocked(cli.getRoom).mockReturnValue(room);
|
||||
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
|
||||
const { container } = await renderTab();
|
||||
|
||||
const selector = container.querySelector(`[placeholder="Remove users"]`)!;
|
||||
fireEvent.change(selector, { target: { value: "0" } });
|
||||
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
"m.room.power_levels",
|
||||
expect.objectContaining({
|
||||
kick: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
+584
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 { mocked } from "jest-mock";
|
||||
import { type RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
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, stubClient } from "../../../../../../test-utils";
|
||||
import { filterBoolean } from "../../../../../../../src/utils/arrays";
|
||||
|
||||
describe("<SecurityRoomSettingsTab />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const client = mocked(stubClient());
|
||||
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" });
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room, JoinRule.Invite);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
||||
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 changing room to private with world_readable history visiblity", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomHistoryVisibility,
|
||||
{
|
||||
history_visibility: HistoryVisibility.Shared,
|
||||
},
|
||||
"",
|
||||
);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomJoinRules,
|
||||
{
|
||||
join_rule: JoinRule.Invite,
|
||||
},
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't change room to private when user lacks permissions for history visibility", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
users: { [userId]: 50 },
|
||||
state_default: 50,
|
||||
events: {
|
||||
[EventType.RoomJoinRules]: 50,
|
||||
[EventType.RoomHistoryVisibility]: 100,
|
||||
},
|
||||
} as RoomPowerLevelsEventContent,
|
||||
sender: userId,
|
||||
state_key: "",
|
||||
room_id: room.roomId,
|
||||
}),
|
||||
]);
|
||||
|
||||
getComponent(room);
|
||||
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
||||
await flushPromises();
|
||||
// Ensure we don't make any changes
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't change room to private when history visibility change fails", async () => {
|
||||
client.sendStateEvent.mockRejectedValue("Failed");
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
||||
|
||||
getComponent(room);
|
||||
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
||||
await flushPromises();
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomHistoryVisibility,
|
||||
{
|
||||
history_visibility: HistoryVisibility.Shared,
|
||||
},
|
||||
"",
|
||||
);
|
||||
// Ensure we don't make any changes
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomJoinRules,
|
||||
{
|
||||
join_rule: JoinRule.Invite,
|
||||
},
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
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")).not.toBeChecked();
|
||||
});
|
||||
|
||||
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")).toBeChecked();
|
||||
|
||||
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")).not.toBeChecked();
|
||||
|
||||
await flushPromises();
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(logger.error).toHaveBeenCalledWith("oups");
|
||||
|
||||
// toggle reset to old value
|
||||
expect(screen.getByLabelText("Enable guest access")).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
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", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() =>
|
||||
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);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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");
|
||||
});
|
||||
|
||||
it("maps 'joined' history visibility to 'invited' for display", () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.Joined);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
// Should display as 'invited' even though underlying value is 'joined'
|
||||
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
|
||||
// Should not have a 'joined' option visible
|
||||
expect(screen.queryByDisplayValue(HistoryVisibility.Joined)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'invited' option for non-public rooms", () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Invite);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'invited' option for encrypted rooms even if public", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room, JoinRule.Public);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("does not show 'invited' option for public unencrypted rooms unless selected", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.Shared);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.queryByDisplayValue(HistoryVisibility.Invited)).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("shows 'world_readable' option for public unencrypted rooms", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room, JoinRule.Public);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("does not show 'world_readable' option for private encrypted rooms unless selected", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room, JoinRule.Invite);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("always shows 'shared' option", () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room);
|
||||
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryption", () => {
|
||||
it("displays encryption as enabled", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
||||
// can't disable encryption once enabled
|
||||
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("asks users to confirm when setting room to encrypted", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => 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);
|
||||
|
||||
await waitFor(() => 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);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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", async () => {
|
||||
// rooms that are already encrypted still show encrypted
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
||||
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
||||
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays unencrypted rooms with toggle disabled", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
|
||||
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
||||
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+135
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, type RenderResult, waitFor } from "jest-matrix-react";
|
||||
import { type MatrixClient, type Room, type 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 { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types";
|
||||
|
||||
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.getByLabelText("Enable Element Call as an additional calling option in this room")!;
|
||||
};
|
||||
|
||||
describe("correct state", () => {
|
||||
it("shows enabled when call member power level is 0", () => {
|
||||
mockPowerLevels({ [ElementCallMemberEventType.name]: 0 });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
expect(getElementCallSwitch(tab)).toBeChecked();
|
||||
});
|
||||
|
||||
it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => {
|
||||
mockPowerLevels({ [ElementCallMemberEventType.name]: level });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
expect(getElementCallSwitch(tab)).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe("enabling/disabling", () => {
|
||||
describe("enabling Element calls", () => {
|
||||
beforeEach(() => {
|
||||
mockPowerLevels({ [ElementCallMemberEventType.name]: 100 });
|
||||
});
|
||||
|
||||
it("enables Element calls in public room", async () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab));
|
||||
await waitFor(() =>
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCallEventType.name]: 50,
|
||||
[ElementCallMemberEventType.name]: 0,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("enables Element calls in private room", async () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab));
|
||||
await waitFor(() =>
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCallEventType.name]: 0,
|
||||
[ElementCallMemberEventType.name]: 0,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("disables Element calls", async () => {
|
||||
mockPowerLevels({ [ElementCallMemberEventType.name]: 0 });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab));
|
||||
await waitFor(() =>
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCallEventType.name]: 100,
|
||||
[ElementCallMemberEventType.name]: 100,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14 5H5v9h1a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1a1 1 0 1 1-2 0z"
|
||||
/>
|
||||
<path
|
||||
d="M8 10a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2zm2 0v9h9v-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</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>
|
||||
`;
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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_zysgz_8 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_zysgz_43"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Approve"
|
||||
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary"
|
||||
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="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</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_zysgz_8 mx_BaseAvatar mx_PeopleRoomSettingsTab_avatar _avatar-imageless_zysgz_55"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Approve"
|
||||
class="mx_AccessibleButton mx_PeopleRoomSettingsTab_action mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon_primary"
|
||||
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="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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>
|
||||
`;
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
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"
|
||||
>
|
||||
<span>
|
||||
Changes won't affect past messages, only new ones.
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/en/help#e2ee-history-sharing"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<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 since invited
|
||||
</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 (full history)
|
||||
</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"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</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"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
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 { type MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type 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 { type 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("deactivate 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 finishedDeferred = Promise.withResolvers<[boolean]>();
|
||||
const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise });
|
||||
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 finishedDeferred = Promise.withResolvers<[boolean]>();
|
||||
const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise });
|
||||
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
||||
|
||||
render(getComponent());
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
|
||||
|
||||
finishedDeferred.resolve([true]);
|
||||
await flushPromises();
|
||||
|
||||
expect(defaultProps.closeSettingsFn).toHaveBeenCalled();
|
||||
});
|
||||
it("should not close settings if account not deactivated", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === UIFeature.Deactivate,
|
||||
);
|
||||
|
||||
const finishedDeferred = Promise.withResolvers<[boolean]>();
|
||||
const createDialogFn = jest.fn().mockReturnValue({ finished: finishedDeferred.promise });
|
||||
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
||||
|
||||
render(getComponent());
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
|
||||
|
||||
finishedDeferred.resolve([false]);
|
||||
await flushPromises();
|
||||
|
||||
expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("3pids", () => {
|
||||
beforeEach(() => {
|
||||
const mockOidcClientStore = {
|
||||
accountManagementEndpoint: undefined,
|
||||
} as unknown as OidcClientStore;
|
||||
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+27
@@ -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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { type 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();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user