diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx new file mode 100644 index 00000000..9df6384c --- /dev/null +++ b/src/Avatar.test.tsx @@ -0,0 +1,122 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { afterEach, expect, test, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { FC, PropsWithChildren } from "react"; + +import { ClientContextProvider } from "./ClientContext"; +import { Avatar } from "./Avatar"; +import { mockMatrixRoomMember } from "./utils/test"; + +const TestComponent: FC> = ({ + client, + children, +}) => { + return ( + + {children} + + ); +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("should just render a placeholder when the user has no avatar", () => { + const client = vi.mocked({ + getAccessToken: () => "my-access-token", + mxcUrlToHttp: () => vi.fn(), + } as unknown as MatrixClient); + + vi.spyOn(client, "mxcUrlToHttp"); + const member = mockMatrixRoomMember({ + userId: "@alice:example.org", + getMxcAvatarUrl: () => undefined, + }); + const displayName = "Alice"; + render( + + + , + ); + const element = screen.getByRole("img", { name: "@alice:example.org" }); + expect(element.tagName).toEqual("SPAN"); + expect(client.mxcUrlToHttp).toBeCalledTimes(0); +}); +test("should attempt to fetch authenticated media", async () => { + const expectedAuthUrl = "http://example.org/media/alice-avatar"; + const expectedObjectURL = "my-object-url"; + const accessToken = "my-access-token"; + const theBlob = new Blob([]); + + // vitest doesn't have a implementation of create/revokeObjectURL, so we need + // to delete the property. It's a bit odd, but it works. + Reflect.deleteProperty(global.window.URL, "createObjectURL"); + globalThis.URL.createObjectURL = vi.fn().mockReturnValue(expectedObjectURL); + Reflect.deleteProperty(global.window.URL, "revokeObjectURL"); + globalThis.URL.revokeObjectURL = vi.fn(); + + const fetchFn = vi.fn().mockResolvedValue({ + blob: async () => Promise.resolve(theBlob), + }); + vi.stubGlobal("fetch", fetchFn); + + const client = vi.mocked({ + getAccessToken: () => accessToken, + mxcUrlToHttp: () => vi.fn(), + } as unknown as MatrixClient); + + vi.spyOn(client, "mxcUrlToHttp").mockReturnValue(expectedAuthUrl); + const member = mockMatrixRoomMember({ + userId: "@alice:example.org", + getMxcAvatarUrl: () => "mxc://example.org/alice-avatar", + }); + const displayName = "Alice"; + render( + + + , + ); + + // Fetch is asynchronous, so wait for this to resolve. + await vi.waitUntil(() => + document.querySelector(`img[src='${expectedObjectURL}']`), + ); + + expect(client.mxcUrlToHttp).toBeCalledTimes(1); + expect(globalThis.fetch).toBeCalledWith(expectedAuthUrl, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); +}); diff --git a/src/Avatar.tsx b/src/Avatar.tsx index 84b0ed48..a68c17f8 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -100,12 +100,11 @@ export const Avatar: FC = ({ }, }) .then(async (req) => req.blob()) - .then((res) => { - objectUrl = URL.createObjectURL(res); + .then((blob) => { + objectUrl = URL.createObjectURL(blob); setAvatarUrl(objectUrl); }) - .catch(() => { - // Error will likely be evident from browser logs. + .catch((ex) => { setAvatarUrl(undefined); }); diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 8b5589d5..07b78dad 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -71,6 +71,8 @@ export type SetClientParams = { const ClientContext = createContext(undefined); +export const ClientContextProvider = ClientContext.Provider; + export const useClientState = (): ClientState | undefined => useContext(ClientContext);