Use React Suspense when rendering async modals (#28386)

* Use React Suspense when rendering async modals

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update src/Modal.tsx

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-11-12 21:19:11 +00:00 committed by GitHub
parent 9b5d0866e0
commit 27a43e860a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 306 additions and 158 deletions

View file

@ -11,6 +11,13 @@ import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { accessSecretStorage } from "../../src/SecurityManager";
import { filterConsole, stubClient } from "../test-utils";
import Modal from "../../src/Modal.tsx";
jest.mock("react", () => {
const React = jest.requireActual("react");
React.lazy = (children: any) => children(); // stub out lazy for dialog test
return React;
});
describe("SecurityManager", () => {
describe("accessSecretStorage", () => {
@ -50,5 +57,21 @@ describe("SecurityManager", () => {
}).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
});
});
it("should show CreateSecretStorageDialog if forceReset=true", async () => {
jest.mock("../../src/async-components/views/dialogs/security/CreateSecretStorageDialog", () => ({
__test: true,
__esModule: true,
default: () => jest.fn(),
}));
const spy = jest.spyOn(Modal, "createDialog");
stubClient();
const func = jest.fn();
accessSecretStorage(func, true);
expect(spy).toHaveBeenCalledTimes(1);
await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true }));
});
});
});

View file

@ -0,0 +1,33 @@
/*
* 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, screen, fireEvent, waitFor } from "jest-matrix-react";
import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import Modal from "../../../../../src/Modal.tsx";
describe("<RecoveryMethodRemovedDialog />", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("should open CreateKeyBackupDialog on primary action click", async () => {
const onFinished = jest.fn();
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
render(<RecoveryMethodRemovedDialog onFinished={onFinished} />);
fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
});

View file

@ -149,6 +149,7 @@ describe("<MatrixChat />", () => {
isRoomEncrypted: jest.fn(),
logout: jest.fn(),
getDeviceId: jest.fn(),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
});
let mockClient: Mocked<MatrixClient>;
const serverConfig = {
@ -1515,7 +1516,7 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialogAsync");
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true,
@ -1530,7 +1531,25 @@ describe("<MatrixChat />", () => {
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(await spy.mock.lastCall![0]).toEqual(expect.objectContaining({ __test: true }));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
it("should show the recovery method removed dialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
getComponent({});
defaultDispatcher.dispatch({
action: "will_start_client",
});
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
});
});

View file

@ -10,7 +10,7 @@ import React from "react";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { CryptoApi, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { render, RenderResult } from "jest-matrix-react";
import { fireEvent, render, RenderResult, screen } from "jest-matrix-react";
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils";
import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog";
@ -61,6 +61,9 @@ describe("LogoutDialog", () => {
const rendered = renderComponent();
await rendered.findByText("Start using Key Backup");
expect(rendered.container).toMatchSnapshot();
fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
});
describe("when there is an error fetching backups", () => {

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { render, screen, waitFor } from "jest-matrix-react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixError, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import React from "react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
@ -16,6 +16,7 @@ import { AddRemoveThreepids } from "../../../../../src/components/views/settings
import { clearAllModals, stubClient } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../src/Modal";
import InteractiveAuthDialog from "../../../../../src/components/views/dialogs/InteractiveAuthDialog.tsx";
const MOCK_IDENTITY_ACCESS_TOKEN = "mock_identity_access_token";
const mockGetAccessToken = jest.fn().mockResolvedValue(MOCK_IDENTITY_ACCESS_TOKEN);
@ -222,13 +223,13 @@ describe("AddRemoveThreepids", () => {
const continueButton = await screen.findByRole("button", { name: /Continue/ });
await expect(continueButton).toHaveAttribute("aria-disabled", "true");
expect(continueButton).toHaveAttribute("aria-disabled", "true");
await expect(
await screen.findByText(
screen.findByText(
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
"GB",
@ -481,4 +482,118 @@ describe("AddRemoveThreepids", () => {
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
expect(onChangeFn).toHaveBeenCalled();
});
it("should show UIA dialog when necessary for adding email", async () => {
const onChangeFn = jest.fn();
const createDialogFn = jest.spyOn(Modal, "createDialog");
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
const continueButton = screen.getByRole("button", { name: "Continue" });
expect(continueButton).toBeEnabled();
mocked(client).addThreePidOnly.mockRejectedValueOnce(
new MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
);
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(
InteractiveAuthDialog,
expect.objectContaining({
title: "Add Email Address",
makeRequest: expect.any(Function),
}),
);
});
it("should show UIA dialog when necessary for adding msisdn", async () => {
const onChangeFn = jest.fn();
const createDialogFn = jest.spyOn(Modal, "createDialog");
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
sid: "1",
msisdn: PHONE1.address,
intl_fmt: PHONE1.address,
success: true,
submit_url: "https://some-url",
});
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
await userEvent.click(countryDropdown);
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
await userEvent.click(gbOption);
const input = screen.getByRole("textbox", { name: "Phone Number" });
await userEvent.type(input, PHONE1_LOCALNUM);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
const continueButton = screen.getByRole("button", { name: "Continue" });
expect(continueButton).toHaveAttribute("aria-disabled", "true");
await expect(
screen.findByText(
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
),
).resolves.toBeInTheDocument();
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
"GB",
PHONE1_LOCALNUM,
client.generateClientSecret(),
1,
);
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
await userEvent.type(verificationInput, "123456");
expect(continueButton).not.toHaveAttribute("aria-disabled", "true");
mocked(client).addThreePidOnly.mockRejectedValueOnce(
new MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
);
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(
InteractiveAuthDialog,
expect.objectContaining({
title: "Add Phone Number",
makeRequest: expect.any(Function),
}),
);
});
});

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, waitFor } from "jest-matrix-react";
import { render, waitFor, screen, fireEvent } from "jest-matrix-react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
@ -64,4 +64,34 @@ describe("CryptographyPanel", () => {
// Then "not supported key
await waitFor(() => expect(codes[1].innerHTML).toEqual("<strong>&lt;not supported&gt;</strong>"));
});
it("should open the export e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Export E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
});
it("should open the import e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Import E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Import room keys" })).resolves.toBeInTheDocument();
});
});