diff --git a/web/src/components/Layout.test.tsx b/web/src/components/Layout.test.tsx
index aca0d5fc..cc44a977 100644
--- a/web/src/components/Layout.test.tsx
+++ b/web/src/components/Layout.test.tsx
@@ -11,6 +11,10 @@ const mocks = vi.hoisted(() => ({
prefetchQuery: vi.fn(),
renderSurface: true,
beginResult: undefined as boolean | undefined,
+ profile: {
+ name: "Admin",
+ avatar_url: "https://example.com/admin-avatar.webp",
+ } as { name: string; avatar_url?: string },
}));
vi.mock("react-router", async () => {
@@ -30,7 +34,7 @@ vi.mock("@tanstack/react-query", async () => {
vi.mock("@/hooks/useAuth", () => ({ useAuth: () => ({ user: { username: "Admin" } }) }));
vi.mock("@/hooks/useCurrentProfile", () => ({
- useCurrentProfile: () => ({ profile: { name: "Admin" } }),
+ useCurrentProfile: () => ({ profile: mocks.profile }),
}));
vi.mock("@/hooks/useIsActingAdmin", () => ({ useIsActingAdmin: () => false }));
vi.mock("@/playback/watchPlaybackContext", () => ({
@@ -43,6 +47,11 @@ vi.mock("@/hooks/queries/catalogRead", () => ({ fetchCatalogItemDetail: vi.fn()
vi.mock("@/components/GlobalSearch", () => ({ GlobalSearch: () => null }));
vi.mock("@/components/ServerActivity", () => ({ default: () => null }));
vi.mock("@/components/SiloBrand", () => ({ SiloBrand: () => Silo }));
+vi.mock("@/components/ui/avatar", () => ({
+ Avatar: ({ children }: { children: ReactNode }) =>
{children}
,
+ AvatarImage: ({ src, alt }: { src: string; alt: string }) =>
,
+ AvatarFallback: ({ children }: { children: ReactNode }) => {children},
+}));
vi.mock("@/components/ViewTransitionLink", () => ({
default: ({ children }: { children: ReactNode }) => {children},
}));
@@ -121,6 +130,10 @@ beforeEach(() => {
mocks.prefetchQuery.mockReset();
mocks.renderSurface = true;
mocks.beginResult = undefined;
+ mocks.profile = {
+ name: "Admin",
+ avatar_url: "https://example.com/admin-avatar.webp",
+ };
vi.stubGlobal("matchMedia", (query: string) => ({
matches: query === "(min-width: 64rem)",
media: query,
@@ -140,6 +153,18 @@ afterEach(() => {
vi.restoreAllMocks();
});
+describe("Layout mobile profile", () => {
+ it("renders the current profile avatar in the settings link", () => {
+ renderLayout();
+
+ const settingsLink = screen.getByRole("link", { name: "Admin settings" });
+ const avatar = screen.getByRole("img", { name: "Admin" });
+
+ expect(settingsLink).toContainElement(avatar);
+ expect(avatar).toHaveAttribute("src", "https://example.com/admin-avatar.webp");
+ });
+});
+
describe("Layout item navigation", () => {
it("declines interception on an item route, for a non-item href, and below lg", () => {
const view = renderLayout();
diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx
index 31216909..b0907126 100644
--- a/web/src/components/Layout.tsx
+++ b/web/src/components/Layout.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router";
import { useQueryClient } from "@tanstack/react-query";
import { Menu, Search } from "lucide-react";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { useAuth } from "@/hooks/useAuth";
import { useCurrentProfile } from "@/hooks/useCurrentProfile";
@@ -267,11 +268,19 @@ export default function Layout({ children }: LayoutProps) {
{showAdminActivity && }
- {profile?.name?.charAt(0).toUpperCase() ??
- user?.username?.charAt(0).toUpperCase() ??
- "?"}
+
+ {profile?.avatar_url ? (
+
+ ) : null}
+
+ {profile?.name?.charAt(0).toUpperCase() ??
+ user?.username?.charAt(0).toUpperCase() ??
+ "?"}
+
+