Merge matrix-react-sdk into element-web

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

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

View file

@ -0,0 +1,125 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentProps } from "react";
import { SecretStorage, MatrixClient } from "matrix-js-sdk/src/matrix";
import { act, fireEvent, render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mockPlatformPeg, stubClient } from "../../../../test-utils";
import AccessSecretStorageDialog from "../../../../../src/components/views/dialogs/security/AccessSecretStorageDialog";
const securityKey = "EsTc WKmb ivvk jLS7 Y1NH 5CcQ mP1E JJwj B3Fd pFWm t4Dp dbyu";
describe("AccessSecretStorageDialog", () => {
let mockClient: MatrixClient;
const defaultProps: ComponentProps<typeof AccessSecretStorageDialog> = {
keyInfo: {} as any,
onFinished: jest.fn(),
checkPrivateKey: jest.fn(),
};
const renderComponent = (props = {}): void => {
render(<AccessSecretStorageDialog {...defaultProps} {...props} />);
};
const enterSecurityKey = (placeholder = "Security Key"): void => {
act(() => {
fireEvent.change(screen.getByPlaceholderText(placeholder), {
target: {
value: securityKey,
},
});
// wait for debounce
jest.advanceTimersByTime(250);
});
};
const submitDialog = async (): Promise<void> => {
await userEvent.click(screen.getByText("Continue"), { delay: null });
};
beforeAll(() => {
jest.useFakeTimers();
mockPlatformPeg();
});
afterAll(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});
beforeEach(() => {
mockClient = stubClient();
});
it("Closes the dialog when the form is submitted with a valid key", async () => {
jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true);
const onFinished = jest.fn();
const checkPrivateKey = jest.fn().mockResolvedValue(true);
renderComponent({ onFinished, checkPrivateKey });
// check that the input field is focused
expect(screen.getByPlaceholderText("Security Key")).toHaveFocus();
await enterSecurityKey();
await submitDialog();
expect(screen.getByText("Looks good!")).toBeInTheDocument();
expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: securityKey });
expect(onFinished).toHaveBeenCalledWith({ recoveryKey: securityKey });
});
it("Notifies the user if they input an invalid Security Key", async () => {
const onFinished = jest.fn();
const checkPrivateKey = jest.fn().mockResolvedValue(true);
renderComponent({ onFinished, checkPrivateKey });
jest.spyOn(mockClient.secretStorage, "checkKey").mockImplementation(() => {
throw new Error("invalid key");
});
await enterSecurityKey();
await submitDialog();
expect(screen.getByText("Continue")).toBeDisabled();
expect(screen.getByText("Invalid Security Key")).toBeInTheDocument();
});
it("Notifies the user if they input an invalid passphrase", async function () {
const keyInfo = {
name: "test",
algorithm: "test",
iv: "test",
mac: "1:2:3:4",
passphrase: {
// this type is weird in js-sdk
// cast 'm.pbkdf2' to itself
algorithm: "m.pbkdf2" as SecretStorage.PassphraseInfo["algorithm"],
iterations: 2,
salt: "nonempty",
},
};
const checkPrivateKey = jest.fn().mockResolvedValue(false);
renderComponent({ checkPrivateKey, keyInfo });
await enterSecurityKey("Security Phrase");
expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey);
await submitDialog();
await expect(
screen.findByText(
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
),
).resolves.toBeInTheDocument();
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
});
});

View file

@ -0,0 +1,72 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { AppDownloadDialog } from "../../../../../src/components/views/dialogs/AppDownloadDialog";
import SdkConfig, { ConfigOptions } from "../../../../../src/SdkConfig";
describe("AppDownloadDialog", () => {
afterEach(() => {
SdkConfig.reset();
});
it("should render with desktop, ios, android, fdroid buttons by default", () => {
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling fdroid build", () => {
SdkConfig.add({
mobile_builds: {
fdroid: null,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling desktop build", () => {
SdkConfig.add({
desktop_builds: {
available: false,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling mobile builds", () => {
SdkConfig.add({
mobile_builds: {
ios: null,
android: null,
fdroid: null,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,85 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { getByText, render, RenderResult } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import React from "react";
import AskInviteAnywayDialog, {
AskInviteAnywayDialogProps,
} from "../../../../../src/components/views/dialogs/AskInviteAnywayDialog";
import SettingsStore from "../../../../../src/settings/SettingsStore";
describe("AskInviteaAnywayDialog", () => {
const onFinished: jest.Mock<any, any> = jest.fn();
const onGiveUp: jest.Mock<any, any> = jest.fn();
const onInviteAnyways: jest.Mock<any, any> = jest.fn();
function renderComponent(props: Partial<AskInviteAnywayDialogProps> = {}): RenderResult {
return render(
<AskInviteAnywayDialog
onFinished={onFinished}
onGiveUp={onGiveUp}
onInviteAnyways={onInviteAnyways}
unknownProfileUsers={[
{
userId: "@alice:localhost",
errorText: "🤷‍♂️",
},
]}
{...props}
/>,
);
}
beforeEach(() => {
jest.resetAllMocks();
});
it("remembers to not warn again", async () => {
const { container } = renderComponent();
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (): Promise<void> => {});
const neverWarnAgainBtn = getByText(container, /never warn/);
await userEvent.click(neverWarnAgainBtn);
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"promptBeforeInviteUnknownUsers",
null,
expect.any(String),
false,
);
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
expect(onFinished).toHaveBeenCalledWith(true);
});
it("invites anyway", async () => {
const { container } = renderComponent();
jest.spyOn(SettingsStore, "setValue");
const inviteAnywayBtn = getByText(container, "Invite anyway");
await userEvent.click(inviteAnywayBtn);
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
expect(onFinished).toHaveBeenCalledWith(true);
});
it("gives up", async () => {
const { container } = renderComponent();
jest.spyOn(SettingsStore, "setValue");
const closeBtn = getByText(container, /Close/);
await userEvent.click(closeBtn);
expect(onGiveUp).toHaveBeenCalledTimes(1);
expect(onFinished).toHaveBeenCalledWith(false);
});
});

View file

@ -0,0 +1,80 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import fetchMock from "fetch-mock-jest";
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
import ChangelogDialog from "../../../../../src/components/views/dialogs/ChangelogDialog";
describe("<ChangelogDialog />", () => {
it("should fetch github proxy url for each repo with old and new version strings", async () => {
const webUrl = "https://riot.im/github/repos/element-hq/element-web/compare/oldsha1...newsha1";
fetchMock.get(webUrl, {
url: "https://api.github.com/repos/element-hq/element-web/compare/master...develop",
html_url: "https://github.com/element-hq/element-web/compare/master...develop",
permalink_url: "https://github.com/element-hq/element-web/compare/vector-im:72ca95e...vector-im:8891698",
diff_url: "https://github.com/element-hq/element-web/compare/master...develop.diff",
patch_url: "https://github.com/element-hq/element-web/compare/master...develop.patch",
base_commit: {},
merge_base_commit: {},
status: "ahead",
ahead_by: 24,
behind_by: 0,
total_commits: 24,
commits: [
{
sha: "commit-sha",
html_url: "https://api.github.com/repos/element-hq/element-web/commit/commit-sha",
commit: { message: "This is the first commit message" },
},
],
files: [],
});
const jsUrl = "https://riot.im/github/repos/matrix-org/matrix-js-sdk/compare/oldsha3...newsha3";
fetchMock.get(jsUrl, {
url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/compare/master...develop",
html_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop",
permalink_url: "https://github.com/matrix-org/matrix-js-sdk/compare/matrix-org:6166a8f...matrix-org:fec350",
diff_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.diff",
patch_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.patch",
base_commit: {},
merge_base_commit: {},
status: "ahead",
ahead_by: 48,
behind_by: 0,
total_commits: 48,
commits: [
{
sha: "commit-sha1",
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1",
commit: { message: "This is a commit message" },
},
{
sha: "commit-sha2",
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2",
commit: { message: "This is another commit message" },
},
],
files: [],
});
const newVersion = "newsha1-react-newsha2-js-newsha3";
const oldVersion = "oldsha1-react-oldsha2-js-oldsha3";
const { asFragment } = render(
<ChangelogDialog newVersion={newVersion} version={oldVersion} onFinished={jest.fn()} />,
);
// Wait for spinners to go away
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
expect(fetchMock).toHaveFetched(webUrl);
expect(fetchMock).toHaveFetched(jsUrl);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,111 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { flushPromises, mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createRedactEventDialog } from "../../../../../src/components/views/dialogs/ConfirmRedactDialog";
describe("ConfirmRedactDialog", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let mxEvent: MatrixEvent;
const setUpVoiceBroadcastStartedEvent = () => {
mxEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
};
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
createRedactEventDialog({ mxEvent });
// double-flush promises required for the dialog to show up
await flushPromises();
await flushPromises();
await userEvent.click(screen.getByTestId("dialog-primary-button"));
};
beforeEach(() => {
client = stubClient();
});
it("should raise an error for an event without ID", async () => {
mxEvent = mkEvent({
event: true,
type: "m.room.message",
room: roomId,
content: {},
user: client.getSafeUserId(),
});
jest.spyOn(mxEvent, "getId").mockReturnValue(undefined);
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow("cannot redact event without ID");
});
it("should raise an error for an event without room-ID", async () => {
mxEvent = mkEvent({
event: true,
type: "m.room.message",
room: roomId,
content: {},
user: client.getSafeUserId(),
});
jest.spyOn(mxEvent, "getRoomId").mockReturnValue(undefined);
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow(
`cannot redact event ${mxEvent.getId()} without room ID`,
);
});
describe("when redacting a voice broadcast started event", () => {
beforeEach(() => {
setUpVoiceBroadcastStartedEvent();
});
describe("and the server does not support relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact without `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
});
});
});
describe("and the server supports relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact with `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
with_rel_types: [RelationType.Reference],
});
});
});
});
});
});

View file

@ -0,0 +1,28 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { KnownMembership } from "matrix-js-sdk/src/types";
import ConfirmUserActionDialog from "../../../../../src/components/views/dialogs/ConfirmUserActionDialog";
import { mkRoomMember } from "../../../../test-utils";
describe("ConfirmUserActionDialog", () => {
it("renders", () => {
const { asFragment } = render(
<ConfirmUserActionDialog
onFinished={jest.fn()}
member={mkRoomMember("123", "@user:test.com", KnownMembership.Join)}
action="Ban"
title="Ban this " // eg. 'Ban this user?'
/>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,347 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, within } from "jest-matrix-react";
import { JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
import CreateRoomDialog from "../../../../../src/components/views/dialogs/CreateRoomDialog";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
describe("<CreateRoomDialog />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getDomain: jest.fn().mockReturnValue("server.org"),
getClientWellKnown: jest.fn(),
doesServerForceEncryptionForPreset: jest.fn(),
// make every alias available
getRoomIdForAlias: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" })),
});
const getE2eeEnableToggleInputElement = () => screen.getByLabelText("Enable end-to-end encryption");
// labelled toggle switch doesn't set the disabled attribute, only aria-disabled
const getE2eeEnableToggleIsDisabled = () =>
getE2eeEnableToggleInputElement().getAttribute("aria-disabled") === "true";
beforeEach(() => {
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(false);
mockClient.getClientWellKnown.mockReturnValue({});
});
const getComponent = (props = {}) => render(<CreateRoomDialog onFinished={jest.fn()} {...props} />);
it("should default to private room", async () => {
getComponent();
await flushPromises();
expect(screen.getByText("Create a private room")).toBeInTheDocument();
});
it("should use defaultName from props", async () => {
const defaultName = "My test room";
getComponent({ defaultName });
await flushPromises();
expect(screen.getByLabelText("Name")).toHaveDisplayValue(defaultName);
});
describe("for a private room", () => {
// default behaviour is a private room
it("should use server .well-known default for encryption setting", async () => {
// default to off
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: false,
},
});
getComponent();
await flushPromises();
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
expect(
screen.getByText(
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
),
).toBeDefined();
});
it("should use server .well-known force_disable for encryption setting", async () => {
// force to off
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: true,
force_disable: true,
},
});
getComponent();
await flushPromises();
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
expect(
screen.getByText(
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
),
).toBeDefined();
});
it("should use defaultEncrypted prop", async () => {
// default to off in server wk
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: false,
},
});
// but pass defaultEncrypted prop
getComponent({ defaultEncrypted: true });
await flushPromises();
// encryption enabled
expect(getE2eeEnableToggleInputElement()).toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
});
it("should use defaultEncrypted prop when it is false", async () => {
// default to off in server wk
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: true,
},
});
// but pass defaultEncrypted prop
getComponent({ defaultEncrypted: false });
await flushPromises();
// encryption disabled
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
// not forced to off
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
});
it("should override defaultEncrypted when server .well-known forces disabled encryption", async () => {
// force to off
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: true,
},
});
getComponent({ defaultEncrypted: true });
await flushPromises();
// server forces encryption to disabled, even though defaultEncrypted is false
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
expect(
screen.getByText(
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
),
).toBeDefined();
});
it("should override defaultEncrypted when server forces enabled encryption", async () => {
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
getComponent({ defaultEncrypted: false });
await flushPromises();
// server forces encryption to enabled, even though defaultEncrypted is true
expect(getE2eeEnableToggleInputElement()).toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
});
it("should enable encryption toggle and disable field when server forces encryption", async () => {
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
getComponent();
await flushPromises();
expect(getE2eeEnableToggleInputElement()).toBeChecked();
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
});
it("should warn when trying to create a room with an invalid form", async () => {
const onFinished = jest.fn();
getComponent({ onFinished });
await flushPromises();
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
// didn't submit room
expect(onFinished).not.toHaveBeenCalled();
});
it("should create a private room", async () => {
const onFinished = jest.fn();
getComponent({ onFinished });
await flushPromises();
const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
},
encryption: true,
parentSpace: undefined,
roomType: undefined,
});
});
});
describe("for a knock room", () => {
describe("when feature is disabled", () => {
it("should not have the option to create a knock room", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
getComponent();
fireEvent.click(screen.getByLabelText("Room visibility"));
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
});
});
describe("when feature is enabled", () => {
const onFinished = jest.fn();
const roomName = "Test Room Name";
beforeEach(async () => {
onFinished.mockReset();
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_ask_to_join",
);
getComponent({ onFinished });
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
fireEvent.click(screen.getByLabelText("Room visibility"));
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
});
it("should have a heading", () => {
expect(screen.getByRole("heading")).toHaveTextContent("Create a room");
});
it("should have a hint", () => {
expect(
screen.getByText(
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
),
).toBeInTheDocument();
});
it("should create a knock room with private visibility", async () => {
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
visibility: Visibility.Private,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
});
it("should create a knock room with public visibility", async () => {
fireEvent.click(
screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
);
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
visibility: Visibility.Public,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
});
});
});
describe("for a public room", () => {
it("should set join rule to public defaultPublic is truthy", async () => {
const onFinished = jest.fn();
getComponent({ defaultPublic: true, onFinished });
await flushPromises();
expect(screen.getByText("Create a public room")).toBeInTheDocument();
// e2e section is not rendered
expect(screen.queryByText("Enable end-to-end encryption")).not.toBeInTheDocument();
const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
});
it("should not create a public room without an alias", async () => {
const onFinished = jest.fn();
getComponent({ onFinished });
await flushPromises();
// set to public
fireEvent.click(screen.getByLabelText("Room visibility"));
fireEvent.click(screen.getByText("Public room"));
expect(within(screen.getByLabelText("Room visibility")).findByText("Public room")).toBeTruthy();
expect(screen.getByText("Create a public room")).toBeInTheDocument();
// set name
const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
// try to create the room
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
// alias field invalid
expect(screen.getByLabelText("Room address").parentElement!).toHaveClass("mx_Field_invalid");
// didn't submit
expect(onFinished).not.toHaveBeenCalled();
});
it("should create a public room", async () => {
const onFinished = jest.fn();
getComponent({ onFinished, defaultPublic: true });
await flushPromises();
// set name
const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
const roomAlias = "test";
fireEvent.change(screen.getByLabelText("Room address"), { target: { value: roomAlias } });
// try to create the room
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
preset: Preset.PublicChat,
room_alias_name: roomAlias,
visibility: Visibility.Public,
},
guestAccess: false,
parentSpace: undefined,
roomType: undefined,
});
});
});
});

View file

@ -0,0 +1,78 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { getByLabelText, getAllByLabelText, render } from "jest-matrix-react";
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import { stubClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import DevtoolsDialog from "../../../../../src/components/views/dialogs/DevtoolsDialog";
describe("DevtoolsDialog", () => {
let cli: MatrixClient;
let room: Room;
function getComponent(roomId: string, threadRootId: string | null = null, onFinished = () => true) {
return render(
<MatrixClientContext.Provider value={cli}>
<DevtoolsDialog roomId={roomId} threadRootId={threadRootId} onFinished={onFinished} />
</MatrixClientContext.Provider>,
);
}
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
room = new Room("!id", cli, "@alice:matrix.org");
jest.spyOn(cli, "getRoom").mockReturnValue(room);
});
afterAll(() => {
jest.restoreAllMocks();
});
it("renders the devtools dialog", () => {
const { asFragment } = getComponent(room.roomId);
expect(asFragment()).toMatchSnapshot();
});
it("copies the roomid", async () => {
const user = userEvent.setup();
jest.spyOn(navigator.clipboard, "writeText");
const { container } = getComponent(room.roomId);
const copyBtn = getByLabelText(container, "Copy");
await user.click(copyBtn);
const copiedBtn = getByLabelText(container, "Copied!");
expect(copiedBtn).toBeInTheDocument();
expect(navigator.clipboard.writeText).toHaveBeenCalled();
await expect(navigator.clipboard.readText()).resolves.toBe(room.roomId);
});
it("copies the thread root id when provided", async () => {
const user = userEvent.setup();
jest.spyOn(navigator.clipboard, "writeText");
const threadRootId = "$test_event_id_goes_here";
const { container } = getComponent(room.roomId, threadRootId);
const copyBtn = getAllByLabelText(container, "Copy")[1];
await user.click(copyBtn);
const copiedBtn = getByLabelText(container, "Copied!");
expect(copiedBtn).toBeInTheDocument();
expect(navigator.clipboard.writeText).toHaveBeenCalled();
await expect(navigator.clipboard.readText()).resolves.toBe(threadRootId);
});
});

View file

