Use semantic headings in user settings Security (#10774)

* split SettingsSection out of SettingsTab, replace usage

* correct copyright

* use semantic headings in GeneralRoomSettingsTab

* use SettingsTab and SettingsSubsection in room settings

* fix VoipRoomSettingsTab

* use SettingsSection components in space settings

* settingssubsection text component

* use semantic headings in HelpUserSetttings tab

* use ExternalLink components for external links

* test

* strict

* lint

* semantic heading in labs settings

* semantic headings in keyboard settings tab

* semantic heading in preferencesusersettingstab

* tidying

* use new settings components in eventindexpanel

* findByTestId

* prettier

* semantic headings and style refresh for crypto settings

* e2e panel

* test cross signing panel

* strict

* more strict

* tweak

* test eventindexpanel

* strict fixes
This commit is contained in:
Kerry 2023-05-19 10:32:10 +12:00 committed by GitHub
parent 6c262fff6b
commit d9a61c093c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 721 additions and 303 deletions

View file

@ -32,13 +32,9 @@ limitations under the License.
}
}
.mx_CryptographyPanel_importExportButtons .mx_AccessibleButton {
margin-right: 10px;
}
.mx_CryptographyPanel_importExportButtons {
margin-bottom: 15px;
display: inline-flex;
flex-flow: wrap;
row-gap: 10px;
row-gap: $spacing-8;
column-gap: $spacing-8;
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
.mx_SettingsTab {
--SettingsTab_section-margin-bottom-preferences-labs: 30px;
--SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */
--SettingsTab_fullWidthField-margin-inline-end: 100px;
--SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */

View file

@ -14,43 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_SecurityUserSettingsTab_bulkOptions .mx_AccessibleButton {
margin-right: 10px;
.mx_SecurityUserSettingsTab_bulkOptions {
display: flex;
flex-direction: row;
column-gap: $spacing-8;
}
.mx_SecurityUserSettingsTab_ignoredUser {
margin-bottom: 5px;
margin-bottom: $spacing-4;
}
.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
margin-right: 10px;
margin-right: $spacing-8;
}
.mx_SecurityUserSettingsTab {
.mx_SettingsTab_section {
.mx_AccessibleButton_kind_link {
font-size: inherit;
}
}
.mx_SecurityUserSettingsTab_warning {
color: $alert;
position: relative;
padding-left: 40px;
.mx_SecurityUserSettingsTab_warning {
color: $alert;
position: relative;
padding-left: 40px;
margin-top: 30px;
&::before {
mask-repeat: no-repeat;
mask-position: 0 center;
mask-size: $font-24px;
position: absolute;
width: $font-24px;
height: $font-24px;
content: "";
top: 0;
left: 0;
background-color: $alert;
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
}
&::before {
mask-repeat: no-repeat;
mask-position: 0 center;
mask-size: $font-24px;
position: absolute;
width: $font-24px;
height: $font-24px;
content: "";
top: 0;
left: 0;
background-color: $alert;
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
}
}

View file

@ -28,6 +28,7 @@ import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroy
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
import { accessSecretStorage } from "../../../SecurityManager";
import AccessibleButton from "../elements/AccessibleButton";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
error?: Error;
@ -178,22 +179,38 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
if (homeserverSupportsCrossSigning === undefined) {
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = <p>{_t("Your homeserver does not support cross-signing.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Your homeserver does not support cross-signing.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> {_t("Cross-signing is ready for use.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready for use.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> {_t("Cross-signing is ready but keys are not backed up.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready but keys are not backed up.")}
</SettingsSubsectionText>
);
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = (
<p>
<SettingsSubsectionText data-testid="summarised-status">
{_t(
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}
</p>
</SettingsSubsectionText>
);
} else {
summarisedStatus = <p>{_t("Cross-signing is not set up.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is not set up.")}
</SettingsSubsectionText>
);
}
const keysExistAnywhere =
@ -238,7 +255,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
}
return (
<div>
<>
{summarisedStatus}
<details>
<summary>{_t("Advanced")}</summary>
@ -275,7 +292,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
</details>
{errorSection}
{actionRow}
</div>
</>
);
}
}

View file

@ -26,6 +26,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IProps {}
@ -72,27 +73,28 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
}
return (
<div className="mx_SettingsTab_section mx_CryptographyPanel">
<span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span>
<table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("Session ID:")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("Session key:")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</table>
<SettingsSubsection heading={_t("Cryptography")}>
<SettingsSubsectionText>
<table className="mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("Session ID:")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("Session key:")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</table>
</SettingsSubsectionText>
{importExportButtons}
{noSendUnverifiedSetting}
</div>
</SettingsSubsection>
);
}

