From d9a61c093c9c5dbd2fecf34ebe201d39844505c6 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 19 May 2023 10:32:10 +1200 Subject: [PATCH] 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 --- .../views/settings/_CryptographyPanel.pcss | 8 +- res/css/views/settings/tabs/_SettingsTab.pcss | 1 - .../tabs/user/_SecurityUserSettingsTab.pcss | 53 ++--- .../views/settings/CrossSigningPanel.tsx | 33 ++- .../views/settings/CryptographyPanel.tsx | 40 ++-- .../views/settings/E2eAdvancedPanel.tsx | 11 +- .../views/settings/EventIndexPanel.tsx | 61 ++--- .../views/settings/SecureBackupPanel.tsx | 25 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 4 - .../tabs/user/SecurityUserSettingsTab.tsx | 151 ++++++------ .../settings/tabs/user/SessionManagerTab.tsx | 1 + .../views/settings/CrossSigningPanel-test.tsx | 113 +++++++++ .../views/settings/EventIndexPanel-test.tsx | 201 ++++++++++++++++ .../CrossSigningPanel-test.tsx.snap | 40 ++++ .../EventIndexPanel-test.tsx.snap | 66 ++++++ .../SecureBackupPanel-test.tsx.snap | 216 +++++++++--------- 16 files changed, 721 insertions(+), 303 deletions(-) create mode 100644 test/components/views/settings/CrossSigningPanel-test.tsx create mode 100644 test/components/views/settings/EventIndexPanel-test.tsx create mode 100644 test/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap create mode 100644 test/components/views/settings/__snapshots__/EventIndexPanel-test.tsx.snap diff --git a/res/css/views/settings/_CryptographyPanel.pcss b/res/css/views/settings/_CryptographyPanel.pcss index 855949d013d..3440ce65543 100644 --- a/res/css/views/settings/_CryptographyPanel.pcss +++ b/res/css/views/settings/_CryptographyPanel.pcss @@ -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; } diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss index fac189e8587..b060a025418 100644 --- a/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/res/css/views/settings/tabs/_SettingsTab.pcss @@ -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 */ diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss index 81777dc2a68..ad5b3f8a111 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss @@ -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; - 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"); - } +.mx_SecurityUserSettingsTab_warning { + color: $alert; + position: relative; + padding-left: 40px; + + &::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"); } } diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx index d3926d954fa..68ec515b542 100644 --- a/src/components/views/settings/CrossSigningPanel.tsx +++ b/src/components/views/settings/CrossSigningPanel.tsx @@ -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 = ; } else if (!homeserverSupportsCrossSigning) { - summarisedStatus =

{_t("Your homeserver does not support cross-signing.")}

; + summarisedStatus = ( + + {_t("Your homeserver does not support cross-signing.")} + + ); } else if (crossSigningReady && crossSigningPrivateKeysInStorage) { - summarisedStatus =

✅ {_t("Cross-signing is ready for use.")}

; + summarisedStatus = ( + + ✅ {_t("Cross-signing is ready for use.")} + + ); } else if (crossSigningReady && !crossSigningPrivateKeysInStorage) { - summarisedStatus =

⚠️ {_t("Cross-signing is ready but keys are not backed up.")}

; + summarisedStatus = ( + + ⚠️ {_t("Cross-signing is ready but keys are not backed up.")} + + ); } else if (crossSigningPrivateKeysInStorage) { summarisedStatus = ( -

+ {_t( "Your account has a cross-signing identity in secret storage, " + "but it is not yet trusted by this session.", )} -

+ ); } else { - summarisedStatus =

{_t("Cross-signing is not set up.")}

; + summarisedStatus = ( + + {_t("Cross-signing is not set up.")} + + ); } const keysExistAnywhere = @@ -238,7 +255,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { } return ( -
+ <> {summarisedStatus}
{_t("Advanced")} @@ -275,7 +292,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
{errorSection} {actionRow} -
+ ); } } diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 79ddad2544e..d947946b755 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -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 { } return ( -
- {_t("Cryptography")} - - - - - - - - - -
{_t("Session ID:")} - {deviceId} -
{_t("Session key:")} - - {identityKey} - -
+ + + + + + + + + + + +
{_t("Session ID:")} + {deviceId} +
{_t("Session key:")} + + {identityKey} + +
+
{importExportButtons} {noSendUnverifiedSetting} -
+ ); } diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx index 2b1d2645046..a2e45642942 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -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 ( -
- {_t("Encryption")} - + -
+ {_t( "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", )} -
-
+ + ); }; diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index f0b3c54c99d..eb33f6707df 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -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 = ( -
-
+ <> + {_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), }, )} -
-
- - {_t("Manage")} - -
-
+ + + {_t("Manage")} + + ); } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) { eventIndexingSettings = ( -
-
+ <> + {_t("Securely cache encrypted messages locally for them to appear in search results.")} -
+
{_t("Enable")} {this.state.enabling ? :
}
-
+ ); } 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 = ( -
+ {_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> { ), }, )} -
+ ); } else if (!EventIndexPeg.platformHasSupport()) { eventIndexingSettings = ( -
+ {_t( "%(brand)s can't securely cache encrypted messages locally " + "while running in a web browser. Use %(brand)s Desktop " + @@ -228,24 +227,28 @@ export default class EventIndexPanel extends React.Component<{}, IState> { ), }, )} -
+ ); } else { eventIndexingSettings = ( -
-

{this.state.enabling ? : _t("Message search initialisation failed")}

+ <> + + {this.state.enabling ? : _t("Message search initialisation failed")} + {EventIndexPeg.error && ( -
- {_t("Advanced")} - {EventIndexPeg.error.message} -

- - {_t("Reset")} - -

-
+ +
+ {_t("Advanced")} + {EventIndexPeg.error.message} +

+ + {_t("Reset")} + +

+
+
)} -
+ ); } diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 4d249c8df8f..874123be574 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -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 = ( <> -

+ {_t( "This session is not backing up your keys, " + "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) => {sub} }, )} -

-

+ + {_t( "Connect this session to key backup before signing out to avoid " + "losing any keys that may only be on this session.", )} -

+ ); restoreButtonCaption = _t("Connect this session to Key Backup"); @@ -425,14 +426,16 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } else { statusDescription = ( <> -

+ {_t( "Your keys are not being backed up from this session.", {}, { b: (sub) => {sub} }, )} -

-

{_t("Back up your keys before signing out to avoid losing them.")}

+ + + {_t("Back up your keys before signing out to avoid losing them.")} + ); actions.push( @@ -466,14 +469,14 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } return ( -
-

+ <> + {_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.", )} -

+ {statusDescription}
{_t("Advanced")} @@ -502,7 +505,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { {extraDetails}
{actionRow} -
+ ); } } diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 71afe65d98b..97135c5804f 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -477,10 +477,6 @@ export default class SecurityRoomSettingsTab extends React.Component - //
- //
{_t("Security & Privacy")}
- - //
); } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 76f243e6ab2..8e94ca69889 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -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 - {_t("Ignored users")} -
{userIds}
-
+ + {userIds} + ); } @@ -260,24 +262,25 @@ export default class SecurityUserSettingsTab extends React.Component - {_t("Bulk options")} - - {_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} - - - {_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} - - {this.state.managingInvites ? :
} -
+ +
+ + {_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} + + + {_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })} + + {this.state.managingInvites ? :
} +
+ ); } @@ -291,19 +294,15 @@ export default class SecurityUserSettingsTab extends React.Component - {_t("Secure Backup")} -
- -
-
+ + + ); const eventIndex = ( -
- {_t("Message search")} + -
+
); // XXX: There's no such panel in the current cross-signing designs, but @@ -311,12 +310,9 @@ export default class SecurityUserSettingsTab extends React.Component - {_t("Cross-signing")} -
- -
- + + + ); let warning; @@ -340,28 +336,24 @@ export default class SecurityUserSettingsTab extends React.Component -
{_t("Privacy")}
-
- {_t("Analytics")} -
-