@ -0,0 +1,326 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { Room } from "matrix-js-sdk/src/matrix";
import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog";
import { ExportType, ExportFormat } from "../../../../../src/utils/exportUtils/exportUtils";
import { createTestClient, mkStubRoom } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import HTMLExporter from "../../../../../src/utils/exportUtils/HtmlExport";
import ChatExport from "../../../../../src/customisations/ChatExport";
import PlainTextExporter from "../../../../../src/utils/exportUtils/PlainTextExport";
jest.useFakeTimers();
const htmlExporterInstance = {
export: jest.fn().mockResolvedValue({}),
};
const plainTextExporterInstance = {
export: jest.fn().mockResolvedValue({}),
};
jest.mock("../../../../../src/utils/exportUtils/HtmlExport", () => jest.fn());
jest.mock("../../../../../src/utils/exportUtils/PlainTextExport", () => jest.fn());
jest.mock("../../../../../src/customisations/ChatExport", () => ({
getForceChatExportParameters: jest.fn().mockReturnValue({}),
}));
const ChatExportMock = mocked(ChatExport);
const HTMLExporterMock = mocked(HTMLExporter);
const PlainTextExporterMock = mocked(PlainTextExporter);
describe("<ExportDialog />", () => {
const mockClient = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
const roomId = "test:test.org";
const defaultProps = {
room: mkStubRoom(roomId, "test", mockClient) as unknown as Room,
onFinished: jest.fn(),
};
const getComponent = (props = {}) => render(<ExportDialog {...defaultProps} {...props} />);
const getSizeInput = ({ container }: RenderResult) => container.querySelector('input[id="size-limit"]')!;
const getExportTypeInput = ({ container }: RenderResult) => container.querySelector('select[id="export-type"]')!;
const getAttachmentsCheckbox = ({ container }: RenderResult) =>
container.querySelector('input[id="include-attachments"]')!;
const getMessageCountInput = ({ container }: RenderResult) => container.querySelector('input[id="message-count"]')!;
const getExportFormatInput = ({ container }: RenderResult, format: ExportFormat) =>
container.querySelector(`input[id="exportFormat-${format}"]`)!;
const getPrimaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-primary-button")!;
const getSecondaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-cancel-button")!;
const submitForm = async (component: RenderResult) => fireEvent.click(getPrimaryButton(component));
const selectExportFormat = async (component: RenderResult, format: ExportFormat) =>
fireEvent.click(getExportFormatInput(component, format));
const selectExportType = async (component: RenderResult, type: ExportType) =>
fireEvent.change(getExportTypeInput(component), { target: { value: type } });
const setMessageCount = async (component: RenderResult, count: number) =>
fireEvent.change(getMessageCountInput(component), { target: { value: count } });
const setSizeLimit = async (component: RenderResult, limit: number) =>
fireEvent.change(getSizeInput(component), { target: { value: limit } });
beforeEach(() => {
HTMLExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(htmlExporterInstance));
PlainTextExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(plainTextExporterInstance));
htmlExporterInstance.export.mockClear();
plainTextExporterInstance.export.mockClear();
// default setting value
mocked(ChatExportMock.getForceChatExportParameters!).mockClear().mockReturnValue({});
});
it("renders export dialog", () => {
const component = getComponent();
expect(component.container.querySelector(".mx_ExportDialog")).toMatchSnapshot();
});
it("calls onFinished when cancel button is clicked", () => {
const onFinished = jest.fn();
const component = getComponent({ onFinished });
fireEvent.click(getSecondaryButton(component));
expect(onFinished).toHaveBeenCalledWith(false);
});
it("exports room on submit", async () => {
const component = getComponent();
await submitForm(component);
await waitFor(() => {
// 4th arg is an component function
const exportConstructorProps = HTMLExporterMock.mock.calls[0].slice(0, 3);
expect(exportConstructorProps).toEqual([
defaultProps.room,
ExportType.Timeline,
{
attachmentsIncluded: false,
maxSize: 8388608, // 8MB to bytes
numberOfMessages: 100,
},
]);
});
expect(htmlExporterInstance.export).toHaveBeenCalled();
});
it("exports room using values set from ForceRoomExportParameters", async () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
format: ExportFormat.PlainText,
range: ExportType.Beginning,
sizeMb: 7000,
numberOfMessages: 30,
includeAttachments: true,
});
const component = getComponent();
await submitForm(component);
// 4th arg is an component function
const exportConstructorProps = PlainTextExporterMock.mock.calls[0].slice(0, 3);
expect(exportConstructorProps).toEqual([
defaultProps.room,
ExportType.Beginning,
{
attachmentsIncluded: true,
maxSize: 7000 * 1024 * 1024,
numberOfMessages: 30,
},
]);
expect(plainTextExporterInstance.export).toHaveBeenCalled();
});
it("renders success screen when export is finished", async () => {
const component = getComponent();
await submitForm(component);
jest.runAllTimers();
expect(component.container.querySelector(".mx_InfoDialog .mx_Dialog_content")).toMatchSnapshot();
});
describe("export format", () => {
it("renders export format with html selected by default", () => {
const component = getComponent();
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
});
it("sets export format on radio button click", async () => {
const component = getComponent();
await selectExportFormat(component, ExportFormat.PlainText);
expect(getExportFormatInput(component, ExportFormat.PlainText)).toBeChecked();
expect(getExportFormatInput(component, ExportFormat.Html)).not.toBeChecked();
});
it("hides export format input when format is valid in ForceRoomExportParameters", () => {
const component = getComponent();
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
});
it("does not render export format when set in ForceRoomExportParameters", () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
format: ExportFormat.PlainText,
});
const component = getComponent();
expect(getExportFormatInput(component, ExportFormat.Html)).toBeFalsy();
});
});
describe("export type", () => {
it("renders export type with timeline selected by default", () => {
const component = getComponent();
expect(getExportTypeInput(component)).toHaveValue(ExportType.Timeline);
});
it("sets export type on change", async () => {
const component = getComponent();
await selectExportType(component, ExportType.Beginning);
expect(getExportTypeInput(component)).toHaveValue(ExportType.Beginning);
});
it("does not render export type when set in ForceRoomExportParameters", () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
range: ExportType.Beginning,
});
const component = getComponent();
expect(getExportTypeInput(component)).toBeFalsy();
});
it("does not render message count input", async () => {
const component = getComponent();
expect(getMessageCountInput(component)).toBeFalsy();
});
it("renders message count input with default value 100 when export type is lastNMessages", async () => {
const component = getComponent();
await selectExportType(component, ExportType.LastNMessages);
expect(getMessageCountInput(component)).toHaveValue(100);
});
it("sets message count on change", async () => {
const component = getComponent();
await selectExportType(component, ExportType.LastNMessages);
await setMessageCount(component, 10);
expect(getMessageCountInput(component)).toHaveValue(10);
});
it("does not export when export type is lastNMessages and message count is falsy", async () => {
const component = getComponent();
await selectExportType(component, ExportType.LastNMessages);
await setMessageCount(component, 0);
await submitForm(component);
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it("does not export when export type is lastNMessages and message count is more than max", async () => {
const component = getComponent();
await selectExportType(component, ExportType.LastNMessages);
await setMessageCount(component, 99999999999);
await submitForm(component);
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it("exports when export type is NOT lastNMessages and message count is falsy", async () => {
const component = getComponent();
await selectExportType(component, ExportType.LastNMessages);
await setMessageCount(component, 0);
await selectExportType(component, ExportType.Timeline);
await submitForm(component);
await waitFor(() => {
expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
});
describe("size limit", () => {
it("renders size limit input with default value", () => {
const component = getComponent();
expect(getSizeInput(component)).toHaveValue(8);
});
it("updates size limit on change", async () => {
const component = getComponent();
await setSizeLimit(component, 20);
expect(getSizeInput(component)).toHaveValue(20);
});
it("does not export when size limit is falsy", async () => {
const component = getComponent();
await setSizeLimit(component, 0);
await submitForm(component);
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it("does not export when size limit is larger than max", async () => {
const component = getComponent();
await setSizeLimit(component, 2001);
await submitForm(component);
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it("exports when size limit is max", async () => {
const component = getComponent();
await setSizeLimit(component, 2000);
await submitForm(component);
await waitFor(() => {
expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
it("does not render size limit input when set in ForceRoomExportParameters", () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
sizeMb: 10000,
});
const component = getComponent();
expect(getSizeInput(component)).toBeFalsy();
});
/**
* 2000mb size limit does not apply when higher limit is configured in config
*/
it("exports when size limit set in ForceRoomExportParameters is larger than 2000", async () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
sizeMb: 10000,
});
const component = getComponent();
await submitForm(component);
expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
describe("include attachments", () => {
it("renders input with default value of false", () => {
const component = getComponent();
expect(getAttachmentsCheckbox(component)).not.toBeChecked();
});
it("updates include attachments on change", async () => {
const component = getComponent();
fireEvent.click(getAttachmentsCheckbox(component));
expect(getAttachmentsCheckbox(component)).toBeChecked();
});
it("does not render input when set in ForceRoomExportParameters", () => {
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
includeAttachments: false,
});
const component = getComponent();
expect(getAttachmentsCheckbox(component)).toBeFalsy();
});
});
});

View file

@ -0,0 +1,27 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import SdkConfig from "../../../../../src/SdkConfig";
import FeedbackDialog from "../../../../../src/components/views/dialogs/FeedbackDialog";
describe("FeedbackDialog", () => {
it("should respect feedback config", () => {
SdkConfig.put({
feedback: {
existing_issues_url: "http://existing?foo=bar",
new_issue_url: "https://new.issue.url?foo=bar",
},
});
const { asFragment } = render(<FeedbackDialog onFinished={jest.fn()} />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,395 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 Robin Townsend <robin@robin.town>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import {
MatrixEvent,
EventType,
LocationAssetType,
M_ASSET,
M_LOCATION,
M_TIMESTAMP,
M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { act, fireEvent, getByTestId, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { sleep } from "matrix-js-sdk/src/utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import ForwardDialog from "../../../../../src/components/views/dialogs/ForwardDialog";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeLegacyLocationEvent,
makeLocationEvent,
mkEvent,
mkMessage,
mkStubRoom,
mockPlatformPeg,
} from "../../../../test-utils";
import { TILE_SERVER_WK_KEY } from "../../../../../src/utils/WellKnownUtils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
get() {
return this.parentNode;
},
});
describe("ForwardDialog", () => {
const sourceRoom = "!111111111111111111:example.org";
const aliceId = "@alice:example.org";
const defaultMessage = mkMessage({
room: sourceRoom,
user: aliceId,
msg: "Hello world!",
event: true,
});
const accountDataEvent = new MatrixEvent({
type: EventType.Direct,
sender: aliceId,
content: {},
});
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
getSafeUserId: jest.fn().mockReturnValue(aliceId),
isGuest: jest.fn().mockReturnValue(false),
getVisibleRooms: jest.fn().mockReturnValue([]),
getRoom: jest.fn(),
getAccountData: jest.fn().mockReturnValue(accountDataEvent),
getPushActionsForEvent: jest.fn(),
mxcUrlToHttp: jest.fn().mockReturnValue(""),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getProfileInfo: jest.fn().mockResolvedValue({
displayname: "Alice",
}),
decryptEventIfNeeded: jest.fn(),
sendEvent: jest.fn(),
getClientWellKnown: jest.fn().mockReturnValue({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
}),
});
const defaultRooms = ["a", "A", "b"].map((name) => mkStubRoom(name, name, mockClient));
const mountForwardDialog = (message = defaultMessage, rooms = defaultRooms) => {
mockClient.getVisibleRooms.mockReturnValue(rooms);
mockClient.getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
const wrapper: RenderResult = render(
<ForwardDialog
matrixClient={mockClient}
event={message}
permalinkCreator={new RoomPermalinkCreator(undefined!, sourceRoom)}
onFinished={jest.fn()}
/>,
);
return wrapper;
};
beforeEach(() => {
DMRoomMap.makeShared(mockClient);
jest.clearAllMocks();
mockClient.getUserId.mockReturnValue("@bob:example.org");
mockClient.getSafeUserId.mockReturnValue("@bob:example.org");
mockClient.sendEvent.mockReset();
});
afterAll(() => {
jest.spyOn(MatrixClientPeg, "get").mockRestore();
});
it("shows a preview with us as the sender", async () => {
const { container } = mountForwardDialog();
expect(screen.queryByText("Hello world!")).toBeInTheDocument();
// We would just test SenderProfile for the user ID, but it's stubbed
const previewAvatar = container.querySelector(".mx_EventTile_avatar .mx_BaseAvatar");
expect(previewAvatar?.getAttribute("title")).toBe("@bob:example.org");
});
it("filters the rooms", async () => {
const { container } = mountForwardDialog();
expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(3);
const searchInput = getByTestId(container, "searchbox-input");
await userEvent.type(searchInput, "a");
expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(2);
});
it("should be navigable using arrow keys", async () => {
const { container } = mountForwardDialog();
const searchBox = getByTestId(container, "searchbox-input");
searchBox.focus();
await waitFor(() =>
expect(container.querySelectorAll(".mx_ForwardList_entry")[0]).toHaveClass("mx_ForwardList_entry_active"),
);
await userEvent.keyboard("[ArrowDown]");
await waitFor(() =>
expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
);
await userEvent.keyboard("[ArrowDown]");
await waitFor(() =>
expect(container.querySelectorAll(".mx_ForwardList_entry")[2]).toHaveClass("mx_ForwardList_entry_active"),
);
await userEvent.keyboard("[ArrowUp]");
await waitFor(() =>
expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
);
await userEvent.keyboard("[Enter]");
expect(mockClient.sendEvent).toHaveBeenCalledWith("A", "m.room.message", {
body: "Hello world!",
msgtype: "m.text",
});
});
it("tracks message sending progress across multiple rooms", async () => {
mockPlatformPeg();
const { container } = mountForwardDialog();
// Make sendEvent require manual resolution so we can see the sending state
let finishSend: (arg?: any) => void;
let cancelSend: () => void;
mockClient.sendEvent.mockImplementation(
<T extends {}>() =>
new Promise<T>((resolve, reject) => {
finishSend = resolve;
cancelSend = reject;
}),
);
let firstButton!: Element;
let secondButton!: Element;
const update = () => {
[firstButton, secondButton] = container.querySelectorAll(".mx_ForwardList_sendButton");
};
update();
expect(firstButton.className).toContain("mx_ForwardList_canSend");
act(() => {
fireEvent.click(firstButton);
});
update();
expect(firstButton.className).toContain("mx_ForwardList_sending");
await act(async () => {
cancelSend();
// Wait one tick for the button to realize the send failed
await sleep(0);
});
update();
expect(firstButton.className).toContain("mx_ForwardList_sendFailed");
expect(secondButton.className).toContain("mx_ForwardList_canSend");
act(() => {
fireEvent.click(secondButton);
});
update();
expect(secondButton.className).toContain("mx_ForwardList_sending");
await act(async () => {
finishSend();
// Wait one tick for the button to realize the send succeeded
await sleep(0);
});
update();
expect(secondButton.className).toContain("mx_ForwardList_sent");
});
it("can render replies", async () => {
const replyMessage = mkEvent({
type: "m.room.message",
room: "!111111111111111111:example.org",
user: "@alice:example.org",
content: {
"msgtype": "m.text",
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$2222222222222222222222222222222222222222222",
},
},
},
event: true,
});
mountForwardDialog(replyMessage);
expect(screen.queryByText("Hi Alice!", { exact: false })).toBeInTheDocument();
});
it("disables buttons for rooms without send permissions", async () => {
const readOnlyRoom = mkStubRoom("a", "a", mockClient);
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
const rooms = [readOnlyRoom, mkStubRoom("b", "b", mockClient)];
const { container } = mountForwardDialog(undefined, rooms);
const [firstButton, secondButton] = container.querySelectorAll<HTMLButtonElement>(".mx_ForwardList_sendButton");
expect(firstButton.getAttribute("aria-disabled")).toBeTruthy();
expect(secondButton.getAttribute("aria-disabled")).toBeFalsy();
});
describe("Location events", () => {
// 14.03.2022 16:15
const now = 1647270879403;
const roomId = "a";
const geoUri = "geo:51.5076,-0.1276";
const legacyLocationEvent = makeLegacyLocationEvent(geoUri);
const modernLocationEvent = makeLocationEvent(geoUri);
const pinDropLocationEvent = makeLocationEvent(geoUri, LocationAssetType.Pin);
beforeEach(() => {
// legacy events will default timestamp to Date.now()
// mock a stable now for easy assertion
jest.spyOn(Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(Date, "now").mockRestore();
});
const sendToFirstRoom = (container: HTMLElement): void =>
act(() => {
const sendToFirstRoomButton = container.querySelector(".mx_ForwardList_sendButton");
fireEvent.click(sendToFirstRoomButton!);
});
it("converts legacy location events to pin drop shares", async () => {
const { container } = mountForwardDialog(legacyLocationEvent);
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
sendToFirstRoom(container);
// text and description from original event are removed
// text gets new default message from event values
// timestamp is defaulted to now
const text = `Location ${geoUri} at ${new Date(now).toISOString()}`;
const expectedStrippedContent = {
...modernLocationEvent.getContent(),
body: text,
[M_TEXT.name]: text,
[M_TIMESTAMP.name]: now,
[M_ASSET.name]: { type: LocationAssetType.Pin },
[M_LOCATION.name]: {
uri: geoUri,
},
};
expect(mockClient.sendEvent).toHaveBeenCalledWith(
roomId,
legacyLocationEvent.getType(),
expectedStrippedContent,
);
});
it("removes personal information from static self location shares", async () => {
const { container } = mountForwardDialog(modernLocationEvent);
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
sendToFirstRoom(container);
const timestamp = M_TIMESTAMP.findIn<number>(modernLocationEvent.getContent())!;
// text and description from original event are removed
// text gets new default message from event values
const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
const expectedStrippedContent = {
...modernLocationEvent.getContent(),
body: text,
[M_TEXT.name]: text,
[M_ASSET.name]: { type: LocationAssetType.Pin },
[M_LOCATION.name]: {
uri: geoUri,
},
};
expect(mockClient.sendEvent).toHaveBeenCalledWith(
roomId,
modernLocationEvent.getType(),
expectedStrippedContent,
);
});
it("forwards beacon location as a pin drop event", async () => {
const timestamp = 123456;
const beaconEvent = makeBeaconEvent("@alice:server.org", { geoUri, timestamp });
const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
const expectedContent = {
msgtype: "m.location",
body: text,
[M_TEXT.name]: text,
[M_ASSET.name]: { type: LocationAssetType.Pin },
[M_LOCATION.name]: {
uri: geoUri,
},
geo_uri: geoUri,
[M_TIMESTAMP.name]: timestamp,
};
const { container } = mountForwardDialog(beaconEvent);
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
sendToFirstRoom(container);
expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, EventType.RoomMessage, expectedContent);
});
it("forwards pin drop event", async () => {
const { container } = mountForwardDialog(pinDropLocationEvent);
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
sendToFirstRoom(container);
expect(mockClient.sendEvent).toHaveBeenCalledWith(
roomId,
pinDropLocationEvent.getType(),
pinDropLocationEvent.getContent(),
);
});
});
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("Passes through the dynamic predecessor setting", async () => {
mockClient.getVisibleRooms.mockClear();
mountForwardDialog();
expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(false);
});
});
describe("If the feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
// Turn on feature_dynamic_room_predecessors setting
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
it("Passes through the dynamic predecessor setting", async () => {
mockClient.getVisibleRooms.mockClear();
mountForwardDialog();
expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
});

View file

@ -0,0 +1,75 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { act, render } from "jest-matrix-react";
import React from "react";
import { Mocked } from "jest-mock";
import {
EmojiMapping,
ShowSasCallbacks,
Verifier,
VerifierEvent,
VerifierEventHandlerMap,
} from "matrix-js-sdk/src/crypto-api";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import IncomingSasDialog from "../../../../../src/components/views/dialogs/IncomingSasDialog";
import { stubClient } from "../../../../test-utils";
describe("IncomingSasDialog", () => {
beforeEach(() => {
stubClient();
});
it("shows a spinner at first", () => {
const mockVerifier = makeMockVerifier();
const { container } = renderComponent(mockVerifier);
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
});
it("should show some emojis once keys are exchanged", () => {
const mockVerifier = makeMockVerifier();
const { container } = renderComponent(mockVerifier);
// fire the ShowSas event
const sasEvent = makeMockSasCallbacks();
act(() => {
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
});
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block");
expect(emojis.length).toEqual(7);
for (const emoji of emojis) {
expect(emoji).toHaveTextContent("🦄Unicorn");
}
});
});
function renderComponent(verifier: Verifier, onFinished = () => true) {
return render(<IncomingSasDialog verifier={verifier} onFinished={onFinished} />);
}
function makeMockVerifier(): Mocked<Verifier> {
const verifier = new TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap>();
Object.assign(verifier, {
cancel: jest.fn(),
});
return verifier as unknown as Mocked<Verifier>;
}
function makeMockSasCallbacks(): ShowSasCallbacks {
const unicorn: EmojiMapping = ["🦄", "unicorn"];
return {
sas: {
emoji: new Array<EmojiMapping>(7).fill(unicorn),
},
cancel: jest.fn(),
confirm: jest.fn(),
mismatch: jest.fn(),
};
}

View file

@ -0,0 +1,188 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { MatrixError } from "matrix-js-sdk/src/matrix";
import InteractiveAuthDialog from "../../../../../src/components/views/dialogs/InteractiveAuthDialog";
import { clearAllModals, flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../../test-utils";
describe("InteractiveAuthDialog", function () {
const homeserverUrl = "https://matrix.org";
const authUrl = "https://auth.com";
const mockClient = getMockClientWithEventEmitter({
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
getFallbackAuthUrl: jest.fn().mockReturnValue(authUrl),
getHomeserverUrl: jest.fn().mockReturnValue(homeserverUrl),
});
const defaultProps = {
matrixClient: mockClient,
makeRequest: jest.fn().mockResolvedValue(undefined),
onFinished: jest.fn(),
};
const renderComponent = (props = {}) => render(<InteractiveAuthDialog {...defaultProps} {...props} />);
const getPasswordField = () => screen.getByLabelText("Password");
const getSubmitButton = () => screen.getByRole("button", { name: "Continue" });
beforeEach(async function () {
jest.clearAllMocks();
mockClient.credentials = { userId: null };
await clearAllModals();
});
afterAll(async () => {
unmockClientPeg();
await clearAllModals();
});
it("Should successfully complete a password flow", async () => {
const onFinished = jest.fn();
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
mockClient.credentials = { userId: "@user:id" };
const authData = {
session: "sess",
flows: [{ stages: ["m.login.password"] }],
};
renderComponent({ makeRequest, onFinished, authData });
const passwordField = getPasswordField();
const submitButton = getSubmitButton();
expect(passwordField).toBeTruthy();
expect(submitButton).toBeTruthy();
// submit should be disabled
expect(submitButton).toBeDisabled();
// put something in the password box
await userEvent.type(passwordField, "s3kr3t");
expect(submitButton).not.toBeDisabled();
// hit enter; that should trigger a request
await userEvent.click(submitButton);
// wait for auth request to resolve
await flushPromises();
expect(makeRequest).toHaveBeenCalledTimes(1);
expect(makeRequest).toHaveBeenCalledWith(
expect.objectContaining({
session: "sess",
type: "m.login.password",
password: "s3kr3t",
identifier: {
type: "m.id.user",
user: "@user:id",
},
}),
);
expect(onFinished).toHaveBeenCalledTimes(1);
expect(onFinished).toHaveBeenCalledWith(true, { a: 1 });
});
describe("SSO flow", () => {
it("should close on cancel", () => {
const onFinished = jest.fn();
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
mockClient.credentials = { userId: "@user:id" };
const authData = {
session: "sess",
flows: [{ stages: ["m.login.sso"] }],
};
renderComponent({ makeRequest, onFinished, authData });
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
fireEvent.click(screen.getByText("Cancel"));
expect(onFinished).toHaveBeenCalledWith(false, null);
});
it("should complete an sso flow", async () => {
jest.spyOn(global.window, "addEventListener");
// @ts-ignore
jest.spyOn(global.window, "open").mockImplementation(() => {});
const onFinished = jest.fn();
const successfulResult = { test: 1 };
const makeRequest = jest
.fn()
.mockRejectedValueOnce(new MatrixError({ flows: [{ stages: ["m.login.sso"] }] }, 401))
.mockResolvedValue(successfulResult);
mockClient.credentials = { userId: "@user:id" };
const authData = {
session: "sess",
flows: [{ stages: ["m.login.sso"] }],
};
renderComponent({ makeRequest, onFinished, authData });
await flushPromises();
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
fireEvent.click(screen.getByText("Single Sign On"));
// no we're on the sso auth screen
expect(screen.getByText("Click the button below to confirm your identity.")).toBeInTheDocument();
// launch sso
fireEvent.click(screen.getByText("Confirm"));
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
const onWindowReceiveMessageCall = mocked(window.addEventListener).mock.calls.find(
(args) => args[0] === "message",
);
expect(onWindowReceiveMessageCall).toBeTruthy();
// get the handle from SSO auth component
// so we can pretend sso auth was completed
const onWindowReceiveMessage = onWindowReceiveMessageCall![1];
// complete sso successfully
act(() => {
// @ts-ignore
onWindowReceiveMessage({ data: "authDone", origin: homeserverUrl });
});
// expect(makeRequest).toHaveBeenCalledWith({ session: authData.session })
// spinner displayed
expect(screen.getByRole("progressbar")).toBeInTheDocument();
// cancel/confirm buttons hidden while request in progress
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
await flushPromises();
await flushPromises();
// nothing in progress
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
// auth completed, now make the request again with auth
fireEvent.click(screen.getByText("Confirm"));
// loading while making request
expect(screen.getByRole("progressbar")).toBeInTheDocument();
expect(makeRequest).toHaveBeenCalledTimes(2);
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, successfulResult);
});
});
});

View file

@ -0,0 +1,488 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { RoomType, MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { sleep } from "matrix-js-sdk/src/utils";
import { mocked, Mocked } from "jest-mock";
import InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog";
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
import {
clearAllModals,
filterConsole,
flushPromises,
getMockClientWithEventEmitter,
mkMembership,
mkMessage,
mkRoomCreateEvent,
} from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import SdkConfig from "../../../../../src/SdkConfig";
import { ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
import { IConfigOptions } from "../../../../../src/IConfigOptions";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { IProfileInfo } from "../../../../../src/hooks/useProfileInfo";
import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages";
import SettingsStore from "../../../../../src/settings/SettingsStore";
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
jest.mock("../../../../../src/IdentityAuthClient", () =>
jest.fn().mockImplementation(() => ({
getAccessToken: mockGetAccessToken,
})),
);
jest.mock("../../../../../src/utils/direct-messages", () => ({
...jest.requireActual("../../../../../src/utils/direct-messages"),
__esModule: true,
startDmOnFirstMessage: jest.fn(),
}));
const getSearchField = () => screen.getByTestId("invite-dialog-input");
const enterIntoSearchField = async (value: string) => {
const searchField = getSearchField();
await userEvent.clear(searchField);
await userEvent.type(searchField, value + "{enter}");
};
const pasteIntoSearchField = async (value: string) => {
const searchField = getSearchField();
await userEvent.clear(searchField);
searchField.focus();
await userEvent.paste(value);
};
const expectPill = (value: string) => {
expect(screen.getByText(value)).toBeInTheDocument();
expect(getSearchField()).toHaveValue("");
};
const expectNoPill = (value: string) => {
expect(screen.queryByText(value)).not.toBeInTheDocument();
expect(getSearchField()).toHaveValue(value);
};
const roomId = "!111111111111111111:example.org";
const aliceId = "@alice:example.org";
const aliceEmail = "foobar@email.com";
const bobId = "@bob:example.org";
const bobEmail = "bobbob@example.com"; // bob@example.com is already used as an example in the invite dialog
const carolId = "@carol:example.com";
const bobbob = "bobbob";
const aliceProfileInfo: IProfileInfo = {
user_id: aliceId,
display_name: "Alice",
};
const bobProfileInfo: IProfileInfo = {
user_id: bobId,
display_name: "Bob",
};
describe("InviteDialog", () => {
let mockClient: Mocked<MatrixClient>;
let room: Room;
filterConsole(
"Error retrieving profile for userId @carol:example.com",
"Error retrieving profile for userId @localpart:server.tld",
"Error retrieving profile for userId @localpart:server:tld",
"[Invite:Recents] Excluding @alice:example.org from recents",
);
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(bobId),
getSafeUserId: jest.fn().mockReturnValue(bobId),
isGuest: jest.fn().mockReturnValue(false),
getVisibleRooms: jest.fn().mockReturnValue([]),
getRoom: jest.fn(),
getRooms: jest.fn(),
getAccountData: jest.fn(),
getPushActionsForEvent: jest.fn(),
mxcUrlToHttp: jest.fn().mockReturnValue(""),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getProfileInfo: jest.fn().mockImplementation(async (userId: string) => {
if (userId === aliceId) return aliceProfileInfo;
if (userId === bobId) return bobProfileInfo;
throw new MatrixError({
errcode: "M_NOT_FOUND",
error: "Profile not found",
});
}),
getIdentityServerUrl: jest.fn(),
searchUserDirectory: jest.fn().mockResolvedValue({}),
lookupThreePid: jest.fn(),
registerWithIdentityServer: jest.fn().mockResolvedValue({
access_token: "access_token",
token: "token",
}),
getOpenIdToken: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
supportsThreads: jest.fn().mockReturnValue(false),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
getClientWellKnown: jest.fn().mockResolvedValue({}),
});
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
DMRoomMap.makeShared(mockClient);
jest.clearAllMocks();
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
room.addLiveEvents([
mkMessage({
msg: "Hello",
relatesTo: undefined,
event: true,
room: roomId,
user: mockClient.getSafeUserId(),
ts: Date.now(),
}),
]);
room.currentState.setStateEvents([
mkRoomCreateEvent(bobId, roomId),
mkMembership({
event: true,
room: roomId,
mship: KnownMembership.Join,
user: aliceId,
skey: aliceId,
}),
]);
jest.spyOn(DMRoomMap.shared(), "getUniqueRoomsWithIndividuals").mockReturnValue({
[aliceId]: room,
});
mockClient.getRooms.mockReturnValue([room]);
mockClient.getRoom.mockReturnValue(room);
SdkContextClass.instance.client = mockClient;
});
afterEach(async () => {
await clearAllModals();
SdkContextClass.instance.onLoggedOut();
SdkContextClass.instance.client = undefined;
});
afterAll(() => {
jest.restoreAllMocks();
});
it("should label with space name", () => {
room.isSpaceRoom = jest.fn().mockReturnValue(true);
room.getType = jest.fn().mockReturnValue(RoomType.Space);
room.name = "Space";
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
expect(screen.queryByText("Invite to Space")).toBeTruthy();
});
it("should label with room name", () => {
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
expect(screen.getByText(`Invite to ${roomId}`)).toBeInTheDocument();
});
it("should not suggest valid unknown MXIDs", async () => {
render(
<InviteDialog
kind={InviteKind.Invite}
roomId={roomId}
onFinished={jest.fn()}
initialText="@localpart:server.tld"
/>,
);
await flushPromises();
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
});
it("should not suggest invalid MXIDs", () => {
render(
<InviteDialog
kind={InviteKind.Invite}
roomId={roomId}
onFinished={jest.fn()}
initialText="@localpart:server:tld"
/>,
);
expect(screen.queryByText("@localpart:server:tld")).toBeFalsy();
});
it.each([[InviteKind.Dm], [InviteKind.Invite]] as [typeof InviteKind.Dm | typeof InviteKind.Invite][])(
"should lookup inputs which look like email addresses (%s)",
async (kind: typeof InviteKind.Dm | typeof InviteKind.Invite) => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({
address: aliceEmail,
medium: "email",
mxid: aliceId,
});
mockClient.getProfileInfo.mockResolvedValue({
displayname: "Mrs Alice",
avatar_url: "mxc://foo/bar",
});
render(
<InviteDialog
kind={kind}
roomId={kind === InviteKind.Invite ? roomId : ""}
onFinished={jest.fn()}
initialText={aliceEmail}
/>,
);
await screen.findByText("Mrs Alice");
// expect the email and MXID to be visible
await screen.findByText(aliceId);
await screen.findByText(aliceEmail);
expect(mockClient.lookupThreePid).toHaveBeenCalledWith("email", aliceEmail, expect.anything());
expect(mockClient.getProfileInfo).toHaveBeenCalledWith(aliceId);
},
);
it("should suggest e-mail even if lookup fails", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});
render(
<InviteDialog
kind={InviteKind.Invite}
roomId={roomId}
onFinished={jest.fn()}
initialText="foobar@email.com"
/>,
);
await screen.findByText("foobar@email.com");
await screen.findByText("Invite by email");
});
it("should add pasted values", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
const input = screen.getByTestId("invite-dialog-input");
input.focus();
await userEvent.paste(`${bobId} ${aliceEmail}`);
await screen.findAllByText(bobId);
await screen.findByText(aliceEmail);
expect(input).toHaveValue("");
});
it("should support pasting one username that is not a mx id or email", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
const input = screen.getByTestId("invite-dialog-input");
input.focus();
await userEvent.paste(`${bobbob}`);
await screen.findAllByText(bobId);
expect(input).toHaveValue(`${bobbob}`);
});
it("should allow to invite multiple emails to a room", async () => {
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
await enterIntoSearchField(aliceEmail);
expectPill(aliceEmail);
await enterIntoSearchField(bobEmail);
expectPill(bobEmail);
});
describe("when encryption by default is disabled", () => {
beforeEach(() => {
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: false,
},
});
});
it("should allow to invite more than one email to a DM", async () => {
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
await enterIntoSearchField(aliceEmail);
expectPill(aliceEmail);
await enterIntoSearchField(bobEmail);
expectPill(bobEmail);
});
});
it("should not allow to invite more than one email to a DM", async () => {
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
// Start with an email → should convert to a pill
await enterIntoSearchField(aliceEmail);
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
expectPill(aliceEmail);
// Everything else from now on should not convert to a pill
await enterIntoSearchField(bobEmail);
expectNoPill(bobEmail);
await enterIntoSearchField(aliceId);
expectNoPill(aliceId);
await pasteIntoSearchField(bobEmail);
expectNoPill(bobEmail);
});
it("should not allow to invite a MXID and an email to a DM", async () => {
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
// Start with a MXID → should convert to a pill
await enterIntoSearchField(carolId);
expect(screen.queryByText("Invites by email can only be sent one at a time")).not.toBeInTheDocument();
expectPill(carolId);
// Add an email → should not convert to a pill
await enterIntoSearchField(bobEmail);
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
expectNoPill(bobEmail);
});
it("should start a DM if the profile is available", async () => {
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
await enterIntoSearchField(aliceId);
await userEvent.click(screen.getByRole("button", { name: "Go" }));
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
new DirectoryMember({
user_id: aliceId,
}),
]);
});
it("should not allow pasting the same user multiple times", async () => {
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
const input = screen.getByTestId("invite-dialog-input");
input.focus();
await userEvent.paste(`${bobId}`);
await userEvent.paste(`${bobId}`);
await userEvent.paste(`${bobId}`);
expect(input).toHaveValue("");
await expect(screen.findAllByText(bobId, { selector: "a" })).resolves.toHaveLength(1);
});
it("should add to selection on click of user tile", async () => {
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
const input = screen.getByTestId("invite-dialog-input");
input.focus();
await userEvent.keyboard(`${aliceId}`);
const btn = await screen.findByText(aliceId, {
selector: ".mx_InviteDialog_tile_nameStack_userId .mx_InviteDialog_tile--room_highlight",
});
fireEvent.click(btn);
const tile = await screen.findByText(aliceId, { selector: ".mx_InviteDialog_userTile_name" });
expect(tile).toBeInTheDocument();
});
describe("when inviting a user with an unknown profile", () => {
beforeEach(async () => {
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
await enterIntoSearchField(carolId);
await userEvent.click(screen.getByRole("button", { name: "Go" }));
// Wait for the »invite anyway« modal to show up
await screen.findByText("The following users may not exist");
});
it("should not start the DM", () => {
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
});
it("should show the »invite anyway« dialog if the profile is not available", () => {
expect(screen.getByText("The following users may not exist")).toBeInTheDocument();
expect(screen.getByText(`${carolId}: Profile not found`)).toBeInTheDocument();
});
describe("when clicking »Start DM anyway«", () => {
beforeEach(async () => {
await userEvent.click(screen.getByRole("button", { name: "Start DM anyway" }));
});
it("should start the DM", () => {
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
new DirectoryMember({
user_id: carolId,
}),
]);
});
});
describe("when clicking »Close«", () => {
beforeEach(async () => {
mocked(startDmOnFirstMessage).mockClear();
await userEvent.click(screen.getByRole("button", { name: "Close" }));
});
it("should not start the DM", () => {
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
});
});
});
describe("when inviting a user with an unknown profile and »promptBeforeInviteUnknownUsers« setting = false", () => {
beforeEach(async () => {
mocked(startDmOnFirstMessage).mockClear();
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName !== "promptBeforeInviteUnknownUsers",
);
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
await enterIntoSearchField(carolId);
await userEvent.click(screen.getByRole("button", { name: "Go" }));
// modal rendering has some weird sleeps - fake timers will mess up the entire test
await sleep(100);
});
it("should not show the »invite anyway« dialog", () => {
expect(screen.queryByText("The following users may not exist")).not.toBeInTheDocument();
});
it("should start the DM directly", () => {
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
new DirectoryMember({
user_id: carolId,
}),
]);
});
});
it("should not suggest users from other server when room has m.federate=false", async () => {
room.currentState.setStateEvents([mkRoomCreateEvent(bobId, roomId, { "m.federate": false })]);
render(
<InviteDialog
kind={InviteKind.Invite}
roomId={roomId}
onFinished={jest.fn()}
initialText="@localpart:server.tld"
/>,
);
await flushPromises();
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,76 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { CryptoApi, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { render, RenderResult } from "jest-matrix-react";
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils";
import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog";
describe("LogoutDialog", () => {
let mockClient: MockedObject<MatrixClient>;
let mockCrypto: MockedObject<CryptoApi>;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn(),
});
mockCrypto = mocked(mockClient.getCrypto()!);
Object.assign(mockCrypto, {
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
});
});
function renderComponent(props: Partial<React.ComponentProps<typeof LogoutDialog>> = {}): RenderResult {
const onFinished = jest.fn();
return render(<LogoutDialog onFinished={onFinished} {...props} />);
}
it("shows a regular dialog when crypto is disabled", async () => {
mocked(mockClient.getCrypto).mockReturnValue(undefined);
const rendered = renderComponent();
await rendered.findByText("Are you sure you want to sign out?");
expect(rendered.container).toMatchSnapshot();
});
it("shows a regular dialog if backups are working", async () => {
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
const rendered = renderComponent();
await rendered.findByText("Are you sure you want to sign out?");
});
it("Prompts user to connect backup if there is a backup on the server", async () => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
const rendered = renderComponent();
await rendered.findByText("Connect this session to Key Backup");
expect(rendered.container).toMatchSnapshot();
});
it("Prompts user to set up backup if there is no backup on the server", async () => {
mockClient.getKeyBackupVersion.mockResolvedValue(null);
const rendered = renderComponent();
await rendered.findByText("Start using Key Backup");
expect(rendered.container).toMatchSnapshot();
});
describe("when there is an error fetching backups", () => {
filterConsole("Unable to fetch key backup status");
it("prompts user to set up backup", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => {
throw new Error("beep");
});
const rendered = renderComponent();
await rendered.findByText("Start using Key Backup");
});
});
});

