From 13acf62544b59f1d04c158e5a862a657b984defd Mon Sep 17 00:00:00 2001 From: Abhinav Rastogi Date: Sat, 17 Oct 2026 09:07:17 +0530 Subject: [PATCH] fix: render fallback for empty avatar sources --- src/components/layout/avatar.test.tsx | 43 +++++++++++++++++++++++++++ src/components/layout/avatar.tsx | 22 ++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/components/layout/avatar.test.tsx diff --git a/src/components/layout/avatar.test.tsx b/src/components/layout/avatar.test.tsx new file mode 100644 index 0000000..15efd8e --- /dev/null +++ b/src/components/layout/avatar.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment jsdom +/* eslint-disable @next/next/no-img-element, jsx-a11y/alt-text */ + +import { cleanup, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { Avatar } from "./avatar"; + +vi.mock("next/image", () => ({ + default: ({ + unoptimized, + priority, + ...props + }: React.ImgHTMLAttributes & { unoptimized?: boolean; priority?: boolean }) => { + void unoptimized; + void priority; + return ; + }, +})); + +describe("Avatar", () => { + afterEach(cleanup); + + test("renders an image for a non-empty source", () => { + render(); + + expect(screen.getByRole("img", { name: "Ada Lovelace" })).toHaveAttribute( + "src", + "https://example.com/avatar.png", + ); + }); + + test.each(["", " ", null, undefined])( + "renders an initial fallback instead of an image for an invalid source", + (src) => { + render(); + + expect(screen.getByRole("img", { name: "Ada Lovelace" })).toHaveTextContent("A"); + expect(screen.queryByRole("img", { name: "Ada Lovelace" })?.tagName).toBe("SPAN"); + expect(document.querySelector("img")).not.toBeInTheDocument(); + }, + ); +}); diff --git a/src/components/layout/avatar.tsx b/src/components/layout/avatar.tsx index 7169598..baf3147 100644 --- a/src/components/layout/avatar.tsx +++ b/src/components/layout/avatar.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { cn } from "@/utils/cn"; type AvatarProps = { - src: string; + src?: string | null; alt: string; size?: number; unoptimized?: boolean; @@ -22,9 +22,27 @@ export function Avatar({ priority = false, className, }: AvatarProps) { + const hasImage = Boolean(src?.trim()); + + if (!hasImage) { + return ( + + {alt.trim().charAt(0).toUpperCase() || "?"} + + ); + } + return (