a8aa4de4b4
* Strict typechecking fixes for Base/Member/Avatar Update the core avatar files to pass `--strict --noImplicitAny` typechecks. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Add tests for Base/Member/Avatar More thoroughly test the core avatar files. Not necessarily the most thorough, but an improvement. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Extract TextAvatar from BaseAvatar Extracted the fallback/textual avatar into its own component. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Use standard HTML for non-image avatars Firefox users with `resistFingerprinting` enabled were seeing random noise for rooms and users without avatars. There's no real reason to use data URLs to present flat colors. This converts non-image avatars to inline blocks with background colors. See https://github.com/vector-im/element-web/issues/23936 Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Have pills use solid backgrounds rather than colored images Similar to room and member avatars, pills now use colored pseudo-elements rather than background images. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> --------- Signed-off-by: Clark Fischer <clark.fischer@gmail.com> Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
/*
|
|
Copyright 2023 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 { fireEvent, render } from "@testing-library/react";
|
|
import { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
import React from "react";
|
|
import { act } from "react-dom/test-utils";
|
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
|
|
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
|
import RoomContext from "../../../../src/contexts/RoomContext";
|
|
import { getRoomContext } from "../../../test-utils/room";
|
|
import { stubClient } from "../../../test-utils/test-utils";
|
|
import BaseAvatar from "../../../../src/components/views/avatars/BaseAvatar";
|
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
|
|
|
type Props = React.ComponentPropsWithoutRef<typeof BaseAvatar>;
|
|
|
|
describe("<BaseAvatar />", () => {
|
|
let client: MatrixClient;
|
|
let room: Room;
|
|
let member: RoomMember;
|
|
|
|
function getComponent(props: Partial<Props>) {
|
|
return (
|
|
<MatrixClientContext.Provider value={client}>
|
|
<RoomContext.Provider value={getRoomContext(room, {})}>
|
|
<BaseAvatar name="" {...props} />
|
|
</RoomContext.Provider>
|
|
</MatrixClientContext.Provider>
|
|
);
|
|
}
|
|
|
|
function failLoadingImg(container: HTMLElement): void {
|
|
const img = container.querySelector<HTMLImageElement>("img")!;
|
|
expect(img).not.toBeNull();
|
|
act(() => {
|
|
fireEvent.error(img);
|
|
});
|
|
}
|
|
|
|
function emitReconnect(): void {
|
|
act(() => {
|
|
client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Reconnecting);
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
client = stubClient();
|
|
|
|
room = new Room("!room:example.com", client, client.getUserId() ?? "", {
|
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
});
|
|
|
|
member = new RoomMember(room.roomId, "@bob:example.org");
|
|
jest.spyOn(room, "getMember").mockReturnValue(member);
|
|
});
|
|
|
|
it("renders with minimal properties", () => {
|
|
const { container } = render(getComponent({}));
|
|
|
|
expect(container.querySelector(".mx_BaseAvatar")).not.toBeNull();
|
|
});
|
|
|
|
it("matches snapshot (avatar)", () => {
|
|
const { container } = render(
|
|
getComponent({
|
|
name: "CoolUser22",
|
|
title: "Hover title",
|
|
url: "https://example.com/images/avatar.gif",
|
|
className: "mx_SomethingArbitrary",
|
|
}),
|
|
);
|
|
|
|
expect(container).toMatchSnapshot();
|
|
});
|
|
|
|
it("matches snapshot (avatar + click)", () => {
|
|
const { container } = render(
|
|
getComponent({
|
|
name: "CoolUser22",
|
|
title: "Hover title",
|
|
url: "https://example.com/images/avatar.gif",
|
|
className: "mx_SomethingArbitrary",
|
|
onClick: () => {},
|
|
}),
|
|
);
|
|
|
|
expect(container).toMatchSnapshot();
|
|
});
|
|
|
|
it("matches snapshot (no avatar)", () => {
|
|
const { container } = render(
|
|
getComponent({
|
|
name: "xX_Element_User_Xx",
|
|
title: ":kiss:",
|
|
defaultToInitialLetter: true,
|
|
className: "big-and-bold",
|
|
}),
|
|
);
|
|
|
|
expect(container).toMatchSnapshot();
|
|
});
|
|
|
|
it("matches snapshot (no avatar + click)", () => {
|
|
const { container } = render(
|
|
getComponent({
|
|
name: "xX_Element_User_Xx",
|
|
title: ":kiss:",
|
|
defaultToInitialLetter: true,
|
|
className: "big-and-bold",
|
|
onClick: () => {},
|
|
}),
|
|
);
|
|
|
|
expect(container).toMatchSnapshot();
|
|
});
|
|
|
|
it("uses fallback images", () => {
|
|
const images = [...Array(10)].map((_, i) => `https://example.com/images/${i}.webp`);
|
|
|
|
const { container } = render(
|
|
getComponent({
|
|
url: images[0],
|
|
urls: images.slice(1),
|
|
}),
|
|
);
|
|
|
|
for (const image of images) {
|
|
expect(container.querySelector("img")!.src).toBe(image);
|
|
failLoadingImg(container);
|
|
}
|
|
});
|
|
|
|
it("re-renders on reconnect", () => {
|
|
const primary = "https://example.com/image.jpeg";
|
|
const fallback = "https://example.com/fallback.png";
|
|
const { container } = render(
|
|
getComponent({
|
|
url: primary,
|
|
urls: [fallback],
|
|
}),
|
|
);
|
|
|
|
failLoadingImg(container);
|
|
expect(container.querySelector("img")!.src).toBe(fallback);
|
|
|
|
emitReconnect();
|
|
expect(container.querySelector("img")!.src).toBe(primary);
|
|
});
|
|
|
|
it("renders with an image", () => {
|
|
const url = "https://example.com/images/small/avatar.gif?size=realBig";
|
|
const { container } = render(getComponent({ url }));
|
|
|
|
const img = container.querySelector("img");
|
|
expect(img!.src).toBe(url);
|
|
});
|
|
|
|
it("renders the initial letter", () => {
|
|
const { container } = render(getComponent({ name: "Yellow", defaultToInitialLetter: true }));
|
|
|
|
const avatar = container.querySelector<HTMLSpanElement>(".mx_BaseAvatar_initial")!;
|
|
expect(avatar.innerHTML).toBe("Y");
|
|
});
|
|
|
|
it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])(
|
|
"includes a click handler",
|
|
(props: Partial<Props>) => {
|
|
const onClick = jest.fn();
|
|
|
|
const { container } = render(
|
|
getComponent({
|
|
...props,
|
|
onClick,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
|
|
});
|
|
|
|
expect(onClick).toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|