View file

@ -0,0 +1,43 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { Room } from "matrix-js-sdk/src/matrix";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
import ManageRestrictedJoinRuleDialog from "../../../../../src/components/views/dialogs/ManageRestrictedJoinRuleDialog";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
describe("<ManageRestrictedJoinRuleDialog />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
});
const room = new Room("!roomId:server", mockClient, userId);
mockClient.getRoom.mockReturnValue(room);
DMRoomMap.makeShared(mockClient);
const onFinished = jest.fn();
const getComponent = (props = {}) =>
render(<ManageRestrictedJoinRuleDialog room={room} onFinished={onFinished} {...props} />);
it("should render empty state", () => {
expect(getComponent().asFragment()).toMatchSnapshot();
});
it("should list spaces which are not parents of the room", () => {
const space1 = new Room("!space:server", mockClient, userId);
space1.name = "Other Space";
jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([space1]);
expect(getComponent().asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,104 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { Device, MatrixClient } from "matrix-js-sdk/src/matrix";
import { stubClient } from "../../../../test-utils";
import { ManualDeviceKeyVerificationDialog } from "../../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
describe("ManualDeviceKeyVerificationDialog", () => {
let mockClient: MatrixClient;
function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) {
return render(
<MatrixClientContext.Provider value={mockClient}>
<ManualDeviceKeyVerificationDialog userId={userId} device={device} onFinished={onLegacyFinished} />
</MatrixClientContext.Provider>,
);
}
beforeEach(() => {
mockClient = stubClient();
});
it("should display the device", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn());
// Then
expect(container).toMatchSnapshot();
});
it("should display the device of another user", () => {
// When
const userId = "@alice:example.com";
const deviceId = "XYZ";
const device = new Device({
userId,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(userId, device, jest.fn());
// Then
expect(container).toMatchSnapshot();
});
it("should call onFinished and matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);
screen.getByRole("button", { name: "Verify session" }).click();
// Then
expect(onFinished).toHaveBeenCalledWith(true);
expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true);
});
it("should call onFinished and not matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);
screen.getByRole("button", { name: "Cancel" }).click();
// Then
expect(onFinished).toHaveBeenCalledWith(false);
expect(mockClient.setDeviceVerified).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,76 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, RenderResult, waitForElementToBeRemoved } from "jest-matrix-react";
import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { flushPromises, mkMessage, stubClient } from "../../../../test-utils";
import MessageEditHistoryDialog from "../../../../../src/components/views/dialogs/MessageEditHistoryDialog";
describe("<MessageEditHistory />", () => {
const roomId = "!aroom:example.com";
let client: jest.Mocked<MatrixClient>;
let event: MatrixEvent;
beforeEach(() => {
client = stubClient() as jest.Mocked<MatrixClient>;
event = mkMessage({
event: true,
user: "@user:example.com",
room: "!room:example.com",
msg: "My Great Message",
});
});
async function renderComponent(): Promise<RenderResult> {
const result = render(<MessageEditHistoryDialog mxEvent={event} onFinished={jest.fn()} />);
await waitForElementToBeRemoved(() => result.queryByRole("progressbar"));
await flushPromises();
return result;
}
function mockEdits(...edits: { msg: string; ts?: number }[]) {
client.relations.mockImplementation(() =>
Promise.resolve({
events: edits.map(
(e) =>
new MatrixEvent({
type: EventType.RoomMessage,
room_id: roomId,
origin_server_ts: e.ts ?? 0,
content: {
body: e.msg,
},
}),
),
}),
);
}
it("should match the snapshot", async () => {
mockEdits({ msg: "My Great Massage", ts: 1234 });
const { container } = await renderComponent();
expect(container).toMatchSnapshot();
});
it("should support events with", async () => {
mockEdits(
{ msg: "My Great Massage", ts: undefined },
{ msg: "My Great Massage?", ts: undefined },
{ msg: "My Great Missage", ts: undefined },
);
const { container } = await renderComponent();
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,185 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import {
EventTimeline,
EventType,
JoinRule,
MatrixEvent,
Room,
RoomStateEvent,
Visibility,
} from "matrix-js-sdk/src/matrix";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
import RoomSettingsDialog from "../../../../../src/components/views/dialogs/RoomSettingsDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
describe("<RoomSettingsDialog />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getRoom: jest.fn(),
getDomain: jest.fn().mockReturnValue("server.org"),
getLocalAliases: jest.fn().mockResolvedValue({ aliases: [] }),
getRoomDirectoryVisibility: jest.fn().mockResolvedValue({ visibility: Visibility.Private }),
getOrCreateFilter: jest.fn(),
});
const roomId = "!room:server.org";
const room = new Room(roomId, mockClient, userId);
room.name = "Test Room";
const room2 = new Room("!room2:server.org", mockClient, userId);
room2.name = "Another Room";
jest.spyOn(SettingsStore, "getValue");
beforeEach(() => {
jest.clearAllMocks();
mockClient.getRoom.mockImplementation((roomId) => {
if (roomId === room.roomId) return room;
if (roomId === room2.roomId) return room2;
return null;
});
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
const dmRoomMap = {
getUserIdForRoomId: jest.fn(),
} as unknown as DMRoomMap;
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
});
const getComponent = (onFinished = jest.fn(), propRoomId = roomId) =>
render(<RoomSettingsDialog roomId={propRoomId} onFinished={onFinished} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
),
});
it("catches errors when room is not found", () => {
getComponent(undefined, "!room-that-does-not-exist");
expect(screen.getByText("Something went wrong!")).toBeInTheDocument();
});
it("updates when roomId prop changes", () => {
const { rerender, getByText } = getComponent(undefined, roomId);
expect(getByText(`Room Settings - ${room.name}`)).toBeInTheDocument();
rerender(<RoomSettingsDialog roomId={room2.roomId} onFinished={jest.fn()} />);
expect(getByText(`Room Settings - ${room2.name}`)).toBeInTheDocument();
});
describe("Settings tabs", () => {
it("renders default tabs correctly", () => {
const { container } = getComponent();
expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot();
});
describe("people settings tab", () => {
it("does not render when disabled and room join rule is not knock", () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
getComponent();
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
});
it("does not render when disabled and room join rule is knock", () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
getComponent();
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
});
it("does not render when enabled and room join rule is not knock", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_ask_to_join",
);
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
getComponent();
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
});
it("renders when enabled and room join rule is knock", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_ask_to_join",
);
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
getComponent();
expect(screen.getByTestId("settings-tab-ROOM_PEOPLE_TAB")).toBeInTheDocument();
});
it("re-renders on room join rule changes", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_ask_to_join",
);
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
getComponent();
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
mockClient.emit(
RoomStateEvent.Events,
new MatrixEvent({ content: {}, type: EventType.RoomJoinRules }),
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
null,
);
await waitFor(() =>
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(),
);
});
});
it("renders voip settings tab when enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_group_calls",
);
getComponent();
expect(screen.getByTestId("settings-tab-ROOM_VOIP_TAB")).toBeInTheDocument();
});
it("renders bridges settings tab when enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_bridge_state",
);
getComponent();
expect(screen.getByTestId("settings-tab-ROOM_BRIDGES_TAB")).toBeInTheDocument();
});
it("renders advanced settings tab when enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.AdvancedSettings,
);
getComponent();
expect(screen.getByTestId("settings-tab-ROOM_ADVANCED_TAB")).toBeInTheDocument();
});
});
describe("poll history", () => {
beforeEach(() => {
mockClient.getOrCreateFilter.mockResolvedValue("filterId");
});
it("renders poll history tab", () => {
getComponent();
expect(screen.getByTestId("settings-tab-ROOM_POLL_HISTORY_TAB")).toBeInTheDocument();
});
it("displays poll history when tab clicked", () => {
const { container } = getComponent();
fireEvent.click(screen.getByText("Polls"));
expect(container.querySelector(".mx_SettingsTab")).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,261 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import ServerPickerDialog from "../../../../../src/components/views/dialogs/ServerPickerDialog";
import SdkConfig from "../../../../../src/SdkConfig";
import { flushPromises } from "../../../../test-utils";
import { ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
/** The matrix versions our mock server claims to support */
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
describe("<ServerPickerDialog />", () => {
const defaultServerConfig = {
hsUrl: "https://matrix.org",
hsName: "matrix.org",
hsNameIsDifferent: true,
isUrl: "https://is.org",
isDefault: true,
isNameResolvable: true,
warning: "",
};
const wkHsUrl = "https://hsbaseurlfrom.wk";
const wkIsUrl = "https://isbaseurlfrom.wk";
const validWellKnown = {
"m.homeserver": {
base_url: wkHsUrl,
},
"m.identity_server": {
base_url: wkIsUrl,
},
};
const defaultProps = {
serverConfig: defaultServerConfig,
onFinished: jest.fn(),
};
const getComponent = (
props: Partial<{
onFinished: any;
serverConfig: ValidatedServerConfig;
}> = {},
) => render(<ServerPickerDialog {...defaultProps} {...props} />);
beforeEach(() => {
SdkConfig.add({
validated_server_config: defaultServerConfig,
});
fetchMock.resetHistory();
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});
});
it("should render dialog", () => {
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
// checkbox and text input have the same aria-label
const getOtherHomeserverCheckBox = () =>
screen.getAllByLabelText("Other homeserver").find((node) => node.getAttribute("type") === "radio")!;
const getOtherHomeserverInput = () =>
screen.getAllByLabelText("Other homeserver").find((node) => node.getAttribute("type") === "text")!;
describe("when default server config is selected", () => {
it("should select other homeserver field on open", () => {
getComponent();
expect(getOtherHomeserverCheckBox()).toBeChecked();
// empty field
expect(getOtherHomeserverInput()).toHaveDisplayValue("");
});
it("should display an error when trying to continue with an empty homeserver field", async () => {
const onFinished = jest.fn();
const { container } = getComponent({ onFinished });
fireEvent.click(screen.getByText("Continue"));
await flushPromises();
// error on field
expect(container.querySelector(".mx_ServerPickerDialog_otherHomeserver.mx_Field_invalid")).toBeTruthy();
// didn't close dialog
expect(onFinished).not.toHaveBeenCalled();
});
it("should close when selecting default homeserver and clicking continue", async () => {
const onFinished = jest.fn();
getComponent({ onFinished });
fireEvent.click(screen.getByTestId("defaultHomeserver"));
expect(screen.getByTestId("defaultHomeserver")).toBeChecked();
fireEvent.click(screen.getByText("Continue"));
// closed dialog with default server
expect(onFinished).toHaveBeenCalledWith(defaultServerConfig);
});
it("should allow user to revert from a custom server to the default", async () => {
fetchMock.get(`https://custom.org/_matrix/client/versions`, {
unstable_features: {},
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
});
const onFinished = jest.fn();
const serverConfig = {
hsUrl: "https://custom.org",
hsName: "custom.org",
hsNameIsDifferent: true,
isUrl: "https://is.org",
isDefault: false,
isNameResolvable: true,
warning: "",
};
getComponent({ onFinished, serverConfig });
fireEvent.click(screen.getByTestId("defaultHomeserver"));
expect(screen.getByTestId("defaultHomeserver")).toBeChecked();
fireEvent.click(screen.getByText("Continue"));
await flushPromises();
// closed dialog with default server and nothing else
expect(onFinished).toHaveBeenCalledWith(defaultServerConfig);
expect(onFinished).toHaveBeenCalledTimes(1);
});
it("should submit successfully with a valid custom homeserver", async () => {
const homeserver = "https://myhomeserver.site";
fetchMock.get(`${homeserver}/_matrix/client/versions`, {
unstable_features: {},
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
});
const onFinished = jest.fn();
getComponent({ onFinished });
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
expect(getOtherHomeserverInput()).toHaveDisplayValue(homeserver);
fireEvent.click(screen.getByText("Continue"));
// validation on submit is async
await flushPromises();
// closed dialog with validated custom server
expect(onFinished).toHaveBeenCalledWith({
hsName: "myhomeserver.site",
hsUrl: homeserver,
hsNameIsDifferent: false,
warning: null,
isDefault: false,
isNameResolvable: false,
isUrl: defaultServerConfig.isUrl,
});
});
describe("validates custom homeserver", () => {
it("should lookup .well-known for homeserver without protocol", async () => {
const homeserver = "myhomeserver1.site";
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
fetchMock.get(wellKnownUrl, {});
getComponent();
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
expect(getOtherHomeserverInput()).toHaveDisplayValue(homeserver);
// trigger validation
fireEvent.blur(getOtherHomeserverInput());
// validation on submit is async
await flushPromises();
expect(fetchMock).toHaveFetched(wellKnownUrl);
});
it("should submit using validated config from a valid .well-known", async () => {
const homeserver = "myhomeserver2.site";
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
// urls from homeserver well-known
const versionsUrl = `${wkHsUrl}/_matrix/client/versions`;
const isWellKnownUrl = `${wkIsUrl}/_matrix/identity/v2`;
fetchMock.getOnce(wellKnownUrl, validWellKnown);
fetchMock.getOnce(versionsUrl, {
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
});
fetchMock.getOnce(isWellKnownUrl, {});
const onFinished = jest.fn();
getComponent({ onFinished });
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
fireEvent.click(screen.getByText("Continue"));
// validation on submit is async
await flushPromises();
expect(fetchMock).toHaveFetched(wellKnownUrl);
// fetched using urls from .well-known
expect(fetchMock).toHaveFetched(versionsUrl);
expect(fetchMock).toHaveFetched(isWellKnownUrl);
expect(onFinished).toHaveBeenCalledWith({
hsName: homeserver,
hsUrl: wkHsUrl,
hsNameIsDifferent: true,
warning: null,
isDefault: false,
isNameResolvable: true,
isUrl: wkIsUrl,
});
await flushPromises();
});
it("should fall back to static config when well-known lookup fails", async () => {
const homeserver = "myhomeserver3.site";
// this server returns 404 for well-known
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
fetchMock.get(wellKnownUrl, { status: 404 });
// but is otherwise live (happy versions response)
fetchMock.get(`https://${homeserver}/_matrix/client/versions`, {
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
});
const onFinished = jest.fn();
getComponent({ onFinished });
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
fireEvent.click(screen.getByText("Continue"));
// validation on submit is async
await flushPromises();
expect(fetchMock).toHaveFetched(wellKnownUrl);
expect(fetchMock).toHaveFetched(`https://${homeserver}/_matrix/client/versions`);
expect(onFinished).toHaveBeenCalledWith({
hsName: homeserver,
hsUrl: "https://" + homeserver,
hsNameIsDifferent: false,
warning: null,
isDefault: false,
isNameResolvable: false,
isUrl: defaultServerConfig.isUrl,
});
});
});
});
});

View file

@ -0,0 +1,117 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, RenderOptions } from "jest-matrix-react";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { _t } from "../../../../../src/languageHandler";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import { stubClient } from "../../../../test-utils";
jest.mock("../../../../../src/utils/ShieldUtils");
function getWrapper(): RenderOptions {
return {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider>
),
};
}
describe("ShareDialog", () => {
let room: Room;
const ROOM_ID = "!1:example.org";
beforeEach(async () => {
stubClient();
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org");
});
afterEach(() => {
jest.restoreAllMocks();
});
it("renders room share dialog", () => {
const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withoutEvents).toHaveTextContent(_t("share|title_room"));
jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline);
const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent"));
});
it("renders user share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />,
getWrapper(),
);
expect(container).toHaveTextContent(_t("share|title_user"));
});
it("renders link share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />,
getWrapper(),
);
expect(container).toHaveTextContent(_t("share|title_link"));
});
it("renders the QR code if configured", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareQRCode) return true;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0;
expect(qrCodesVisible).toBe(true);
});
it("renders the social button if configured", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareSocial) return true;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0;
expect(qrCodesVisible).toBe(true);
});
it("renders custom title and subtitle", () => {
const { container } = render(
<ShareDialog
target={room}
customTitle="test_title_123"
subtitle="custom_subtitle_1234"
onFinished={jest.fn()}
/>,
getWrapper(),
);
expect(container).toHaveTextContent("test_title_123");
expect(container).toHaveTextContent("custom_subtitle_1234");
});
});
/**
*
* @param count the number of users to create
*/
function mockRoomMembers(room: Room, count: number) {
const members = Array(count)
.fill(0)
.map((_, index) => new RoomMember(room.roomId, "@alice:example.org"));
room.currentState.setJoinedMemberCount(members.length);
room.getJoinedMembers = jest.fn().mockReturnValue(members);
}