View file

@ -20,21 +20,20 @@ import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel: React.FC = () => {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
<SettingsSubsection heading={_t("Encryption")}>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}
</div>
</div>
</SettingsSubsectionText>
</SettingsSubsection>
);
};

View file

@ -27,6 +27,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
import InlineSpinner from "../elements/InlineSpinner";
import ExternalLink from "../elements/ExternalLink";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
enabling: boolean;
@ -145,8 +146,8 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<div>
<div className="mx_SettingsTab_subsectionText">
<>
<SettingsSubsectionText>
{_t(
"Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
@ -158,27 +159,25 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
rooms: formatCountLong(this.state.roomCount),
},
)}
</div>
<div>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")}
</AccessibleButton>
</div>
</div>
</SettingsSubsectionText>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")}
</AccessibleButton>
</>
);
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<div>
<div className="mx_SettingsTab_subsectionText">
<>
<SettingsSubsectionText>
{_t("Securely cache encrypted messages locally for them to appear in search results.")}
</div>
</SettingsSubsectionText>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
{_t("Enable")}
</AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />}
</div>
</div>
</>
);
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
const nativeLink =
@ -187,7 +186,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
"adding-seshat-for-search-in-e2e-encrypted-rooms";
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
@ -204,11 +203,11 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
),
},
)}
</div>
</SettingsSubsectionText>
);
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
@ -228,24 +227,28 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
),
},
)}
</div>
</SettingsSubsectionText>
);
} else {
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<p>{this.state.enabling ? <InlineSpinner /> : _t("Message search initialisation failed")}</p>
<>
<SettingsSubsectionText>
{this.state.enabling ? <InlineSpinner /> : _t("Message search initialisation failed")}
</SettingsSubsectionText>
{EventIndexPeg.error && (
<details>
<summary>{_t("Advanced")}</summary>
<code>{EventIndexPeg.error.message}</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")}
</AccessibleButton>
</p>
</details>
<SettingsSubsectionText>
<details>
<summary>{_t("Advanced")}</summary>
<code>{EventIndexPeg.error.message}</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")}
</AccessibleButton>
</p>
</details>
</SettingsSubsectionText>
)}
</div>
</>
);
}

View file

