Merge branch 'develop' into midhun/new-memberlist

This commit is contained in:
R Midhun Suresh
2024-12-16 21:32:46 +05:30
876 changed files with 10347 additions and 21754 deletions

View File

@@ -15,10 +15,11 @@ import RecordingPlayback, {
PlaybackLayout,
} from "../../../../../src/components/views/audio_messages/RecordingPlayback";
import { Playback } from "../../../../../src/audio/Playback";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { createAudioContext } from "../../../../../src/audio/compat";
import { flushPromises } from "../../../../test-utils";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/WorkerManager", () => ({
WorkerManager: jest.fn(() => ({
@@ -56,9 +57,9 @@ describe("<RecordingPlayback />", () => {
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
render(
<RoomContext.Provider value={room}>
<ScopedRoomContextProvider {...room}>
<RecordingPlayback {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
beforeEach(() => {

View File

@@ -14,11 +14,11 @@ exports[`<AuthFooter /> should match snapshot 1`] = `
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"

View File

@@ -30,11 +30,11 @@ exports[`<AuthPage /> should match snapshot 1`] = `
Blog
</a>
<a
href="https://twitter.com/element_hq"
href="https://mastodon.matrix.org/@Element"
rel="noreferrer noopener"
target="_blank"
>
Twitter
Mastodon
</a>
<a
href="https://github.com/element-hq/element-web"

View File

@@ -12,11 +12,11 @@ import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js-
import React, { ComponentProps } from "react";
import MemberAvatar from "../../../../../src/components/views/avatars/MemberAvatar";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { getRoomContext } from "../../../../test-utils/room";
import { stubClient } from "../../../../test-utils/test-utils";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("MemberAvatar", () => {
const ROOM_ID = "roomId";
@@ -27,9 +27,9 @@ describe("MemberAvatar", () => {
function getComponent(props: Partial<ComponentProps<typeof MemberAvatar>>) {
return (
<RoomContext.Provider value={getRoomContext(room, {})}>
<ScopedRoomContextProvider {...getRoomContext(room, {})}>
<MemberAvatar member={null} size="35px" {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
);
}

View File

@@ -8,9 +8,18 @@ exports[`<BeaconViewDialog /> renders a fallback when there are no locations 1`]
<div
class="mx_MapFallback_bg"
/>
<div
<svg
class="mx_MapFallback_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
<span
class="mx_BeaconViewDialog_mapFallbackMessage"
>

View File

@@ -27,7 +27,7 @@ import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { canEditContent } from "../../../../../src/utils/EventUtils";
import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings";
@@ -37,9 +37,8 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { ReadPinsEventId } from "../../../../../src/components/views/right_panel/types";
import { Action } from "../../../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createMessageEventContent } from "../../../../test-utils/events";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/strings", () => ({
copyPlaintext: jest.fn(),
@@ -233,17 +232,6 @@ describe("MessageContextMenu", () => {
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
it("should not allow forwarding a voice broadcast", () => {
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
createMenu(broadcastStartEvent);
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
describe("forwarding beacons", () => {
const aliceId = "@alice:server.org";
@@ -546,8 +534,8 @@ function createMenu(
client.getRoom = jest.fn().mockReturnValue(room);
return render(
<RoomContext.Provider value={context as IRoomState}>
<ScopedRoomContextProvider {...(context as IRoomState)}>
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
}

View File

@@ -127,7 +127,7 @@ describe("RoomGeneralContextMenu", () => {
user: "@user:id",
ts: 1000,
});
room.addLiveEvents([event], {});
room.addLiveEvents([event], { addToState: true });
const { container } = getComponent({});
@@ -150,7 +150,7 @@ describe("RoomGeneralContextMenu", () => {
await sleep(0);
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", {
unread: true,
});
expect(onFinished).toHaveBeenCalled();

View File

@@ -122,4 +122,34 @@ describe("AccessSecretStorageDialog", () => {
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
});
it("Can reset secret storage", async () => {
jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true);
const onFinished = jest.fn();
const checkPrivateKey = jest.fn().mockResolvedValue(true);
renderComponent({ onFinished, checkPrivateKey });
await userEvent.click(screen.getByText("Reset all"), { delay: null });
// It will prompt the user to confirm resetting
expect(screen.getByText("Reset everything")).toBeInTheDocument();
await userEvent.click(screen.getByText("Reset"), { delay: null });
// Then it will prompt the user to create a key/passphrase
await screen.findByText("Set up Secure Backup");
document.execCommand = jest.fn().mockReturnValue(true);
jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({
privateKey: new Uint8Array(),
encodedPrivateKey: securityKey,
});
screen.getByRole("button", { name: "Continue" }).click();
await screen.findByText(/Save your Security Key/);
screen.getByRole("button", { name: "Copy" }).click();
await screen.findByText("Copied!");
screen.getByRole("button", { name: "Continue" }).click();
await screen.findByText("Secure Backup successful");
});
});

View File

@@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { flushPromises, mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createRedactEventDialog } from "../../../../../src/components/views/dialogs/ConfirmRedactDialog";
describe("ConfirmRedactDialog", () => {
@@ -21,15 +18,6 @@ describe("ConfirmRedactDialog", () => {
let client: MatrixClient;
let mxEvent: MatrixEvent;
const setUpVoiceBroadcastStartedEvent = () => {
mxEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
};
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
act(() => createRedactEventDialog({ mxEvent }));
// double-flush promises required for the dialog to show up
@@ -68,44 +56,4 @@ describe("ConfirmRedactDialog", () => {
`cannot redact event ${mxEvent.getId()} without room ID`,
);
});
describe("when redacting a voice broadcast started event", () => {
beforeEach(() => {
setUpVoiceBroadcastStartedEvent();
});
describe("and the server does not support relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact without `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
});
});
});
describe("and the server supports relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact with `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
with_rel_types: [RelationType.Reference],
});
});
});
});
});
});

View File

@@ -67,7 +67,6 @@ describe("ForwardDialog", () => {
getAccountData: jest.fn().mockReturnValue(accountDataEvent),
getPushActionsForEvent: jest.fn(),
mxcUrlToHttp: jest.fn().mockReturnValue(""),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getProfileInfo: jest.fn().mockResolvedValue({
displayname: "Alice",
}),

View File

@@ -141,16 +141,19 @@ describe("InviteDialog", () => {
jest.clearAllMocks();
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
room.addLiveEvents([
mkMessage({
msg: "Hello",
relatesTo: undefined,
event: true,
room: roomId,
user: mockClient.getSafeUserId(),
ts: Date.now(),
}),
]);
room.addLiveEvents(
[
mkMessage({
msg: "Hello",
relatesTo: undefined,
event: true,
room: roomId,
user: mockClient.getSafeUserId(),
ts: Date.now(),
}),
],
{ addToState: true },
);
room.currentState.setStateEvents([
mkRoomCreateEvent(bobId, roomId),
mkMembership({

View File

@@ -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();

View File

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

View File

@@ -7,111 +7,139 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, RenderOptions } from "jest-matrix-react";
import { MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { waitFor } from "@testing-library/dom";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { _t } from "../../../../../src/languageHandler";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import { stubClient } from "../../../../test-utils";
jest.mock("../../../../../src/utils/ShieldUtils");
function getWrapper(): RenderOptions {
return {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider>
),
};
}
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import * as StringsModule from "../../../../../src/utils/strings";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks.ts";
describe("ShareDialog", () => {
let client: MatrixClient;
let room: Room;
const ROOM_ID = "!1:example.org";
const copyTextFunc = jest.fn();
beforeEach(async () => {
stubClient();
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org");
client = stubClient();
room = new Room("!1:example.org", client, "@alice:example.org");
jest.spyOn(StringsModule, "copyPlaintext").mockImplementation(copyTextFunc);
});
afterEach(() => {
jest.restoreAllMocks();
copyTextFunc.mockClear();
});
it("renders room share dialog", () => {
const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withoutEvents).toHaveTextContent(_t("share|title_room"));
function renderComponent(target: Room | RoomMember | URL) {
return render(<ShareDialog target={target} onFinished={jest.fn()} />, withClientContextRenderOptions(client));
}
jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline);
const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent"));
const getUrl = () => new URL("https://matrix.org/");
const getRoomMember = () => new RoomMember(room.roomId, "@alice:example.org");
test.each([
{ name: "an URL", title: "Share Link", url: "https://matrix.org/", getTarget: getUrl },
{
name: "a room member",
title: "Share User",
url: "https://matrix.to/#/@alice:example.org",
getTarget: getRoomMember,
},
])("should render a share dialog for $name", async ({ title, url, getTarget }) => {
const { asFragment } = renderComponent(getTarget());
expect(screen.getByRole("heading", { name: title })).toBeInTheDocument();
expect(screen.getByText(url)).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(url);
});
it("renders user share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />,
getWrapper(),
it("should render a share dialog for a room", async () => {
const expectedURL = "https://matrix.to/#/!1:example.org";
jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([new MatrixEvent({ event_id: "!eventId" })]);
const { asFragment } = renderComponent(room);
expect(screen.getByRole("heading", { name: "Share Room" })).toBeInTheDocument();
expect(screen.getByText(expectedURL)).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Link to most recent message" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(expectedURL);
// Click on the checkbox to link to the most recent message
await userEvent.click(screen.getByRole("checkbox", { name: "Link to most recent message" }));
const newExpectedURL = "https://matrix.to/#/!1:example.org/!eventId";
expect(screen.getByText(newExpectedURL)).toBeInTheDocument();
});
it("should render a share dialog for a matrix event", async () => {
const matrixEvent = new MatrixEvent({ event_id: "!eventId" });
const permalinkCreator = new RoomPermalinkCreator(room);
const expectedURL = "https://matrix.to/#/!1:example.org/!eventId";
const { asFragment } = render(
<ShareDialog target={matrixEvent} permalinkCreator={permalinkCreator} onFinished={jest.fn()} />,
withClientContextRenderOptions(client),
);
expect(container).toHaveTextContent(_t("share|title_user"));
expect(screen.getByRole("heading", { name: "Share Room Message" })).toBeInTheDocument();
expect(screen.getByText(expectedURL)).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(expectedURL);
// Click on the checkbox to link to the room
await userEvent.click(screen.getByRole("checkbox", { name: "Link to selected message" }));
expect(screen.getByText("https://matrix.to/#/!1:example.org")).toBeInTheDocument();
});
it("renders link share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />,
getWrapper(),
);
expect(container).toHaveTextContent(_t("share|title_link"));
it("should change the copy button text when clicked", async () => {
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
// To not be bother with rtl warnings about QR code state update
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
renderComponent(room);
await user.click(screen.getByRole("button", { name: "Copy link" }));
// Move after `copyPlaintext`
await jest.advanceTimersToNextTimerAsync();
expect(screen.getByRole("button", { name: "Link copied" })).toBeInTheDocument();
// 2 sec after the button should be back to normal
act(() => jest.advanceTimersByTime(2000));
await waitFor(() => expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument());
});
it("renders the QR code if configured", () => {
it("should not render the QR code if disabled", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareQRCode) return true;
if (feature === UIFeature.ShareQRCode) return false;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0;
expect(qrCodesVisible).toBe(true);
const { asFragment } = renderComponent(room);
expect(screen.queryByRole("img", { name: "QR code" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
it("renders the social button if configured", () => {
it("should not render the socials if disabled", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareSocial) return true;
if (feature === UIFeature.ShareSocial) return false;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0;
expect(qrCodesVisible).toBe(true);
});
it("renders custom title and subtitle", () => {
const { container } = render(
<ShareDialog
target={room}
customTitle="test_title_123"
subtitle="custom_subtitle_1234"
onFinished={jest.fn()}
/>,
getWrapper(),
);
expect(container).toHaveTextContent("test_title_123");
expect(container).toHaveTextContent("custom_subtitle_1234");
const { asFragment } = renderComponent(room);
expect(screen.queryByRole("link", { name: "Reddit" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
});
/**
*
* @param count the number of users to create
*/
function mockRoomMembers(room: Room, count: number) {
const members = Array(count)
.fill(0)
.map((_, index) => new RoomMember(room.roomId, "@alice:example.org"));
room.currentState.setJoinedMemberCount(members.length);
room.getJoinedMembers = jest.fn().mockReturnValue(members);
}

View File

@@ -239,7 +239,7 @@ describe("Spotlight Dialog", () => {
});
it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
render(<SpotlightDialog onFinished={() => null} />, { legacyRoot: false });
render(<SpotlightDialog onFinished={() => null} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true);

View File

@@ -0,0 +1,59 @@
/*
* 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 { Device, MatrixClient, User } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { stubClient } from "../../../../test-utils";
import UntrustedDeviceDialog from "../../../../../src/components/views/dialogs/UntrustedDeviceDialog.tsx";
describe("<UntrustedDeviceDialog />", () => {
let client: MatrixClient;
let user: User;
let device: Device;
const onFinished = jest.fn();
beforeEach(() => {
client = stubClient();
user = User.createUser("@alice:example.org", client);
user.setDisplayName("Alice");
device = new Device({ deviceId: "device_id", userId: user.userId, algorithms: [], keys: new Map() });
});
afterEach(() => {
onFinished.mockReset();
});
function renderComponent() {
return render(<UntrustedDeviceDialog user={user} device={device} onFinished={onFinished} />);
}
it("should display the dialog for the device of another user", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should display the dialog for the device of the current user", () => {
jest.spyOn(client, "getUserId").mockReturnValue(user.userId);
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should call onFinished without parameter when Done is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Done" }).click();
expect(onFinished).toHaveBeenCalledWith();
});
it("should call onFinished with sas when Interactively verify by emoji is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Interactively verify by emoji" }).click();
expect(onFinished).toHaveBeenCalledWith("sas");
});
});

View File

@@ -185,33 +185,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
<span
class="mx_SettingsFlag_labelText"
>
Force 15s voice broadcast chunk length
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Force 15s voice broadcast chunk length"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div

View File

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

View File

@@ -0,0 +1,852 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShareDialog should not render the QR code if disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should not render the socials if disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a matrix event 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room Message
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org/!eventId
</span>
</div>
<label>
<div
class="_container_1wloq_18"
>
<input
checked=""
class="_input_1wloq_26"
type="checkbox"
/>
<div
class="_ui_1wloq_27"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
Link to selected message
</label>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a room 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<label>
<div
class="_container_1wloq_18"
>
<input
class="_input_1wloq_26"
type="checkbox"
/>
<div
class="_ui_1wloq_27"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
Link to most recent message
</label>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a room member 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share User
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/@alice:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for an URL 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Link
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.org/
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -0,0 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of another user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon mx_E2EIcon_warning"
style="width: 24px; height: 24px;"
/>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
Alice (@alice:example.org) signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Ask this user to verify their session, or manually verify it below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of the current user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon mx_E2EIcon_warning"
style="width: 24px; height: 24px;"
/>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
You signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Verify your other session using one of the options below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -13,7 +13,7 @@ import { mocked, MockedObject } from "jest-mock";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import { filterConsole, stubClient } from "../../../../../test-utils";
import { filterConsole, flushPromises, stubClient } from "../../../../../test-utils";
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
describe("CreateSecretStorageDialog", () => {
@@ -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,9 +92,44 @@ 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.");
});
});
it("resets keys in the right order when resetting secret storage and cross-signing", async () => {
const result = renderComponent({ forceReset: true, resetCrossSigning: true });
await result.findByText(/Set up Secure Backup/);
jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({
privateKey: new Uint8Array(),
encodedPrivateKey: "abcd efgh ijkl",
});
result.getByRole("button", { name: "Continue" }).click();
await result.findByText(/Save your Security Key/);
result.getByRole("button", { name: "Copy" }).click();
// Resetting should reset secret storage, cross signing, and key
// backup. We make sure that all three are reset, and done in the
// right order.
const resetFunctionCallLog: string[] = [];
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => {
resetFunctionCallLog.push("bootstrapSecretStorage");
});
jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => {
resetFunctionCallLog.push("bootstrapCrossSigning");
});
jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => {
resetFunctionCallLog.push("resetKeyBackup");
});
await flushPromises();
result.getByRole("button", { name: "Continue" }).click();
await result.findByText("Your keys are now being backed up from this device.");
expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]);
});
});

View File

@@ -7,9 +7,10 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { screen, fireEvent, render, waitFor } from "jest-matrix-react";
import { screen, fireEvent, render, waitFor, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
import { IMegolmSessionData } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
import ExportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ExportE2eKeysDialog";
@@ -23,12 +24,12 @@ describe("ExportE2eKeysDialog", () => {
expect(asFragment()).toMatchSnapshot();
});
it("should have disabled submit button initially", () => {
it("should have disabled submit button initially", async () => {
const cli = createTestClient();
const onFinished = jest.fn();
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
fireEvent.click(container.querySelector("[type=submit]")!);
expect(screen.getByText("Enter passphrase")).toBeInTheDocument();
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
expect(screen.getByLabelText("Enter passphrase")).toBeInTheDocument();
});
it("should complain about weak passphrases", async () => {
@@ -38,7 +39,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
const input = screen.getByLabelText("Enter passphrase");
await userEvent.type(input, "password");
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
await expect(screen.findByText("This is a top-10 common password")).resolves.toBeInTheDocument();
});
@@ -49,7 +50,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$");
await userEvent.type(screen.getByLabelText("Confirm passphrase"), "ThisIsAMoreSecurePW124$$");
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
await expect(screen.findByText("Passphrases must match")).resolves.toBeInTheDocument();
});
@@ -62,7 +63,7 @@ describe("ExportE2eKeysDialog", () => {
cli.getCrypto = () => {
return {
exportRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
} as unknown as CryptoApi;
};
// Mock the result of encrypting the sessions. If we don't do this, the
@@ -74,7 +75,7 @@ describe("ExportE2eKeysDialog", () => {
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />);
await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase);
await userEvent.type(screen.getByLabelText("Confirm passphrase"), passphrase);
fireEvent.click(container.querySelector("[type=submit]")!);
await act(() => fireEvent.click(container.querySelector("[type=submit]")!));
// Then it exports keys and encrypts them
await waitFor(() => expect(exportRoomKeysAsJson).toHaveBeenCalled());

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { fireEvent, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import ImportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ImportE2eKeysDialog";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
@@ -67,7 +67,7 @@ describe("ImportE2eKeysDialog", () => {
cli.getCrypto = () => {
return {
importRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
} as unknown as CryptoApi;
};
// Mock the result of decrypting the sessions, to avoid needing to

View File

@@ -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",

View File

@@ -10,7 +10,7 @@ import React from "react";
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { act, render, RenderResult } from "jest-matrix-react";
import { act, render, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import {
ApprovalOpts,
@@ -29,7 +29,6 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
import AppTile from "../../../../../src/components/views/elements/AppTile";
@@ -59,16 +58,6 @@ describe("AppTile", () => {
let app1: IApp;
let app2: IApp;
const waitForRps = (roomId: string) =>
new Promise<void>((resolve) => {
const update = () => {
if (RightPanelStore.instance.currentCardForRoom(roomId).phase !== RightPanelPhases.Widget) return;
RightPanelStore.instance.off(UPDATE_EVENT, update);
resolve();
};
RightPanelStore.instance.on(UPDATE_EVENT, update);
});
beforeAll(async () => {
stubClient();
cli = MatrixClientPeg.safeGet();
@@ -160,29 +149,28 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated;
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
await expect(renderResult.findByText("Example 1")).resolves.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { container, asFragment } = renderResult;
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot();
// We want to verify that as we change to room 2, we should close the
// right panel and destroy the widget.
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
}),
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
@@ -233,16 +221,17 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated1 = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated1;
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
});
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === "RightPanel.phases") {
@@ -263,13 +252,13 @@ describe("AppTile", () => {
}
return realGetValue(name, roomId);
});
// Wait for RPS room 2 updates to fire
const rpsUpdated2 = waitForRps("r2");
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
}),
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
@@ -279,10 +268,11 @@ describe("AppTile", () => {
/>
</MatrixClientContext.Provider>,
);
await rpsUpdated2;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
await waitFor(() => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
});
});
it("preserves non-persisted widget on container move", async () => {
@@ -345,7 +335,7 @@ describe("AppTile", () => {
let renderResult: RenderResult;
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(() => {
beforeEach(async () => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
@@ -353,12 +343,12 @@ describe("AppTile", () => {
);
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { container, asFragment } = renderResult;
const { asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy(); // Assert that the spinner is gone
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
});
@@ -459,18 +449,19 @@ describe("AppTile", () => {
describe("for a persistent app", () => {
let renderResult: RenderResult;
beforeEach(() => {
beforeEach(async () => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
});
it("should render", () => {
const { container, asFragment } = renderResult;
it("should render", async () => {
const { asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,40 @@
/*
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 { render } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import MiniAvatarUploader from "../../../../../src/components/views/elements/MiniAvatarUploader.tsx";
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
type: "image/gif",
});
describe("<MiniAvatarUploader />", () => {
it("calls setAvatarUrl when a file is uploaded", async () => {
const cli = stubClient();
mocked(cli.uploadContent).mockResolvedValue({ content_uri: "mxc://example.com/1234" });
const setAvatarUrl = jest.fn();
const user = userEvent.setup();
const { container, findByText } = render(
<MiniAvatarUploader hasAvatar={false} noAvatarLabel="Upload" setAvatarUrl={setAvatarUrl} isUserAvatar />,
withClientContextRenderOptions(cli),
);
await findByText("Upload");
await user.upload(container.querySelector("input")!, AVATAR_FILE);
expect(cli.uploadContent).toHaveBeenCalledWith(AVATAR_FILE);
expect(setAvatarUrl).toHaveBeenCalledWith("mxc://example.com/1234");
});
});

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, RenderResult, screen } from "jest-matrix-react";
import { render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked, Mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
@@ -86,7 +86,7 @@ describe("<Pill>", () => {
room: room1Id,
msg: "Room 1 Message",
});
room1.addLiveEvents([room1Message]);
room1.addLiveEvents([room1Message], { addToState: true });
room2 = new Room(room2Id, client, user1Id);
room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]);
@@ -214,9 +214,7 @@ describe("<Pill>", () => {
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(renderResult.asFragment()).toMatchSnapshot();
});
@@ -228,9 +226,7 @@ describe("<Pill>", () => {
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(renderResult.asFragment()).toMatchSnapshot();
});

View File

@@ -41,7 +41,7 @@ describe("<RoomTopic/>", () => {
ts: 123,
event: true,
});
room.addLiveEvents([topicEvent]);
room.addLiveEvents([topicEvent], { addToState: true });
return room;
}

View File

@@ -60,29 +60,9 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] =
id="1"
>
<div
class="mx_AppTileBody mx_AppTileBody--large"
class="mx_AppTile_persistedWrapper"
>
<div
class="mx_AppTileBody_fadeInSpinner"
>
<div
class="mx_Spinner"
>
<div
class="mx_Spinner_Msg"
>
Loading…
</div>
 
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div />
</div>
</div>
</div>

View File

@@ -17,7 +17,9 @@ describe("<VerificationQRCode />", () => {
});
it("renders a QR code", async () => {
const { container, getAllByAltText } = render(<VerificationQRCode qrCodeBytes={Buffer.from("asd")} />);
const { container, getAllByAltText } = render(
<VerificationQRCode qrCodeBytes={new Uint8ClampedArray(Buffer.from("asd"))} />,
);
// wait for the spinner to go away
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { createRef } from "react";
import { render, waitFor } from "jest-matrix-react";
import { render, waitFor, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker";
@@ -27,12 +27,12 @@ describe("EmojiPicker", function () {
// Apply a filter and assert that the HTML has changed
//@ts-ignore private access
ref.current!.onChangeFilter("test");
act(() => ref.current!.onChangeFilter("test"));
expect(beforeHtml).not.toEqual(container.innerHTML);
// Clear the filter and assert that the HTML matches what it was before filtering
//@ts-ignore private access
ref.current!.onChangeFilter("");
act(() => ref.current!.onChangeFilter(""));
await waitFor(() => expect(beforeHtml).toEqual(container.innerHTML));
});
@@ -40,7 +40,7 @@ describe("EmojiPicker", function () {
const ep = new EmojiPicker({ onChoose: (str: string) => false, onFinished: jest.fn() });
//@ts-ignore private access
ep.onChangeFilter("heart");
act(() => ep.onChangeFilter("heart"));
//@ts-ignore private access
expect(ep.memoizedDataByCategory["people"][0].shortcodes[0]).toEqual("heart");

View File

@@ -139,7 +139,7 @@ describe("<LocationShareMenu />", () => {
const [, onGeolocateCallback] = mocked(mockGeolocate.on).mock.calls.find(([event]) => event === "geolocate")!;
// set the location
onGeolocateCallback(position);
act(() => onGeolocateCallback(position));
};
const setLocationClick = () => {
@@ -151,7 +151,7 @@ describe("<LocationShareMenu />", () => {
lngLat: { lng: position.coords.longitude, lat: position.coords.latitude },
} as unknown as maplibregl.MapMouseEvent;
// set the location
onMapClickCallback(event);
act(() => onMapClickCallback(event));
};
const shareTypeLabels: Record<LocationShareType, string> = {

View File

@@ -13,9 +13,18 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>

View File

@@ -9,9 +9,18 @@ exports[`<Marker /> renders with location icon when no room member 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</DocumentFragment>

View File

@@ -9,9 +9,18 @@ exports[`<SmartMarker /> creates a marker on mount 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>
@@ -27,9 +36,18 @@ exports[`<SmartMarker /> removes marker on unmount 1`] = `
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>

View File

@@ -264,10 +264,12 @@ describe("DateSeparator", () => {
fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition.
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
await expect(screen.findByTestId("jump-to-date-error-content")).resolves.toBeInTheDocument();
// Expect an option to submit debug logs to be shown when a non-network error occurs
expect(await screen.findByTestId("jump-to-date-error-submit-debug-logs-button")).toBeInTheDocument();
await expect(
screen.findByTestId("jump-to-date-error-submit-debug-logs-button"),
).resolves.toBeInTheDocument();
});
[
@@ -280,19 +282,20 @@ describe("DateSeparator", () => {
),
].forEach((fakeError) => {
it(`should show error dialog without submit debug logs option when networking error (${fakeError.name}) occurs`, async () => {
// Try to jump to "last week" but we want a network error to occur
mockClient.timestampToEvent.mockRejectedValue(fakeError);
// Render the component
getComponent();
// Open the jump to date context menu
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
// Try to jump to "last week" but we want a network error to occur
mockClient.timestampToEvent.mockRejectedValue(fakeError);
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
fireEvent.click(jumpToLastWeekButton);
// Expect error to be shown. We have to wait for the UI to transition.
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
await expect(screen.findByTestId("jump-to-date-error-content")).resolves.toBeInTheDocument();
// The submit debug logs option should *NOT* be shown for network errors.
//

View File

@@ -10,6 +10,7 @@ import React from "react";
import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import EncryptionEvent from "../../../../../src/components/views/messages/EncryptionEvent";
import { createTestClient, mkMessage } from "../../../../test-utils";
@@ -26,9 +27,9 @@ const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => {
);
};
const checkTexts = (title: string, subTitle: string) => {
screen.getByText(title);
screen.getByText(subTitle);
const checkTexts = async (title: string, subTitle: string) => {
await screen.findByText(title);
await screen.findByText(subTitle);
};
describe("EncryptionEvent", () => {
@@ -55,17 +56,19 @@ describe("EncryptionEvent", () => {
describe("for an encrypted room", () => {
beforeEach(() => {
event.event.content!.algorithm = algorithm;
mocked(client.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const room = new Room(roomId, client, client.getUserId()!);
mocked(client.getRoom).mockReturnValue(room);
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts(
"Encryption enabled",
"Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their profile picture.",
await waitFor(() =>
checkTexts(
"Encryption enabled",
"Messages in this room are end-to-end encrypted. " +
"When people join, you can verify them in their profile, just tap on their profile picture.",
),
);
});
@@ -76,9 +79,9 @@ describe("EncryptionEvent", () => {
});
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts("Encryption enabled", "Some encryption parameters have been changed.");
await waitFor(() => checkTexts("Encryption enabled", "Some encryption parameters have been changed."));
});
});
@@ -87,37 +90,39 @@ describe("EncryptionEvent", () => {
event.event.content!.algorithm = "unknown";
});
it("should show the expected texts", () => {
it("should show the expected texts", async () => {
renderEncryptionEvent(client, event);
checkTexts("Encryption enabled", "Ignored attempt to disable encryption");
await waitFor(() => checkTexts("Encryption enabled", "Ignored attempt to disable encryption"));
});
});
});
describe("for an unencrypted room", () => {
beforeEach(() => {
mocked(client.isRoomEncrypted).mockReturnValue(false);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
renderEncryptionEvent(client, event);
});
it("should show the expected texts", () => {
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported.");
it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
await waitFor(() =>
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."),
);
});
});
describe("for an encrypted local room", () => {
beforeEach(() => {
event.event.content!.algorithm = algorithm;
mocked(client.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
const localRoom = new LocalRoom(roomId, client, client.getUserId()!);
mocked(client.getRoom).mockReturnValue(localRoom);
renderEncryptionEvent(client, event);
});
it("should show the expected texts", () => {
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
it("should show the expected texts", async () => {
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
await checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
});
});
});

View File

@@ -20,7 +20,8 @@ import {
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("matrix-encrypt-attachment", () => ({
decryptAttachment: jest.fn(),
@@ -72,14 +73,14 @@ describe("<MFileBody/>", () => {
it("should show a download button in file rendering type", async () => {
const { container, getByRole } = render(
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.File } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.File } as any)}>
<MFileBody
{...props}
mxEvent={mediaEvent}
mediaEventHelper={new MediaEventHelper(mediaEvent)}
showGenericPlaceholder={false}
/>
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
expect(getByRole("link", { name: "Download" })).toBeInTheDocument();

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import { act, fireEvent, render, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import {
MatrixEvent,
Relations,
@@ -226,7 +226,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza");
// When a new vote from me comes in
await room.processPollEvents([responseEvent("@me:example.com", "wings", 101)]);
await act(() => room.processPollEvents([responseEvent("@me:example.com", "wings", 101)]));
// Then the new vote is counted, not the old one
expect(votesCount(renderResult, "pizza")).toBe("0 votes");
@@ -255,7 +255,7 @@ describe("MPollBody", () => {
clickOption(renderResult, "pizza");
// When a new vote from someone else comes in
await room.processPollEvents([responseEvent("@xx:example.com", "wings", 101)]);
await act(() => room.processPollEvents([responseEvent("@xx:example.com", "wings", 101)]));
// Then my vote is still for pizza
// NOTE: the new event does not affect the counts for other people -
@@ -596,11 +596,13 @@ describe("MPollBody", () => {
];
const renderResult = await newMPollBody(votes, ends);
expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes");
expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes");
expect(endedVotesCount(renderResult, "italian")).toBe("0 votes");
expect(endedVotesCount(renderResult, "wings")).toBe('<div class="mx_PollOption_winnerIcon"></div>3 votes');
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes");
await waitFor(() => {
expect(endedVotesCount(renderResult, "pizza")).toBe("2 votes");
expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes");
expect(endedVotesCount(renderResult, "italian")).toBe("0 votes");
expect(endedVotesCount(renderResult, "wings")).toBe('<div class="mx_PollOption_winnerIcon"></div>3 votes');
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes");
});
});
it("ignores votes that arrived after the first end poll event", async () => {
@@ -890,12 +892,14 @@ async function newMPollBody(
room_id: "#myroom:example.com",
content: newPollStart(answers, undefined, disclosed),
});
const result = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
// flush promises from loading relations
const prom = newMPollBodyFromEvent(mxEvent, relationEvents, endEvents);
if (waitForResponsesLoad) {
await flushPromises();
const result = await prom;
if (result.queryByTestId("spinner")) {
await waitForElementToBeRemoved(() => result.getByTestId("spinner"));
}
}
return result;
return prom;
}
function getMPollBodyPropsFromEvent(mxEvent: MatrixEvent): IBodyProps {

View File

@@ -121,12 +121,13 @@ describe("<MPollEndBody />", () => {
describe("when poll start event does not exist in current timeline", () => {
it("fetches the related poll start event and displays a poll tile", async () => {
await setupRoomWithEventsTimeline(pollEndEvent);
const { container, getByTestId, getByRole } = getComponent();
const { container, getByTestId, getByRole, queryByRole } = getComponent();
// while fetching event, only icon is shown
expect(container).toMatchSnapshot();
await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument());
await waitFor(() => expect(queryByRole("progressbar")).not.toBeInTheDocument());
expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId());

View File

@@ -35,6 +35,7 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { Action } from "../../../../../src/dispatcher/actions";
import PinningUtils from "../../../../../src/utils/PinningUtils";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/dispatcher/dispatcher");
@@ -117,9 +118,9 @@ describe("<MessageActionBar />", () => {
} as unknown as IRoomState;
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
render(
<RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}>
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
<MessageActionBar {...defaultProps} {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
beforeEach(() => {

View File

@@ -14,7 +14,6 @@ import fs from "fs";
import path from "path";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkEvent, mkRoom, stubClient } from "../../../../test-utils";
import MessageEvent from "../../../../../src/components/views/messages/MessageEvent";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
@@ -24,10 +23,6 @@ jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({
default: () => <div data-testid="unknown-body" />,
}));
jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({
VoiceBroadcastBody: () => <div data-testid="voice-broadcast-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
__esModule: true,
default: () => <div data-testid="image-body" />,
@@ -81,27 +76,6 @@ describe("MessageEvent", () => {
jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn());
});
describe("when a voice broadcast start event occurs", () => {
let result: RenderResult;
beforeEach(() => {
event = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId()!,
room: room.roomId,
content: {
state: VoiceBroadcastInfoState.Started,
},
});
result = renderMessageEvent();
});
it("should render a VoiceBroadcast component", () => {
result.getByTestId("voice-broadcast-body");
});
});
describe("when an image with a caption is sent", () => {
let result: RenderResult;

View File

@@ -20,9 +20,9 @@ import {
} from "../../../../../src/components/views/messages/RoomPredecessorTile";
import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils";
import { Action } from "../../../../../src/dispatcher/actions";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { filterConsole, getRoomContext } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/dispatcher/dispatcher");
@@ -99,9 +99,9 @@ describe("<RoomPredecessorTile />", () => {
expect(createEvent).toBeTruthy();
return render(
<RoomContext.Provider value={getRoomContext(room, {})}>
<ScopedRoomContextProvider {...getRoomContext(room, {})}>
<RoomPredecessorTile mxEvent={createEvent} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
}

View File

@@ -49,9 +49,18 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
<div
class="mx_Marker_border"
>
<div
<svg
class="mx_Marker_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.325a2.07 2.07 0 0 1-.7-.125 1.84 1.84 0 0 1-.625-.375A39.112 39.112 0 0 1 7.8 17.9c-.833-.95-1.53-1.87-2.087-2.762-.559-.892-.984-1.75-1.276-2.575C4.146 11.738 4 10.95 4 10.2c0-2.5.804-4.492 2.412-5.975C8.021 2.742 9.883 2 12 2s3.98.742 5.587 2.225C19.197 5.708 20 7.7 20 10.2c0 .75-.146 1.538-.438 2.363-.291.824-.716 1.683-1.274 2.574A21.678 21.678 0 0 1 16.2 17.9a39.112 39.112 0 0 1-2.875 2.925 1.84 1.84 0 0 1-.625.375 2.07 2.07 0 0 1-.7.125ZM12 12c.55 0 1.02-.196 1.412-.588.392-.391.588-.862.588-1.412 0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 12 8c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 10 10c0 .55.196 1.02.588 1.412.391.392.862.588 1.412.588Z"
/>
</svg>
</div>
</div>
</span>

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import { fireEvent, render } from "jest-matrix-react";
import { Filter, EventTimeline, Room, MatrixEvent, M_POLL_START } from "matrix-js-sdk/src/matrix";
import { PollHistory } from "../../../../../../src/components/views/polls/pollHistory/PollHistory";
@@ -110,7 +110,7 @@ describe("<PollHistory />", () => {
expect(getByText("Loading polls")).toBeInTheDocument();
// flush filter creation request
await act(flushPromises);
await flushPromises();
expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS);
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true });
@@ -140,7 +140,7 @@ describe("<PollHistory />", () => {
);
// flush filter creation request
await act(flushPromises);
await flushPromises();
// once per page
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
@@ -175,7 +175,7 @@ describe("<PollHistory />", () => {
it("renders a no polls message when there are no active polls in the room", async () => {
const { getByText } = getComponent();
await act(flushPromises);
await flushPromises();
expect(getByText("There are no active polls in this room")).toBeTruthy();
});
@@ -199,7 +199,7 @@ describe("<PollHistory />", () => {
.mockReturnValueOnce("test-pagination-token-3");
const { getByText } = getComponent();
await act(flushPromises);
await flushPromises();
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
@@ -212,7 +212,7 @@ describe("<PollHistory />", () => {
// load more polls button still in UI, with loader
expect(getByText("Load more polls")).toMatchSnapshot();
await act(flushPromises);
await flushPromises();
// no more spinner
expect(getByText("Load more polls")).toMatchSnapshot();

View File

@@ -91,7 +91,7 @@ exports[`<PollHistory /> renders a list of active polls when there are polls in
tabindex="0"
>
<div
aria-labelledby=":ra:"
aria-labelledby=":rc:"
class="mx_PollListItem_content"
>
<span>
@@ -116,7 +116,7 @@ exports[`<PollHistory /> renders a list of active polls when there are polls in
tabindex="0"
>
<div
aria-labelledby=":rg:"
aria-labelledby=":rh:"
class="mx_PollListItem_content"
>
<span>

View File

@@ -15,7 +15,7 @@ import userEvent from "@testing-library/user-event";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import RoomSummaryCard from "../../../../../src/components/views/right_panel/RoomSummaryCard";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
@@ -30,7 +30,8 @@ import { _t } from "../../../../../src/languageHandler";
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
import { Action } from "../../../../../src/dispatcher/actions";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/room/tagRoom");
@@ -172,14 +173,14 @@ describe("<RoomSummaryCard />", () => {
const onSearchChange = jest.fn();
const { rerender } = render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Search } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Search } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
focusRoomSearch={true}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
@@ -188,13 +189,13 @@ describe("<RoomSummaryCard />", () => {
rerender(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Room } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Room } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("");
@@ -254,10 +255,7 @@ describe("<RoomSummaryCard />", () => {
fireEvent.click(getByText("People"));
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
{ phase: RightPanelPhases.RoomMemberList },
true,
);
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }, true);
});
it("opens room threads list on button click", () => {

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, waitFor, cleanup, act, within } from "jest-matrix-react";
import { fireEvent, render, screen, cleanup, act, within, waitForElementToBeRemoved } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Mocked, mocked } from "jest-mock";
import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType, Device } from "matrix-js-sdk/src/matrix";
@@ -49,7 +49,7 @@ import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { Action } from "../../../../../src/dispatcher/actions";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import BulkRedactDialog from "../../../../../src/components/views/dialogs/BulkRedactDialog";
jest.mock("../../../../../src/utils/direct-messages", () => ({
@@ -188,7 +188,7 @@ describe("<UserInfo />", () => {
const defaultProps = {
user: defaultUser,
// idk what is wrong with this type
phase: RightPanelPhases.RoomMemberInfo as RightPanelPhases.RoomMemberInfo,
phase: RightPanelPhases.MemberInfo as RightPanelPhases.MemberInfo,
onClose: jest.fn(),
};
@@ -439,7 +439,7 @@ describe("<UserInfo />", () => {
it("renders a device list which can be expanded", async () => {
renderComponent();
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text
const devicesButton = screen.getByRole("button", { name: "1 session" });
@@ -455,13 +455,13 @@ describe("<UserInfo />", () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
const { container } = renderComponent({
phase: RightPanelPhases.SpaceMemberInfo,
phase: RightPanelPhases.MemberInfo,
verificationRequest,
room: mockRoom,
});
await act(flushPromises);
await flushPromises();
await waitFor(() => expect(screen.getByRole("button", { name: "Verify" })).toBeInTheDocument());
await expect(screen.findByRole("button", { name: "Verify" })).resolves.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
@@ -490,7 +490,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 session" });
@@ -538,7 +538,7 @@ describe("<UserInfo />", () => {
} as DeviceVerificationStatus);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "1 verified session" });
@@ -583,7 +583,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// the dehydrated device should be shown as an unverified device, which means
// there should now be a button with the device id ...
@@ -618,7 +618,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
// check the button exists with the expected text (the dehydrated device shouldn't be counted)
const devicesButton = screen.getByRole("button", { name: "2 sessions" });
@@ -649,11 +649,14 @@ describe("<UserInfo />", () => {
mockClient.getDomain.mockReturnValue("example.com");
const { container } = renderComponent({
phase: RightPanelPhases.RoomMemberInfo,
phase: RightPanelPhases.MemberInfo,
room: mockRoom,
});
await waitFor(() => expect(screen.getByRole("button", { name: "Deactivate user" })).toBeInTheDocument());
await expect(screen.findByRole("button", { name: "Deactivate user" })).resolves.toBeInTheDocument();
if (screen.queryAllByRole("progressbar").length) {
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
}
expect(container).toMatchSnapshot();
});
});
@@ -666,7 +669,7 @@ describe("<UserInfo />", () => {
it("renders unverified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
@@ -677,7 +680,7 @@ describe("<UserInfo />", () => {
it("renders verified user info", async () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, false, false));
renderComponent({ room: mockRoom });
await act(flushPromises);
await flushPromises();
const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
@@ -768,7 +771,7 @@ describe("<DeviceItem />", () => {
it("with unverified user and device, displays button without a label", async () => {
renderComponent();
await act(flushPromises);
await flushPromises();
expect(screen.getByRole("button", { name: device.displayName! })).toBeInTheDocument();
expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument();
@@ -776,7 +779,7 @@ describe("<DeviceItem />", () => {
it("with verified user only, displays button with a 'Not trusted' label", async () => {
renderComponent({ isUserVerified: true });
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName });
expect(button).toHaveTextContent(`${device.displayName}Not trusted`);
@@ -785,7 +788,7 @@ describe("<DeviceItem />", () => {
it("with verified device only, displays no button without a label", async () => {
setMockDeviceTrust(true);
renderComponent();
await act(flushPromises);
await flushPromises();
expect(screen.getByText(device.displayName!)).toBeInTheDocument();
expect(screen.queryByText(/trusted/)).not.toBeInTheDocument();
@@ -798,7 +801,7 @@ describe("<DeviceItem />", () => {
mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
mockClient.getUserId.mockReturnValueOnce(defaultUserId);
renderComponent();
await act(flushPromises);
await flushPromises();
// set trust to be false for isVerified, true for isCrossSigningVerified
deferred.resolve({
@@ -814,7 +817,7 @@ describe("<DeviceItem />", () => {
it("with verified user and device, displays no button and a 'Trusted' label", async () => {
setMockDeviceTrust(true);
renderComponent({ isUserVerified: true });
await act(flushPromises);
await flushPromises();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
expect(screen.getByText(device.displayName!)).toBeInTheDocument();
@@ -824,7 +827,7 @@ describe("<DeviceItem />", () => {
it("does not call verifyDevice if client.getUser returns null", async () => {
mockClient.getUser.mockReturnValueOnce(null);
renderComponent();
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument();
@@ -839,7 +842,7 @@ describe("<DeviceItem />", () => {
// even more mocking
mockClient.isGuest.mockReturnValueOnce(true);
renderComponent();
await act(flushPromises);
await flushPromises();
const button = screen.getByRole("button", { name: device.displayName! });
expect(button).toBeInTheDocument();
@@ -851,7 +854,7 @@ describe("<DeviceItem />", () => {
it("with display name", async () => {
const { container } = renderComponent();
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@@ -859,7 +862,7 @@ describe("<DeviceItem />", () => {
it("without display name", async () => {
const device = { deviceId: "deviceId" } as Device;
const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@@ -867,7 +870,7 @@ describe("<DeviceItem />", () => {
it("ambiguous display name", async () => {
const device = { deviceId: "deviceId", ambiguous: true, displayName: "my display name" };
const { container } = renderComponent({ device, userId: defaultUserId });
await act(flushPromises);
await flushPromises();
expect(container).toMatchSnapshot();
});
@@ -1033,9 +1036,7 @@ describe("<UserOptionsSection />", () => {
expect(inviteSpy).toHaveBeenCalledWith([member.userId]);
// check that the test error message is displayed
await waitFor(() => {
expect(screen.getByText(mockErrorMessage.message)).toBeInTheDocument();
});
await expect(screen.findByText(mockErrorMessage.message)).resolves.toBeInTheDocument();
});
it("if calling .invite throws something strange, show default error message", async () => {
@@ -1048,9 +1049,7 @@ describe("<UserOptionsSection />", () => {
await userEvent.click(inviteButton);
// check that the default test error message is displayed
await waitFor(() => {
expect(screen.getByText(/operation failed/i)).toBeInTheDocument();
});
await expect(screen.findByText(/operation failed/i)).resolves.toBeInTheDocument();
});
it.each([

View File

@@ -46,7 +46,7 @@ describe("<VerificationPanel />", () => {
const request = makeMockVerificationRequest({
phase: Phase.Ready,
});
request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8"));
request.generateQRCode.mockResolvedValue(new Uint8ClampedArray(Buffer.from("test", "utf-8")));
const container = renderComponent({
request: request,
layout: "dialog",
@@ -71,7 +71,7 @@ describe("<VerificationPanel />", () => {
const request = makeMockVerificationRequest({
phase: Phase.Ready,
});
request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8"));
request.generateQRCode.mockResolvedValue(new Uint8ClampedArray(Buffer.from("test", "utf-8")));
const container = renderComponent({
request: request,
member: new User("@other:user"),

View File

@@ -135,8 +135,9 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
style="--mx-box-flex: 1;"
>
<a
class="_link_1mzip_17"
class="_link_ue21z_17"
data-kind="primary"
data-size="medium"
rel="noreferrer noopener"
>
<p
@@ -752,8 +753,9 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
style="--mx-box-flex: 1;"
>
<a
class="_link_1mzip_17"
class="_link_ue21z_17"
data-kind="primary"
data-size="medium"
rel="noreferrer noopener"
>
<p
@@ -1406,8 +1408,9 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
style="--mx-box-flex: 1;"
>
<a
class="_link_1mzip_17"
class="_link_ue21z_17"
data-kind="primary"
data-size="medium"
rel="noreferrer noopener"
>
<p

View File

@@ -88,7 +88,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
</p>
</div>
<button
aria-labelledby=":r6s:"
aria-labelledby=":r74:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"
@@ -402,7 +402,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
</p>
</div>
<button
aria-labelledby=":r92:"
aria-labelledby=":r9a:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"

View File

@@ -0,0 +1,90 @@
/*
* 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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx";
import SettingsStore from "../../../../../src/settings/SettingsStore.ts";
import dis from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";
describe("UrlPreviewSettings", () => {
let client: MatrixClient;
let room: Room;
beforeEach(() => {
client = createTestClient();
room = mkStubRoom("roomId", "room", client);
});
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent() {
return render(<UrlPreviewSettings room={room} />, withClientContextRenderOptions(client));
}
it("should display the correct preview when the setting is in a loading state", () => {
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
const { asFragment } = renderComponent();
expect(screen.getByText("URL Previews")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(
screen.getByText(
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
jest.spyOn(dis, "fire").mockReturnValue(undefined);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are enabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
screen.getByRole("button", { name: "enabled" }).click();
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
});
it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are disabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,236 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UrlPreviewSettings should display the correct preview when the room is encrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.
</p>
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
/>
</label>
<div
aria-checked="true"
aria-disabled="false"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
disabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are disabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
enabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are enabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="true"
aria-disabled="false"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_content"
>
<svg
class="_icon_1ye7b_27"
fill="currentColor"
height="1em"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2Z"
fill-rule="evenodd"
/>
</svg>
</div>
</fieldset>
</DocumentFragment>
`;

View File

@@ -27,12 +27,12 @@ import {
import DocumentOffset from "../../../../../src/editor/offset";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Autocompleter, { IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter";
import NotifProvider from "../../../../../src/autocomplete/NotifProvider";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("<EditMessageComposer/>", () => {
const userId = "@alice:server.org";
@@ -79,7 +79,7 @@ describe("<EditMessageComposer/>", () => {
render(<EditMessageComposerWithMatrixClient editState={editState} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={roomContext}>{children}</RoomContext.Provider>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
),
});
@@ -128,7 +128,7 @@ describe("<EditMessageComposer/>", () => {
const expectedBody = {
...editedEvent.getContent(),
"body": " * original message + edit",
"body": "* original message + edit",
"m.new_content": {
"body": "original message + edit",
"msgtype": "m.text",
@@ -160,7 +160,7 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * hello world",
"body": "* hello world",
"msgtype": "m.text",
"m.new_content": {
"body": "hello world",
@@ -183,10 +183,10 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * hello *world*",
"body": "* hello *world*",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": " * hello <em>world</em>",
"formatted_body": "* hello <em>world</em>",
"m.new_content": {
"body": "hello *world*",
"msgtype": "m.text",
@@ -210,10 +210,10 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * blinks __quickly__",
"body": "* blinks __quickly__",
"msgtype": "m.emote",
"format": "org.matrix.custom.html",
"formatted_body": " * blinks <strong>quickly</strong>",
"formatted_body": "* blinks <strong>quickly</strong>",
"m.new_content": {
"body": "blinks __quickly__",
"msgtype": "m.emote",
@@ -238,7 +238,7 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * ✨sparkles✨",
"body": "* ✨sparkles✨",
"msgtype": "m.emote",
"m.new_content": {
"body": "✨sparkles✨",
@@ -264,7 +264,7 @@ describe("<EditMessageComposer/>", () => {
// TODO Edits do not properly strip the double slash used to skip
// command processing.
expect(content).toEqual({
"body": " * //dev/null is my favourite place",
"body": "* //dev/null is my favourite place",
"msgtype": "m.text",
"m.new_content": {
"body": "//dev/null is my favourite place",

View File

@@ -30,7 +30,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils";
import { mkThread } from "../../../../test-utils/threads";
@@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import PinningUtils from "../../../../../src/utils/PinningUtils";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("EventTile", () => {
const ROOM_ID = "!roomId:example.org";
@@ -56,13 +57,13 @@ describe("EventTile", () => {
}) {
return (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={props.roomContext}>
<ScopedRoomContextProvider {...props.roomContext}>
<EventTile
mxEvent={mxEvent}
replacingEventId={mxEvent.replacingEventId()}
{...(props.eventTilePropertyOverrides ?? {})}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
}
@@ -70,9 +71,11 @@ describe("EventTile", () => {
function getComponent(
overrides: Partial<EventTileProps> = {},
renderingType: TimelineRenderingType = TimelineRenderingType.Room,
roomContext: Partial<IRoomState> = {},
) {
const context = getRoomContext(room, {
timelineRenderingType: renderingType,
...roomContext,
});
return render(<WrappedEventTile roomContext={context} eventTilePropertyOverrides={overrides} />);
}
@@ -260,7 +263,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@@ -285,7 +288,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@@ -301,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" },
@@ -314,7 +319,7 @@ describe("EventTile", () => {
} as EventEncryptionInfo);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
expect(e2eIcons).toHaveLength(1);
@@ -346,7 +351,7 @@ describe("EventTile", () => {
await mxEvent.attemptDecryption(mockCrypto);
const { container } = getComponent();
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@@ -400,7 +405,7 @@ describe("EventTile", () => {
const roomContext = getRoomContext(room, {});
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@@ -434,8 +439,6 @@ describe("EventTile", () => {
});
it("should update the warning when the event is replaced with an unencrypted one", async () => {
jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
// we start out with an event from the trusted device
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
@@ -449,9 +452,9 @@ describe("EventTile", () => {
shieldReason: null,
} as EventEncryptionInfo);
const roomContext = getRoomContext(room, {});
const roomContext = getRoomContext(room, { isRoomEncrypted: true });
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await act(flushPromises);
await flushPromises();
const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1);
@@ -578,4 +581,28 @@ describe("EventTile", () => {
});
});
});
it("should display the not encrypted status for an unencrypted event when the room becomes encrypted", async () => {
jest.spyOn(client.getCrypto()!, "getEncryptionInfoForEvent").mockResolvedValue({
shieldColour: EventShieldColour.NONE,
shieldReason: null,
});
const { rerender } = getComponent();
await flushPromises();
// The room and the event are unencrypted, the tile should not show the not encrypted status
expect(screen.queryByText("Not encrypted")).toBeNull();
// The room is now encrypted
rerender(
<WrappedEventTile
roomContext={getRoomContext(room, {
isRoomEncrypted: true,
})}
/>,
);
// The event tile should now show the not encrypted status
await waitFor(() => expect(screen.getByText("Not encrypted")).toBeInTheDocument());
});
});

View File

@@ -22,7 +22,17 @@ exports[`EventTileThreadToolbar renders 1`] = `
role="button"
tabindex="-1"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
</div>
</div>
</DocumentFragment>

View File

@@ -8,7 +8,16 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render, RenderResult, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
import {
act,
fireEvent,
render,
RenderResult,
screen,
waitFor,
waitForElementToBeRemoved,
cleanup,
} from "jest-matrix-react";
import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked, MockedObject } from "jest-mock";
@@ -361,6 +370,7 @@ describe("MemberList", () => {
afterEach(() => {
jest.restoreAllMocks();
cleanup();
});
const renderComponent = () => {
@@ -397,21 +407,22 @@ describe("MemberList", () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(false);
renderComponent();
await flushPromises();
const { findByLabelText } = renderComponent();
// button rendered but disabled
expect(screen.getByText("Invite to this room")).toHaveAttribute("aria-disabled", "true");
await expect(findByLabelText("You do not have permission to invite users")).resolves.toHaveAttribute(
"aria-disabled",
"true",
);
});
it("renders enabled invite button when current user is a member and has rights to invite", async () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(room, "canInvite").mockReturnValue(true);
renderComponent();
await flushPromises();
const { findByText } = renderComponent();
expect(screen.getByText("Invite to this room")).not.toBeDisabled();
await expect(findByText("Invite to this room")).resolves.not.toBeDisabled();
});
it("opens room inviter on button click", async () => {

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixEvent, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
@@ -19,17 +19,14 @@ import {
mkStubRoom,
mockPlatformPeg,
stubClient,
waitEnoughCyclesForModal,
} from "../../../../test-utils";
import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import { Features } from "../../../../../src/settings/Settings";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import dis from "../../../../../src/dispatcher/dispatcher";
@@ -37,31 +34,16 @@ import { E2EStatus } from "../../../../../src/utils/ShieldUtils";
import { addTextToComposerRTL } from "../../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { Action } from "../../../../../src/dispatcher/actions";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
const openStickerPicker = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Sticker"));
});
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Sticker"));
};
const startVoiceMessage = async (): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Voice Message"));
});
};
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
const recording = new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
MatrixClientPeg.safeGet(),
state,
);
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
await userEvent.click(screen.getByLabelText("More options"));
await userEvent.click(screen.getByLabelText("Voice Message"));
};
const expectVoiceMessageRecordingTriggered = (): void => {
@@ -82,14 +64,11 @@ describe("MessageComposer", () => {
await clearAllModals();
jest.useRealTimers();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
// restore settings
act(() => {
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
Features.VoiceBroadcast,
"feature_wysiwyg_composer",
].forEach((setting: string): void => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
@@ -97,6 +76,45 @@ describe("MessageComposer", () => {
});
});
it("wysiwyg correctly persists state to and from localStorage", async () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
const messageText = "Test Text";
await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const { renderResult, rawComponent } = wrapAndRender({ room });
const { unmount } = renderResult;
await flushPromises();
const key = `mx_wysiwyg_state_${room.roomId}`;
await userEvent.click(screen.getByRole("textbox"));
fireEvent.input(screen.getByRole("textbox"), {
data: messageText,
inputType: "insertText",
});
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
// Wait for event dispatch to happen
await flushPromises();
// assert there is state persisted
expect(localStorage.getItem(key)).toBeNull();
// ensure the right state was persisted to localStorage
unmount();
// assert the persisted state
expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({
content: messageText,
isRichText: true,
});
// ensure the correct state is re-loaded
render(rawComponent);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
}, 10000);
describe("for a Room", () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
@@ -177,22 +195,16 @@ describe("MessageComposer", () => {
setting: "MessageComposerInput.showPollsButton",
buttonLabel: "Poll",
},
{
setting: Features.VoiceBroadcast,
buttonLabel: "Voice broadcast",
},
].forEach(({ setting, buttonLabel }) => {
[true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => {
beforeEach(async () => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
await act(() => SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value));
wrapAndRender({ room });
await act(async () => {
await userEvent.click(screen.getByLabelText("More options"));
});
await userEvent.click(screen.getByLabelText("More options"));
});
it(`should${value || "not"} display the button`, () => {
it(`should${value ? "" : " not"} display the button`, () => {
if (value) {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
@@ -205,15 +217,17 @@ describe("MessageComposer", () => {
describe(`and setting ${setting} to ${!value}`, () => {
beforeEach(async () => {
// simulate settings update
await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
dis.dispatch(
{
action: Action.SettingUpdated,
settingName: setting,
newValue: !value,
},
true,
);
await act(async () => {
await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
dis.dispatch(
{
action: Action.SettingUpdated,
settingName: setting,
newValue: !value,
},
true,
);
});
});
it(`should${!value || "not"} display the button`, () => {
@@ -273,7 +287,7 @@ describe("MessageComposer", () => {
beforeEach(async () => {
wrapAndRender({ room }, true, true);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
act(() => resizeCallback(UI_EVENTS.Resize, {}));
});
it("should close the menu", () => {
@@ -295,7 +309,7 @@ describe("MessageComposer", () => {
beforeEach(async () => {
wrapAndRender({ room }, true, false);
await openStickerPicker();
resizeCallback(UI_EVENTS.Resize, {});
act(() => resizeCallback(UI_EVENTS.Resize, {}));
});
it("should close the menu", () => {
@@ -402,34 +416,6 @@ describe("MessageComposer", () => {
expectVoiceMessageRecordingTriggered();
});
});
describe("when recording a voice broadcast and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should not start a voice message and display the info dialog", async () => {
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
});
});
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should try to start a voice message and should not display the info dialog", async () => {
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
expectVoiceMessageRecordingTriggered();
});
});
});
describe("for a LocalRoom", () => {
@@ -443,51 +429,6 @@ describe("MessageComposer", () => {
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
});
});
it("wysiwyg correctly persists state to and from localStorage", async () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
const messageText = "Test Text";
await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const { renderResult, rawComponent } = wrapAndRender({ room });
const { unmount, rerender } = renderResult;
await act(async () => {
await flushPromises();
});
const key = `mx_wysiwyg_state_${room.roomId}`;
await act(async () => {
await userEvent.click(screen.getByRole("textbox"));
});
fireEvent.input(screen.getByRole("textbox"), {
data: messageText,
inputType: "insertText",
});
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// assert there is state persisted
expect(localStorage.getItem(key)).toBeNull();
// ensure the right state was persisted to localStorage
unmount();
// assert the persisted state
expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({
content: messageText,
isRichText: true,
});
// ensure the correct state is re-loaded
rerender(rawComponent);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText));
}, 10000);
});
function wrapAndRender(
@@ -522,9 +463,9 @@ function wrapAndRender(
const getRawComponent = (props = {}, context = roomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={context}>
<ScopedRoomContextProvider {...context}>
<MessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
return {

View File

@@ -10,11 +10,11 @@ import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("MessageComposerButtons", () => {
// @ts-ignore - we're deliberately not implementing the whole interface here, but
@@ -54,7 +54,7 @@ describe("MessageComposerButtons", () => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>{component}</RoomContext.Provider>
<ScopedRoomContextProvider {...defaultRoomContext}>{component}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
}
@@ -168,27 +168,4 @@ describe("MessageComposerButtons", () => {
]);
});
});
describe("with showVoiceBroadcastButton = true", () => {
it("should render the »Voice broadcast« button", () => {
wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
showVoiceBroadcastButton={true}
/>,
false,
);
expect(getButtonLabels()).toEqual([
"Emoji",
"Attachment",
"More options",
["Sticker", "Voice Message", "Voice broadcast", "Poll", "Location"],
]);
});
});
});

View File

@@ -13,19 +13,19 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../../test-utils";
import RoomContext from "../../../../../src/contexts/RoomContext";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}>
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as IRoomState)}>
<NewRoomIntro />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -165,7 +165,7 @@ describe("UnreadNotificationBadge", () => {
},
ts: 5,
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
const { container } = render(getComponent(THREAD_ID));
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();

View File

@@ -20,6 +20,7 @@ import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelSto
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
import { Action } from "../../../../../src/dispatcher/actions";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier.ts";
describe("<PinnedMessageBanner />", () => {
const userId = "@alice:server.org";
@@ -28,10 +29,12 @@ describe("<PinnedMessageBanner />", () => {
let mockClient: MatrixClient;
let room: Room;
let permalinkCreator: RoomPermalinkCreator;
let resizeNotifier: ResizeNotifier;
beforeEach(() => {
mockClient = stubClient();
room = new Room(roomId, mockClient, userId);
permalinkCreator = new RoomPermalinkCreator(room);
resizeNotifier = new ResizeNotifier();
jest.spyOn(dis, "dispatch").mockReturnValue(undefined);
});
@@ -77,7 +80,7 @@ describe("<PinnedMessageBanner />", () => {
*/
function renderBanner() {
return render(
<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />,
<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />,
withClientContextRenderOptions(mockClient),
);
}
@@ -145,7 +148,9 @@ describe("<PinnedMessageBanner />", () => {
event3.getId()!,
]);
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]);
rerender(<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />);
rerender(
<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />,
);
await expect(screen.findByText("Third pinned message")).resolves.toBeVisible();
expect(asFragment()).toMatchSnapshot();
});
@@ -206,6 +211,42 @@ describe("<PinnedMessageBanner />", () => {
expect(asFragment()).toMatchSnapshot();
});
describe("Notify the timeline to resize", () => {
beforeEach(() => {
jest.spyOn(resizeNotifier, "notifyTimelineHeightChanged");
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]);
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]);
});
it("should notify the timeline to resize when we display the banner", async () => {
renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
// The banner is displayed, so we need to resize the timeline
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByRole("button", { name: "View the pinned message in the timeline." }));
await expect(screen.findByText("First pinned message")).resolves.toBeVisible();
// The banner is already displayed, so we don't need to resize the timeline
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1);
});
it("should notify the timeline to resize when we hide the banner", async () => {
const { rerender } = renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
// The banner is displayed, so we need to resize the timeline
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1);
// The banner has no event to display and is hidden
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([]);
jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([]);
rerender(
<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />,
);
// The timeline should be resized
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2);
});
});
describe("Right button", () => {
beforeEach(() => {
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]);
@@ -217,6 +258,8 @@ describe("<PinnedMessageBanner />", () => {
jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false);
renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
expect(screen.getByRole("button", { name: "View all" })).toBeVisible();
});
@@ -224,10 +267,12 @@ describe("<PinnedMessageBanner />", () => {
// The Right panel is opened on another card
jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(true);
jest.spyOn(RightPanelStore.instance, "currentCard", "get").mockReturnValue({
phase: RightPanelPhases.RoomMemberList,
phase: RightPanelPhases.MemberList,
});
renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
expect(screen.getByRole("button", { name: "View all" })).toBeVisible();
});
@@ -239,6 +284,8 @@ describe("<PinnedMessageBanner />", () => {
});
renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
expect(screen.getByRole("button", { name: "Close list" })).toBeVisible();
});
@@ -263,6 +310,7 @@ describe("<PinnedMessageBanner />", () => {
});
renderBanner();
await expect(screen.findByText("Second pinned message")).resolves.toBeVisible();
expect(screen.getByRole("button", { name: "Close list" })).toBeVisible();
jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false);

View File

@@ -10,6 +10,7 @@ import React, { ComponentProps } from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import {
determineAvatarPosition,
@@ -20,6 +21,9 @@ import * as languageHandler from "../../../../../src/languageHandler";
import { stubClient } from "../../../../test-utils";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { formatDate } from "../../../../../src/DateUtils";
jest.mock("../../../../../src/DateUtils");
describe("ReadReceiptGroup", () => {
describe("TooltipText", () => {
@@ -87,6 +91,10 @@ describe("ReadReceiptGroup", () => {
describe("<ReadReceiptPerson />", () => {
stubClient();
// We pick a fixed time but this can still vary depending on the locale
// the tests are run in. We are not testing date formatting here, so stub it out.
mocked(formatDate).mockReturnValue("==MOCK FORMATTED DATE==");
const ROOM_ID = "roomId";
const USER_ID = "@alice:example.org";

View File

@@ -8,9 +8,19 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { EventType, JoinRule, MatrixEvent, PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import {
EventType,
JoinRule,
MatrixEvent,
PendingEventOrdering,
Room,
RoomStateEvent,
RoomMember,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import {
act,
createEvent,
fireEvent,
getAllByLabelText,
@@ -148,7 +158,7 @@ describe("RoomHeader", () => {
fireEvent.click(facePile);
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});
it("has room info icon that opens the room info panel", async () => {
@@ -579,7 +589,7 @@ describe("RoomHeader", () => {
state_key: "",
room_id: room.roomId,
});
room.addLiveEvents([joinRuleEvent]);
room.addLiveEvents([joinRuleEvent], { addToState: true });
render(<RoomHeader room={room} />, getWrapper());
@@ -632,6 +642,52 @@ describe("RoomHeader", () => {
expect(asFragment()).toMatchSnapshot();
});
it("updates the icon when the encryption status changes", async () => {
// The room starts verified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
render(<RoomHeader room={room} />, getWrapper());
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
// A new member joins, and the room becomes unverified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
act(() => {
room.emit(
RoomStateEvent.Members,
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",
}),
room.currentState,
new RoomMember(room.roomId, "@alice:example.org"),
);
});
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
// The user becomes verified
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified);
act(() => {
MatrixClientPeg.get()!.emit(
CryptoEvent.UserTrustStatusChanged,
"@alice:example.org",
new UserVerificationStatus(true, true, true, false),
);
});
await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument());
// An unverified device is added
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning);
act(() => {
MatrixClientPeg.get()!.emit(CryptoEvent.DevicesUpdated, ["@alice:example.org"], false);
});
await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument());
});
});
it("renders additionalButtons", async () => {

View File

@@ -19,7 +19,7 @@ import {
} from "../../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton";
import Modal from "../../../../../../src/Modal";
import SdkConfig from "../../../../../../src/SdkConfig";
import ShareDialog from "../../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../../src/components/views/dialogs/ShareDialog";
import { _t } from "../../../../../../src/languageHandler";
import SettingsStore from "../../../../../../src/settings/SettingsStore";

View File

@@ -9,14 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render, screen, act, RenderResult } from "jest-matrix-react";
import { mocked, Mocked } from "jest-mock";
import {
MatrixClient,
PendingEventOrdering,
Room,
MatrixEvent,
RoomStateEvent,
Thread,
} from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent, Thread } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { Widget } from "matrix-widget-api";
@@ -40,8 +33,6 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import PlatformPeg from "../../../../../src/PlatformPeg";
import BasePlatform from "../../../../../src/BasePlatform";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { TestSdkContext } from "../../../TestSdkContext";
import { SDKContext } from "../../../../../src/contexts/SDKContext";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
@@ -61,20 +52,6 @@ describe("RoomTile", () => {
} as unknown as BasePlatform);
useMockedCalls();
const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise<void> => {
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
state,
client.getSafeUserId(),
client.getDeviceId()!,
);
await act(async () => {
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
await flushPromises();
});
};
const renderRoomTile = (): RenderResult => {
return render(
<SDKContext.Provider value={sdkContext}>
@@ -89,7 +66,6 @@ describe("RoomTile", () => {
};
let client: Mocked<MatrixClient>;
let voiceBroadcastInfoEvent: MatrixEvent;
let room: Room;
let sdkContext: TestSdkContext;
let showMessagePreview = false;
@@ -303,49 +279,6 @@ describe("RoomTile", () => {
});
expect(screen.queryByLabelText(/participant/)).toBe(null);
});
describe("and a live broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should still render the call subtitle", () => {
expect(screen.queryByText("Video")).toBeInTheDocument();
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
describe("when a live voice broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(async () => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
client.getSafeUserId(),
client.getDeviceId()!,
voiceBroadcastInfoEvent,
);
await act(async () => {
room.currentState.setStateEvents([stopEvent]);
await flushPromises();
});
});
it("should not render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
});

View File

@@ -18,7 +18,7 @@ import SendMessageComposer, {
isQuickReaction,
} from "../../../../../src/components/views/rooms/SendMessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import EditorModel from "../../../../../src/editor/model";
import { createPartCreator } from "../../../editor/mock";
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils";
@@ -30,6 +30,7 @@ import { IRoomState, MainSplitContentType } from "../../../../../src/components/
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { addTextToComposer } from "../../../../test-utils/composer";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
@@ -77,6 +78,7 @@ describe("<SendMessageComposer/>", () => {
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
};
describe("createMessageContent", () => {
it("sends plaintext messages correctly", () => {
@@ -364,9 +366,9 @@ describe("<SendMessageComposer/>", () => {
};
const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<SendMessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => {
@@ -385,7 +387,7 @@ describe("<SendMessageComposer/>", () => {
it("correctly persists state to and from localStorage", () => {
const props = { replyToEvent: mockEvent };
const { container, unmount, rerender } = getComponent(props);
let { container, unmount } = getComponent(props);
addTextToComposer(container, "Test Text");
@@ -402,7 +404,7 @@ describe("<SendMessageComposer/>", () => {
});
// ensure the correct model is re-loaded
rerender(getRawComponent(props));
({ container, unmount } = getComponent(props));
expect(container.textContent).toBe("Test Text");
expect(spyDispatcher).toHaveBeenCalledWith({
action: "reply_to_event",
@@ -413,7 +415,7 @@ describe("<SendMessageComposer/>", () => {
// now try with localStorage wiped out
unmount();
localStorage.removeItem(key);
rerender(getRawComponent(props));
({ container } = getComponent(props));
expect(container.textContent).toBe("");
});

View File

@@ -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(),
);
});
});
});

View File

@@ -84,7 +84,7 @@ exports[`ReadReceiptGroup <ReadReceiptPerson /> should render 1`] = `
<p
class="mx_ReadReceiptGroup_secondary"
>
Wed, 15 May, 0:00
==MOCK FORMATTED DATE==
</p>
</div>
</div>

View File

@@ -47,7 +47,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
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-labelledby=":r154:"
aria-labelledby=":r16c:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -73,7 +73,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r159:"
aria-labelledby=":r16h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby=":r15e:"
aria-labelledby=":r16m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -123,7 +123,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby=":r15j:"
aria-labelledby=":r16r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"

View File

@@ -8,10 +8,9 @@ Please see LICENSE files in the repository root for full details.
import "@testing-library/jest-dom";
import React from "react";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { flushPromises, mkEvent } from "../../../../../test-utils";
@@ -23,6 +22,7 @@ import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispa
import { ActionPayload } from "../../../../../../src/dispatcher/payloads";
import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton";
import { createMocks } from "./utils";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("EditWysiwygComposer", () => {
afterEach(() => {
@@ -39,9 +39,9 @@ describe("EditWysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};
@@ -64,9 +64,9 @@ describe("EditWysiwygComposer", () => {
rerender(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ ...defaultRoomContext, room: undefined }}>
<ScopedRoomContextProvider {...defaultRoomContext} room={undefined}>
<EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
@@ -196,9 +196,9 @@ describe("EditWysiwygComposer", () => {
// Then
screen.getByText("Save").click();
const expectedContent = {
"body": ` * foo bar`,
"body": `* foo bar`,
"format": "org.matrix.custom.html",
"formatted_body": ` * foo bar`,
"formatted_body": `* foo bar`,
"m.new_content": {
body: "foo bar",
format: "org.matrix.custom.html",
@@ -253,9 +253,7 @@ describe("EditWysiwygComposer", () => {
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
await flushPromises();
// Then we don't get it because we are disabled
expect(screen.getByRole("textbox")).not.toHaveFocus();
@@ -277,10 +275,10 @@ describe("EditWysiwygComposer", () => {
);
render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<EditWysiwygComposer editorStateTransfer={editorStateTransfer} />
<Emoji menuPosition={{ chevronFace: ChevronFace.Top }} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
// Same behavior as in RoomView.tsx

View File

@@ -11,7 +11,6 @@ import React from "react";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { flushPromises } from "../../../../../test-utils";
@@ -20,6 +19,7 @@ import { aboveLeftOf } from "../../../../../../src/components/structures/Context
import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispatcher/payloads/ComposerInsertPayload";
import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
import { createMocks } from "./utils";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
@@ -66,7 +66,7 @@ describe("SendWysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<SendWysiwygComposer
onChange={onChange}
onSend={onSend}
@@ -75,7 +75,7 @@ describe("SendWysiwygComposer", () => {
menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })}
placeholder={placeholder}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -14,7 +14,7 @@ import { PlainTextComposer } from "../../../../../../../src/components/views/roo
import * as mockUseSettingsHook from "../../../../../../../src/hooks/useSettings";
import * as mockKeyboard from "../../../../../../../src/Keyboard";
import { createMocks } from "../utils";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("PlainTextComposer", () => {
const customRender = (
@@ -275,9 +275,9 @@ describe("PlainTextComposer", () => {
const { defaultRoomContext } = createMocks();
render(
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} disabled={false} initialContent="" />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument();

View File

@@ -11,12 +11,12 @@ import React, { createRef } from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import { WysiwygAutocomplete } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete";
import { getRoomContext, mkStubRoom, stubClient } from "../../../../../../test-utils";
import Autocomplete from "../../../../../../../src/components/views/rooms/Autocomplete";
import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplete/Autocompleter";
import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
const mockCompletion: ICompletion[] = [
{
@@ -71,7 +71,7 @@ describe("WysiwygAutocomplete", () => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={mockRoomContext}>
<ScopedRoomContextProvider {...mockRoomContext}>
<WysiwygAutocomplete
ref={autocompleteRef}
suggestion={null}
@@ -80,7 +80,7 @@ describe("WysiwygAutocomplete", () => {
handleAtRoomMention={mockHandleAtRoomMention}
{...props}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -18,7 +18,6 @@ import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher";
import * as EventUtils from "../../../../../../../src/utils/EventUtils";
import { Action } from "../../../../../../../src/dispatcher/actions";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import {
ComposerContext,
getDefaultContextValue,
@@ -32,20 +31,21 @@ import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplet
import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider";
import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks";
import { PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("WysiwygComposer", () => {
const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => {
const { mockClient, defaultRoomContext } = createMocks();
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<WysiwygComposer
onChange={onChange}
onSend={onSend}
disabled={disabled}
initialContent={initialContent}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};
@@ -523,7 +523,7 @@ describe("WysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<ComposerContext.Provider
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
>
@@ -537,7 +537,7 @@ describe("WysiwygComposer", () => {
}
/>
</ComposerContext.Provider>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -88,9 +88,9 @@ describe("createMessageContent", () => {
// Then
expect(content).toEqual({
"body": " * *__hello__ world*",
"body": "* *__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": ` * ${message}`,
"formatted_body": `* ${message}`,
"msgtype": "m.text",
"m.new_content": {
body: "*__hello__ world*",

View File

@@ -418,8 +418,8 @@ describe("message", () => {
// Then
const { msgtype, format } = mockEvent.getContent();
const expectedContent = {
"body": ` * ${newMessage}`,
"formatted_body": ` * ${newMessage}`,
"body": `* ${newMessage}`,
"formatted_body": `* ${newMessage}`,
"m.new_content": {
body: "Replying to this new content",
format: "org.matrix.custom.html",

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render, screen, waitFor } from "jest-matrix-react";
import { render, screen, waitFor, cleanup } from "jest-matrix-react";
import { MatrixClient, MatrixError, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import React from "react";
import userEvent from "@testing-library/user-event";
@@ -48,54 +48,13 @@ describe("AddRemoveThreepids", () => {
afterEach(() => {
jest.restoreAllMocks();
clearAllModals();
cleanup();
});
const clientProviderWrapper: React.FC = ({ children }: React.PropsWithChildren) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
it("should render a loader while loading", async () => {
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={true}
onChange={() => {}}
/>,
);
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
});
it("should render email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
it("should render phone numbers", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[PHONE1]}
isLoading={false}
onChange={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
it("should handle no email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
@@ -107,6 +66,7 @@ describe("AddRemoveThreepids", () => {
/>,
);
await expect(screen.findByText("Email Address")).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
@@ -127,7 +87,7 @@ describe("AddRemoveThreepids", () => {
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
const input = await screen.findByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
@@ -166,7 +126,7 @@ describe("AddRemoveThreepids", () => {
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
const input = await screen.findByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
@@ -210,7 +170,7 @@ describe("AddRemoveThreepids", () => {
},
);
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
const countryDropdown = await screen.findByRole("button", { name: /Country Dropdown/ });
await userEvent.click(countryDropdown);
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
await userEvent.click(gbOption);
@@ -270,7 +230,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
@@ -297,7 +257,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
@@ -326,7 +286,7 @@ describe("AddRemoveThreepids", () => {
},
);
const removeButton = screen.getByRole("button", { name: /Remove/ });
const removeButton = await screen.findByRole("button", { name: /Remove/ });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible();
@@ -357,7 +317,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton);
@@ -408,7 +368,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
const shareButton = screen.getByRole("button", { name: /Share/ });
await userEvent.click(shareButton);
@@ -452,7 +412,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton);
@@ -475,7 +435,7 @@ describe("AddRemoveThreepids", () => {
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
const revokeButton = screen.getByRole("button", { name: /Revoke/ });
await userEvent.click(revokeButton);
@@ -596,4 +556,48 @@ describe("AddRemoveThreepids", () => {
}),
);
});
it("should render a loader while loading", async () => {
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={true}
onChange={() => {}}
/>,
);
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
});
it("should render email addresses", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={() => {}}
/>,
);
await expect(screen.findByText(EMAIL1.address)).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
it("should render phone numbers", async () => {
const { container } = render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[PHONE1]}
isLoading={false}
onChange={() => {}}
/>,
);
await expect(screen.findByText(PHONE1.address)).resolves.toBeVisible();
expect(container).toMatchSnapshot();
});
});

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { render, screen, fireEvent } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import AvatarSetting from "../../../../../src/components/views/settings/AvatarSetting";
@@ -16,6 +16,9 @@ const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
type: "image/gif",
});
const GENERIC_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "not-avatar.doc", {
type: "application/msword",
});
describe("<AvatarSetting />", () => {
beforeEach(() => {
@@ -70,4 +73,45 @@ describe("<AvatarSetting />", () => {
expect(onChange).toHaveBeenCalledWith(AVATAR_FILE);
});
it("should noop when selecting no file", async () => {
const onChange = jest.fn();
render(
<AvatarSetting
placeholderId="blee"
placeholderName="boo"
avatar="mxc://example.org/my-avatar"
avatarAltText="Avatar of Peter Fox"
onChange={onChange}
/>,
);
const fileInput = screen.getByAltText("Upload");
// Can't use userEvent.upload here as it doesn't support uploading invalid files
fireEvent.change(fileInput, { target: { files: [] } });
expect(onChange).not.toHaveBeenCalled();
});
it("should show error if user tries to use non-image file", async () => {
const onChange = jest.fn();
render(
<AvatarSetting
placeholderId="blee"
placeholderName="boo"
avatar="mxc://example.org/my-avatar"
avatarAltText="Avatar of Peter Fox"
onChange={onChange}
/>,
);
const fileInput = screen.getByAltText("Upload");
// Can't use userEvent.upload here as it doesn't support uploading invalid files
fireEvent.change(fileInput, { target: { files: [GENERIC_FILE] } });
expect(onChange).not.toHaveBeenCalled();
await expect(screen.findByRole("heading", { name: "Upload Failed" })).resolves.toBeInTheDocument();
});
});

View File

@@ -59,7 +59,7 @@ describe("<JoinRuleSettings />", () => {
onError: jest.fn(),
};
const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
render(<JoinRuleSettings {...defaultProps} {...props} />, { legacyRoot: false });
render(<JoinRuleSettings {...defaultProps} {...props} />);
const setRoomStateEvents = (
room: Room,

View File

@@ -915,7 +915,7 @@ describe("<Notifications />", () => {
user: "@alice:example.org",
ts: 1,
});
await room.addLiveEvents([message]);
await room.addLiveEvents([message], { addToState: true });
const { container } = await getComponentAndWait();
const clearNotificationEl = getByTestId(container, "clear-notifications");

View File

@@ -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();
});
});

View File

@@ -35,7 +35,7 @@ describe("SetIntegrationManager", () => {
deleteThreePid: jest.fn(),
});
let stores: SdkContextClass;
let stores!: SdkContextClass;
const getComponent = () => (
<MatrixClientContext.Provider value={mockClient}>

View File

@@ -11,14 +11,14 @@ exports[`AddRemoveThreepids should handle no email addresses 1`] = `
>
<input
autocomplete="email"
id="mx_Field_3"
id="mx_Field_1"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_3"
for="mx_Field_1"
>
Email Address
</label>
@@ -61,14 +61,14 @@ exports[`AddRemoveThreepids should render email addresses 1`] = `
>
<input
autocomplete="email"
id="mx_Field_1"
id="mx_Field_14"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_1"
for="mx_Field_14"
>
Email Address
</label>
@@ -148,14 +148,14 @@ exports[`AddRemoveThreepids should render phone numbers 1`] = `
</span>
<input
autocomplete="tel-national"
id="mx_Field_2"
id="mx_Field_15"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_2"
for="mx_Field_15"
>
Phone Number
</label>

View File

@@ -10,7 +10,7 @@ exports[`<LayoutSwitcher /> should render 1`] = `
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Message layout
</h3>
@@ -19,14 +19,14 @@ exports[`<LayoutSwitcher /> should render 1`] = `
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24 mx_LayoutSwitcher_LayoutSelector"
class="_root_ssths_24 mx_LayoutSwitcher_LayoutSelector"
>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Modern"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r0:"
>
<div
@@ -149,11 +149,11 @@ exports[`<LayoutSwitcher /> should render 1`] = `
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Message bubbles"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r9:"
>
<div
@@ -275,11 +275,11 @@ exports[`<LayoutSwitcher /> should render 1`] = `
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="IRC (experimental)"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:ri:"
>
<div
@@ -402,13 +402,13 @@ exports[`<LayoutSwitcher /> should render 1`] = `
</div>
</form>
<form
class="_root_dgy0u_24"
class="_root_ssths_24"
>
<div
class="_inline-field_dgy0u_40"
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
@@ -427,16 +427,16 @@ exports[`<LayoutSwitcher /> should render 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:rr:"
>
Show compact text and messages
</label>
<span
class="_message_dgy0u_98 _help-message_dgy0u_104"
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:rs:"
>
Modern layout must be selected to use this feature.

View File

@@ -10,7 +10,7 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Theme
</h3>
@@ -19,13 +19,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24"
class="_root_ssths_24"
>
<div
class="_inline-field_dgy0u_40"
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
@@ -43,10 +43,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r28:"
>
Match system theme
@@ -55,13 +55,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</form>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors"
>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -81,10 +81,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r29:"
>
Light
@@ -92,10 +92,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -114,10 +114,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r2a:"
>
Dark
@@ -125,10 +125,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -147,10 +147,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r2b:"
>
High contrast
@@ -158,10 +158,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -180,10 +180,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r2c:"
>
Alice theme
@@ -195,13 +195,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
class="mx_ThemeChoicePanel_CustomTheme"
>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
class="_root_ssths_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
>
<div
class="_field_dgy0u_34"
class="_field_ssths_34"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r2d:"
>
Add custom theme
@@ -219,7 +219,7 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
/>
</div>
<span
class="_message_dgy0u_98 _help-message_dgy0u_104"
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r2e:"
>
Enter the URL of a custom theme you want to apply.
@@ -287,7 +287,7 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Theme
</h3>
@@ -296,13 +296,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24"
class="_root_ssths_24"
>
<div
class="_inline-field_dgy0u_40"
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
@@ -320,10 +320,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r10:"
>
Match system theme
@@ -332,13 +332,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</form>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors"
>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -358,10 +358,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r11:"
>
Light
@@ -369,10 +369,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -391,10 +391,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r12:"
>
Dark
@@ -402,10 +402,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -424,10 +424,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r13:"
>
High contrast
@@ -435,10 +435,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -457,10 +457,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r14:"
>
Alice theme
@@ -472,13 +472,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
class="mx_ThemeChoicePanel_CustomTheme"
>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
class="_root_ssths_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace"
>
<div
class="_field_dgy0u_34"
class="_field_ssths_34"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r15:"
>
Add custom theme
@@ -496,7 +496,7 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
/>
</div>
<span
class="_message_dgy0u_98 _help-message_dgy0u_104"
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:r16:"
>
Enter the URL of a custom theme you want to apply.
@@ -564,7 +564,7 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Theme
</h3>
@@ -573,13 +573,13 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24"
class="_root_ssths_24"
>
<div
class="_inline-field_dgy0u_40"
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
@@ -597,10 +597,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r0:"
>
Match system theme
@@ -609,13 +609,13 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</form>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors"
>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -635,10 +635,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r1:"
>
Light
@@ -646,10 +646,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -668,10 +668,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r2:"
>
Dark
@@ -679,10 +679,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -701,10 +701,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r3:"
>
High contrast

View File

@@ -79,9 +79,7 @@ describe("<LoginWithQR />", () => {
describe("MSC4108", () => {
const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => (
<React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
</React.StrictMode>
<LoginWithQR {...defaultProps} {...props} />
);
test("render QR then back", async () => {

View File

@@ -56,7 +56,6 @@ describe("<LoginWithQRSection />", () => {
const defaultProps = {
onShowQr: () => {},
versions: makeVersions({ "org.matrix.msc4108": true }),
wellKnown: {},
};
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;

View File

@@ -716,7 +716,7 @@ describe("<Notifications />", () => {
user: "@alice:example.org",
ts: 1,
});
room.addLiveEvents([message]);
room.addLiveEvents([message], { addToState: true });
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
const user = userEvent.setup();

View File

@@ -17,7 +17,6 @@ import userEvent from "@testing-library/user-event";
import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../../src/models/Call";
@@ -27,16 +26,11 @@ describe("RolesRoomSettingsTab", () => {
let cli: MatrixClient;
let room: Room;
const renderTab = (propRoom: Room = room): RenderResult => {
return render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
};
const getVoiceBroadcastsSelect = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts']")!;
};
const getVoiceBroadcastsSelectedOption = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked")!;
const renderTab = async (propRoom: Room = room): Promise<RenderResult> => {
const renderResult = render(<RolesRoomSettingsTab room={propRoom} />, withClientContextRenderOptions(cli));
// Wait for the tab to be ready
await waitFor(() => expect(screen.getByText("Permissions")).toBeInTheDocument());
return renderResult;
};
beforeEach(() => {
@@ -45,7 +39,7 @@ describe("RolesRoomSettingsTab", () => {
room = mkStubRoom(roomId, "test room", cli);
});
it("should allow an Admin to demote themselves but not others", () => {
it("should allow an Admin to demote themselves but not others", async () => {
mocked(cli.getRoom).mockReturnValue(room);
// @ts-ignore - mocked doesn't support overloads properly
mocked(room.currentState.getStateEvents).mockImplementation((type, key) => {
@@ -67,32 +61,12 @@ describe("RolesRoomSettingsTab", () => {
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
const { container } = await renderTab();
expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled();
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", () => {
expect(getVoiceBroadcastsSelectedOption().textContent).toBe("Moderator");
});
describe("when setting »Default« permission for »Voice broadcasts«", () => {
beforeEach(() => {
fireEvent.change(getVoiceBroadcastsSelect(), {
target: { value: 0 },
});
});
it("should update the power levels", () => {
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[VoiceBroadcastInfoEventType]: 0,
},
});
});
});
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
@@ -122,12 +96,12 @@ describe("RolesRoomSettingsTab", () => {
});
describe("Join Element calls", () => {
it("defaults to moderator for joining calls", () => {
expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
it("defaults to moderator for joining calls", async () => {
expect(getJoinCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
});
it("can change joining calls power level", () => {
const tab = renderTab();
it("can change joining calls power level", async () => {
const tab = await renderTab();
fireEvent.change(getJoinCallSelect(tab), {
target: { value: 0 },
@@ -143,12 +117,12 @@ describe("RolesRoomSettingsTab", () => {
});
describe("Start Element calls", () => {
it("defaults to moderator for starting calls", () => {
expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
it("defaults to moderator for starting calls", async () => {
expect(getStartCallSelectedOption(await renderTab())?.textContent).toBe("Moderator");
});
it("can change starting calls power level", () => {
const tab = renderTab();
it("can change starting calls power level", async () => {
const tab = await renderTab();
fireEvent.change(getStartCallSelect(tab), {
target: { value: 0 },
@@ -164,10 +138,10 @@ describe("RolesRoomSettingsTab", () => {
});
});
it("hides when group calls disabled", () => {
it("hides when group calls disabled", async () => {
setGroupCallsEnabled(false);
const tab = renderTab();
const tab = await renderTab();
expect(getStartCallSelect(tab)).toBeFalsy();
expect(getStartCallSelectedOption(tab)).toBeFalsy();
@@ -250,7 +224,7 @@ describe("RolesRoomSettingsTab", () => {
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
const { container } = await renderTab();
const selector = container.querySelector(`[placeholder="${cli.getUserId()}"]`)!;
fireEvent.change(selector, { target: { value: "50" } });

View File

@@ -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();

View File

@@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => {
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getKeyBackupVersion: jest.fn(),
});
const sdkContext = new SdkContextClass();

View File

@@ -277,9 +277,7 @@ describe("<SessionManagerTab />", () => {
mockClient.getDevices.mockRejectedValue({ httpStatus: 404 });
const { container } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy();
});
@@ -302,9 +300,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledTimes(3);
expect(
@@ -337,9 +333,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
// twice for each device
expect(mockClient.getAccountData).toHaveBeenCalledTimes(4);
@@ -356,9 +350,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId, queryByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
// application metadata section not rendered
@@ -369,9 +361,7 @@ describe("<SessionManagerTab />", () => {
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] });
const { queryByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(queryByTestId("other-sessions-section")).toBeFalsy();
});
@@ -382,9 +372,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
expect(getByTestId("other-sessions-section")).toBeTruthy();
});
@@ -395,9 +383,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId, container } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
fireEvent.click(getByTestId("unverified-devices-cta"));
@@ -908,7 +894,8 @@ describe("<SessionManagerTab />", () => {
});
it("deletes a device when interactive auth is not required", async () => {
mockClient.deleteMultipleDevices.mockResolvedValue({});
const deferredDeleteMultipleDevices = defer<{}>();
mockClient.deleteMultipleDevices.mockReturnValue(deferredDeleteMultipleDevices.promise);
mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
});
@@ -933,6 +920,7 @@ describe("<SessionManagerTab />", () => {
fireEvent.click(signOutButton);
await confirmSignout(getByTestId);
await prom;
deferredDeleteMultipleDevices.resolve({});
// delete called
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
@@ -991,7 +979,7 @@ describe("<SessionManagerTab />", () => {
const { getByTestId, getByLabelText } = render(getComponent());
await act(flushPromises);
await flushPromises();
// reset mock count after initial load
mockClient.getDevices.mockClear();
@@ -1025,7 +1013,7 @@ describe("<SessionManagerTab />", () => {
fireEvent.submit(getByLabelText("Password"));
});
await act(flushPromises);
await flushPromises();
// called again with auth
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([alicesMobileDevice.device_id], {
@@ -1551,7 +1539,7 @@ describe("<SessionManagerTab />", () => {
});
const { getByTestId, container } = render(getComponent());
await act(flushPromises);
await flushPromises();
// filter for inactive sessions
await setFilter(container, DeviceSecurityVariation.Inactive);
@@ -1577,9 +1565,7 @@ describe("<SessionManagerTab />", () => {
it("lets you change the pusher state", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
@@ -1598,9 +1584,7 @@ describe("<SessionManagerTab />", () => {
it("lets you change the local notification settings state", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
@@ -1621,9 +1605,7 @@ describe("<SessionManagerTab />", () => {
it("updates the UI when another session changes the local notifications", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromises();
});
await flushPromises();
toggleDeviceDetails(getByTestId, alicesDevice.device_id);

View File

@@ -42,14 +42,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
>
<input
autocomplete="email"
id="mx_Field_9"
id="mx_Field_3"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_9"
for="mx_Field_3"
>
Email Address
</label>
@@ -145,14 +145,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
</span>
<input
autocomplete="tel-national"
id="mx_Field_10"
id="mx_Field_4"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_10"
for="mx_Field_4"
>
Phone Number
</label>

View File

@@ -23,7 +23,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Theme
</h3>
@@ -32,13 +32,13 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors"
class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors"
>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -58,10 +58,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r0:"
>
Light
@@ -69,10 +69,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-dark"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-dark"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -92,10 +92,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r1:"
>
Dark
@@ -103,10 +103,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_1vw5h_18"
@@ -126,10 +126,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label"
class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label"
for="radix-:r2:"
>
High contrast
@@ -153,7 +153,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Message layout
</h3>
@@ -162,14 +162,14 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_dgy0u_24 mx_LayoutSwitcher_LayoutSelector"
class="_root_ssths_24 mx_LayoutSwitcher_LayoutSelector"
>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Modern"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:r3:"
>
<div
@@ -292,11 +292,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="Message bubbles"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:rc:"
>
<div
@@ -418,11 +418,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</label>
</div>
<div
class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio"
>
<label
aria-label="IRC (experimental)"
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:rl:"
>
<div
@@ -545,13 +545,13 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</form>
<form
class="_root_dgy0u_24"
class="_root_ssths_24"
>
<div
class="_inline-field_dgy0u_40"
class="_inline-field_ssths_40"
>
<div
class="_inline-field-control_dgy0u_52"
class="_inline-field-control_ssths_52"
>
<div
class="_container_qnvru_18"
@@ -570,16 +570,16 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
</div>
</div>
<div
class="_inline-field-body_dgy0u_46"
class="_inline-field-body_ssths_46"
>
<label
class="_label_dgy0u_67"
class="_label_ssths_67"
for="radix-:ru:"
>
Show compact text and messages
</label>
<span
class="_message_dgy0u_98 _help-message_dgy0u_104"
class="_message_ssths_93 _help-message_ssths_99"
id="radix-:rv:"
>
Modern layout must be selected to use this feature.

View File

@@ -85,7 +85,6 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Ignore
</div>
@@ -150,7 +149,6 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Subscribe
</div>

View File

@@ -388,7 +388,7 @@ exports[`<SessionManagerTab /> goes to filtered list from security recommendatio
>
<input
aria-label="Select all"
aria-labelledby=":r4e:"
aria-labelledby=":r3s:"
data-testid="device-select-all-checkbox"
id="device-select-all-checkbox"
type="checkbox"

View File

@@ -135,7 +135,17 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings with guest spa url
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.897 2.817 2.336 4.733 5.223.76a1 1 0 0 1 .555 1.705L17.23 13.7l.892 5.202a1 1 0 0 1-1.45 1.054L12 17.5l-4.672 2.456a1 1 0 0 1-1.451-1.054l.892-5.202-3.78-3.685a1 1 0 0 1 .555-1.706l5.223-.759 2.336-4.733a1 1 0 0 1 1.794 0Z"
/>
</svg>
Favourites
</div>
<div
@@ -167,7 +177,20 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings with guest spa url
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15c-1.1 0-2.042-.392-2.825-1.175C8.392 13.042 8 12.1 8 11s.392-2.042 1.175-2.825C9.958 7.392 10.9 7 12 7s2.042.392 2.825 1.175C15.608 8.958 16 9.9 16 11s-.392 2.042-1.175 2.825C14.042 14.608 13.1 15 12 15Z"
/>
<path
d="M19.528 18.583A9.962 9.962 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 2.52.933 4.824 2.472 6.583A9.976 9.976 0 0 0 12 22a9.976 9.976 0 0 0 7.528-3.417ZM8.75 16.388c-.915.221-1.818.538-2.709.95a8 8 0 1 1 11.918 0 14.679 14.679 0 0 0-2.709-.95A13.76 13.76 0 0 0 12 16c-1.1 0-2.183.13-3.25.387Z"
/>
</svg>
People
</div>
<div
@@ -396,7 +419,17 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings without guest spa u
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.897 2.817 2.336 4.733 5.223.76a1 1 0 0 1 .555 1.705L17.23 13.7l.892 5.202a1 1 0 0 1-1.45 1.054L12 17.5l-4.672 2.456a1 1 0 0 1-1.451-1.054l.892-5.202-3.78-3.685a1 1 0 0 1 .555-1.706l5.223-.759 2.336-4.733a1 1 0 0 1 1.794 0Z"
/>
</svg>
Favourites
</div>
<div
@@ -428,7 +461,20 @@ exports[`<SidebarUserSettingsTab /> renders sidebar settings without guest spa u
<div
class="mx_SettingsSubsection_text"
>
<div />
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15c-1.1 0-2.042-.392-2.825-1.175C8.392 13.042 8 12.1 8 11s.392-2.042 1.175-2.825C9.958 7.392 10.9 7 12 7s2.042.392 2.825 1.175C15.608 8.958 16 9.9 16 11s-.392 2.042-1.175 2.825C14.042 14.608 13.1 15 12 15Z"
/>
<path
d="M19.528 18.583A9.962 9.962 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 2.52.933 4.824 2.472 6.583A9.976 9.976 0 0 0 12 22a9.976 9.976 0 0 0 7.528-3.417ZM8.75 16.388c-.915.221-1.818.538-2.709.95a8 8 0 1 1 11.918 0 14.679 14.679 0 0 0-2.709-.95A13.76 13.76 0 0 0 12 16c-1.1 0-2.183.13-3.25.387Z"
/>
</svg>
People
</div>
<div

View File

@@ -8,6 +8,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
>
<div
class="mx_UserMenu"
data-floating-ui-inert=""
>
<div
aria-expanded="false"
@@ -42,6 +43,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
<ul
aria-label="Spaces"
class="mx_AutoHideScrollbar mx_SpaceTreeLevel"
data-floating-ui-inert=""
data-rbd-droppable-context-id="0"
data-rbd-droppable-id="top-level-spaces"
role="tree"
@@ -236,6 +238,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
aria-label="Threads"
aria-labelledby=":r14:"
class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton"
data-floating-ui-inert=""
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
@@ -260,6 +263,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
</button>
<span
aria-hidden="true"
data-floating-ui-inert=""
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="-1"
/>
@@ -272,6 +276,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
/>
<span
aria-owns=":r19:"
data-floating-ui-inert=""
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
/>
<span
@@ -286,6 +291,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
aria-expanded="false"
aria-label="Quick settings"
class="mx_AccessibleButton mx_QuickSettingsButton"
data-floating-ui-inert=""
role="button"
tabindex="0"
/>

View File

@@ -477,9 +477,7 @@ exports[`ThreadsActivityCentre should order the room with the same notification
exports[`ThreadsActivityCentre should render the release announcement 1`] = `
<body>
<div
data-floating-ui-inert=""
>
<div>
<div
class="mx_ThreadsActivityCentre_container"
>
@@ -491,6 +489,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
aria-label="Threads"
aria-labelledby=":rc:"
class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton"
data-floating-ui-inert=""
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
@@ -515,6 +514,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
</button>
<span
aria-hidden="true"
data-floating-ui-inert=""
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="-1"
/>
@@ -527,6 +527,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
/>
<span
aria-owns=":rh:"
data-floating-ui-inert=""
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
/>
<span
@@ -585,7 +586,6 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
>
<span
data-floating-ui-focus-guard=""
data-floating-ui-inert=""
data-type="inside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
@@ -648,7 +648,6 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
</div>
<span
data-floating-ui-focus-guard=""
data-floating-ui-inert=""
data-type="inside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"

Some files were not shown because too many files have changed in this diff Show More