View file

@ -0,0 +1,643 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import {
ConnectionError,
IProtocol,
IPublicRoomsChunkRoom,
JoinRule,
MatrixClient,
Room,
RoomMember,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import sanitizeHtml from "sanitize-html";
import { fireEvent, render, screen } from "jest-matrix-react";
import SpotlightDialog from "../../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
import { Filter } from "../../../../../src/components/views/dialogs/spotlight/Filter";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../../src/models/LocalRoom";
import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { flushPromisesWithFakeTimers, mkRoom, stubClient } from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import SdkConfig from "../../../../../src/SdkConfig";
import { Action } from "../../../../../src/dispatcher/actions";
jest.useFakeTimers();
jest.mock("../../../../../src/utils/Feedback");
jest.mock("../../../../../src/utils/direct-messages", () => ({
// @ts-ignore
...jest.requireActual("../../../../../src/utils/direct-messages"),
startDmOnFirstMessage: jest.fn(),
}));
jest.mock("../../../../../src/dispatcher/dispatcher", () => ({
register: jest.fn(),
dispatch: jest.fn(),
}));
interface IUserChunkMember {
user_id: string;
display_name?: string;
avatar_url?: string;
}
interface MockClientOptions {
userId?: string;
homeserver?: string;
thirdPartyProtocols?: Record<string, IProtocol>;
rooms?: IPublicRoomsChunkRoom[];
members?: RoomMember[];
users?: IUserChunkMember[];
}
function mockClient({
userId = "testuser",
homeserver = "example.tld",
thirdPartyProtocols = {},
rooms = [],
members = [],
users = [],
}: MockClientOptions = {}): MatrixClient {
stubClient();
const cli = MatrixClientPeg.safeGet();
cli.getUserId = jest.fn(() => userId);
cli.getDomain = jest.fn(() => homeserver);
cli.getHomeserverUrl = jest.fn(() => homeserver);
cli.getThirdpartyProtocols = jest.fn(() => Promise.resolve(thirdPartyProtocols));
cli.publicRooms = jest.fn((options) => {
const searchTerm = options?.filter?.generic_search_term?.toLowerCase();
const chunk = rooms.filter(
(it) =>
!searchTerm ||
it.room_id.toLowerCase().includes(searchTerm) ||
it.name?.toLowerCase().includes(searchTerm) ||
sanitizeHtml(it?.topic || "", { allowedTags: [] })
.toLowerCase()
.includes(searchTerm) ||
it.canonical_alias?.toLowerCase().includes(searchTerm) ||
it.aliases?.find((alias) => alias.toLowerCase().includes(searchTerm)),
);
return Promise.resolve({
chunk,
total_room_count_estimate: chunk.length,
});
});
cli.searchUserDirectory = jest.fn(({ term, limit }) => {
const searchTerm = term?.toLowerCase();
const results = users.filter(
(it) =>
!searchTerm ||
it.user_id.toLowerCase().includes(searchTerm) ||
it.display_name?.toLowerCase().includes(searchTerm),
);
return Promise.resolve({
results: results.slice(0, limit ?? +Infinity),
limited: !!limit && limit < results.length,
});
});
cli.getProfileInfo = jest.fn(async (userId) => {
const member = members.find((it) => it.userId === userId);
if (member) {
return Promise.resolve({
displayname: member.rawDisplayName,
avatar_url: member.getMxcAvatarUrl(),
});
} else {
return Promise.reject();
}
});
return cli;
}
describe("Spotlight Dialog", () => {
const testPerson: IUserChunkMember = {
user_id: "@janedoe:matrix.org",
display_name: "Jane Doe",
avatar_url: undefined,
};
const testPublicRoom: IPublicRoomsChunkRoom = {
room_id: "!room247:matrix.org",
name: "Room #247",
topic: "We hope you'll have a <b>shining</b> experience!",
world_readable: false,
num_joined_members: 1,
guest_can_join: false,
};
const testDMRoomId = "!testDM:example.com";
const testDMUserId = "@alice:matrix.org";
let testRoom: Room;
let testDM: Room;
let testLocalRoom: LocalRoom;
let mockedClient: MatrixClient;
beforeEach(() => {
mockedClient = mockClient({ rooms: [testPublicRoom], users: [testPerson] });
testRoom = mkRoom(mockedClient, "!test23:example.com");
mocked(testRoom.getMyMembership).mockReturnValue(KnownMembership.Join);
testLocalRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test23", mockedClient, mockedClient.getUserId()!);
testLocalRoom.updateMyMembership(KnownMembership.Join);
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom]);
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
getUserIdForRoomId: jest.fn(),
} as unknown as DMRoomMap);
testDM = mkRoom(mockedClient, testDMRoomId);
testDM.name = "Chat with Alice";
mocked(testDM.getMyMembership).mockReturnValue(KnownMembership.Join);
mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId: string) => {
if (roomId === testDMRoomId) {
return testDMUserId;
}
return undefined;
});
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom, testDM]);
});
describe("should apply filters supplied via props", () => {
it("without filter", async () => {
render(<SpotlightDialog onFinished={() => null} />);
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
expect(filterChip).not.toBeInTheDocument();
});
it("with public room filter", async () => {
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).toBeInTheDocument();
expect(filterChip.innerHTML).toContain("Public rooms");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0].innerHTML).toContain(testPublicRoom.name);
});
it("with people filter", async () => {
render(
<SpotlightDialog
initialFilter={Filter.People}
initialText={testPerson.display_name}
onFinished={() => null}
/>,
);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).toBeInTheDocument();
expect(filterChip.innerHTML).toContain("People");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
});
});
describe("when MSC3946 dynamic room predecessors is enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, excludeDefault) => {
if (settingName === "feature_dynamic_room_predecessors") {
return true;
} else {
return []; // SpotlightSearch.recentSearches
}
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
render(<SpotlightDialog onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
describe("should apply manually selected filter", () => {
it("with public rooms", async () => {
render(<SpotlightDialog onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
fireEvent.click(screen.getByText("Public rooms"));
// wrapper.find("#mx_SpotlightDialog_button_explorePublicRooms").first().simulate("click");
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).toBeInTheDocument();
expect(filterChip.innerHTML).toContain("Public rooms");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0]!.innerHTML).toContain(testPublicRoom.name);
// assert that getVisibleRooms is called without MSC3946 dynamic room predecessors
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(false);
});
it("with people", async () => {
render(<SpotlightDialog initialText={testPerson.display_name} onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
fireEvent.click(screen.getByText("People"));
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).toBeInTheDocument();
expect(filterChip.innerHTML).toContain("People");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
});
});
describe("should allow clearing filter manually", () => {
it("with public room filter", async () => {
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
let filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).toBeInTheDocument();
expect(filterChip.innerHTML).toContain("Public rooms");
fireEvent.click(filterChip.querySelector("div.mx_SpotlightDialog_filter--close")!);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
expect(filterChip).not.toBeInTheDocument();
});
it("with people filter", async () => {
render(
<SpotlightDialog
initialFilter={Filter.People}
initialText={testPerson.display_name}
onFinished={() => null}
/>,
);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
let filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
expect(filterChip).toBeInTheDocument();
expect(filterChip!.innerHTML).toContain("People");
fireEvent.click(filterChip!.querySelector("div.mx_SpotlightDialog_filter--close")!);
jest.advanceTimersByTime(1);
await flushPromisesWithFakeTimers();
filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
expect(filterChip).not.toBeInTheDocument();
});
});
describe("searching for rooms", () => {
let options: NodeListOf<Element>;
beforeAll(async () => {
render(<SpotlightDialog initialText="test23" onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
options = content.querySelectorAll("li.mx_SpotlightDialog_option");
});
it("should find Rooms", () => {
expect(options).toHaveLength(4);
expect(options[0]!.innerHTML).toContain(testRoom.name);
});
it("should not find LocalRooms", () => {
expect(options).toHaveLength(4);
expect(options[0]!.innerHTML).not.toContain(testLocalRoom.name);
});
});
it("should not filter out users sent by the server", async () => {
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
results: [
{ user_id: "@user1:server", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
{ user_id: "@user2:server", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
],
limited: false,
});
render(<SpotlightDialog initialFilter={Filter.People} initialText="Alpha" onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(2);
expect(options[0]).toHaveTextContent("User Alpha");
expect(options[1]).toHaveTextContent("User Beta");
});
it("should not filter out users sent by the server even if a local suggestion gets filtered out", async () => {
const member = new RoomMember(testRoom.roomId, testPerson.user_id);
member.name = member.rawDisplayName = testPerson.display_name!;
member.getMxcAvatarUrl = jest.fn().mockReturnValue("mxc://0/avatar");
mocked(testRoom.getJoinedMembers).mockReturnValue([member]);
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
results: [
{ user_id: "@janedoe:matrix.org", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
{ user_id: "@johndoe:matrix.org", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
],
limited: false,
});
render(<SpotlightDialog initialFilter={Filter.People} initialText="Beta" onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(2);
expect(options[0]).toHaveTextContent(testPerson.display_name!);
expect(options[1]).toHaveTextContent("User Beta");
});
it("show non-matching query members with DMs if they are present in the server search results", async () => {
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
results: [
{ user_id: testDMUserId, display_name: "Alice Wonder", avatar_url: "mxc://1/avatar" },
{ user_id: "@bob:matrix.org", display_name: "Bob Wonder", avatar_url: "mxc://2/avatar" },
],
limited: false,
});
render(
<SpotlightDialog initialFilter={Filter.People} initialText="Something Wonder" onFinished={() => null} />,
);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(2);
expect(options[0]).toHaveTextContent(testDMUserId);
expect(options[1]).toHaveTextContent("Bob Wonder");
});
it("don't sort the order of users sent by the server", async () => {
const serverList = [
{ user_id: "@user2:server", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
{ user_id: "@user1:server", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
];
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
results: serverList,
limited: false,
});
render(<SpotlightDialog initialFilter={Filter.People} initialText="User" onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(2);
expect(options[0]).toHaveTextContent("User Beta");
expect(options[1]).toHaveTextContent("User Alpha");
});
it("should start a DM when clicking a person", async () => {
render(
<SpotlightDialog
initialFilter={Filter.People}
initialText={testPerson.display_name}
onFinished={() => null}
/>,
);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const options = document.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
fireEvent.click(options[0]!);
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockedClient, [new DirectoryMember(testPerson)]);
});
it("should pass via of the server being explored when joining room from directory", async () => {
SdkConfig.put({
room_directory: {
servers: ["example.tld"],
},
});
localStorage.setItem("mx_last_room_directory_server", "example.tld");
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0].innerHTML).toContain(testPublicRoom.name);
fireEvent.click(options[0].querySelector("[role='button']")!);
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: "view_room",
room_id: testPublicRoom.room_id,
via_servers: ["example.tld"],
}),
);
});
describe("nsfw public rooms filter", () => {
const nsfwNameRoom: IPublicRoomsChunkRoom = {
room_id: "@room1:matrix.org",
name: "Room 1 [NSFW]",
topic: undefined,
world_readable: false,
num_joined_members: 1,
guest_can_join: false,
};
const nsfwTopicRoom: IPublicRoomsChunkRoom = {
room_id: "@room2:matrix.org",
name: "Room 2",
topic: "A room with a topic that includes nsfw",
world_readable: false,
num_joined_members: 1,
guest_can_join: false,
};
const potatoRoom: IPublicRoomsChunkRoom = {
room_id: "@room3:matrix.org",
name: "Potato Room 3",
topic: "Room where we discuss potatoes",
world_readable: false,
num_joined_members: 1,
guest_can_join: false,
};
beforeEach(() => {
mockedClient = mockClient({ rooms: [nsfwNameRoom, nsfwTopicRoom, potatoRoom], users: [testPerson] });
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, false);
});
afterAll(() => {
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, false);
});
it("does not display rooms with nsfw keywords in results when showNsfwPublicRooms is falsy", async () => {
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(screen.getByText(potatoRoom.name!)).toBeInTheDocument();
expect(screen.queryByText(nsfwTopicRoom.name!)).not.toBeInTheDocument();
expect(screen.queryByText(nsfwTopicRoom.name!)).not.toBeInTheDocument();
});
it("displays rooms with nsfw keywords in results when showNsfwPublicRooms is truthy", async () => {
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, true);
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(screen.getByText(nsfwTopicRoom.name!)).toBeInTheDocument();
expect(screen.getByText(nsfwNameRoom.name!)).toBeInTheDocument();
expect(screen.getByText(potatoRoom.name!)).toBeInTheDocument();
});
});
it("should show error if /publicRooms API failed", async () => {
mocked(mockedClient.publicRooms).mockRejectedValue(new ConnectionError("Failed to fetch"));
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(screen.getByText("Failed to query public rooms")).toBeInTheDocument();
});
describe("knock rooms", () => {
const knockRoom: IPublicRoomsChunkRoom = {
guest_can_join: false,
join_rule: JoinRule.Knock,
num_joined_members: 0,
room_id: "some-room-id",
world_readable: false,
};
const viewRoomParams = {
action: Action.ViewRoom,
metricsTrigger: "WebUnifiedSearch",
metricsViaKeyboard: false,
room_alias: undefined,
room_id: knockRoom.room_id,
should_peek: false,
via_servers: ["example.tld"],
};
beforeEach(() => (mockedClient = mockClient({ rooms: [knockRoom] })));
describe("when disabling feature", () => {
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) =>
setting === "feature_ask_to_join" ? false : [],
);
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => {}} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
fireEvent.click(screen.getByRole("button", { name: "View" }));
});
it("should not skip to auto join", async () => {
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ ...viewRoomParams, auto_join: true });
});
it("should not prompt ask to join", async () => {
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.PromptAskToJoin });
});
});
describe("when enabling feature", () => {
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) =>
setting === "feature_ask_to_join" ? true : [],
);
jest.spyOn(mockedClient, "getRoom").mockReturnValue(null);
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => {}} />);
// search is debounced
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
fireEvent.click(screen.getByRole("button", { name: "Ask to join" }));
});
it("should skip to auto join", async () => {
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ ...viewRoomParams, auto_join: false });
});
it("should prompt ask to join", async () => {
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.PromptAskToJoin });
});
});
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { EventType } from "matrix-js-sdk/src/matrix";
import { UnpinAllDialog } from "../../../../../src/components/views/dialogs/UnpinAllDialog";
import { createTestClient } from "../../../../test-utils";
describe("<UnpinAllDialog />", () => {
const client = createTestClient();
const roomId = "!room:example.org";
function renderDialog(onFinished = jest.fn()) {
return render(<UnpinAllDialog matrixClient={client} roomId={roomId} onFinished={onFinished} />);
}
it("should render", () => {
const { asFragment } = renderDialog();
expect(asFragment()).toMatchSnapshot();
});
it("should remove all pinned events when clicked on Continue", async () => {
const onFinished = jest.fn();
renderDialog(onFinished);
await userEvent.click(screen.getByText("Continue"));
expect(client.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPinnedEvents, { pinned: [] }, "");
expect(onFinished).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,249 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ReactElement } from "react";
import { render, screen } from "jest-matrix-react";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import SettingsStore, { CallbackFn } from "../../../../../src/settings/SettingsStore";
import SdkConfig from "../../../../../src/SdkConfig";
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab";
import UserSettingsDialog from "../../../../../src/components/views/dialogs/UserSettingsDialog";
import {
getMockClientWithEventEmitter,
mockClientMethodsUser,
mockClientMethodsServer,
mockPlatformPeg,
mockClientMethodsCrypto,
mockClientMethodsRooms,
useMockMediaDevices,
} from "../../../../test-utils";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
mockPlatformPeg({
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
getAppVersion: jest.fn().mockResolvedValue("1"),
});
jest.mock("../../../../../src/settings/SettingsStore", () => ({
getValue: jest.fn(),
getValueAt: jest.fn(),
canSetValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
unwatchSetting: jest.fn(),
getFeatureSettingNames: jest.fn(),
getBetaInfo: jest.fn(),
getDisplayName: jest.fn(),
getDescription: jest.fn(),
shouldHaveWarning: jest.fn(),
disabledMessage: jest.fn(),
settingIsOveriddenAtConfigLevel: jest.fn(),
}));
describe("<UserSettingsDialog />", () => {
const userId = "@alice:server.org";
const mockSettingsStore = mocked(SettingsStore);
let mockClient!: MockedObject<MatrixClient>;
let sdkContext: SdkContextClass;
const defaultProps = { onFinished: jest.fn() };
const getComponent = (props: Partial<typeof defaultProps & { initialTabId?: UserTab }> = {}): ReactElement => (
<UserSettingsDialog sdkContext={sdkContext} {...defaultProps} {...props} />
);
beforeEach(() => {
jest.clearAllMocks();
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
...mockClientMethodsCrypto(),
...mockClientMethodsRooms(),
getIgnoredUsers: jest.fn().mockResolvedValue([]),
getPushers: jest.fn().mockResolvedValue([]),
getProfileInfo: jest.fn().mockResolvedValue({}),
});
sdkContext = new SdkContextClass();
sdkContext.client = mockClient;
mockSettingsStore.getValue.mockReturnValue(false);
mockSettingsStore.getValueAt.mockReturnValue(false);
mockSettingsStore.getFeatureSettingNames.mockReturnValue([]);
SdkConfig.reset();
SdkConfig.put({ brand: "Test" });
});
const getActiveTabLabel = (container: Element) =>
container.querySelector(".mx_TabbedView_tabLabel_active")?.textContent;
it("should render general settings tab when no initialTabId", () => {
const { container } = render(getComponent());
expect(getActiveTabLabel(container)).toEqual("Account");
});
it("should render initial tab when initialTabId is set", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Help }));
expect(getActiveTabLabel(container)).toEqual("Help & About");
});
it("should render general tab if initialTabId tab cannot be rendered", () => {
// mjolnir tab is only rendered in some configs
const { container } = render(getComponent({ initialTabId: UserTab.Mjolnir }));
expect(getActiveTabLabel(container)).toEqual("Account");
});
it("renders tabs correctly", () => {
SdkConfig.add({
show_labs_settings: true,
});
const { container } = render(getComponent());
expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot();
});
it("renders ignored users tab when feature_mjolnir is enabled", () => {
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === "feature_mjolnir");
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy();
});
it("renders voip tab when voip is enabled", () => {
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
});
it("renders with session manager tab selected", () => {
const { getByTestId } = render(getComponent({ initialTabId: UserTab.SessionManager }));
expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Sessions");
});
it("renders with appearance tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Appearance }));
expect(getActiveTabLabel(container)).toEqual("Appearance");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Appearance");
});
it("renders with notifications tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Notifications }));
expect(getActiveTabLabel(container)).toEqual("Notifications");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Notifications");
});
it("renders with preferences tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Preferences }));
expect(getActiveTabLabel(container)).toEqual("Preferences");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Preferences");
});
it("renders with keyboard tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Keyboard }));
expect(getActiveTabLabel(container)).toEqual("Keyboard");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Keyboard");
});
it("renders with sidebar tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Sidebar }));
expect(getActiveTabLabel(container)).toEqual("Sidebar");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Sidebar");
});
it("renders with voip tab selected", () => {
useMockMediaDevices();
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
const { container } = render(getComponent({ initialTabId: UserTab.Voice }));
expect(getActiveTabLabel(container)).toEqual("Voice & Video");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Voice & Video");
});
it("renders with security tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Security }));
expect(getActiveTabLabel(container)).toEqual("Security & Privacy");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Security & Privacy");
});
it("renders with labs tab selected", () => {
SdkConfig.add({
show_labs_settings: true,
});
const { container } = render(getComponent({ initialTabId: UserTab.Labs }));
expect(getActiveTabLabel(container)).toEqual("Labs");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Labs");
});
it("renders with mjolnir tab selected", () => {
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === "feature_mjolnir");
const { container } = render(getComponent({ initialTabId: UserTab.Mjolnir }));
expect(getActiveTabLabel(container)).toEqual("Ignored users");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Ignored Users");
});
it("renders with help tab selected", () => {
const { container } = render(getComponent({ initialTabId: UserTab.Help }));
expect(getActiveTabLabel(container)).toEqual("Help & About");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Help & About");
});
it("renders labs tab when show_labs_settings is enabled in config", () => {
SdkConfig.add({
show_labs_settings: true,
});
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy();
});
it("renders labs tab when some feature is in beta", () => {
mockSettingsStore.getFeatureSettingNames.mockReturnValue(["feature_beta_setting", "feature_just_normal_labs"]);
mockSettingsStore.getBetaInfo.mockImplementation((settingName) =>
settingName === "feature_beta_setting" ? ({} as any) : undefined,
);
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy();
});
it("watches settings", async () => {
const watchSettingCallbacks: Record<string, CallbackFn> = {};
mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => {
watchSettingCallbacks[settingName] = callback;
return `mock-watcher-id-${settingName}`;
});
mockSettingsStore.getValue.mockReturnValue(false);
const { queryByTestId, findByTestId, unmount } = render(getComponent());
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy();
expect(mockSettingsStore.watchSetting).toHaveBeenCalledWith("feature_mjolnir", null, expect.anything());
// call the watch setting callback
mockSettingsStore.getValue.mockReturnValue(true);
watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true);
// tab is rendered now
await expect(findByTestId(`settings-tab-${UserTab.Mjolnir}`)).resolves.toBeTruthy();
unmount();
// unwatches settings on unmount
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir");
});
});

