Merge branch 'develop' into florianduros/rip-out-legacy-crypto/migrate-EventTile-isRoomEncrypted
# Conflicts: # test/test-utils/test-utils.ts
This commit is contained in:
commit
2e303902a3
59 changed files with 2115 additions and 472 deletions
|
@ -96,12 +96,12 @@ describe("DeviceListener", () => {
|
|||
}),
|
||||
getSessionBackupPrivateKey: jest.fn(),
|
||||
isEncryptionEnabledInRoom: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
isGuest: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
isVersionSupported: jest.fn().mockResolvedValue(true),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
|
@ -354,7 +354,7 @@ describe("DeviceListener", () => {
|
|||
|
||||
it("shows set up encryption toast when user has a key backup available", async () => {
|
||||
// non falsy response
|
||||
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
|
@ -673,7 +673,7 @@ describe("DeviceListener", () => {
|
|||
describe("When Room Key Backup is not enabled", () => {
|
||||
beforeEach(() => {
|
||||
// no backup
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("Should report recovery state as Enabled", async () => {
|
||||
|
@ -722,7 +722,7 @@ describe("DeviceListener", () => {
|
|||
});
|
||||
|
||||
// no backup
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||
|
||||
await createAndStart();
|
||||
|
||||
|
@ -872,7 +872,7 @@ describe("DeviceListener", () => {
|
|||
describe("When Room Key Backup is enabled", () => {
|
||||
beforeEach(() => {
|
||||
// backup enabled - just need a mock object
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
|
|
|
@ -139,6 +139,7 @@ describe("<MatrixChat />", () => {
|
|||
globalBlacklistUnverifiedDevices: false,
|
||||
// This needs to not finish immediately because we need to test the screen appears
|
||||
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
}),
|
||||
secretStorage: {
|
||||
isStored: jest.fn().mockReturnValue(null),
|
||||
|
@ -148,7 +149,6 @@ describe("<MatrixChat />", () => {
|
|||
whoami: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
getDeviceId: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
const serverConfig = {
|
||||
|
|
|
@ -10,18 +10,19 @@ import React, { createRef, RefObject } from "react";
|
|||
import { mocked, MockedObject } from "jest-mock";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventTimeline,
|
||||
EventType,
|
||||
IEvent,
|
||||
JoinRule,
|
||||
MatrixClient,
|
||||
MatrixError,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
EventType,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
RoomStateEvent,
|
||||
MatrixEvent,
|
||||
SearchResult,
|
||||
IEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
import { CryptoApi, UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import {
|
||||
fireEvent,
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
cleanup,
|
||||
} from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
|
@ -87,8 +89,7 @@ describe("RoomView", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg({ reload: () => {} });
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.safeGet());
|
||||
cli = mocked(stubClient());
|
||||
|
||||
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
|
||||
jest.spyOn(room, "findPredecessor");
|
||||
|
@ -247,8 +248,9 @@ describe("RoomView", () => {
|
|||
|
||||
it("updates url preview visibility on encryption state change", async () => {
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
|
||||
// we should be starting unencrypted
|
||||
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
|
||||
expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false);
|
||||
|
||||
const roomViewInstance = await getRoomViewInstance();
|
||||
|
||||
|
@ -263,23 +265,38 @@ describe("RoomView", () => {
|
|||
expect(roomViewInstance.state.showUrlPreview).toBe(true);
|
||||
|
||||
// now enable encryption
|
||||
cli.isRoomEncrypted.mockReturnValue(true);
|
||||
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
|
||||
// and fake an encryption event into the room to prompt it to re-check
|
||||
await act(() =>
|
||||
room.addLiveEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.room.encryption",
|
||||
sender: cli.getUserId()!,
|
||||
content: {},
|
||||
event_id: "someid",
|
||||
room_id: room.roomId,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
act(() => {
|
||||
const encryptionEvent = new MatrixEvent({
|
||||
type: EventType.RoomEncryption,
|
||||
sender: cli.getUserId()!,
|
||||
content: {},
|
||||
event_id: "someid",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null);
|
||||
});
|
||||
|
||||
// URL previews should now be disabled
|
||||
expect(roomViewInstance.state.showUrlPreview).toBe(false);
|
||||
await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false));
|
||||
});
|
||||
|
||||
it("should not display the timeline when the room encryption is loading", async () => {
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
||||
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
|
||||
const deferred = defer<boolean>();
|
||||
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(() => deferred.promise);
|
||||
|
||||
const { asFragment, container } = await mountRoomView();
|
||||
expect(container.querySelector(".mx_RoomView_messagePanel")).toBeNull();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
deferred.resolve(true);
|
||||
await waitFor(() => expect(container.querySelector(".mx_RoomView_messagePanel")).not.toBeNull());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("updates live timeline when a timeline reset happens", async () => {
|
||||
|
@ -290,6 +307,32 @@ describe("RoomView", () => {
|
|||
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
|
||||
});
|
||||
|
||||
it("should update when the e2e status when the user verification changed", async () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId),
|
||||
mkRoomMemberJoinEvent("user@example.com", room.roomId),
|
||||
]);
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
// Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both.
|
||||
mocked(cli.isRoomEncrypted).mockReturnValue(true);
|
||||
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
|
||||
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
jest.spyOn(cli.getCrypto()!, "getUserDeviceInfo").mockResolvedValue(
|
||||
new Map([["user@example.com", new Map<string, any>()]]),
|
||||
);
|
||||
|
||||
const { container } = await renderRoomView();
|
||||
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument());
|
||||
|
||||
const verificationStatus = new UserVerificationStatus(true, true, false);
|
||||
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus);
|
||||
cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus);
|
||||
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
describe("with virtual rooms", () => {
|
||||
it("checks for a virtual room on initial load", async () => {
|
||||
const { container } = await renderRoomView();
|
||||
|
@ -427,7 +470,8 @@ describe("RoomView", () => {
|
|||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
|
||||
mocked(cli).isRoomEncrypted.mockReturnValue(true);
|
||||
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
|
||||
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
await renderRoomView();
|
||||
});
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
|||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":rbc:"
|
||||
aria-labelledby=":rg4:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -78,7 +78,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
|||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby=":rbh:"
|
||||
aria-labelledby=":rg9:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -103,7 +103,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
|||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":rbm:"
|
||||
aria-labelledby=":rge:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
|||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":rbr:"
|
||||
aria-labelledby=":rgj:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -157,7 +157,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
|||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby=":rc0:"
|
||||
aria-labelledby=":rgo:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -280,7 +280,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
|||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":rca:"
|
||||
aria-labelledby=":rh2:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -296,7 +296,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
|||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby=":rcf:"
|
||||
aria-labelledby=":rh7:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -321,7 +321,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
|||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":rck:"
|
||||
aria-labelledby=":rhc:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -346,7 +346,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
|||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":rcp:"
|
||||
aria-labelledby=":rhh:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -375,7 +375,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
|||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby=":rcu:"
|
||||
aria-labelledby=":rhm:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
|||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":r70:"
|
||||
aria-labelledby=":rbo:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
|||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby=":r75:"
|
||||
aria-labelledby=":rbt:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -624,7 +624,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
|||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r7a:"
|
||||
aria-labelledby=":rc2:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -649,7 +649,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
|||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r7f:"
|
||||
aria-labelledby=":rc7:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -678,7 +678,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
|||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby=":r7k:"
|
||||
aria-labelledby=":rcc:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -963,7 +963,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":r96:"
|
||||
aria-labelledby=":rdu:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -979,7 +979,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby=":r9b:"
|
||||
aria-labelledby=":re3:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1004,7 +1004,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r9g:"
|
||||
aria-labelledby=":re8:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1029,7 +1029,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r9l:"
|
||||
aria-labelledby=":red:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1058,7 +1058,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby=":r9q:"
|
||||
aria-labelledby=":rei:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -1276,6 +1276,571 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomView should not display the timeline when the room encryption is loading 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_RoomView"
|
||||
>
|
||||
<canvas
|
||||
aria-hidden="true"
|
||||
height="768"
|
||||
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
|
||||
width="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_MainSplit"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_body mx_MainSplit_timeline"
|
||||
data-layout="group"
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 40px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
!
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
class="mx_RoomHeader_infoWrapper"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Box mx_RoomHeader_info mx_Box--flex"
|
||||
style="--mx-box-flex: 1;"
|
||||
>
|
||||
<div
|
||||
aria-level="1"
|
||||
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!5:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":r2c:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
aria-labelledby=":r2h:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r2m:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r2r:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby=":r30:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_mcap2_111"
|
||||
/>
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_AuxPanel"
|
||||
role="region"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<main
|
||||
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
|
||||
/>
|
||||
<div
|
||||
aria-label="Room status bar"
|
||||
class="mx_RoomView_statusArea"
|
||||
role="region"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_statusAreaBox"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_statusAreaBox_line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RoomView should not display the timeline when the room encryption is loading 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_RoomView"
|
||||
>
|
||||
<canvas
|
||||
aria-hidden="true"
|
||||
height="768"
|
||||
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
|
||||
width="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_MainSplit"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_body mx_MainSplit_timeline"
|
||||
data-layout="group"
|
||||
>
|
||||
<header
|
||||
class="mx_Flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 40px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
!
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
class="mx_RoomHeader_infoWrapper"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Box mx_RoomHeader_info mx_Box--flex"
|
||||
style="--mx-box-flex: 1;"
|
||||
>
|
||||
<div
|
||||
aria-level="1"
|
||||
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!5:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||
>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":r2c:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
aria-labelledby=":r2h:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r2m:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r2r:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby=":r30:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_mcap2_111"
|
||||
/>
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_AuxPanel"
|
||||
role="region"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<main
|
||||
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
|
||||
>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
style="height: 400px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div
|
||||
aria-label="Room status bar"
|
||||
class="mx_RoomView_statusArea"
|
||||
role="region"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_statusAreaBox"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_statusAreaBox_line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Message composer"
|
||||
class="mx_MessageComposer mx_MessageComposer_e2eStatus"
|
||||
role="region"
|
||||
>
|
||||
<div
|
||||
class="mx_MessageComposer_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_MessageComposer_row"
|
||||
>
|
||||
<div
|
||||
class="mx_MessageComposer_e2eIconWrapper"
|
||||
>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
aria-labelledby=":r3e:"
|
||||
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SendMessageComposer"
|
||||
>
|
||||
<div
|
||||
class="mx_BasicMessageComposer"
|
||||
>
|
||||
<div
|
||||
aria-label="Formatting"
|
||||
class="mx_MessageComposerFormatBar"
|
||||
role="toolbar"
|
||||
>
|
||||
<button
|
||||
aria-label="Bold"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label="Italics"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label="Strikethrough"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label="Code block"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label="Quote"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label="Insert link"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-autocomplete="list"
|
||||
aria-disabled="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Send an encrypted message…"
|
||||
aria-multiline="true"
|
||||
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
|
||||
contenteditable="true"
|
||||
data-testid="basicmessagecomposer"
|
||||
dir="auto"
|
||||
role="textbox"
|
||||
style="--placeholder: 'Send\\ an\\ encrypted\\ message…';"
|
||||
tabindex="0"
|
||||
translate="no"
|
||||
>
|
||||
<div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageComposer_actions"
|
||||
>
|
||||
<div
|
||||
aria-label="Emoji"
|
||||
class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Attachment"
|
||||
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="More options"
|
||||
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RoomView should show error view if failed to look up room alias 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
|
@ -1332,7 +1897,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
|
@ -1359,7 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!10:example.org
|
||||
!12:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1370,7 +1935,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r2k:"
|
||||
aria-labelledby=":r7c:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1395,7 +1960,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
</button>
|
||||
<button
|
||||
aria-label="Chat"
|
||||
aria-labelledby=":r2p:"
|
||||
aria-labelledby=":r7h:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1420,7 +1985,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r2u:"
|
||||
aria-labelledby=":r7m:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
@ -1449,7 +2014,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby=":r33:"
|
||||
aria-labelledby=":r7r:"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -1487,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby=":r3c:"
|
||||
aria-labelledby=":r84:"
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
|
|
|
@ -22,7 +22,6 @@ describe("LogoutDialog", () => {
|
|||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsCrypto(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
});
|
||||
|
||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
||||
|
@ -50,14 +49,14 @@ describe("LogoutDialog", () => {
|
|||
});
|
||||
|
||||
it("Prompts user to connect backup if there is a backup on the server", async () => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
mockCrypto.getKeyBackupInfo.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);
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Start using Key Backup");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
|
@ -69,7 +68,7 @@ describe("LogoutDialog", () => {
|
|||
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 () => {
|
||||
mockCrypto.getKeyBackupInfo.mockImplementation(async () => {
|
||||
throw new Error("beep");
|
||||
});
|
||||
const rendered = renderComponent();
|
||||
|
|
|
@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => {
|
|||
filterConsole("Error fetching backup data from server");
|
||||
|
||||
it("shows an error", async () => {
|
||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
||||
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
|
||||
throw new Error("bleh bleh");
|
||||
});
|
||||
|
||||
|
@ -92,7 +92,7 @@ describe("CreateSecretStorageDialog", () => {
|
|||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
// Now we can get the backup and we retry
|
||||
mockClient.getKeyBackupVersion.mockRestore();
|
||||
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockRestore();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||
await screen.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ describe("<RestoreKeyBackupDialog />", () => {
|
|||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
|
||||
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ version: "1" } as KeyBackupInfo);
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1" } as KeyBackupInfo);
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
|
@ -99,7 +99,7 @@ describe("<RestoreKeyBackupDialog />", () => {
|
|||
|
||||
test("should restore key backup when passphrase is filled", async () => {
|
||||
// Determine that the passphrase is required
|
||||
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
auth_data: {
|
||||
private_key_salt: "salt",
|
||||
|
|
|
@ -304,6 +304,8 @@ describe("EventTile", () => {
|
|||
[EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"],
|
||||
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
|
||||
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
|
||||
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
|
||||
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity has changed"],
|
||||
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
|
||||
mxEvent = await mkEncryptedMatrixEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
|
|
|
@ -77,6 +77,7 @@ describe("<SendMessageComposer/>", () => {
|
|||
canAskToJoin: false,
|
||||
promptAskToJoin: false,
|
||||
viewRoomOpts: { buttons: [] },
|
||||
isRoomEncrypted: false,
|
||||
};
|
||||
describe("createMessageContent", () => {
|
||||
it("sends plaintext messages correctly", () => {
|
||||
|
|
|
@ -0,0 +1,534 @@
|
|||
/*
|
||||
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 { sleep } from "matrix-js-sdk/src/utils";
|
||||
import {
|
||||
EventType,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomMember,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
import { act, render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { UserIdentityWarning } from "../../../../../src/components/views/rooms/UserIdentityWarning";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
function mockRoom(): Room {
|
||||
const room = {
|
||||
getEncryptionTargetMembers: jest.fn(async () => []),
|
||||
getMember: jest.fn((userId) => {}),
|
||||
roomId: ROOM_ID,
|
||||
shouldEncryptForInvitedMembers: jest.fn(() => true),
|
||||
} as unknown as Room;
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
function mockRoomMember(userId: string, name?: string): RoomMember {
|
||||
return {
|
||||
userId,
|
||||
name: name ?? userId,
|
||||
rawDisplayName: name ?? userId,
|
||||
roomId: ROOM_ID,
|
||||
getMxcAvatarUrl: jest.fn(),
|
||||
} as unknown as RoomMember;
|
||||
}
|
||||
|
||||
function dummyRoomState(): RoomState {
|
||||
return new RoomState(ROOM_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the warning element, given the warning text (excluding the "Learn more"
|
||||
* link). This is needed because the warning text contains a `<b>` tag, so the
|
||||
* normal `getByText` doesn't work.
|
||||
*/
|
||||
function getWarningByText(text: string): Element {
|
||||
return screen.getByText((content?: string, element?: Element | null): boolean => {
|
||||
return (
|
||||
!!element &&
|
||||
element.classList.contains("mx_UserIdentityWarning_main") &&
|
||||
element.textContent === text + " Learn more"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderComponent(client: MatrixClient, room: Room) {
|
||||
return render(<UserIdentityWarning room={room} key={ROOM_ID} />, {
|
||||
wrapper: ({ ...rest }) => <MatrixClientContext.Provider value={client} {...rest} />,
|
||||
});
|
||||
}
|
||||
|
||||
describe("UserIdentityWarning", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = stubClient();
|
||||
room = mockRoom();
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// This tests the basic functionality of the component. If we have a room
|
||||
// member whose identity needs accepting, we should display a warning. When
|
||||
// the "OK" button gets pressed, it should call `pinCurrentUserIdentity`.
|
||||
it("displays a warning when a user's identity needs approval", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
]);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
crypto.pinCurrentUserIdentity = jest.fn();
|
||||
renderComponent(client, room);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button")!);
|
||||
await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org"));
|
||||
});
|
||||
|
||||
// We don't display warnings in non-encrypted rooms, but if encryption is
|
||||
// enabled, then we should display a warning if there are any users whose
|
||||
// identity need accepting.
|
||||
it("displays pending warnings when encryption is enabled", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
]);
|
||||
// Start the room off unencrypted. We shouldn't display anything.
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
||||
|
||||
// Encryption gets enabled in the room. We should now warn that Alice's
|
||||
// identity changed.
|
||||
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomEncryption,
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
// When a user's identity needs approval, or has been approved, the display
|
||||
// should update appropriately.
|
||||
it("updates the display when identity changes", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
]);
|
||||
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, false),
|
||||
);
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
||||
|
||||
// The user changes their identity, so we should show the warning.
|
||||
act(() => {
|
||||
client.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
"@alice:example.org",
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
// Simulate the user's new identity having been approved, so we no
|
||||
// longer show the warning.
|
||||
act(() => {
|
||||
client.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
"@alice:example.org",
|
||||
new UserVerificationStatus(false, false, false, false),
|
||||
);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(),
|
||||
);
|
||||
});
|
||||
|
||||
// We only display warnings about users in the room. When someone
|
||||
// joins/leaves, we should update the warning appropriately.
|
||||
describe("updates the display when a member joins/leaves", () => {
|
||||
it("when invited users can see encrypted messages", async () => {
|
||||
// Nobody in the room yet
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(true);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
|
||||
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "join",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
// Bob is invited. His identity needs approval, so we should show a
|
||||
// warning for him after Alice's warning is resolved by her leaving.
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@bob:example.org",
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@carol:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
|
||||
// Alice leaves, so we no longer show her warning, but we will show
|
||||
// a warning for Bob.
|
||||
act(() => {
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("when invited users cannot see encrypted messages", async () => {
|
||||
// Nobody in the room yet
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
|
||||
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "join",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
// Bob is invited. His identity needs approval, but we don't encrypt
|
||||
// to him, so we won't show a warning. (When Alice leaves, the
|
||||
// display won't be updated to show a warningfor Bob.)
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@bob:example.org",
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@carol:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
|
||||
// Alice leaves, so we no longer show her warning, and we don't show
|
||||
// a warning for Bob.
|
||||
act(() => {
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(() => getWarningByText("@bob:example.org's identity appears to have changed.")).toThrow(),
|
||||
);
|
||||
});
|
||||
|
||||
it("when member leaves immediately after component is loaded", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockImplementation(async () => {
|
||||
setTimeout(() => {
|
||||
// Alice immediately leaves after we get the room
|
||||
// membership, so we shouldn't show the warning any more
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
});
|
||||
return [mockRoomMember("@alice:example.org")];
|
||||
});
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
renderComponent(client, room);
|
||||
|
||||
await sleep(10);
|
||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
||||
});
|
||||
|
||||
it("when member leaves immediately after joining", async () => {
|
||||
// Nobody in the room yet
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
|
||||
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "join",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
// ... but she immediately leaves, so we shouldn't show the warning any more
|
||||
client.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: EventType.RoomMember,
|
||||
state_key: "@alice:example.org",
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
sender: "@alice:example.org",
|
||||
}),
|
||||
dummyRoomState(),
|
||||
null,
|
||||
);
|
||||
await sleep(10); // give it some time to finish
|
||||
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// When we have multiple users whose identity needs approval, one user's
|
||||
// identity no longer needs approval (e.g. their identity was approved),
|
||||
// then we show the next one.
|
||||
it("displays the next user when the current user's identity is approved", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
mockRoomMember("@bob:example.org"),
|
||||
]);
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
|
||||
renderComponent(client, room);
|
||||
// We should warn about Alice's identity first.
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
// Simulate Alice's new identity having been approved, so now we warn
|
||||
// about Bob's identity.
|
||||
act(() => {
|
||||
client.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
"@alice:example.org",
|
||||
new UserVerificationStatus(false, false, false, false),
|
||||
);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
// If we get an update for a user's verification status while we're fetching
|
||||
// that user's verification status, we should display based on the updated
|
||||
// value.
|
||||
describe("handles races between fetching verification status and receiving updates", () => {
|
||||
// First case: check that if the update says that the user identity
|
||||
// needs approval, but the fetch says it doesn't, we show the warning.
|
||||
it("update says identity needs approval", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
]);
|
||||
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
|
||||
act(() => {
|
||||
client.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
"@alice:example.org",
|
||||
new UserVerificationStatus(false, false, false, true),
|
||||
);
|
||||
});
|
||||
return Promise.resolve(new UserVerificationStatus(false, false, false, false));
|
||||
});
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
// Second case: check that if the update says that the user identity
|
||||
// doesn't needs approval, but the fetch says it does, we don't show the
|
||||
// warning.
|
||||
it("update says identity doesn't need approval", async () => {
|
||||
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||
mockRoomMember("@alice:example.org", "Alice"),
|
||||
]);
|
||||
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||
const crypto = client.getCrypto()!;
|
||||
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
|
||||
act(() => {
|
||||
client.emit(
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
"@alice:example.org",
|
||||
new UserVerificationStatus(false, false, false, false),
|
||||
);
|
||||
});
|
||||
return Promise.resolve(new UserVerificationStatus(false, false, false, true));
|
||||
});
|
||||
renderComponent(client, room);
|
||||
await sleep(10); // give it some time to finish initialising
|
||||
await waitFor(() =>
|
||||
expect(() =>
|
||||
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||
).toThrow(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => {
|
|||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
});
|
||||
|
||||
const getComponent = () => render(<SecureBackupPanel />);
|
||||
|
||||
beforeEach(() => {
|
||||
client.getKeyBackupVersion.mockResolvedValue({
|
||||
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
algorithm: "test",
|
||||
auth_data: {
|
||||
|
@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => {
|
|||
});
|
||||
|
||||
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
|
||||
client.getKeyBackupVersion.mockClear();
|
||||
|
||||
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||
});
|
||||
|
@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => {
|
|||
});
|
||||
|
||||
it("handles error fetching backup", async () => {
|
||||
// getKeyBackupVersion can fail for various reasons
|
||||
client.getKeyBackupVersion.mockImplementation(async () => {
|
||||
// getKeyBackupInfo can fail for various reasons
|
||||
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
|
||||
throw new Error("beep beep");
|
||||
});
|
||||
const renderResult = getComponent();
|
||||
|
@ -75,9 +73,9 @@ describe("<SecureBackupPanel />", () => {
|
|||
});
|
||||
|
||||
it("handles absence of backup", async () => {
|
||||
client.getKeyBackupVersion.mockResolvedValue(null);
|
||||
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
|
||||
getComponent();
|
||||
// flush getKeyBackupVersion promise
|
||||
// flush getKeyBackupInfo promise
|
||||
await flushPromises();
|
||||
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
|
||||
});
|
||||
|
@ -120,7 +118,7 @@ describe("<SecureBackupPanel />", () => {
|
|||
});
|
||||
|
||||
it("deletes backup after confirmation", async () => {
|
||||
client.getKeyBackupVersion
|
||||
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
|
||||
.mockResolvedValueOnce({
|
||||
version: "1",
|
||||
algorithm: "test",
|
||||
|
@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => {
|
|||
// flush checkKeyBackup promise
|
||||
await flushPromises();
|
||||
|
||||
client.getKeyBackupVersion.mockClear();
|
||||
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear();
|
||||
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByText("Reset"));
|
||||
|
@ -167,7 +165,7 @@ describe("<SecureBackupPanel />", () => {
|
|||
await flushPromises();
|
||||
|
||||
// backup status refreshed
|
||||
expect(client.getKeyBackupVersion).toHaveBeenCalled();
|
||||
expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
|
||||
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ describe("<SecurityRoomSettingsTab />", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
|
||||
client.isRoomEncrypted.mockReturnValue(false);
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
client.getClientWellKnown.mockReturnValue(undefined);
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
|
||||
|
@ -313,7 +313,7 @@ describe("<SecurityRoomSettingsTab />", () => {
|
|||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Encrypted"));
|
||||
|
||||
|
@ -330,7 +330,7 @@ describe("<SecurityRoomSettingsTab />", () => {
|
|||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Encrypted"));
|
||||
|
||||
|
@ -416,12 +416,12 @@ describe("<SecurityRoomSettingsTab />", () => {
|
|||
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays unencrypted rooms with toggle disabled", () => {
|
||||
it("displays unencrypted rooms with toggle disabled", async () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
setRoomStateEvents(room);
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
|
||||
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
|
||||
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
|
||||
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();
|
||||
|
|
|
@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => {
|
|||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
});
|
||||
|
||||
const sdkContext = new SdkContextClass();
|
||||
|
|
|
@ -332,7 +332,7 @@ describe("linkify-matrix", () => {
|
|||
|
||||
const event = new MouseEvent("mousedown");
|
||||
event.preventDefault = jest.fn();
|
||||
handlers.click(event);
|
||||
handlers!.click(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -372,7 +372,7 @@ describe("linkify-matrix", () => {
|
|||
|
||||
const event = new MouseEvent("mousedown");
|
||||
event.preventDefault = jest.fn();
|
||||
handlers.click(event);
|
||||
handlers!.click(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => {
|
|||
getDeviceVerificationStatus: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||
startDehydration: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
client.getCrypto.mockReturnValue(mockCrypto);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue