2024-12-06 18:30:05 +00:00
/*
Copyright 2024 New Vector Ltd.
2025-02-18 17:59:58 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
2024-12-06 18:30:05 +00:00
Please see LICENSE in the repository root for full details.
*/
import { afterEach , expect , test , vi } from "vitest" ;
import { render , screen } from "@testing-library/react" ;
2025-03-13 13:58:43 +01:00
import { type MatrixClient } from "matrix-js-sdk" ;
2024-12-11 09:27:55 +00:00
import { type FC , type PropsWithChildren } from "react" ;
2024-12-06 18:30:05 +00:00
import { ClientContextProvider } from "./ClientContext" ;
import { Avatar } from "./Avatar" ;
import { mockMatrixRoomMember , mockRtcMembership } from "./utils/test" ;
2026-03-07 19:44:00 -06:00
import { widget } from "./widget" ;
2026-03-12 22:18:38 -05:00
import { WidgetApi } from "matrix-widget-api" ;
2024-12-06 18:30:05 +00:00
const TestComponent : FC <
2026-03-09 22:25:54 -05:00
PropsWithChildren < {
client : MatrixClient ;
} >
2026-03-12 22:18:38 -05:00
> = ({ client , children }) => {
2024-12-06 18:30:05 +00:00
return (
< ClientContextProvider
value = {{
state : "valid" ,
disconnected : false ,
supportedFeatures : {
reactions : true ,
},
setClient : vi.fn (),
authenticated : {
client ,
isPasswordlessUser : true ,
changePassword : vi.fn (),
logout : vi.fn (),
},
}}
>
{ children }
</ ClientContextProvider >
);
};
2026-03-07 19:44:00 -06:00
vi . mock ( "./widget" , () => ({
widget : {
2026-03-12 22:18:38 -05:00
api : null , // Ideally we'd only mock this in the as a widget test so the whole module is otherwise null, but just nulling `api` by default works well enough
2026-03-07 19:44:00 -06:00
},
}));
2024-12-06 18:30:05 +00:00
afterEach (() => {
vi . unstubAllGlobals ();
});
test ( "should just render a placeholder when the user has no avatar" , () => {
const client = vi . mocked < MatrixClient >({
getAccessToken : () => "my-access-token" ,
mxcUrlToHttp : () => vi . fn (),
} as unknown as MatrixClient );
vi . spyOn ( client , "mxcUrlToHttp" );
const member = mockMatrixRoomMember (
mockRtcMembership ( "@alice:example.org" , "AAAA" ),
{
getMxcAvatarUrl : () => undefined ,
},
);
const displayName = "Alice" ;
render (
< TestComponent client = { client }>
< Avatar
id = { member . userId }
name = { displayName }
size = { 96 }
src = { member . getMxcAvatarUrl ()}
/>
</ TestComponent >,
);
const element = screen . getByRole ( "img" , { name : "@alice:example.org" });
expect ( element . tagName ). toEqual ( "SPAN" );
expect ( client . mxcUrlToHttp ). toBeCalledTimes ( 0 );
});
2026-03-12 22:18:38 -05:00
test ( "should attempt to fetch authenticated media from the server" , async () => {
2024-12-06 18:30:05 +00:00
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 < MatrixClient >({
getAccessToken : () => accessToken ,
mxcUrlToHttp : () => vi . fn (),
} as unknown as MatrixClient );
vi . spyOn ( client , "mxcUrlToHttp" ). mockReturnValue ( expectedAuthUrl );
const member = mockMatrixRoomMember (
mockRtcMembership ( "@alice:example.org" , "AAAA" ),
{
getMxcAvatarUrl : () => "mxc://example.org/alice-avatar" ,
},
);
const displayName = "Alice" ;
render (
2026-03-12 22:18:38 -05:00
< TestComponent client = { client }>
2024-12-06 18:30:05 +00:00
< Avatar
id = { member . userId }
name = { displayName }
size = { 96 }
src = { member . getMxcAvatarUrl ()}
/>
</ TestComponent >,
);
// 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 } ` },
});
});
2026-03-07 19:44:00 -06:00
2026-03-12 22:18:38 -05:00
test ( "should attempt to use widget API if running as a widget" , async () => {
2026-03-07 19:44:00 -06:00
const expectedMXCUrl = "mxc://example.org/alice-avatar" ;
const expectedObjectURL = "my-object-url" ;
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 client = vi . mocked < MatrixClient >({
getAccessToken : () => undefined ,
} as unknown as MatrixClient );
2026-03-12 22:18:38 -05:00
widget ! . api = { downloadFile : vi.fn () } as unknown as WidgetApi ;
2026-03-07 19:44:00 -06:00
vi . spyOn ( widget ! . api , "downloadFile" ). mockResolvedValue ({ file : theBlob });
const member = mockMatrixRoomMember (
mockRtcMembership ( "@alice:example.org" , "AAAA" ),
{
getMxcAvatarUrl : () => expectedMXCUrl ,
},
);
const displayName = "Alice" ;
render (
2026-03-12 22:18:38 -05:00
< TestComponent client = { client }>
2026-03-07 19:44:00 -06:00
< Avatar
id = { member . userId }
name = { displayName }
size = { 96 }
src = { member . getMxcAvatarUrl ()}
/>
</ TestComponent >,
);
// Fetch is asynchronous, so wait for this to resolve.
await vi . waitUntil (() =>
document . querySelector ( `img[src=' ${ expectedObjectURL } ']` ),
);
expect ( widget ! . api . downloadFile ). toBeCalledWith ( expectedMXCUrl );
});