View file

@ -0,0 +1,540 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppDownloadDialog should allow disabling desktop build 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
<a
aria-label="Get it on F-Droid"
class="mx_AccessibleButton"
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should allow disabling fdroid build 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should allow disabling mobile builds 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
/>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should render with desktop, ios, android, fdroid buttons by default 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
<a
aria-label="Get it on F-Droid"
class="mx_AccessibleButton"
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,141 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChangelogDialog /> should fetch github proxy url for each repo with old and new version strings 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Changelog
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div
class="mx_ChangelogDialog_content"
>
<div>
<h2
class="mx_Heading_h4"
>
element-hq/element-web
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/element-hq/element-web/commit/commit-sha"
rel="noreferrer noopener"
target="_blank"
>
This is the first commit message
</a>
</li>
</ul>
</div>
<div>
<h2
class="mx_Heading_h4"
>
element-hq/matrix-react-sdk
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/element-hq/matrix-react-sdk/commit/commit-sha"
rel="noreferrer noopener"
target="_blank"
>
This is a commit message
</a>
</li>
</ul>
</div>
<div>
<h2
class="mx_Heading_h4"
>
matrix-org/matrix-js-sdk
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1"
rel="noreferrer noopener"
target="_blank"
>
This is a commit message
</a>
</li>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2"
rel="noreferrer noopener"
target="_blank"
>
This is another commit message
</a>
</li>
</ul>
</div>
</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"
>
Update
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmUserActionDialog renders 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ConfirmUserActionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Ban this
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div
class="mx_ConfirmUserActionDialog_user"
>
<div
class="mx_ConfirmUserActionDialog_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 48px;"
title="@user:test.com"
>
u
</span>
</div>
<div
class="mx_ConfirmUserActionDialog_name"
>
@user:test.com
</div>
<div
class="mx_ConfirmUserActionDialog_userId"
>
@user:test.com
</div>
</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"
>
Ban
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,243 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DevtoolsDialog renders the devtools dialog 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Developer Tools
</h1>
</div>
<div
class="mx_DevTools_label_left"
>
Toolbox
</div>
<div
class="mx_CopyableText mx_DevTools_label_right"
>
Room ID: !id
<div
aria-describedby="floating-ui-2"
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_DevTools_label_bottom"
/>
<div
class="mx_DevTools_content"
>
<div>
<h3>
Room
</h3>
<button
class="mx_DevTools_button"
>
Send custom timeline event
</button>
<button
class="mx_DevTools_button"
>
Explore room state
</button>
<button
class="mx_DevTools_button"
>
Explore room account data
</button>
<button
class="mx_DevTools_button"
>
View servers in room
</button>
<button
class="mx_DevTools_button"
>
Notifications debug
</button>
<button
class="mx_DevTools_button"
>
Verification explorer
</button>
<button
class="mx_DevTools_button"
>
Active Widgets
</button>
</div>
<div>
<h3>
Other
</h3>
<button
class="mx_DevTools_button"
>
Explore account data
</button>
<button
class="mx_DevTools_button"
>
Settings explorer
</button>
<button
class="mx_DevTools_button"
>
Server info
</button>
</div>
<div>
<h3>
Options
</h3>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Developer mode
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Developer mode"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_QgU2PomxwKpa"
>
<span
class="mx_SettingsFlag_labelText"
>
Show hidden events in timeline
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Show hidden events in timeline"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_QgU2PomxwKpa"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_6hpi3YEetmBG"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable widget screenshots on supported widgets
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Enable widget screenshots on supported widgets"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_6hpi3YEetmBG"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
<span
class="mx_SettingsFlag_labelText"
>
Force 15s voice broadcast chunk length
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Force 15s voice broadcast chunk length"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<button>
Back
</button>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,206 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ExportDialog /> renders export dialog 1`] = `
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ExportDialog false mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Export Chat
</h1>
</div>
<p>
Select from the options below to export chats from your timeline
</p>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
<input
checked=""
id="exportFormat-Html"
name="exportFormat"
type="radio"
value="Html"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
HTML
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="exportFormat-PlainText"
name="exportFormat"
type="radio"
value="PlainText"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Plain Text
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="exportFormat-Json"
name="exportFormat"
type="radio"
value="Json"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
JSON
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
class="mx_ExportDialog_subheading"
>
Messages
</span>
<div
class="mx_Field mx_Field_select"
>
<select
id="export-type"
type="text"
>
<option
value="Timeline"
>
Current Timeline
</option>
<option
value="Beginning"
>
From the beginning
</option>
<option
value="LastNMessages"
>
Specify a number of messages
</option>
</select>
<label
for="export-type"
/>
</div>
<span
class="mx_ExportDialog_subheading"
>
Size Limit
</span>
<div
class="mx_Field mx_Field_input"
>
<input
autocomplete="off"
id="size-limit"
type="number"
value="8"
/>
<label
for="size-limit"
/>
<span
class="mx_Field_postfix"
>
<span>
MB
</span>
</span>
</div>
<span
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
type="checkbox"
/>
<label
for="include-attachments"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
<div>
Include Attachments
</div>
</label>
</span>
</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"
>
Export
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
`;
exports[`<ExportDialog /> renders success screen when export is finished 1`] = `null`;

View file

@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FeedbackDialog should respect feedback config 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_FeedbackDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Feedback
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div
class="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug"
>
<h3>
Report a bug
</h3>
<p>
<span>
Please view
<a
class="mx_ExternalLink"
href="http://existing?foo=bar"
rel="noreferrer noopener"
target="_blank"
>
existing bugs on Github
<i
class="mx_ExternalLink_icon"
/>
</a>
first. No match?
<a
class="mx_ExternalLink"
href="https://new.issue.url?foo=bar"
rel="noreferrer noopener"
target="_blank"
>
Start a new one
<i
class="mx_ExternalLink_icon"
/>
</a>
.
</span>
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Go back
</button>
</span>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,243 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LogoutDialog Prompts user to connect backup if there is a backup on the server 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
You'll lose access to your encrypted messages
</h1>
</div>
<div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
</p>
<p>
When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
</p>
<p>
Back up your keys before signing out to avoid losing them.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button>
I don't want my encrypted messages
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Connect this session to Key Backup
</button>
</span>
</div>
<details>
<summary
class="mx_LogoutDialog_ExportKeyAdvanced"
>
Advanced
</summary>
<p>
<button>
Manually export keys
</button>
</p>
</details>
</div>
<div
aria-describedby="floating-ui-22"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`LogoutDialog Prompts user to set up backup if there is no backup on the server 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
You'll lose access to your encrypted messages
</h1>
</div>
<div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
</p>
<p>
When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
</p>
<p>
Back up your keys before signing out to avoid losing them.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button>
I don't want my encrypted messages
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Start using Key Backup
</button>
</span>
</div>
<details>
<summary
class="mx_LogoutDialog_ExportKeyAdvanced"
>
Advanced
</summary>
<p>
<button>
Manually export keys
</button>
</p>
</details>
</div>
<div
aria-describedby="floating-ui-28"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`LogoutDialog shows a regular dialog when crypto is disabled 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Sign out
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
Are you sure you want to sign out?
</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"
>
Sign out
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,252 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ManageRestrictedJoinRuleDialog /> should list spaces which are not parents of the room 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_ManageRestrictedJoinRuleDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Select spaces
</h1>
</div>
<p>
<span>
Decide which spaces can access this room. If a space is selected, its members can find and join
<strong>
!roomId:server
</strong>
.
</span>
</p>
<div
class="mx_SearchBox mx_textinput"
>
<input
autocomplete="off"
class="mx_textinput_icon mx_textinput_search mx_textinput_icon mx_textinput_search"
data-testid="searchbox-input"
placeholder="Search spaces"
type="text"
value=""
/>
<div
class="mx_AccessibleButton mx_SearchBox_closeButton"
role="button"
tabindex="-1"
/>
</div>
<div
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
tabindex="-1"
>
<div
class="mx_ManageRestrictedJoinRuleDialog_section"
>
<h3>
Other spaces you know
</h3>
<label
class="mx_ManageRestrictedJoinRuleDialog_entry"
>
<div>
<div>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 20px;"
>
O
</span>
<span
class="mx_ManageRestrictedJoinRuleDialog_entry_name"
>
Other Space
</span>
</div>
<div
class="mx_ManageRestrictedJoinRuleDialog_entry_description"
>
0 members
</div>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<label
for="checkbox_vY7Q4uEh9K"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
</label>
</span>
</label>
</div>
</div>
<div
class="mx_ManageRestrictedJoinRuleDialog_footer"
>
<div
class="mx_ManageRestrictedJoinRuleDialog_section_info"
>
You're removing all spaces. Access will default to invite only
</div>
<div
class="mx_ManageRestrictedJoinRuleDialog_footer_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Confirm
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<ManageRestrictedJoinRuleDialog /> should render empty state 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_ManageRestrictedJoinRuleDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Select spaces
</h1>
</div>
<p>
<span>
Decide which spaces can access this room. If a space is selected, its members can find and join
<strong>
!roomId:server
</strong>
.
</span>
</p>
<div
class="mx_SearchBox mx_textinput"
>
<input
autocomplete="off"
class="mx_textinput_icon mx_textinput_search mx_textinput_icon mx_textinput_search"
data-testid="searchbox-input"
placeholder="Search spaces"
type="text"
value=""
/>
<div
class="mx_AccessibleButton mx_SearchBox_closeButton"
role="button"
tabindex="-1"
/>
</div>
<div
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
tabindex="-1"
>
<span
class="mx_ManageRestrictedJoinRuleDialog_noResults"
>
No results
</span>
</div>
<div
class="mx_ManageRestrictedJoinRuleDialog_footer"
>
<div
class="mx_ManageRestrictedJoinRuleDialog_section_info"
>
You're removing all spaces. Access will default to invite only
</div>
<div
class="mx_ManageRestrictedJoinRuleDialog_footer_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Confirm
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,231 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManualDeviceKeyVerificationDialog should display the device 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Verify session
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Confirm by comparing the following with the User Settings in your other session:
</p>
<div
class="mx_DeviceVerifyDialog_cryptoSection"
>
<ul>
<li>
<label>
Session name
:
</label>
<span>
my device
</span>
</li>
<li>
<label>
Session ID
:
</label>
<span>
<code>
XYZ
</code>
</span>
</li>
<li>
<label>
Session key
:
</label>
<span>
<code>
<strong>
ABCD EFGH
</strong>
</code>
</span>
</li>
</ul>
</div>
<p>
If they don't match, the security of your communication may be compromised.
</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"
>
Verify session
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`ManualDeviceKeyVerificationDialog should display the device of another user 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Verify session
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Confirm this user's session by comparing the following with their User Settings:
</p>
<div
class="mx_DeviceVerifyDialog_cryptoSection"
>
<ul>
<li>
<label>
Session name
:
</label>
<span>
my device
</span>
</li>
<li>
<label>
Session ID
:
</label>
<span>
<code>
XYZ
</code>
</span>
</li>
<li>
<label>
Session key
:
</label>
<span>
<code>
<strong>
ABCD EFGH
</strong>
</code>
</span>
</li>
</ul>
</div>
<p>
If they don't match, the security of your communication may be compromised.
</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"
>
Verify session
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,332 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MessageEditHistory /> should match the snapshot 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Message edits
</h1>
</div>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
tabindex="-1"
>
<div
class="mx_RoomView_messageListWrapper"
>
<ol
aria-live="polite"
class="mx_RoomView_MessageList"
>
<ul
class="mx_MessageEditHistoryDialog_edits"
>
<li>
<div
aria-label="Thu, Jan 1, 1970"
class="mx_TimelineSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="mx_DateSeparator_dateHeading"
>
Thu, Jan 1, 1970
</h2>
</div>
<hr
role="none"
/>
</div>
</li>
<li>
<div
class="mx_EventTile"
>
<div
class="mx_EventTile_line"
>
<span
class="mx_MessageTimestamp"
>
00:00
</span>
<div
class="mx_EventTile_content"
>
<span
class="mx_EventTile_body translate"
dir="auto"
>
My Great Massage
</span>
</div>
<div
class="mx_MessageActionBar"
>
<div
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
Remove
</div>
</div>
</div>
</div>
</li>
</ul>
</ol>
</div>
</div>
<div
aria-describedby="floating-ui-2"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`<MessageEditHistory /> should support events with 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Message edits
</h1>
</div>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
tabindex="-1"
>
<div
class="mx_RoomView_messageListWrapper"
>
<ol
aria-live="polite"
class="mx_RoomView_MessageList"
>
<ul
class="mx_MessageEditHistoryDialog_edits"
>
<li>
<div
aria-label="Thu, Jan 1, 1970"
class="mx_TimelineSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="mx_DateSeparator_dateHeading"
>
Thu, Jan 1, 1970
</h2>
</div>
<hr
role="none"
/>
</div>
</li>
<li>
<div
class="mx_EventTile"
>
<div
class="mx_EventTile_line"
>
<span
class="mx_MessageTimestamp"
>
00:00
</span>
<div
class="mx_EventTile_content"
>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
My Great Massage
<span
class="mx_EditHistoryMessage_deletion"
>
?
</span>
</span>
</span>
</div>
<div
class="mx_MessageActionBar"
>
<div
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
Remove
</div>
</div>
</div>
</div>
</li>
<li>
<div
class="mx_EventTile"
>
<div
class="mx_EventTile_line"
>
<span
class="mx_MessageTimestamp"
>
00:00
</span>
<div
class="mx_EventTile_content"
>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
My Great M
<span
class="mx_EditHistoryMessage_deletion"
>
i
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
a
</span>
ssage
<span
class="mx_EditHistoryMessage_insertion"
>
?
</span>
</span>
</span>
</div>
<div
class="mx_MessageActionBar"
>
<div
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
Remove
</div>
</div>
</div>
</div>
</li>
<li>
<div
class="mx_EventTile"
>
<div
class="mx_EventTile_line"
>
<span
class="mx_MessageTimestamp"
>
00:00
</span>
<div
class="mx_EventTile_content"
>
<span
class="mx_EventTile_body translate"
dir="auto"
>
My Great Missage
</span>
</div>
<div
class="mx_MessageActionBar"
>
<div
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
Remove
</div>
</div>
</div>
</div>
</li>
</ul>
</ol>
</div>
</div>
<div
aria-describedby="floating-ui-8"
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,159 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomSettingsDialog /> Settings tabs renders default tabs correctly 1`] = `
NodeList [
<li
aria-controls="mx_tabpanel_ROOM_GENERAL_TAB"
aria-selected="true"
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
data-testid="settings-tab-ROOM_GENERAL_TAB"
role="tab"
tabindex="0"
>
<span
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon"
/>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_GENERAL_TAB_label"
>
General
</span>
</li>,
<li
aria-controls="mx_tabpanel_ROOM_SECURITY_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-ROOM_SECURITY_TAB"
role="tab"
tabindex="-1"
>
<span
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon"
/>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_SECURITY_TAB_label"
>
Security & Privacy
</span>
</li>,
<li
aria-controls="mx_tabpanel_ROOM_ROLES_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-ROOM_ROLES_TAB"
role="tab"
tabindex="-1"
>
<span
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon"
/>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_ROLES_TAB_label"
>
Roles & Permissions
</span>
</li>,
<li
aria-controls="mx_tabpanel_ROOM_NOTIFICATIONS_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-ROOM_NOTIFICATIONS_TAB"
role="tab"
tabindex="-1"
>
<span
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_notificationsIcon"
/>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_NOTIFICATIONS_TAB_label"
>
Notifications
</span>
</li>,
<li
aria-controls="mx_tabpanel_ROOM_POLL_HISTORY_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-ROOM_POLL_HISTORY_TAB"
role="tab"
tabindex="-1"
>
<span
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_pollsIcon"
/>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_POLL_HISTORY_TAB_label"
>
Polls
</span>
</li>,
]
`;
exports[`<RoomSettingsDialog /> poll history displays poll history when tab clicked 1`] = `
<div
class="mx_SettingsTab"
>
<div
class="mx_PollHistory_content"
>
<h2
class="mx_Heading_h2 mx_PollHistory_header"
>
Polls
</h2>
<div
class="mx_PollHistoryList"
>
<fieldset
class="mx_FilterTabGroup"
>
<label
data-testid="filter-tab-PollHistory_filter-ACTIVE"
>
<input
checked=""
name="PollHistory_filter"
type="radio"
value="ACTIVE"
/>
<span>
Active polls
</span>
</label>
<label
data-testid="filter-tab-PollHistory_filter-ENDED"
>
<input
name="PollHistory_filter"
type="radio"
value="ENDED"
/>
<span>
Past polls
</span>
</label>
</fieldset>
<div
class="mx_PollHistoryList_loading mx_PollHistoryList_noResultsYet"
>
<div
class="mx_InlineSpinner"
>
<div
aria-label="Loading…"
class="mx_InlineSpinner_icon mx_Spinner_icon"
style="width: 16px; height: 16px;"
/>
</div>
Loading polls
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ServerPickerDialog /> should render dialog 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_ServerPickerDialog"
aria-labelledby="mx_BaseDialog_title"
class="mx_ServerPickerDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Sign into your homeserver
</h1>
</div>
<form
class="mx_Dialog_content"
id="mx_ServerPickerDialog"
>
<p>
We call the places where you can host your account 'homeservers'.
Matrix.org is the biggest public homeserver in the world, so it's a good place for many.
</p>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
checked=""
data-testid="defaultHomeserver"
name="defaultChosen"
type="radio"
value="true"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<span
aria-labelledby="floating-ui-1"
class="mx_Login_underlinedServerName"
tabindex="0"
>
matrix.org
</span>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<div
class="mx_StyledRadioButton mx_ServerPickerDialog_otherHomeserverRadio mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
<label
class="mx_StyledRadioButton_innerLabel"
>
<input
aria-label="Other homeserver"
name="defaultChosen"
type="radio"
value="false"
/>
<div>
<div />
</div>
</label>
<div
class="mx_StyledRadioButton_content"
>
<div
class="mx_Field mx_Field_input mx_ServerPickerDialog_otherHomeserver"
>
<input
id="mx_homeserverInput"
label="Other homeserver"
placeholder="Other homeserver"
type="text"
value=""
/>
<label
for="mx_homeserverInput"
>
Other homeserver
</label>
</div>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</div>
<p>
Use your preferred Matrix homeserver if you have one, or host your own.
</p>
<div
class="mx_AccessibleButton mx_ServerPickerDialog_continue mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Continue
</div>
<h2>
Learn more
</h2>
<a
class="mx_ExternalLink"
href="https://matrix.org/docs/matrix-concepts/elements-of-matrix/#homeserver"
rel="noreferrer noopener"
target="_blank"
>
About homeservers
<i
class="mx_ExternalLink_icon"
/>
</a>
</form>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UnpinAllDialog /> should render 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UnpinAllDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title mx_UnpinAllDialog_title"
id="mx_BaseDialog_title"
>
Unpin all messages?
</h1>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
>
Make sure that you really want to remove all pinned messages. This action cant be undone.
</span>
<div
class="mx_UnpinAllDialog_buttons"
>
<button
class="_button_i91xf_17 _destructive_i91xf_116"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
Continue
</button>
<button
class="_button_i91xf_17"
data-kind="tertiary"
data-size="lg"
role="button"
tabindex="0"
>
Cancel
</button>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,292 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UserSettingsDialog /> renders tabs correctly 1`] = `
NodeList [
<li
aria-controls="mx_tabpanel_USER_ACCOUNT_TAB"
aria-selected="true"
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
data-testid="settings-tab-USER_ACCOUNT_TAB"
role="tab"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.175 13.825C9.958 14.608 10.9 15 12 15s2.042-.392 2.825-1.175C15.608 13.042 16 12.1 16 11s-.392-2.042-1.175-2.825C14.042 7.392 13.1 7 12 7s-2.042.392-2.825 1.175C8.392 8.958 8 9.9 8 11s.392 2.042 1.175 2.825Zm4.237-1.412A1.926 1.926 0 0 1 12 13c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 11c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 9c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412Z"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
/>
<path
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_ACCOUNT_TAB_label"
>
Account
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_SESSION_MANAGER_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_SESSION_MANAGER_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 20c-.417 0-.77-.146-1.063-.438A1.447 1.447 0 0 1 2 18.5c0-.417.146-.77.438-1.063A1.446 1.446 0 0 1 3.5 17H4V6c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 4h14a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 20 6H6v11h4.5c.417 0 .77.146 1.063.438.291.291.437.645.437 1.062 0 .417-.146.77-.438 1.063A1.446 1.446 0 0 1 10.5 20h-7ZM15 20a.968.968 0 0 1-.713-.288A.968.968 0 0 1 14 19V9c0-.283.096-.52.287-.713A.968.968 0 0 1 15 8h6a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v10c0 .283-.096.52-.288.712A.968.968 0 0 1 21 20h-6Zm1-3h4v-7h-4v7Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_SESSION_MANAGER_TAB_label"
>
Sessions
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_APPEARANCE_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_APPEARANCE_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16c1.25 0 2.313-.438 3.188-1.313.874-.874 1.312-1.937 1.312-3.187 0-1.25-.438-2.313-1.313-3.188C14.313 7.439 13.25 7 12 7c-1.25 0-2.312.438-3.187 1.313C7.938 9.187 7.5 10.25 7.5 11.5c0 1.25.438 2.313 1.313 3.188C9.688 15.562 10.75 16 12 16Zm0-1.8c-.75 0-1.387-.262-1.912-.787A2.604 2.604 0 0 1 9.3 11.5c0-.75.263-1.387.787-1.912A2.604 2.604 0 0 1 12 8.8c.75 0 1.387.262 1.912.787.525.526.788 1.163.788 1.913s-.262 1.387-.787 1.912A2.604 2.604 0 0 1 12 14.2Zm0 4.8c-2.317 0-4.433-.613-6.35-1.837-1.917-1.226-3.367-2.88-4.35-4.963a.812.812 0 0 1-.1-.313 2.93 2.93 0 0 1 0-.774.812.812 0 0 1 .1-.313c.983-2.083 2.433-3.738 4.35-4.963C7.567 4.614 9.683 4 12 4c2.317 0 4.433.612 6.35 1.838 1.917 1.224 3.367 2.879 4.35 4.962a.81.81 0 0 1 .1.313 2.925 2.925 0 0 1 0 .774.81.81 0 0 1-.1.313c-.983 2.083-2.433 3.738-4.35 4.963C16.433 18.387 14.317 19 12 19Zm0-2a9.544 9.544 0 0 0 5.188-1.488A9.773 9.773 0 0 0 20.8 11.5a9.773 9.773 0 0 0-3.613-4.013A9.544 9.544 0 0 0 12 6a9.545 9.545 0 0 0-5.187 1.487A9.773 9.773 0 0 0 3.2 11.5a9.773 9.773 0 0 0 3.613 4.012A9.544 9.544 0 0 0 12 17Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_APPEARANCE_TAB_label"
>
Appearance
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_NOTIFICATIONS_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_NOTIFICATIONS_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 3c7 0 7 7 7 7v6l1.293 1.293c.63.63.184 1.707-.707 1.707H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-7 7-7Zm5 7.01v-.022l-.009-.146a6.591 6.591 0 0 0-.073-.607 6.608 6.608 0 0 0-.582-1.84c-.319-.638-.766-1.215-1.398-1.637C14.318 5.344 13.4 5 12 5c-1.4 0-2.317.344-2.937.758-.633.422-1.08.999-1.4 1.636a6.606 6.606 0 0 0-.58 1.841A6.596 6.596 0 0 0 7 9.988v6.84L6.828 17h10.344L17 16.828V10.01ZM12 22a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_NOTIFICATIONS_TAB_label"
>
Notifications
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_PREFERENCES_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_PREFERENCES_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M6.5 2h11a4.5 4.5 0 1 1 0 9h-11a4.5 4.5 0 0 1 0-9Zm0 2h7.258A4.479 4.479 0 0 0 13 6.5c0 .925.28 1.785.758 2.5H6.5a2.5 2.5 0 0 1 0-5ZM15 6.5a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm-13 11A4.5 4.5 0 0 1 6.5 13h11a4.5 4.5 0 1 1 0 9h-11A4.5 4.5 0 0 1 2 17.5Zm8.242-2.5H17.5a2.5 2.5 0 0 1 0 5h-7.258A4.478 4.478 0 0 0 11 17.5c0-.925-.28-1.785-.758-2.5ZM6.5 15a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"
fill-rule="evenodd"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_PREFERENCES_TAB_label"
>
Preferences
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_KEYBOARD_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_KEYBOARD_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.188 8v2h2V8h-2Zm3.875 0v2h2V8h-2Zm3.875 0v2h2V8h-2Zm3.875 0v2h2V8h-2ZM5.188 11.531v2h2v-2h-2Zm3.875 0v2h2v-2h-2Zm3.875 0v2h2v-2h-2Zm3.875 0v2h2v-2h-2ZM9 15a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H9Z"
/>
<path
clip-rule="evenodd"
d="M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Zm2 0h16v12H4V6Z"
fill-rule="evenodd"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_KEYBOARD_TAB_label"
>
Keyboard
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_SIDEBAR_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_SIDEBAR_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M18 3a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12Zm-8 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-8V5ZM8 19H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2v14Z"
fill-rule="evenodd"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_SIDEBAR_TAB_label"
>
Sidebar
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_SECURITY_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_SECURITY_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 20V10c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 8h1V6c0-1.383.487-2.563 1.463-3.538C9.438 1.487 10.617 1 12 1s2.563.488 3.537 1.462C16.512 3.438 17 4.617 17 6v2h1c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v10c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 22H6Zm0-2h12V10H6v10ZM9 8h6V6c0-.833-.292-1.542-.875-2.125A2.893 2.893 0 0 0 12 3c-.833 0-1.542.292-2.125.875A2.893 2.893 0 0 0 9 6v2Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_SECURITY_TAB_label"
>
Security & Privacy
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_LABS_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_LABS_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 5a1 1 0 0 1-1-1V3a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1Zm-7.071-.071a1 1 0 0 1 1.414 0l.707.707A1 1 0 0 1 5.636 7.05l-.707-.707a1 1 0 0 1 0-1.414Z"
/>
<path
clip-rule="evenodd"
d="M15.734 15.325C15.316 15.795 15 16.371 15 17v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2c0-.63-.316-1.205-.734-1.675a5 5 0 1 1 7.468 0Zm-1.493-1.33a3 3 0 1 0-4.482 0c.433.486.894 1.166 1.112 2.005h2.258c.218-.84.679-1.52 1.112-2.005ZM13 18h-2v1h2v-1Z"
fill-rule="evenodd"
/>
<path
d="M2 12a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm18-1a1 1 0 1 0 0 2h1a1 1 0 1 0 0-2h-1Zm-3.05-5.364a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0-1.414-1.414l-.707.707Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_LABS_TAB_label"
>
Labs
</span>
</li>,
<li
aria-controls="mx_tabpanel_USER_HELP_TAB"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-USER_HELP_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 8a1.5 1.5 0 0 0-1.5 1.5 1 1 0 1 1-2 0 3.5 3.5 0 1 1 6.01 2.439c-.122.126-.24.243-.352.355-.287.288-.54.54-.76.824-.293.375-.398.651-.398.882a1 1 0 1 1-2 0c0-.874.407-1.58.819-2.11.305-.392.688-.775 1-1.085l.257-.26A1.5 1.5 0 0 0 12 8Zm1 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
/>
<path
d="M8.1 21.212A9.738 9.738 0 0 0 12 22a9.738 9.738 0 0 0 3.9-.788 10.098 10.098 0 0 0 3.175-2.137c.9-.9 1.613-1.958 2.137-3.175A9.738 9.738 0 0 0 22 12a9.738 9.738 0 0 0-.788-3.9 10.099 10.099 0 0 0-2.137-3.175c-.9-.9-1.958-1.612-3.175-2.137A9.738 9.738 0 0 0 12 2a9.738 9.738 0 0 0-3.9.788 10.099 10.099 0 0 0-3.175 2.137c-.9.9-1.612 1.958-2.137 3.175A9.738 9.738 0 0 0 2 12a9.74 9.74 0 0 0 .788 3.9 10.098 10.098 0 0 0 2.137 3.175c.9.9 1.958 1.613 3.175 2.137Zm9.575-3.537C16.125 19.225 14.233 20 12 20c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675Z"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_USER_HELP_TAB_label"
>
Help & About
</span>
</li>,
]
`;

