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:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user