@ -31,6 +31,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import QuestionDialog from "../dialogs/QuestionDialog";
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
import { accessSecretStorage } from "../../../SecurityManager";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
loading: boolean;
@ -247,7 +248,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} else {
statusDescription = (
<>
<p>
<SettingsSubsectionText>
{_t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
@ -255,13 +256,13 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>
</SettingsSubsectionText>
<SettingsSubsectionText>
{_t(
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}
</p>
</SettingsSubsectionText>
</>
);
restoreButtonCaption = _t("Connect this session to Key Backup");
@ -425,14 +426,16 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} else {
statusDescription = (
<>
<p>
<SettingsSubsectionText>
{_t(
"Your keys are <b>not being backed up from this session</b>.",
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</SettingsSubsectionText>
<SettingsSubsectionText>
{_t("Back up your keys before signing out to avoid losing them.")}
</SettingsSubsectionText>
</>
);
actions.push(
@ -466,14 +469,14 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
}
return (
<div>
<p>
<>
<SettingsSubsectionText>
{_t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Security Key.",
)}
</p>
</SettingsSubsectionText>
{statusDescription}
<details>
<summary>{_t("Advanced")}</summary>
@ -502,7 +505,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
{extraDetails}
</details>
{actionRow}
</div>
</>
);
}
}

View file

@ -477,10 +477,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
{historySection}
</SettingsSection>
</SettingsTab>
// <div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
// <div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
// </div>
);
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -41,6 +41,9 @@ import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
import LoginWithQRSection from "../../devices/LoginWithQRSection";
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
interface IIgnoredUserProps {
userId: string;
@ -245,10 +248,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
});
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Ignored users")}</span>
<div className="mx_SettingsTab_subsectionText">{userIds}</div>
</div>
<SettingsSubsection heading={_t("Ignored users")}>
<SettingsSubsectionText>{userIds}</SettingsSubsectionText>
</SettingsSubsection>
);
}
@ -260,24 +262,25 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
return (
<div className="mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions">
<span className="mx_SettingsTab_subheading">{_t("Bulk options")}</span>
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary"
disabled={this.state.managingInvites}
>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton
onClick={this.onRejectAllInvitesClicked}
kind="danger"
disabled={this.state.managingInvites}
>
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
<SettingsSubsection heading={_t("Bulk options")}>
<div className="mx_SecurityUserSettingsTab_bulkOptions">
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary"
disabled={this.state.managingInvites}
>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton
onClick={this.onRejectAllInvitesClicked}
kind="danger"
disabled={this.state.managingInvites}
>
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
</SettingsSubsection>
);
}
@ -291,19 +294,15 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public render(): React.ReactNode {
const secureBackup = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
<div className="mx_SettingsTab_subsectionText">
<SecureBackupPanel />
</div>
</div>
<SettingsSubsection heading={_t("Secure Backup")}>
<SecureBackupPanel />
</SettingsSubsection>
);
const eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<SettingsSubsection heading={_t("Message search")}>
<EventIndexPanel />
</div>
</SettingsSubsection>
);
// XXX: There's no such panel in the current cross-signing designs, but
@ -311,12 +310,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
// in having advanced details here once all flows are implemented, we
// can remove this.
const crossSigning = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className="mx_SettingsTab_subsectionText">
<CrossSigningPanel />
</div>
</div>
<SettingsSubsection heading={_t("Cross-signing")}>
<CrossSigningPanel />
</SettingsSubsection>
);
let warning;
@ -340,28 +336,24 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
});
};
privacySection = (
<React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className="mx_SettingsTab_subsectionText">
<p>
{_t(
"Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.",
)}
</p>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("Learn more")}
</AccessibleButton>
</div>
<SettingsSection heading={_t("Privacy")}>
<SettingsSubsection
heading={_t("Analytics")}
description={_t(
"Share anonymous data to help us identify issues. Nothing personal. No third parties.",
)}
>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("Learn more")}
</AccessibleButton>
{PosthogAnalytics.instance.isEnabled() && (
<SettingsFlag name="pseudonymousAnalyticsOptIn" level={SettingLevel.ACCOUNT} />
)}
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
</SettingsSubsection>
<SettingsSubsection heading={_t("Sessions")}>
<SettingsFlag name="deviceClientInformationOptIn" level={SettingLevel.ACCOUNT} />
</div>
</React.Fragment>
</SettingsSubsection>
</SettingsSection>
);
}
@ -373,67 +365,60 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
advancedSection = (
<>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</div>
</>
<SettingsSection heading={_t("Advanced")}>
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</SettingsSection>
);
}
}
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
const devicesSection = useNewSessionManager ? null : (
<>
<div className="mx_SettingsTab_heading">{_t("Where you're signed in")}</div>
<div className="mx_SettingsTab_section" data-testid="devices-section">
<span className="mx_SettingsTab_subsectionText">
{_t(
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
)}
</span>
<DevicesPanel />
</div>
<SettingsSection heading={_t("Where you're signed in")} data-testid="devices-section">
<SettingsSubsectionText>
{_t(
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
)}
</SettingsSubsectionText>
<DevicesPanel />
<LoginWithQRSection
onShowQr={this.onShowQRClicked}
versions={this.state.versions}
capabilities={this.state.capabilities}
/>
</>
</SettingsSection>
);
const client = MatrixClientPeg.get();
if (this.state.showLoginWithQR) {
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<SettingsTab>
<LoginWithQR
onFinished={this.onLoginWithQRFinished}
mode={this.state.showLoginWithQR}
client={client}
/>
</div>
</SettingsTab>
);
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<SettingsTab>
{warning}
{devicesSection}
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_section">
<SettingsSection heading={_t("Encryption")}>
{secureBackup}
{eventIndex}
{crossSigning}
<CryptographyPanel />
</div>
</SettingsSection>
{privacySection}
{advancedSection}
</div>
</SettingsTab>
);
}
}