View file

@ -0,0 +1,62 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { stubClient } from "../../../../../test-utils";
import { DevtoolsContext } from "../../../../../../src/components/views/dialogs/devtools/BaseTool";
import { TimelineEventEditor } from "../../../../../../src/components/views/dialogs/devtools/Event";
describe("<EventEditor />", () => {
beforeEach(() => {
stubClient();
});
it("should render", () => {
const cli = MatrixClientPeg.safeGet();
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<DevtoolsContext.Provider
value={{
room: new Room("!roomId", cli, "@alice:example.com", {
pendingEventOrdering: PendingEventOrdering.Detached,
}),
}}
>
<TimelineEventEditor onBack={() => {}} />
</DevtoolsContext.Provider>
</MatrixClientContext.Provider>,
);
expect(asFragment()).toMatchSnapshot();
});
describe("thread context", () => {
it("should pre-populate a thread relationship", () => {
const cli = MatrixClientPeg.safeGet();
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<DevtoolsContext.Provider
value={{
room: new Room("!roomId", cli, "@alice:example.com", {
pendingEventOrdering: PendingEventOrdering.Detached,
}),
threadRootId: "$this_is_a_thread_id",
}}
>
<TimelineEventEditor onBack={() => {}} />
</DevtoolsContext.Provider>
</MatrixClientContext.Provider>,
);
expect(asFragment()).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,41 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
import RoomNotifications from "../../../../../../src/components/views/dialogs/devtools/RoomNotifications";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { stubClient } from "../../../../../test-utils";
import { DevtoolsContext } from "../../../../../../src/components/views/dialogs/devtools/BaseTool";
describe("<RoomNotifications />", () => {
beforeEach(() => {
stubClient();
});
it("should render", () => {
const cli = MatrixClientPeg.safeGet();
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<DevtoolsContext.Provider
value={{
room: new Room("!roomId", cli, "@alice:example.com", {
pendingEventOrdering: PendingEventOrdering.Detached,
}),
}}
>
<RoomNotifications onBack={() => {}} setTool={() => {}} />
</DevtoolsContext.Provider>
</MatrixClientContext.Provider>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EventEditor /> should render 1`] = `
<DocumentFragment>
<div
class="mx_DevTools_content"
>
<div
class="mx_DevTools_eventTypeStateKeyGroup"
>
<div
class="mx_Field mx_Field_input"
>
<input
autocomplete="on"
id="eventType"
label="Event Type"
placeholder="Event Type"
size="42"
type="text"
value=""
/>
<label
for="eventType"
>
Event Type
</label>
</div>
</div>
<div
class="mx_Field mx_Field_textarea mx_DevTools_textarea"
>
<textarea
autocomplete="off"
id="evContent"
label="Event Content"
placeholder="Event Content"
type="text"
>
{
}
</textarea>
<label
for="evContent"
>
Event Content
</label>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<button>
Back
</button>
<button>
Send
</button>
</div>
</DocumentFragment>
`;
exports[`<EventEditor /> thread context should pre-populate a thread relationship 1`] = `
<DocumentFragment>
<div
class="mx_DevTools_content"
>
<div
class="mx_DevTools_eventTypeStateKeyGroup"
>
<div
class="mx_Field mx_Field_input"
>
<input
autocomplete="on"
id="eventType"
label="Event Type"
placeholder="Event Type"
size="42"
type="text"
value=""
/>
<label
for="eventType"
>
Event Type
</label>
</div>
</div>
<div
class="mx_Field mx_Field_textarea mx_DevTools_textarea"
>
<textarea
autocomplete="off"
id="evContent"
label="Event Content"
placeholder="Event Content"
type="text"
>
{
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$this_is_a_thread_id"
}
}
</textarea>
<label
for="evContent"
>
Event Content
</label>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<button>
Back
</button>
<button>
Send
</button>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomNotifications /> should render 1`] = `
<DocumentFragment>
<div
class="mx_DevTools_content"
>
<section>
<h2>
Room status
</h2>
<ul>
<li>
<span>
Room unread status:
<strong>
None
</strong>
, count:
<strong>
0
</strong>
</span>
</li>
<li>
<span>
Notification state is
<strong />
</span>
</li>
<li>
<span>
Room is
<strong>
not encrypted 🚨
</strong>
</span>
</li>
</ul>
</section>
<section>
<h2>
Main timeline
</h2>
<ul>
<li>
Total: 0
</li>
<li>
Highlight: 0
</li>
<li>
Dot: false
</li>
</ul>
</section>
<section>
<h2>
Threads timeline
</h2>
<ul />
</section>
</div>
<div
class="mx_Dialog_buttons"
>
<button>
Back
</button>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,77 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import { render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import { mocked } from "jest-mock";
import CreateKeyBackupDialog from "../../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog";
import { createTestClient } from "../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
jest.mock("../../../../../../src/SecurityManager", () => ({
accessSecretStorage: jest.fn().mockResolvedValue(undefined),
withSecretStorageKeyCache: jest.fn().mockImplementation((fn) => fn()),
}));
describe("CreateKeyBackupDialog", () => {
beforeEach(() => {
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => createTestClient();
});
it("should display the spinner when creating backup", () => {
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
// Check if the spinner is displayed
expect(screen.getByTestId("spinner")).toBeDefined();
expect(asFragment()).toMatchSnapshot();
});
it("should display an error message when backup creation failed", async () => {
const matrixClient = createTestClient();
mocked(matrixClient.hasSecretStorageKey).mockResolvedValue(true);
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => {
throw new Error("failed");
});
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
// Check if the error message is displayed
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
expect(asFragment()).toMatchSnapshot();
});
it("should display an error message when there is no Crypto available", async () => {
const matrixClient = createTestClient();
mocked(matrixClient.hasSecretStorageKey).mockResolvedValue(true);
mocked(matrixClient.getCrypto).mockReturnValue(undefined);
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
// Check if the error message is displayed
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
});
it("should display the success dialog when the key backup is finished", async () => {
const onFinished = jest.fn();
const { asFragment } = render(<CreateKeyBackupDialog onFinished={onFinished} />);
await waitFor(() =>
expect(
screen.getByText("Your keys are being backed up (the first backup could take a few minutes)."),
).toBeDefined(),
);
expect(asFragment()).toMatchSnapshot();
// Click on the OK button
screen.getByRole("button", { name: "OK" }).click();
expect(onFinished).toHaveBeenCalledWith(true);
});
});

View file

@ -0,0 +1,217 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { mocked, MockedObject } from "jest-mock";
import { Crypto, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import {
filterConsole,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsServer,
} from "../../../../../test-utils";
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "../../../../../../src/Modal";
import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog";
describe("CreateSecretStorageDialog", () => {
let mockClient: MockedObject<MatrixClient>;
let mockCrypto: MockedObject<Crypto.CryptoApi>;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsServer(),
...mockClientMethodsCrypto(),
uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => {
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
throw new MatrixError({ flows: [] });
}),
});
mockCrypto = mocked(mockClient.getCrypto()!);
Object.assign(mockCrypto, {
isKeyBackupTrusted: jest.fn(),
isDehydrationSupported: jest.fn(() => false),
bootstrapCrossSigning: jest.fn(),
bootstrapSecretStorage: jest.fn(),
});
});
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent(
props: Partial<React.ComponentProps<typeof CreateSecretStorageDialog>> = {},
): RenderResult {
const onFinished = jest.fn();
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
}
it("shows a loading spinner initially", async () => {
const { container } = renderComponent();
expect(screen.getByTestId("spinner")).toBeDefined();
expect(container).toMatchSnapshot();
await flushPromises();
});
describe("when there is an error fetching the backup version", () => {
filterConsole("Error fetching backup data from server");
it("shows an error", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => {
throw new Error("bleh bleh");
});
const result = renderComponent();
// XXX the error message is... misleading.
await result.findByText("Unable to query secret storage status");
expect(result.container).toMatchSnapshot();
});
});
it("shows 'Generate a Security Key' text if no key backup is present", async () => {
const result = renderComponent();
await flushPromises();
expect(result.container).toMatchSnapshot();
result.getByText("Generate a Security Key");
});
describe("when canUploadKeysWithPasswordOnly", () => {
// spy on Modal.createDialog
let modalSpy: jest.SpyInstance;
// deferred which should be resolved to indicate that the created dialog has completed
let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>;
beforeEach(() => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
await sleep(0);
throw new MatrixError({
flows: [{ stages: ["m.login.password"] }],
});
});
restoreDialogFinishedDefer = defer<[done?: boolean]>();
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: restoreDialogFinishedDefer.promise,
close: jest.fn(),
});
});
it("prompts for a password and then shows RestoreKeyBackupDialog", async () => {
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalledWith(
RestoreKeyBackupDialog,
{
keyCallback: expect.any(Function),
showSummary: false,
},
undefined,
false,
false,
);
restoreDialogFinishedDefer.resolve([]);
});
it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => {
mockClient.isCryptoEnabled.mockReturnValue(true);
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalled();
// While we restore the key backup, its signature becomes accepted
mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo);
restoreDialogFinishedDefer.resolve([]);
await flushPromises();
// XXX no idea why this is a sensible thing to do. I just work here.
expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled();
expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled();
await result.findByText("Your keys are now being backed up from this device.");
});
describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => {
filterConsole("Error fetching backup data from server");
it("handles the error sensibly", async () => {
const result = renderComponent();
await result.findByText(/Enter your account password to confirm the upgrade/);
expect(result.container).toMatchSnapshot();
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
result.getByRole("button", { name: "Next" }).click();
expect(modalSpy).toHaveBeenCalled();
mockClient.getKeyBackupVersion.mockImplementation(async () => {
throw new Error("bleh bleh");
});
restoreDialogFinishedDefer.resolve([]);
await result.findByText("Unable to query secret storage status");
});
});
});
describe("when backup is present but not trusted", () => {
beforeEach(() => {
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
});
it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => {
const result = renderComponent();
await result.findByText("Restore your key backup to upgrade your encryption");
expect(result.container).toMatchSnapshot();
// before we click "Restore", set up a spy on createDialog
const restoreDialogFinishedDefer = defer<[done?: boolean]>();
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: restoreDialogFinishedDefer.promise,
close: jest.fn(),
});
result.getByRole("button", { name: "Restore" }).click();
expect(modalSpy).toHaveBeenCalledWith(
RestoreKeyBackupDialog,
{
keyCallback: expect.any(Function),
showSummary: false,
},
undefined,
false,
false,
);
// simulate RestoreKeyBackupDialog completing, to run that code path
restoreDialogFinishedDefer.resolve([]);
});
});
});

