diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts index 2deb58574bc..f07746c0f56 100644 --- a/cypress/e2e/spaces/spaces.spec.ts +++ b/cypress/e2e/spaces/spaces.spec.ts @@ -153,7 +153,10 @@ describe("Spaces", () => { openSpaceCreateMenu().within(() => { cy.get(".mx_SpaceCreateMenuType_private").click(); - // We don't set an avatar here to get a Percy snapshot of the default avatar style for spaces + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile( + "cypress/fixtures/riot.png", + { force: true }, + ); cy.get('input[label="Address"]').should("not.exist"); cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im..."); cy.get('input[label="Name"]').type("This is my Riot{enter}"); @@ -166,7 +169,6 @@ describe("Spaces", () => { cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist"); cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist"); - cy.get(".mx_LeftPanel_outerWrapper").percySnapshotElement("Left panel with default avatar space"); }); it("should allow user to invite another to a space", () => { diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 859714bfb9f..70c65b3f74f 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -277,11 +277,14 @@ $activeBorderColor: $primary-content; .mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial { color: $secondary-content; border-radius: 8px; + background-color: $panel-actions; + font-size: $font-15px !important; /* override inline style */ font-weight: $font-semi-bold; line-height: $font-18px; - /* override inline styles which are part of the default avatar style as these uses a monochrome style */ - background-color: $panel-actions !important; - font-size: $font-15px !important; + + & + .mx_BaseAvatar_image { + visibility: hidden; + } } .mx_SpaceTreeLevel { diff --git a/res/css/views/avatars/_BaseAvatar.pcss b/res/css/views/avatars/_BaseAvatar.pcss index 43e273b6ffe..a6a4b0b74b2 100644 --- a/res/css/views/avatars/_BaseAvatar.pcss +++ b/res/css/views/avatars/_BaseAvatar.pcss @@ -16,7 +16,16 @@ limitations under the License. .mx_BaseAvatar { position: relative; - display: block; + /* In at least Firefox, the case of relative positioned inline elements */ + /* (such as mx_BaseAvatar) with absolute positioned children (such as */ + /* mx_BaseAvatar_initial) is a dark corner full of spider webs. It will give */ + /* different results during full reflow of the page vs. incremental reflow */ + /* of small portions. While that's surely a browser bug, we can avoid it by */ + /* using `inline-block` instead of the default `inline`. */ + /* https://github.com/vector-im/element-web/issues/5594 */ + /* https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 */ + /* https://bugzilla.mozilla.org/show_bug.cgi?id=255139 */ + display: inline-block; user-select: none; &.mx_RoomAvatar_isSpaceRoom { diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss index 32e7c5288f6..7b88a058153 100644 --- a/res/css/views/rooms/_BasicMessageComposer.pcss +++ b/res/css/views/rooms/_BasicMessageComposer.pcss @@ -78,7 +78,7 @@ limitations under the License. min-width: $font-16px; /* ensure the avatar is not compressed */ height: $font-16px; margin-inline-end: 0.24rem; - background: var(--avatar-background); + background: var(--avatar-background), $background; color: $avatar-initial-color; background-repeat: no-repeat; background-size: $font-16px; diff --git a/src/Avatar.ts b/src/Avatar.ts index 3e6b18dbc79..8a3f10a22ca 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2023 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,19 +24,16 @@ import DMRoomMap from "./utils/DMRoomMap"; import { mediaFromMxc } from "./customisations/Media"; import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; -const DEFAULT_COLORS: Readonly = ["#0DBD8B", "#368bd6", "#ac3ba8"]; - // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( - member: RoomMember | null | undefined, + member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod, ): string { - let url: string | undefined; - const mxcUrl = member?.getMxcAvatarUrl(); - if (mxcUrl) { - url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); + let url: string; + if (member?.getMxcAvatarUrl()) { + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } if (!url) { // member can be null here currently since on invites, the JS SDK @@ -47,17 +44,6 @@ export function avatarUrlForMember( return url; } -export function getMemberAvatar( - member: RoomMember | null | undefined, - width: number, - height: number, - resizeMethod: ResizeMethod, -): string | undefined { - const mxcUrl = member?.getMxcAvatarUrl(); - if (!mxcUrl) return undefined; - return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); -} - export function avatarUrlForUser( user: Pick, width: number, @@ -100,10 +86,18 @@ function urlForColor(color: string): string { // hard to install a listener here, even if there were a clear event to listen to const colorToDataURLCache = new Map(); -export function defaultAvatarUrlForString(s: string | undefined): string { +export function defaultAvatarUrlForString(s: string): string { if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake - - const color = getColorForString(s); + const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"]; + let total = 0; + for (let i = 0; i < s.length; ++i) { + total += s.charCodeAt(i); + } + const colorIndex = total % defaultColors.length; + // overwritten color value in custom themes + const cssVariable = `--avatar-background-colors_${colorIndex}`; + const cssValue = document.body.style.getPropertyValue(cssVariable); + const color = cssValue || defaultColors[colorIndex]; let dataUrl = colorToDataURLCache.get(color); if (!dataUrl) { // validate color as this can come from account_data @@ -118,23 +112,13 @@ export function defaultAvatarUrlForString(s: string | undefined): string { return dataUrl; } -export function getColorForString(input: string): string { - const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0); - const index = charSum % DEFAULT_COLORS.length; - - // overwritten color value in custom themes - const cssVariable = `--avatar-background-colors_${index}`; - const cssValue = document.body.style.getPropertyValue(cssVariable); - return cssValue || DEFAULT_COLORS[index]!; -} - /** * returns the first (non-sigil) character of 'name', * converted to uppercase * @param {string} name * @return {string} the first letter */ -export function getInitialLetter(name: string): string | undefined { +export function getInitialLetter(name: string): string { if (!name) { // XXX: We should find out what causes the name to sometimes be falsy. console.trace("`name` argument to `getInitialLetter` not supplied"); @@ -150,20 +134,19 @@ export function getInitialLetter(name: string): string | undefined { } // rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis - return split(name, "", 1)[0]!.toUpperCase(); + return split(name, "", 1)[0].toUpperCase(); } export function avatarUrlForRoom( - room: Room | undefined, + room: Room, width: number, height: number, resizeMethod?: ResizeMethod, ): string | null { if (!room) return null; // null-guard - const mxcUrl = room.getMxcAvatarUrl(); - if (mxcUrl) { - return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); + if (room.getMxcAvatarUrl()) { + return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } // space rooms cannot be DMs so skip the rest @@ -176,9 +159,8 @@ export function avatarUrlForRoom( // If there are only two members in the DM use the avatar of the other member const otherMember = room.getAvatarFallbackMember(); - const otherMemberMxc = otherMember?.getMxcAvatarUrl(); - if (otherMemberMxc) { - return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod); + if (otherMember?.getMxcAvatarUrl()) { + return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } return null; } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 7fb0ba458f5..025cb9d2711 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -1,6 +1,8 @@ /* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015, 2016, 2018, 2019, 2020, 2023 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -15,46 +17,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { CSSProperties, useCallback, useContext, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useState } from "react"; import classNames from "classnames"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import { ClientEvent } from "matrix-js-sdk/src/client"; -import { SyncState } from "matrix-js-sdk/src/sync"; import * as AvatarLogic from "../../../Avatar"; +import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { toPx } from "../../../utils/units"; import { _t } from "../../../languageHandler"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; interface IProps { - /** The name (first initial used as default) */ - name: string; - /** ID for generating hash colours */ - idName?: string; - /** onHover title text */ - title?: string; - /** highest priority of them all, shortcut to set in urls[0] */ - url?: string; - /** [highest_priority, ... , lowest_priority] */ - urls?: string[]; + name: string; // The name (first initial used as default) + idName?: string; // ID for generating hash colours + title?: string; // onHover title text + url?: string; // highest priority of them all, shortcut to set in urls[0] + urls?: string[]; // [highest_priority, ... , lowest_priority] width?: number; height?: number; - /** @deprecated not actually used */ + // XXX: resizeMethod not actually used. resizeMethod?: ResizeMethod; - /** true to add default url */ - defaultToInitialLetter?: boolean; - onClick?: React.ComponentPropsWithoutRef["onClick"]; + defaultToInitialLetter?: boolean; // true to add default url + onClick?: React.MouseEventHandler; inputRef?: React.RefObject; className?: string; tabIndex?: number; - style?: CSSProperties; } -const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => { +const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, ...props.urls ] @@ -72,26 +66,11 @@ const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowB return Array.from(new Set(_urls)); }; -/** - * Hook for cycling through a changing set of images. - * - * The set of images is updated whenever `url` or `urls` change, the user's - * `lowBandwidth` preference changes, or the client reconnects. - * - * Returns `[imageUrl, onError]`. When `onError` is called, the next image in - * the set will be displayed. - */ -const useImageUrl = ({ - url, - urls, -}: { - url: string | undefined; - urls: string[] | undefined; -}): [string | undefined, () => void] => { +const useImageUrl = ({ url, urls }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists const roomContext = useContext(RoomContext); - const lowBandwidth = roomContext.lowBandwidth; + const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState(0); @@ -106,10 +85,10 @@ const useImageUrl = ({ }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps const cli = useContext(MatrixClientContext); - const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => { + const onClientSync = useCallback((syncState, prevState) => { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. - const reconnected = syncState !== SyncState.Error && prevState !== syncState; + const reconnected = syncState !== "ERROR" && prevState !== syncState; if (reconnected) { setIndex(0); } @@ -129,25 +108,46 @@ const BaseAvatar: React.FC = (props) => { urls, width = 40, height = 40, + resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars defaultToInitialLetter = true, onClick, inputRef, className, - style: parentStyle, - resizeMethod: _unused, // to keep it from being in `otherProps` ...otherProps } = props; - const style = { - ...parentStyle, - width: toPx(width), - height: toPx(height), - }; - const [imageUrl, onError] = useImageUrl({ url, urls }); if (!imageUrl && defaultToInitialLetter && name) { - const avatar = ; + const initialLetter = AvatarLogic.getInitialLetter(name); + const textNode = ( + + ); + const imgNode = ( + + ); if (onClick) { return ( @@ -159,9 +159,9 @@ const BaseAvatar: React.FC = (props) => { className={classNames("mx_BaseAvatar", className)} onClick={onClick} inputRef={inputRef} - style={style} > - {avatar} + {textNode} + {imgNode} ); } else { @@ -170,10 +170,10 @@ const BaseAvatar: React.FC = (props) => { className={classNames("mx_BaseAvatar", className)} ref={inputRef} {...otherProps} - style={style} role="presentation" > - {avatar} + {textNode} + {imgNode} ); } @@ -187,7 +187,10 @@ const BaseAvatar: React.FC = (props) => { src={imageUrl} onClick={onClick} onError={onError} - style={style} + style={{ + width: toPx(width), + height: toPx(height), + }} title={title} alt={_t("Avatar")} inputRef={inputRef} @@ -201,7 +204,10 @@ const BaseAvatar: React.FC = (props) => { className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)} src={imageUrl} onError={onError} - style={style} + style={{ + width: toPx(width), + height: toPx(height), + }} title={title} alt="" ref={inputRef} @@ -214,31 +220,3 @@ const BaseAvatar: React.FC = (props) => { export default BaseAvatar; export type BaseAvatarType = React.FC; - -const TextAvatar: React.FC<{ - name: string; - idName?: string; - width: number; - height: number; - title?: string; -}> = ({ name, idName, width, height, title }) => { - const initialLetter = AvatarLogic.getInitialLetter(name); - - return ( - - ); -}; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index f493c58f8cf..48138714559 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -1,5 +1,6 @@ /* -Copyright 2015, 2016, 2019 - 2023 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 - 2022 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. @@ -25,7 +26,6 @@ import { mediaFromMxc } from "../../../customisations/Media"; import { CardContext } from "../right_panel/context"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile"; -import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember | null; @@ -33,13 +33,14 @@ interface IProps extends Omit, "name" | width: number; height: number; resizeMethod?: ResizeMethod; - /** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */ + // The onClick to give the avatar + onClick?: React.MouseEventHandler; + // Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` viewUserOnClick?: boolean; pushUserOnClick?: boolean; title?: string; - style?: React.CSSProperties; - /** true to deny `useOnlyCurrentProfiles` usage. Default false. */ - forceHistorical?: boolean; + style?: any; + forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false. hideTitle?: boolean; } @@ -76,8 +77,8 @@ export default function MemberAvatar({ if (!title) { title = - UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, { - roomId: member.roomId, + UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", { + roomId: member?.roomId ?? "", }) ?? fallbackUserId; } } @@ -87,6 +88,7 @@ export default function MemberAvatar({ {...props} width={width} height={height} + resizeMethod={resizeMethod} name={name ?? ""} title={hideTitle ? undefined : title} idName={member?.userId ?? fallbackUserId} @@ -94,9 +96,9 @@ export default function MemberAvatar({ onClick={ viewUserOnClick ? () => { - dis.dispatch({ + dis.dispatch({ action: Action.ViewUser, - member: propsMember || undefined, + member: propsMember, push: card.isCard, }); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 4abfdbbf67e..50389c77491 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -109,8 +109,7 @@ export default class RoomAvatar extends React.Component { } private onRoomAvatarClick = (): void => { - const avatarMxc = this.props.room?.getMxcAvatarUrl(); - const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null; + const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null); const params = { src: avatarUrl, name: this.props.room.name, diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index a09804babee..20df21beb49 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -28,11 +28,4 @@ export interface ViewUserPayload extends ActionPayload { * should be shown (hide whichever relevant components). */ member?: RoomMember | User; - - /** - * Should this event be pushed as a card into the right panel? - * - * @see RightPanelStore#pushCard - */ - push?: boolean; } diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 100d58c78fc..2c8596b492e 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -1,5 +1,6 @@ /* -Copyright 2019, 2023 The Matrix.org Foundation C.I.C. +Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -300,9 +301,9 @@ export abstract class PillPart extends BasePart implements IPillPart { } // helper method for subclasses - protected setAvatarVars(node: HTMLElement, avatarBackground: string, initialLetter: string | undefined): void { - // const avatarBackground = `url('${avatarUrl}')`; - const avatarLetter = `'${initialLetter || ""}'`; + protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void { + const avatarBackground = `url('${avatarUrl}')`; + const avatarLetter = `'${initialLetter}'`; // check if the value is changing, // otherwise the avatars flicker on every keystroke while updating. if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) { @@ -418,15 +419,13 @@ class RoomPillPart extends PillPart { } protected setAvatar(node: HTMLElement): void { - const avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); - if (avatarUrl) { - this.setAvatarVars(node, `url('${avatarUrl}')`, ""); - return; + let initialLetter = ""; + let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); + if (!avatarUrl) { + initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId); + avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId); } - - const initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId); - const color = Avatar.getColorForString(this.room?.roomId ?? this.resourceId); - this.setAvatarVars(node, color, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } public get type(): IPillPart["type"] { @@ -472,17 +471,14 @@ class UserPillPart extends PillPart { if (!this.member) { return; } - - const avatar = Avatar.getMemberAvatar(this.member, 16, 16, "crop"); - if (avatar) { - this.setAvatarVars(node, `url('${avatar}')`, ""); - return; - } - const name = this.member.name || this.member.userId; - const initialLetter = Avatar.getInitialLetter(name); - const color = Avatar.getColorForString(this.member.userId); - this.setAvatarVars(node, color, initialLetter); + const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId); + const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop"); + let initialLetter = ""; + if (avatarUrl === defaultAvatarUrl) { + initialLetter = Avatar.getInitialLetter(name); + } + this.setAvatarVars(node, avatarUrl, initialLetter); } protected onClick = (): void => { diff --git a/test/Avatar-test.ts b/test/Avatar-test.ts index 8b4ee03b7fc..0ff064ed57d 100644 --- a/test/Avatar-test.ts +++ b/test/Avatar-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. +Copyright 2022 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. @@ -15,106 +15,33 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { Room, RoomMember, RoomType, User } from "matrix-js-sdk/src/matrix"; - -import { - avatarUrlForMember, - avatarUrlForRoom, - avatarUrlForUser, - defaultAvatarUrlForString, - getColorForString, - getInitialLetter, -} from "../src/Avatar"; -import { mediaFromMxc } from "../src/customisations/Media"; +import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix"; + +import { avatarUrlForRoom } from "../src/Avatar"; +import { Media, mediaFromMxc } from "../src/customisations/Media"; import DMRoomMap from "../src/utils/DMRoomMap"; -import { filterConsole, stubClient } from "./test-utils"; + +jest.mock("../src/customisations/Media", () => ({ + mediaFromMxc: jest.fn(), +})); const roomId = "!room:example.com"; const avatarUrl1 = "https://example.com/avatar1"; const avatarUrl2 = "https://example.com/avatar2"; -describe("avatarUrlForMember", () => { - let member: RoomMember; - - beforeEach(() => { - stubClient(); - member = new RoomMember(roomId, "@user:example.com"); - }); - - it("returns the member's url", () => { - const mxc = "mxc://example.com/a/b/c/d/avatar.gif"; - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxc); - - expect(avatarUrlForMember(member, 32, 32, "crop")).toBe( - mediaFromMxc(mxc).getThumbnailOfSourceHttp(32, 32, "crop"), - ); - }); - - it("returns a default if the member has no avatar", () => { - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined); - - expect(avatarUrlForMember(member, 32, 32, "crop")).toMatch(/^data:/); - }); -}); - -describe("avatarUrlForUser", () => { - let user: User; - - beforeEach(() => { - stubClient(); - user = new User("@user:example.com"); - }); - - it("should return the user's avatar", () => { - const mxc = "mxc://example.com/a/b/c/d/avatar.gif"; - user.avatarUrl = mxc; - - expect(avatarUrlForUser(user, 64, 64, "scale")).toBe( - mediaFromMxc(mxc).getThumbnailOfSourceHttp(64, 64, "scale"), - ); - }); - - it("should not provide a fallback", () => { - expect(avatarUrlForUser(user, 64, 64, "scale")).toBeNull(); - }); -}); - -describe("defaultAvatarUrlForString", () => { - it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { - expect(defaultAvatarUrlForString(s)).not.toBe(""); - }); -}); - -describe("getColorForString", () => { - it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { - expect(getColorForString(s)).toMatch(/^#\w+$/); - }); - - it("should return different values for different strings", () => { - expect(getColorForString("a")).not.toBe(getColorForString("b")); - }); -}); - -describe("getInitialLetter", () => { - filterConsole("argument to `getInitialLetter` not supplied"); - - it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { - expect(getInitialLetter(s)).not.toBe(""); - }); - - it("should return undefined for empty strings", () => { - expect(getInitialLetter("")).toBeUndefined(); - }); -}); - describe("avatarUrlForRoom", () => { + let getThumbnailOfSourceHttp: jest.Mock; let room: Room; let roomMember: RoomMember; let dmRoomMap: DMRoomMap; beforeEach(() => { - stubClient(); - + getThumbnailOfSourceHttp = jest.fn(); + mocked(mediaFromMxc).mockImplementation((): Media => { + return { + getThumbnailOfSourceHttp, + } as unknown as Media; + }); room = { roomId, getMxcAvatarUrl: jest.fn(), @@ -132,14 +59,14 @@ describe("avatarUrlForRoom", () => { }); it("should return null for a null room", () => { - expect(avatarUrlForRoom(undefined, 128, 128)).toBeNull(); + expect(avatarUrlForRoom(null, 128, 128)).toBeNull(); }); it("should return the HTTP source if the room provides a MXC url", () => { mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); - expect(avatarUrlForRoom(room, 128, 256, "crop")).toBe( - mediaFromMxc(avatarUrl1).getThumbnailOfSourceHttp(128, 256, "crop"), - ); + getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); + expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); }); it("should return null for a space room", () => { @@ -156,7 +83,7 @@ describe("avatarUrlForRoom", () => { it("should return null if there is no other member in the room", () => { mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); - mocked(room.getAvatarFallbackMember).mockReturnValue(undefined); + mocked(room.getAvatarFallbackMember).mockReturnValue(null); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); }); @@ -170,8 +97,8 @@ describe("avatarUrlForRoom", () => { mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); - expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual( - mediaFromMxc(avatarUrl2).getThumbnailOfSourceHttp(128, 256, "crop"), - ); + getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); + expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); }); }); diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index c81e180c421..47318525d56 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -20,16 +20,22 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 + @@ -113,16 +119,22 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] + @@ -203,17 +215,23 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" - style="width: 52px; height: 52px;" tabindex="0" > +

@user:example.com @@ -296,16 +314,22 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = + @@ -386,17 +410,23 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" - style="width: 52px; height: 52px;" tabindex="0" > +

@user:example.com @@ -551,16 +581,22 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t + @@ -636,17 +672,23 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" - style="width: 52px; height: 52px;" tabindex="0" > +

@user:example.com diff --git a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap index 0546900abb3..769711434a8 100644 --- a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap +++ b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap @@ -20,16 +20,22 @@ exports[` when rendered should render as expected 1`] = ` + diff --git a/test/components/views/avatars/BaseAvatar-test.tsx b/test/components/views/avatars/BaseAvatar-test.tsx deleted file mode 100644 index 294a64c4362..00000000000 --- a/test/components/views/avatars/BaseAvatar-test.tsx +++ /dev/null @@ -1,201 +0,0 @@ -/* -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; - -describe("", () => { - let client: MatrixClient; - let room: Room; - let member: RoomMember; - - function getComponent(props: Partial) { - return ( - - - - - - ); - } - - function failLoadingImg(container: HTMLElement): void { - const img = container.querySelector("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(".mx_BaseAvatar_initial")!; - expect(avatar.innerHTML).toBe("Y"); - }); - - it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])( - "includes a click handler", - (props: Partial) => { - const onClick = jest.fn(); - - const { container } = render( - getComponent({ - ...props, - onClick, - }), - ); - - act(() => { - fireEvent.click(container.querySelector(".mx_BaseAvatar")!); - }); - - expect(onClick).toHaveBeenCalled(); - }, - ); -}); diff --git a/test/components/views/avatars/MemberAvatar-test.tsx b/test/components/views/avatars/MemberAvatar-test.tsx index 3dc793bd929..4895b70f217 100644 --- a/test/components/views/avatars/MemberAvatar-test.tsx +++ b/test/components/views/avatars/MemberAvatar-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. +Copyright 2022 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. @@ -14,25 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { fireEvent, getByTestId, render } from "@testing-library/react"; +import { getByTestId, render, waitFor } from "@testing-library/react"; +import { mocked } from "jest-mock"; import { MatrixClient, 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 MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar"; import RoomContext from "../../../../src/contexts/RoomContext"; -import { mediaFromMxc } from "../../../../src/customisations/Media"; -import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload"; -import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; -import { SettingLevel } from "../../../../src/settings/SettingLevel"; +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 { Action } from "../../../../src/dispatcher/actions"; - -type Props = React.ComponentPropsWithoutRef; describe("MemberAvatar", () => { const ROOM_ID = "roomId"; @@ -41,7 +35,7 @@ describe("MemberAvatar", () => { let room: Room; let member: RoomMember; - function getComponent(props: Partial) { + function getComponent(props) { return ( @@ -50,7 +44,10 @@ describe("MemberAvatar", () => { } beforeEach(() => { - mockClient = stubClient(); + jest.clearAllMocks(); + + stubClient(); + mockClient = mocked(MatrixClientPeg.get()); room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { pendingEventOrdering: PendingEventOrdering.Detached, @@ -58,77 +55,22 @@ describe("MemberAvatar", () => { member = new RoomMember(ROOM_ID, "@bob:example.org"); jest.spyOn(room, "getMember").mockReturnValue(member); - }); - - it("supports 'null' members", () => { - const { container } = render(getComponent({ member: null })); - - expect(container.querySelector("img")).not.toBeNull(); - }); - - it("matches the snapshot", () => { jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); - const { container } = render( - getComponent({ - member, - fallbackUserId: "Fallback User ID", - title: "Hover title", - style: { - color: "pink", - }, - }), - ); - - expect(container).toMatchSnapshot(); }); - it("shows an avatar for useOnlyCurrentProfiles", () => { - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); - - SettingsStore.setValue("useOnlyCurrentProfiles", null, SettingLevel.DEVICE, true); + it("shows an avatar for useOnlyCurrentProfiles", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + return settingName === "useOnlyCurrentProfiles"; + }); const { container } = render(getComponent({})); - const avatar = getByTestId(container, "avatar-img"); - expect(avatar).toBeInTheDocument(); - expect(avatar.getAttribute("src")).not.toBe(""); - }); - - it("uses the member's configured avatar", () => { - const mxcUrl = "mxc://example.com/avatars/user.tiff"; - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxcUrl); - - const { container } = render(getComponent({ member })); - - const img = container.querySelector("img"); - expect(img).not.toBeNull(); - expect(img!.src).toBe(mediaFromMxc(mxcUrl).srcHttp); - }); - - it("uses a fallback when the member has no avatar", () => { - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined); - - const { container } = render(getComponent({ member })); - - const img = container.querySelector(".mx_BaseAvatar_image"); - expect(img).not.toBeNull(); - }); - - it("dispatches on click", () => { - const { container } = render(getComponent({ member, viewUserOnClick: true })); - - const spy = jest.spyOn(defaultDispatcher, "dispatch"); - - act(() => { - fireEvent.click(container.querySelector(".mx_BaseAvatar")!); + let avatar: HTMLElement; + await waitFor(() => { + avatar = getByTestId(container, "avatar-img"); + expect(avatar).toBeInTheDocument(); }); - expect(spy).toHaveBeenCalled(); - const [payload] = spy.mock.lastCall!; - expect(payload).toStrictEqual({ - action: Action.ViewUser, - member, - push: false, - }); + expect(avatar!.getAttribute("src")).not.toBe(""); }); }); diff --git a/test/components/views/avatars/RoomAvatar-test.tsx b/test/components/views/avatars/RoomAvatar-test.tsx index 7be7dd65e90..e23cd96f02d 100644 --- a/test/components/views/avatars/RoomAvatar-test.tsx +++ b/test/components/views/avatars/RoomAvatar-test.tsx @@ -39,7 +39,7 @@ describe("RoomAvatar", () => { const dmRoomMap = new DMRoomMap(client); jest.spyOn(dmRoomMap, "getUserIdForRoomId"); jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); - jest.spyOn(AvatarModule, "getColorForString"); + jest.spyOn(AvatarModule, "defaultAvatarUrlForString"); }); afterAll(() => { @@ -48,14 +48,14 @@ describe("RoomAvatar", () => { afterEach(() => { mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset(); - mocked(AvatarModule.getColorForString).mockClear(); + mocked(AvatarModule.defaultAvatarUrlForString).mockClear(); }); it("should render as expected for a Room", () => { const room = new Room("!room:example.com", client, client.getSafeUserId()); room.name = "test room"; expect(render().container).toMatchSnapshot(); - expect(AvatarModule.getColorForString).toHaveBeenCalledWith(room.roomId); + expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId); }); it("should render as expected for a DM room", () => { @@ -64,7 +64,7 @@ describe("RoomAvatar", () => { room.name = "DM room"; mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId); expect(render().container).toMatchSnapshot(); - expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); + expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId); }); it("should render as expected for a LocalRoom", () => { @@ -73,6 +73,6 @@ describe("RoomAvatar", () => { localRoom.name = "local test room"; localRoom.targets.push(new DirectoryMember({ user_id: userId })); expect(render().container).toMatchSnapshot(); - expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); + expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId); }); }); diff --git a/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap deleted file mode 100644 index da62540b90e..00000000000 --- a/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap +++ /dev/null @@ -1,72 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` matches snapshot (avatar + click) 1`] = ` -
- Avatar -
-`; - -exports[` matches snapshot (avatar) 1`] = ` -
- -
-`; - -exports[` matches snapshot (no avatar + click) 1`] = ` -
- - - -
-`; - -exports[` matches snapshot (no avatar) 1`] = ` -
- - - -
-`; diff --git a/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap deleted file mode 100644 index ef29b03bc80..00000000000 --- a/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MemberAvatar matches the snapshot 1`] = ` -
- -
-`; diff --git a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap index 699113689e4..6bffa157b63 100644 --- a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap +++ b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap @@ -5,16 +5,22 @@ exports[`RoomAvatar should render as expected for a DM room 1`] = ` + `; @@ -24,16 +30,22 @@ exports[`RoomAvatar should render as expected for a LocalRoom 1`] = ` + `; @@ -43,16 +55,22 @@ exports[`RoomAvatar should render as expected for a Room 1`] = ` + `; diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index b965f50b2f9..b42ccb83ee3 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -13,17 +13,23 @@ exports[` renders marker when beacon has location 1`] = ` + diff --git a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index 37b3130bd30..c0227c7ab1e 100644 --- a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -22,16 +22,22 @@ exports[` renders the room summary 1`] = ` +
{ // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image).toBeTruthy(); + expect(image.prop("src")).toEqual("data:image/png;base64,00"); }); it("shows the room avatar in a room with 2 people", () => { @@ -86,7 +86,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image).toBeTruthy(); + expect(image.prop("src")).toEqual("data:image/png;base64,00"); }); it("shows the room avatar in a room with >2 people", () => { @@ -100,7 +100,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image).toBeTruthy(); + expect(image.prop("src")).toEqual("data:image/png;base64,00"); }); it("shows the room avatar in a DM with only ourselves", () => { @@ -114,7 +114,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image).toBeTruthy(); + expect(image.prop("src")).toEqual("data:image/png;base64,00"); }); it("shows the user avatar in a DM with 2 people", () => { @@ -148,7 +148,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image).toBeTruthy(); + expect(image.prop("src")).toEqual("data:image/png;base64,00"); }); it("renders call buttons normally", () => { diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap index a78a452e890..f35467e1efd 100644 --- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -161,16 +161,22 @@ exports[` with an invite without an invited email for a dm roo +

@@ -230,16 +236,22 @@ exports[` with an invite without an invited email for a non-dm +

diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap index 557d97c243e..bcbb7932c69 100644 --- a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap @@ -15,16 +15,22 @@ exports[`RoomTile should render the room 1`] = ` +

- !room:example.com - -`; - -exports[`RoomPillPart matches snapshot (no avatar) 1`] = ` - - !room:example.com - -`; - -exports[`UserPillPart matches snapshot (avatar) 1`] = ` - - DisplayName - -`; - -exports[`UserPillPart matches snapshot (no avatar) 1`] = ` - - DisplayName - -`; diff --git a/test/editor/parts-test.ts b/test/editor/parts-test.ts index 31c620c94ad..534221ece3a 100644 --- a/test/editor/parts-test.ts +++ b/test/editor/parts-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. +Copyright 2022 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. @@ -14,11 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts"; -import DMRoomMap from "../../src/utils/DMRoomMap"; -import { stubClient } from "../test-utils"; +import { EmojiPart, PlainPart } from "../../src/editor/parts"; import { createPartCreator } from "./mock"; describe("editor/parts", () => { @@ -44,67 +40,3 @@ describe("editor/parts", () => { expect(() => part.toDOMNode()).not.toThrow(); }); }); - -describe("UserPillPart", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let creator: PartCreator; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, "@me:example.com"); - creator = new PartCreator(room, client); - }); - - it("matches snapshot (no avatar)", () => { - jest.spyOn(room, "getMember").mockReturnValue(new RoomMember(room.roomId, "@user:example.com")); - const pill = creator.userPill("DisplayName", "@user:example.com"); - const el = pill.toDOMNode(); - - expect(el).toMatchSnapshot(); - }); - - it("matches snapshot (avatar)", () => { - const member = new RoomMember(room.roomId, "@user:example.com"); - jest.spyOn(room, "getMember").mockReturnValue(member); - jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatar.png"); - - const pill = creator.userPill("DisplayName", "@user:example.com"); - const el = pill.toDOMNode(); - - expect(el).toMatchSnapshot(); - }); -}); - -describe("RoomPillPart", () => { - const roomId = "!room:example.com"; - let client: jest.Mocked; - let room: Room; - let creator: PartCreator; - - beforeEach(() => { - client = stubClient() as jest.Mocked; - DMRoomMap.makeShared(); - - room = new Room(roomId, client, "@me:example.com"); - client.getRoom.mockReturnValue(room); - creator = new PartCreator(room, client); - }); - - it("matches snapshot (no avatar)", () => { - jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(null); - const pill = creator.roomPill("super-secret clubhouse"); - const el = pill.toDOMNode(); - - expect(el).toMatchSnapshot(); - }); - - it("matches snapshot (avatar)", () => { - jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatars/room1.jpeg"); - const pill = creator.roomPill("cool chat club"); - const el = pill.toDOMNode(); - - expect(el).toMatchSnapshot(); - }); -}); diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index 0645a993ae0..c47170d3eda 100644 --- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -25,7 +25,7 @@ exports[`HTMLExport should export 1`] = `
- +
@@ -59,13 +59,13 @@ exports[`HTMLExport should export 1`] = ` role="list" >
- +

!myroom:example.org

created this room.

This is the start of export of !myroom:example.org. Exported by @userId:matrix.org at 2022/11/17.


-
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • +
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0