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
This commit is contained in:
Quick
2026-08-12 09:34:55 -04:00
committed by GitHub
parent 1dcdd4b27a
commit 75936cdae2
2 changed files with 39 additions and 5 deletions
+26 -1
View File
@@ -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: () => <span>Silo</span> }));
vi.mock("@/components/ui/avatar", () => ({
Avatar: ({ children }: { children: ReactNode }) => <div>{children}</div>,
AvatarImage: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
AvatarFallback: ({ children }: { children: ReactNode }) => <span>{children}</span>,
}));
vi.mock("@/components/ViewTransitionLink", () => ({
default: ({ children }: { children: ReactNode }) => <a href="/">{children}</a>,
}));
@@ -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();
+13 -4
View File
@@ -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 && <ServerActivity hideWhenEmpty />}
<Link
to="/settings"
className="bg-primary text-primary-foreground flex h-9 w-9 items-center justify-center rounded-xl text-xs font-bold shadow-[0_16px_32px_-22px_rgba(0,0,0,0.7)]"
aria-label={`${profile?.name ?? user?.username ?? "User"} settings`}
className="flex h-9 w-9 items-center justify-center rounded-full transition-transform active:scale-[0.98]"
>
{profile?.name?.charAt(0).toUpperCase() ??
user?.username?.charAt(0).toUpperCase() ??
"?"}
<Avatar className="h-9 w-9 shadow-[0_16px_32px_-22px_rgba(0,0,0,0.7)]">
{profile?.avatar_url ? (
<AvatarImage src={profile.avatar_url} alt={profile.name} />
) : null}
<AvatarFallback className="bg-primary text-primary-foreground text-xs font-bold">
{profile?.name?.charAt(0).toUpperCase() ??
user?.username?.charAt(0).toUpperCase() ??
"?"}
</AvatarFallback>
</Avatar>
</Link>
</div>
</div>