View file

@ -0,0 +1,85 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { screen, fireEvent, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
import ExportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ExportE2eKeysDialog";
import { createTestClient } from "../../../../../test-utils";
describe("ExportE2eKeysDialog", () => {
it("renders", () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { asFragment } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
expect(asFragment()).toMatchSnapshot();
});
it("should have disabled submit button initially", () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.click(container.querySelector("[type=submit]")!);
expect(screen.getByText("Enter passphrase")).toBeInTheDocument();
});
it("should complain about weak passphrases", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
const input = screen.getByLabelText("Enter passphrase");
await userEvent.type(input, "password");
fireEvent.click(container.querySelector("[type=submit]")!);
await expect(screen.findByText("This is a top-10 common password")).resolves.toBeInTheDocument();
});
it("should complain if passphrases don't match", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$");
await userEvent.type(screen.getByLabelText("Confirm passphrase"), "ThisIsAMoreSecurePW124$$");
fireEvent.click(container.querySelector("[type=submit]")!);
await expect(screen.findByText("Passphrases must match")).resolves.toBeInTheDocument();
});
it("should export if everything is fine", async () => {
// Given a client able to export keys
const cli = createTestClient();
const keys: IMegolmSessionData[] = [];
const passphrase = "ThisIsAMoreSecurePW123$$";
const exportRoomKeysAsJson = jest.fn().mockResolvedValue(JSON.stringify(keys));
cli.getCrypto = () => {
return {
exportRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
};
// Mock the result of encrypting the sessions. If we don't do this, the
// encryption process fails, possibly because we didn't initialise
// something.
jest.spyOn(MegolmExportEncryption, "encryptMegolmKeyFile").mockResolvedValue(new ArrayBuffer(3));
// When we tell the dialog to export
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase);
await userEvent.type(screen.getByLabelText("Confirm passphrase"), passphrase);
fireEvent.click(container.querySelector("[type=submit]")!);
// Then it exports keys and encrypts them
await waitFor(() => expect(exportRoomKeysAsJson).toHaveBeenCalled());
await waitFor(() =>
expect(MegolmExportEncryption.encryptMegolmKeyFile).toHaveBeenCalledWith(JSON.stringify(keys), passphrase),
);
});
});