- {_t( - "Share anonymous data to help us identify issues. Nothing personal. " + - "No third parties.", - )} -

- - {_t("Learn more")} - -
+ + + + {_t("Learn more")} + {PosthogAnalytics.instance.isEnabled() && ( )} - {_t("Sessions")} + + -
- + + ); } @@ -373,67 +365,60 @@ export default class SecurityUserSettingsTab extends React.Component -
{_t("Advanced")}
-
- {ignoreUsersPanel} - {invitesPanel} - {e2ePanel} -
- + + {ignoreUsersPanel} + {invitesPanel} + {e2ePanel} + ); } } const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager"); const devicesSection = useNewSessionManager ? null : ( - <> -
{_t("Where you're signed in")}
-
- - {_t( - "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", - )} - - -
+ + + {_t( + "Manage your signed-in devices below. " + + "A device's name is visible to people you communicate with.", + )} + + - + ); const client = MatrixClientPeg.get(); if (this.state.showLoginWithQR) { return ( -
+ -
+ ); } return ( -
+ {warning} {devicesSection} -
{_t("Encryption")}
-
+ {secureBackup} {eventIndex} {crossSigning} -
+ {privacySection} {advancedSection} -
+ ); } } diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2b86e5b60f7..1c338681fca 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -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 > ", () => { + const userId = "@alice:server.org"; + const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsCrypto(), + doesServerSupportUnstableFeature: jest.fn(), + }); + const getComponent = () => render(); + + 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, + ); + }); + }); +}); diff --git a/test/components/views/settings/EventIndexPanel-test.tsx b/test/components/views/settings/EventIndexPanel-test.tsx new file mode 100644 index 00000000000..34956f44c1b --- /dev/null +++ b/test/components/views/settings/EventIndexPanel-test.tsx @@ -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("", () => { + getMockClientWithEventEmitter({ + getRooms: jest.fn().mockReturnValue([]), + }); + + const getComponent = () => render(); + + 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 | undefined; + jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => { + deferredInitEventIndex = defer(); + 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(); + }); + }); +}); diff --git a/test/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap new file mode 100644 index 00000000000..d484ba4a3be --- /dev/null +++ b/test/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when cross signing is not ready should render when keys are backed up 1`] = ` + + + Cross-signing private keys: + + + in secret storage + + +`; + +exports[` when cross signing is ready should render when keys are backed up 1`] = ` + + + Cross-signing private keys: + + + in secret storage + + +`; + +exports[` when cross signing is ready should render when keys are not backed up 1`] = ` + + + Cross-signing private keys: + + + not found in storage + + +`; diff --git a/test/components/views/settings/__snapshots__/EventIndexPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/EventIndexPanel-test.tsx.snap new file mode 100644 index 00000000000..305b9e2d6a5 --- /dev/null +++ b/test/components/views/settings/__snapshots__/EventIndexPanel-test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when event index is initialised renders event index information 1`] = ` +
+
+ Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms. +
+
+ Manage +
+
+`; + +exports[` when event indexing is not supported renders link to download a desktop client 1`] = ` +
+
+ + Element can't securely cache encrypted messages locally while running in a web browser. Use + + Element Desktop + + + for encrypted messages to appear in search results. + +
+
+`; + +exports[` when event indexing is supported but not installed renders link to install seshat 1`] = ` +
+
+ + 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 + + search components added + + + . + +
+
+`; diff --git a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap index e17dfd0064c..bc8f2400cc8 100644 --- a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap @@ -2,114 +2,118 @@ exports[` suggests connecting session to key backup when backup exists 1`] = `
-
-

- 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. -

-

- - This session is - - not backing up your keys - - , but you do have an existing backup you can restore from and add to going forward. - -

-

- Connect this session to key backup before signing out to avoid losing any keys that may only be on this session. -

-
- - Advanced - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Backup key stored: - - not stored -
- Backup key cached: - - not found locally - -
- Secret storage public key: - - not found -
- Secret storage: - - not ready -
- Backup version: - - 1 -
- Algorithm: - - test -
- -
- Backup is not signed by any of your sessions -
-
-
+
+ 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. +
+
+ + This session is + + not backing up your keys + + , but you do have an existing backup you can restore from and add to going forward. + +
+
+ Connect this session to key backup before signing out to avoid losing any keys that may only be on this session. +
+
+ + Advanced + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Backup key stored: + + not stored +
+ Backup key cached: + + not found locally + +
+ Secret storage public key: + + not found +
+ Secret storage: + + not ready +
+ Backup version: + + 1 +
+ Algorithm: + + test +
+ +
+ Backup is not signed by any of your sessions +
+
+
+
+
+ Connect this session to Key Backup +
-
- Connect this session to Key Backup -
-
- Delete Backup -
+ Delete Backup