View file

@ -259,6 +259,7 @@ const SessionManagerTab: React.FC = () => {
`from any session that you don't recognize or use anymore.`,
)}
data-testid="other-sessions-section"
stretchContent
>
<FilteredDeviceList
devices={otherDevices}

View file

@ -0,0 +1,113 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { render, screen } from "@testing-library/react";
import { mocked } from "jest-mock";
import CrossSigningPanel from "../../../../src/components/views/settings/CrossSigningPanel";
import {
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsUser,
} from "../../../test-utils";
describe("<CrossSigningPanel />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
doesServerSupportUnstableFeature: jest.fn(),
});
const getComponent = () => render(<CrossSigningPanel />);
beforeEach(() => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
mockClient.isCrossSigningReady.mockResolvedValue(false);
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockClear().mockResolvedValue(null);
});
it("should render a spinner while loading", () => {
getComponent();
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("should render when homeserver does not support cross-signing", async () => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
getComponent();
await flushPromises();
expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument();
});
describe("when cross signing is ready", () => {
beforeEach(() => {
mockClient.isCrossSigningReady.mockResolvedValue(true);
});
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"⚠️ Cross-signing is ready but keys are not backed up.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
});
it("should render when keys are backed up", async () => {
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
mockClient.crypto!.secretStorage,
);
});
});
describe("when cross signing is not ready", () => {
beforeEach(() => {
mockClient.isCrossSigningReady.mockResolvedValue(false);
});
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up.");
});
it("should render when keys are backed up", async () => {
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
mockClient.crypto!.secretStorage,
);
});
});
});

View file

