New user profile UI in User Settings (#12548)
* New user profile UI in User Settings Using new Edit In Place component. * Show avatar upload error * Fix avatar upload error * Wire up errors & feedback for display name setting * Implement avatar upload / remove progress toast * Add 768px breakpoint * Fix room profile display * Update to released compund-web with required components / fixes * Require compound-web 4.4.0 because we do need it * Update snapshots Because of course all the auto-generated IDs of unrelated things have changed. * Fix duplicate import * Fix CSS comment * Update snapshot * Run all the tests so the ids stay the same * Start of a test for ProfileSettings * More tests * Test that a toast appears * Test ToastRack * Update snapshots * Add the usernamee control * Fix playwright tests * New compound version for editinplace fixes * Fix useId to not just generate a constant ID * Use the label in the username component * Fix widths of test boxes * Update screenshots * Put ^ back on compound-web version * Split CSS for room & user profile settings and name the components correspondingly * Fix playwright test * Update room settings screenshot * Use original screenshot instead * Fix styling of unrelated buttons Needed to be added in other places otherwise the specificity changes. Also put the old screenshots back. * Add copyright year * Fix copyright year
This commit is contained in:
parent
c4c1faff97
commit
cfa322cd62
25 changed files with 919 additions and 307 deletions
201
test/components/views/settings/UserProfileSettings-test.tsx
Normal file
201
test/components/views/settings/UserProfileSettings-test.tsx
Normal file
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { MatrixClient, UploadResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import UserProfileSettings from "../../../../src/components/views/settings/UserProfileSettings";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { ToastContext, ToastRack } from "../../../../src/contexts/ToastContext";
|
||||
import { OwnProfileStore } from "../../../../src/stores/OwnProfileStore";
|
||||
|
||||
interface MockedAvatarSettingProps {
|
||||
removeAvatar: () => void;
|
||||
onChange: (file: File) => void;
|
||||
}
|
||||
|
||||
let removeAvatarFn: () => void;
|
||||
let changeAvatarFn: (file: File) => void;
|
||||
|
||||
jest.mock(
|
||||
"../../../../src/components/views/settings/AvatarSetting",
|
||||
() =>
|
||||
(({ removeAvatar, onChange }) => {
|
||||
removeAvatarFn = removeAvatar;
|
||||
changeAvatarFn = onChange;
|
||||
return <div>Mocked AvatarSetting</div>;
|
||||
}) as React.FC<MockedAvatarSettingProps>,
|
||||
);
|
||||
|
||||
let editInPlaceOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
let editInPlaceOnSave: () => void;
|
||||
let editInPlaceOnCancel: () => void;
|
||||
|
||||
interface MockedEditInPlaceProps {
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
jest.mock("@vector-im/compound-web", () => ({
|
||||
EditInPlace: (({ onChange, onSave, onCancel, value }) => {
|
||||
editInPlaceOnChange = onChange;
|
||||
editInPlaceOnSave = onSave;
|
||||
editInPlaceOnCancel = onCancel;
|
||||
return <div>Mocked EditInPlace: {value}</div>;
|
||||
}) as React.FC<MockedEditInPlaceProps>,
|
||||
}));
|
||||
|
||||
describe("ProfileSettings", () => {
|
||||
let client: MatrixClient;
|
||||
let toastRack: Partial<ToastRack>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
toastRack = {
|
||||
displayToast: jest.fn().mockReturnValue(jest.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
it("removes avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(removeAvatarFn).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
removeAvatarFn();
|
||||
});
|
||||
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("changes avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
const returnedMxcUri = "mxc://example.org/my-avatar";
|
||||
mocked(client).uploadContent.mockResolvedValue({ content_uri: returnedMxcUri });
|
||||
|
||||
const fileSentinel = {};
|
||||
await act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(client.uploadContent).toHaveBeenCalledWith(fileSentinel);
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith(returnedMxcUri);
|
||||
});
|
||||
|
||||
it("displays toast while uploading avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
const clearToastFn = jest.fn();
|
||||
mocked(toastRack.displayToast!).mockReturnValue(clearToastFn);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
let resolveUploadPromise = (r: UploadResponse) => {};
|
||||
const uploadPromise = new Promise<UploadResponse>((r) => {
|
||||
resolveUploadPromise = r;
|
||||
});
|
||||
mocked(client).uploadContent.mockReturnValue(uploadPromise);
|
||||
|
||||
const fileSentinel = {};
|
||||
const changeAvatarActPromise = act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(toastRack.displayToast).toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
resolveUploadPromise({ content_uri: "bloop" });
|
||||
});
|
||||
await changeAvatarActPromise;
|
||||
|
||||
expect(clearToastFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("changes display name", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnSave).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "The Value" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await editInPlaceOnSave();
|
||||
});
|
||||
|
||||
expect(client.setDisplayName).toHaveBeenCalledWith("The Value");
|
||||
});
|
||||
|
||||
it("resets on cancel", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnChange).toBeDefined();
|
||||
expect(editInPlaceOnCancel).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "Alicia Zattic" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alicia Zattic")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnCancel();
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -42,14 +42,14 @@ exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||
>
|
||||
<input
|
||||
autocomplete="email"
|
||||
id="mx_Field_61"
|
||||
id="mx_Field_50"
|
||||
label="Email Address"
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_61"
|
||||
for="mx_Field_50"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
|
@ -150,14 +150,14 @@ exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||
</span>
|
||||
<input
|
||||
autocomplete="tel-national"
|
||||
id="mx_Field_62"
|
||||
id="mx_Field_51"
|
||||
label="Phone Number"
|
||||
placeholder="Phone Number"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_62"
|
||||
for="mx_Field_51"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
|
|
|
@ -127,7 +127,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
|
|||
|
||||
exports[`ThreadsActivityCentre should close the release announcement when the TAC button is clicked 1`] = `
|
||||
<body
|
||||
data-scroll-locked=""
|
||||
data-scroll-locked="1"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<span
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue