element-web/test/components/views/settings/AddRemoveThreepids-test.tsx
David Baker 9aa09d4b15
Maybe fix flakey AddRemoveThreepid test (#81)
I have no idea why this is flaking. There are warnings about
things not being wrapped in act() which may be relevant... this makes
the warnings happy, although apparently should not be necessary.
https://github.com/testing-library/user-event/discussions/906 and
https://github.com/testing-library/user-event/issues/497 are
depressing reading (making the versions the same didn't help). I think
my conclusion might be to do this until we're able to upgrade to the
latest testing-library, then re-evaluate.

It still may or may not fix the flake.
2024-09-23 08:29:24 +00:00

535 lines
18 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2024 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 { render, screen, waitFor } from "@testing-library/react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import React from "react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { AddRemoveThreepids } from "../../../../src/components/views/settings/AddRemoveThreepids";
import { clearAllModals, stubClient } from "../../../test-utils";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../src/Modal";
const MOCK_IDENTITY_ACCESS_TOKEN = "mock_identity_access_token";
const mockGetAccessToken = jest.fn().mockResolvedValue(MOCK_IDENTITY_ACCESS_TOKEN);
jest.mock("../../../../src/IdentityAuthClient", () =>
jest.fn().mockImplementation(() => ({
getAccessToken: mockGetAccessToken,
})),
);
const EMAIL1 = {
medium: ThreepidMedium.Email,
address: "alice@nowhere.dummy",
};
const PHONE1 = {
medium: ThreepidMedium.Phone,
address: "447700900000",
};
const PHONE1_LOCALNUM = "07700900000";
describe("AddRemoveThreepids", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
});
afterEach(() => {
jest.restoreAllMocks();
clearAllModals();
});
const clientProviderWrapper: React.FC = ({ children }) => (
<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
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={false}
onChange={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
it("should add an email address", async () => {
const onChangeFn = jest.fn();
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);
expect(client.requestAdd3pidEmailToken).toHaveBeenCalledWith(EMAIL1.address, client.generateClientSecret(), 1);
const continueButton = screen.getByRole("button", { name: "Continue" });
expect(continueButton).toBeEnabled();
await userEvent.click(continueButton);
expect(client.addThreePidOnly).toHaveBeenCalledWith({
client_secret: client.generateClientSecret(),
sid: "1",
auth: undefined,
});
expect(onChangeFn).toHaveBeenCalled();
});
it("should display an error if the link has not been clicked", 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 Error("Unauthorized"));
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
description: "Unauthorized",
title: "Unable to verify email address.",
});
expect(onChangeFn).not.toHaveBeenCalled();
});
it("should add a phone number", async () => {
const onChangeFn = jest.fn();
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
sid: "1",
msisdn: PHONE1.address,
intl_fmt: "+" + PHONE1.address,
success: true,
submit_url: "https://example.dummy",
});
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" });
userEvent.click(addButton);
const continueButton = await screen.findByRole("button", { name: "Continue" });
await expect(continueButton).toHaveAttribute("aria-disabled", "true");
await expect(
await screen.findByText(
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
),
).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");
userEvent.click(continueButton);
await waitFor(() => expect(continueButton).toHaveAttribute("aria-disabled", "true"));
expect(client.addThreePidOnly).toHaveBeenCalledWith({
client_secret: client.generateClientSecret(),
sid: "1",
auth: undefined,
});
expect(onChangeFn).toHaveBeenCalled();
});
it("should display an error if the code is incorrect", 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://example.dummy",
});
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const input = screen.getByRole("textbox", { name: "Phone Number" });
await userEvent.type(input, PHONE1_LOCALNUM);
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 addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
mocked(client).addThreePidOnly.mockRejectedValueOnce(new Error("Unauthorized"));
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
await userEvent.type(verificationInput, "123457");
const continueButton = screen.getByRole("button", { name: "Continue" });
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
description: "Unauthorized",
title: "Unable to verify phone number.",
});
expect(onChangeFn).not.toHaveBeenCalled();
});
it("should remove an email address", async () => {
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const removeButton = screen.getByRole("button", { name: "Remove" });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
const confirmRemoveButton = screen.getByRole("button", { name: "Remove" });
await userEvent.click(confirmRemoveButton);
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
expect(onChangeFn).toHaveBeenCalled();
});
it("should return to default view if adding is cancelled", async () => {
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[EMAIL1]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const removeButton = screen.getByRole("button", { name: "Remove" });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${EMAIL1.address}?`)).toBeVisible();
const confirmRemoveButton = screen.getByRole("button", { name: "Cancel" });
await userEvent.click(confirmRemoveButton);
expect(screen.queryByText(`Remove ${EMAIL1.address}?`)).not.toBeInTheDocument();
expect(client.deleteThreePid).not.toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
expect(onChangeFn).not.toHaveBeenCalled();
});
it("should remove a phone number", async () => {
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[PHONE1]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const removeButton = screen.getByRole("button", { name: "Remove" });
await userEvent.click(removeButton);
expect(screen.getByText(`Remove ${PHONE1.address}?`)).toBeVisible();
const confirmRemoveButton = screen.getByRole("button", { name: "Remove" });
await userEvent.click(confirmRemoveButton);
expect(client.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
expect(onChangeFn).toHaveBeenCalled();
});
it("should bind an email address", async () => {
mocked(client).requestEmailToken.mockResolvedValue({ sid: "1" });
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="is"
medium={ThreepidMedium.Email}
threepids={[Object.assign({}, EMAIL1, { bound: false })]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
const shareButton = screen.getByRole("button", { name: "Share" });
await userEvent.click(shareButton);
expect(screen.getByText("Verify the link in your inbox")).toBeVisible();
expect(client.requestEmailToken).toHaveBeenCalledWith(
EMAIL1.address,
client.generateClientSecret(),
1,
undefined,
MOCK_IDENTITY_ACCESS_TOKEN,
);
const completeButton = screen.getByRole("button", { name: "Complete" });
await userEvent.click(completeButton);
expect(client.bindThreePid).toHaveBeenCalledWith({
sid: "1",
client_secret: client.generateClientSecret(),
id_server: "https://the_best_id_server.dummy",
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
});
expect(onChangeFn).toHaveBeenCalled();
});
it("should bind a phone number", async () => {
mocked(client).requestMsisdnToken.mockResolvedValue({
success: true,
sid: "1",
msisdn: PHONE1.address,
intl_fmt: "+" + PHONE1.address,
});
mocked(client).getIdentityServerUrl.mockReturnValue("https://the_best_id_server.dummy");
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="is"
medium={ThreepidMedium.Phone}
threepids={[Object.assign({}, PHONE1, { bound: false })]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
const shareButton = screen.getByRole("button", { name: "Share" });
await userEvent.click(shareButton);
expect(screen.getByText("Please enter verification code sent via text.")).toBeVisible();
expect(client.requestMsisdnToken).toHaveBeenCalledWith(
null,
"+" + PHONE1.address,
client.generateClientSecret(),
1,
undefined,
MOCK_IDENTITY_ACCESS_TOKEN,
);
const codeInput = screen.getByRole("textbox", { name: "Verification code" });
await userEvent.type(codeInput, "123456");
await userEvent.keyboard("{Enter}");
expect(client.bindThreePid).toHaveBeenCalledWith({
sid: "1",
client_secret: client.generateClientSecret(),
id_server: "https://the_best_id_server.dummy",
id_access_token: MOCK_IDENTITY_ACCESS_TOKEN,
});
expect(onChangeFn).toHaveBeenCalled();
});
it("should revoke a bound email address", async () => {
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="is"
medium={ThreepidMedium.Email}
threepids={[Object.assign({}, EMAIL1, { bound: true })]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
expect(screen.getByText(EMAIL1.address)).toBeVisible();
const revokeButton = screen.getByRole("button", { name: "Revoke" });
await userEvent.click(revokeButton);
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, EMAIL1.address);
expect(onChangeFn).toHaveBeenCalled();
});
it("should revoke a bound phone number", async () => {
const onChangeFn = jest.fn();
render(
<AddRemoveThreepids
mode="is"
medium={ThreepidMedium.Phone}
threepids={[Object.assign({}, PHONE1, { bound: true })]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
expect(screen.getByText(PHONE1.address)).toBeVisible();
const revokeButton = screen.getByRole("button", { name: "Revoke" });
await userEvent.click(revokeButton);
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
expect(onChangeFn).toHaveBeenCalled();
});
});