View file

@ -0,0 +1,87 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto } from "matrix-js-sdk/src/matrix";
import ImportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ImportE2eKeysDialog";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
import { createTestClient } from "../../../../../test-utils";
describe("ImportE2eKeysDialog", () => {
it("renders", () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { asFragment } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
expect(asFragment()).toMatchSnapshot();
});
it("should have disabled submit button initially", () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
expect(container.querySelector("[type=submit]")!).toBeDisabled();
});
it("should enable submit once file is uploaded and passphrase typed in", () => {
const cli = createTestClient();
const onFinished = jest.fn();
const file = new File(["test"], "file.txt", { type: "text/plain" });
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.change(container.querySelector("[type=file]")!, {
target: { files: [file] },
});
fireEvent.change(container.querySelector("[type=password]")!, {
target: { value: "passphrase" },
});
expect(container.querySelector("[type=submit]")!).toBeEnabled();
});
it("should enable submit once file is uploaded and passphrase pasted in", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const file = new File(["test"], "file.txt", { type: "text/plain" });
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.change(container.querySelector("[type=file]")!, {
target: { files: [file] },
});
await userEvent.click(container.querySelector("[type=password]")!);
await userEvent.paste("passphrase");
expect(container.querySelector("[type=submit]")!).toBeEnabled();
});
it("should import exported keys on submit", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const file = new File(["test"], "file.txt", { type: "text/plain" });
const importRoomKeysAsJson = jest.fn();
cli.getCrypto = () => {
return {
importRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
};
// Mock the result of decrypting the sessions, to avoid needing to
// create encrypted input data.
jest.spyOn(MegolmExportEncryption, "decryptMegolmKeyFile").mockResolvedValue("[]");
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.change(container.querySelector("[type=file]")!, {
target: { files: [file] },
});
await userEvent.click(container.querySelector("[type=password]")!);
await userEvent.paste("passphrase");
fireEvent.click(container.querySelector("[type=submit]")!);
await waitFor(() => expect(importRoomKeysAsJson).toHaveBeenCalled());
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*
*/
import React from "react";
import { screen, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
// Needed to be able to mock decodeRecoveryKey
// eslint-disable-next-line no-restricted-imports
import * as recoveryKeyModule from "matrix-js-sdk/src/crypto-api/recovery-key";
import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx";
import { stubClient } from "../../../../../test-utils";
describe("<RestoreKeyBackupDialog />", () => {
beforeEach(() => {
stubClient();
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
});
it("should render", async () => {
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
it("should display an error when recovery key is invalid", async () => {
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockImplementation(() => {
throw new Error("Invalid recovery key");
});
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
await userEvent.type(screen.getByRole("textbox"), "invalid key");
await waitFor(() => expect(screen.getByText("👎 Not a valid Security Key")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
it("should not raise an error when recovery is valid", async () => {
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
await userEvent.type(screen.getByRole("textbox"), "valid key");
await waitFor(() => expect(screen.getByText("👍 This looks like a valid Security Key!")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateKeyBackupDialog should display an error message when backup creation failed 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Starting backup…
</h1>
</div>
<div>
<div>
<p>
Unable to create key backup
</p>
<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"
>
Retry
</button>
</span>
</div>
</div>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`CreateKeyBackupDialog should display the spinner when creating backup 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Starting backup…
</h1>
</div>
<div>
<div>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`CreateKeyBackupDialog should display the success dialog when the key backup is finished 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Success!
</h1>
</div>
<div>
<div>
<p>
Your keys are being backed up (the first backup could take a few minutes).
</p>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,551 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_centeredTitle"
id="mx_BaseDialog_title"
>
Set up Secure Backup
</h1>
</div>
<div>
<form>
<p
class="mx_CreateSecretStorageDialog_centeredBody"
>
Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.
</p>
<div
class="mx_CreateSecretStorageDialog_primaryContainer"
role="radiogroup"
>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked mx_StyledRadioButton_outlined"
>
<input
checked=""
name="keyPassphrase"
type="radio"
value="key"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<div
class="mx_CreateSecretStorageDialog_optionTitle"
>
<span
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"
/>
Generate a Security Key
</div>
<div>
We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.
</div>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_outlined"
>
<input
name="keyPassphrase"
type="radio"
value="passphrase"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<div
class="mx_CreateSecretStorageDialog_optionTitle"
>
<span
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"
/>
Enter a Security Phrase
</div>
<div>
Use a secret phrase only you know, and optionally save a Security Key to use for backup.
</div>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
</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"
>
Continue
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
/>
<div>
<div>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Restore your key backup to upgrade your encryption
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Restore
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Upgrade your encryption
</h1>
</div>
<div>
<form>
<p>
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
</p>
<div>
<div>
<div>
Enter your account password to confirm the upgrade:
</div>
<div>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_CreateSecretStorageDialog_password"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_CreateSecretStorageDialog_password"
>
Password
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="danger"
type="button"
>
Skip
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</form>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_CreateSecretStorageDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
/>
<div>
<div>
<p>
Unable to query secret storage status
</p>
<div
class="mx_Dialog_buttons"
>
<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"
>
Retry
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,112 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExportE2eKeysDialog renders 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_exportE2eKeysDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Export room keys
</h1>
</div>
<form>
<div
class="mx_Dialog_content"
>
<p>
This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.
</p>
<p>
The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.
</p>
<div
class="error"
/>
<div
class="mx_E2eKeysDialog_inputTable"
>
<div
class="mx_E2eKeysDialog_inputRow"
>
<div
class="mx_Field mx_Field_input mx_PassphraseField"
>
<input
autocomplete="new-password"
id="mx_Field_1"
label="Enter passphrase"
placeholder="Enter passphrase"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Enter passphrase
</label>
</div>
</div>
<div
class="mx_E2eKeysDialog_inputRow"
>
<div
class="mx_Field mx_Field_input"
>
<input
autocomplete="new-password"
id="mx_Field_2"
label="Confirm passphrase"
placeholder="Confirm passphrase"
type="password"
value=""
/>
<label
for="mx_Field_2"
>
Confirm passphrase
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<input
class="mx_Dialog_primary"
type="submit"
value="Export"
/>
<button>
Cancel
</button>
</div>
</form>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportE2eKeysDialog renders 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_importE2eKeysDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Import room keys
</h1>
</div>
<form>
<div
class="mx_Dialog_content"
>
<p>
This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.
</p>
<p>
The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.
</p>
<div
class="error"
/>
<div
class="mx_E2eKeysDialog_inputTable"
>
<div
class="mx_E2eKeysDialog_inputRow"
>
<div
class="mx_E2eKeysDialog_inputLabel"
>
<label
for="importFile"
>
File to import
</label>
</div>
<div
class="mx_E2eKeysDialog_inputCell"
>
<input
id="importFile"
type="file"
/>
</div>
</div>
<div
class="mx_E2eKeysDialog_inputRow"
>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Enter passphrase"
placeholder="Enter passphrase"
size="64"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Enter passphrase
</label>
</div>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<input
class="mx_Dialog_primary"
disabled=""
type="submit"
value="Import"
/>
<button>
Cancel
</button>
</div>
</form>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,298 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RestoreKeyBackupDialog /> should display an error when recovery key is invalid 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Enter Security Key
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
<span>
<strong>
Warning
</strong>
: you should only set up key backup from a trusted computer.
</span>
</p>
<p>
Access your secure message history and set up secure messaging by entering your Security Key.
</p>
<div
class="mx_RestoreKeyBackupDialog_primaryContainer"
>
<input
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
value="invalid key"
/>
<div
class="mx_RestoreKeyBackupDialog_keyStatus"
>
👎 Not a valid Security Key
</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"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</div>
<span>
If you've forgotten your Security Key you can
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
set up new recovery options
</div>
</span>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should not raise an error when recovery is valid 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Enter Security Key
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
<span>
<strong>
Warning
</strong>
: you should only set up key backup from a trusted computer.
</span>
</p>
<p>
Access your secure message history and set up secure messaging by entering your Security Key.
</p>
<div
class="mx_RestoreKeyBackupDialog_primaryContainer"
>
<input
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
value="valid key"
/>
<div
class="mx_RestoreKeyBackupDialog_keyStatus"
>
👍 This looks like a valid Security Key!
</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"
>
Next
</button>
</span>
</div>
</div>
<span>
If you've forgotten your Security Key you can
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
set up new recovery options
</div>
</span>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should render 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Enter Security Key
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
<span>
<strong>
Warning
</strong>
: you should only set up key backup from a trusted computer.
</span>
</p>
<p>
Access your secure message history and set up secure messaging by entering your Security Key.
</p>
<div
class="mx_RestoreKeyBackupDialog_primaryContainer"
>
<input
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
value=""
/>
<div
class="mx_RestoreKeyBackupDialog_keyStatus"
/>
<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"
disabled=""
type="button"
>
Next
</button>
</span>
</div>
</div>
<span>
If you've forgotten your Security Key you can
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
set up new recovery options
</div>
</span>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,65 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { IPublicRoomsChunkRoom } from "matrix-js-sdk/src/matrix";
import { PublicRoomResultDetails } from "../../../../../../src/components/views/dialogs/spotlight/PublicRoomResultDetails";
describe("PublicRoomResultDetails", () => {
it("renders", () => {
const { asFragment } = render(
<PublicRoomResultDetails
room={{
room_id: "room-id",
name: "hello?",
canonical_alias: "canonical-alias",
world_readable: true,
guest_can_join: false,
num_joined_members: 666,
}}
labelId="label-id"
descriptionId="description-id"
detailsId="details-id"
/>,
);
expect(asFragment()).toMatchSnapshot();
});
it.each([
{ canonical_alias: "canonical-alias" },
{ aliases: ["alias-from-aliases"] },
{ name: "name over alias", canonical_alias: "canonical-alias" },
{
name: "with an overly long name that will be truncated for sure, you can't say anything about it",
topic: "with a topic!",
},
{ topic: "Very long topic " + new Array(1337).join("a") },
])("Public room results", (partialPublicRoomChunk: Partial<IPublicRoomsChunkRoom>) => {
const roomChunk: IPublicRoomsChunkRoom = {
room_id: "room-id",
world_readable: true,
guest_can_join: false,
num_joined_members: 666,
...partialPublicRoomChunk,
};
const { asFragment } = render(
<PublicRoomResultDetails
room={roomChunk}
labelId="label-id"
descriptionId="description-id"
detailsId="details-id"
/>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,57 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, RenderResult } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { Room, MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
import { RoomResultContextMenus } from "../../../../../../src/components/views/dialogs/spotlight/RoomResultContextMenus";
import { filterConsole, stubClient } from "../../../../../test-utils";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../../src/settings/UIFeature";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("RoomResultContextMenus", () => {
let client: MatrixClient;
let room: Room;
const renderRoomResultContextMenus = (): RenderResult => {
return render(<RoomResultContextMenus room={room} />);
};
filterConsole(
// irrelevant for this test
"Room !1:example.org does not have an m.room.create event",
);
beforeEach(() => {
client = stubClient();
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
});
it("does not render the room options context menu when UIComponent customisations disable room options", () => {
mocked(shouldShowComponent).mockReturnValue(false);
renderRoomResultContextMenus();
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
expect(screen.queryByRole("button", { name: "Room options" })).not.toBeInTheDocument();
});
it("renders the room options context menu when UIComponent customisations enable room options", () => {
mocked(shouldShowComponent).mockReturnValue(true);
renderRoomResultContextMenus();
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
expect(screen.queryByRole("button", { name: "Room options" })).toBeInTheDocument();
});
});

View file

@ -0,0 +1,223 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PublicRoomResultDetails Public room results 1`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
canonical-alias
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
canonical-alias
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`PublicRoomResultDetails Public room results 2`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
alias-from-aliases
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
room-id
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`PublicRoomResultDetails Public room results 3`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
name over alias
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
canonical-alias
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`PublicRoomResultDetails Public room results 4`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
with an overly long name that will be truncated for sure, you can't say anything...
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
room-id
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
 · 
<span
class="mx_SpotlightDialog_result_publicRoomTopic"
>
with a topic!
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`PublicRoomResultDetails Public room results 5`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
Unnamed Room
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
room-id
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
 · 
<span
class="mx_SpotlightDialog_result_publicRoomTopic"
>
Very long topic aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`PublicRoomResultDetails renders 1`] = `
<DocumentFragment>
<div
class="mx_SpotlightDialog_result_publicRoomDetails"
>
<div
class="mx_SpotlightDialog_result_publicRoomHeader"
>
<span
class="mx_SpotlightDialog_result_publicRoomName"
id="label-id"
>
hello?
</span>
<span
class="mx_SpotlightDialog_result_publicRoomAlias"
id="description-id"
>
canonical-alias
</span>
</div>
<div
class="mx_SpotlightDialog_result_publicRoomDescription"
id="details-id"
>
<span
class="mx_SpotlightDialog_result_publicRoomMemberCount"
>
666 Members
</span>
</div>
</div>
</DocumentFragment>
`;