From 75936cdae2a4b0093db466803d7a3ff39af32bf4 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:34:55 -0400 Subject: [PATCH] fix(web): show profile avatar in mobile settings link (#614) - Preserve the profile initial fallback when no avatar is set - Add an accessible settings label and avatar coverage --- web/src/components/Layout.test.tsx | 27 ++++++++++++++++++++++++++- web/src/components/Layout.tsx | 17 +++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) 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 }) => {alt}, + 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() ?? + "?"} + +