@ -0,0 +1,201 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
import EventIndexPanel from "../../../../src/components/views/settings/EventIndexPanel";
import EventIndexPeg from "../../../../src/indexing/EventIndexPeg";
import EventIndex from "../../../../src/indexing/EventIndex";
import { clearAllModals, flushPromises, getMockClientWithEventEmitter } from "../../../test-utils";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
describe("<EventIndexPanel />", () => {
getMockClientWithEventEmitter({
getRooms: jest.fn().mockReturnValue([]),
});
const getComponent = () => render(<EventIndexPanel />);
beforeEach(() => {
jest.spyOn(EventIndexPeg, "get").mockRestore();
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "initEventIndex").mockClear().mockResolvedValue(true);
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockClear();
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
jest.spyOn(SettingsStore, "setValue").mockClear();
// @ts-ignore private property
EventIndexPeg.error = null;
});
afterEach(async () => {
await clearAllModals();
});
describe("when event index is initialised", () => {
it("renders event index information", () => {
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("opens event index management dialog", async () => {
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
getComponent();
fireEvent.click(screen.getByText("Manage"));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Message search")).toBeInTheDocument();
// close the modal
fireEvent.click(within(dialog).getByText("Done"));
});
});
describe("when event indexing is fully supported and enabled but not initialised", () => {
beforeEach(() => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
// @ts-ignore private property
EventIndexPeg.error = { message: "Test error message" };
});
it("displays an error when no event index is found and enabling not in progress", () => {
getComponent();
expect(screen.getByText("Message search initialisation failed")).toBeInTheDocument();
});
it("displays an error from the event index", () => {
getComponent();
expect(screen.getByText("Test error message")).toBeInTheDocument();
});
it("asks for confirmation when resetting seshat", async () => {
getComponent();
fireEvent.click(screen.getByText("Reset"));
// wait for reset modal to open
await screen.findByText("Reset event store?");
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Reset event store?")).toBeInTheDocument();
fireEvent.click(within(dialog).getByText("Cancel"));
// didn't reset
expect(SettingsStore.setValue).not.toHaveBeenCalled();
expect(EventIndexPeg.deleteEventIndex).not.toHaveBeenCalled();
});
it("resets seshat", async () => {
getComponent();
fireEvent.click(screen.getByText("Reset"));
// wait for reset modal to open
await screen.findByText("Reset event store?");
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("Reset event store"));
await flushPromises();
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"enableEventIndexing",
null,
SettingLevel.DEVICE,
false,
);
expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled();
await clearAllModals();
});
});
describe("when event indexing is supported but not enabled", () => {
it("renders enable text", () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
getComponent();
expect(
screen.getByText("Securely cache encrypted messages locally for them to appear in search results."),
).toBeInTheDocument();
});
it("enables event indexing on enable button click", async () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
let deferredInitEventIndex: IDeferred<boolean> | undefined;
jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => {
deferredInitEventIndex = defer<boolean>();
return deferredInitEventIndex.promise;
});
getComponent();
fireEvent.click(screen.getByText("Enable"));
await flushPromises();
// spinner shown while enabling
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
// add an event indx to the peg and resolve the init promise
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
expect(EventIndexPeg.initEventIndex).toHaveBeenCalled();
deferredInitEventIndex!.resolve(true);
await flushPromises();
expect(SettingsStore.setValue).toHaveBeenCalledWith("enableEventIndexing", null, SettingLevel.DEVICE, true);
// message for enabled event index
expect(
screen.getByText(
"Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.",
),
).toBeInTheDocument();
});
});
describe("when event indexing is supported but not installed", () => {
it("renders link to install seshat", () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});
describe("when event indexing is not supported", () => {
it("renders link to download a desktop client", () => {
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CrossSigningPanel /> when cross signing is not ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are not backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
not found in storage
</td>
</tr>
`;

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EventIndexPanel /> when event index is initialised renders event index information 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Manage
</div>
</div>
`;
exports[`<EventIndexPanel /> when event indexing is not supported renders link to download a desktop client 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Element can't securely cache encrypted messages locally while running in a web browser. Use
<a
class="mx_ExternalLink"
href="https://element.io/get-started"
rel="noreferrer noopener"
target="_blank"
>
Element Desktop
<i
class="mx_ExternalLink_icon"
/>
</a>
for encrypted messages to appear in search results.
</span>
</div>
</div>
`;
exports[`<EventIndexPanel /> when event indexing is supported but not installed renders link to install seshat 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Element is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Element Desktop with
<a
class="mx_ExternalLink"
href="https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md#adding-seshat-for-search-in-e2e-encrypted-rooms"
rel="noreferrer noopener"
target="_blank"
>
search components added
<i
class="mx_ExternalLink_icon"
/>
</a>
.
</span>
</div>
</div>
`;

View file

@ -2,114 +2,118 @@
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
<div>
<div>
<p>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
</p>
<p>
<span>
This session is
<b>
not backing up your keys
</b>
, but you do have an existing backup you can restore from and add to going forward.
</span>
</p>
<p>
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
</p>
<details>
<summary>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
<tr>
<th
scope="row"
>
Backup version:
</th>
<td>
1
</td>
</tr>
<tr>
<th
scope="row"
>
Algorithm:
</th>
<td>
test
</td>
</tr>
</table>
<div>
Backup is not signed by any of your sessions
</div>
<div />
</details>
<div
class="mx_SecureBackupPanel_buttonRow"
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
</div>
<div
class="mx_SettingsSubsection_text"
>
<span>
This session is
<b>
not backing up your keys
</b>
, but you do have an existing backup you can restore from and add to going forward.
</span>
</div>
<div
class="mx_SettingsSubsection_text"
>
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
</div>
<details>
<summary>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Connect this session to Key Backup
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Delete Backup
</div>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
<tr>
<th
scope="row"
>
Backup version:
</th>
<td>
1
</td>
</tr>
<tr>
<th
scope="row"
>
Algorithm:
</th>
<td>
test
</td>
</tr>
</table>
<div>
Backup is not signed by any of your sessions
</div>
<div />
</details>
<div
class="mx_SecureBackupPanel_buttonRow"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Connect this session to Key Backup
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Delete Backup
</div>
</div>
</div>