Give Avatar initials, person-mark, and load-error fallbacks so missing photos still look like the design-system atom.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-26 13:18:33 -06:00
co-authored by Cursor
parent 95781452bd
commit 4c4cf99572
6 changed files with 395 additions and 29 deletions
+135
View File
@@ -0,0 +1,135 @@
import "@testing-library/jest-dom/vitest";
import React from "react";
import { describe, it, expect, vi } from "vitest";
import {
renderWithProviders as render,
screen,
fireEvent,
} from "../utils/test-utils";
import Avatar from "../../app/components/asset/Avatar";
import {
componentTestSuite,
type ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type AvatarProps = React.ComponentProps<typeof Avatar>;
const baseProps: AvatarProps = {
src: "/assets/marketing/avatar-1.svg",
alt: "User avatar",
size: "medium",
};
const config: ComponentTestSuiteConfig<AvatarProps> = {
component: Avatar,
name: "Avatar",
props: baseProps,
primaryRole: "img",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<AvatarProps>(config);
describe("Avatar (behavioral tests)", () => {
it("renders the image with alt text", () => {
render(<Avatar {...baseProps} />);
const img = screen.getByRole("img", { name: "User avatar" });
expect(img).toHaveAttribute("src", baseProps.src);
});
it("does not show initials while the image is loading", () => {
render(
<Avatar
src="/assets/marketing/avatar-1.svg"
alt="Ada Lovelace"
initials="AL"
/>,
);
expect(screen.queryByText("AL")).not.toBeInTheDocument();
});
it("applies the small photo ring after the image loads", () => {
const { container } = render(
<Avatar
src="/assets/marketing/avatar-1.svg"
alt="User avatar"
size="small"
/>,
);
fireEvent.load(screen.getByRole("img", { name: "User avatar" }));
expect(container.firstChild).toHaveClass("border-[#FFFFFF4D]");
expect(container.firstChild).toHaveClass("box-border");
});
it("shows initials when src is omitted", () => {
render(<Avatar alt="Ada Lovelace" initials="Ada Lovelace" />);
expect(screen.getByRole("img", { name: "Ada Lovelace" })).toHaveTextContent(
"AL",
);
expect(document.querySelector("img")).not.toBeInTheDocument();
});
it("shows a person mark when src and initials are omitted", () => {
const { container } = render(<Avatar alt="Member" />);
expect(screen.getByRole("img", { name: "Member" })).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(document.querySelector("img")).not.toBeInTheDocument();
});
it("switches to initials when the image errors", () => {
render(
<Avatar
src="/missing-avatar.png"
alt="Ada Lovelace"
initials="AL"
size="large"
/>,
);
fireEvent.error(screen.getByRole("img", { name: "Ada Lovelace" }));
expect(document.querySelector("img")).not.toBeInTheDocument();
expect(screen.getByRole("img", { name: "Ada Lovelace" })).toHaveTextContent(
"AL",
);
});
it("applies rounded-square radius", () => {
const { container } = render(
<Avatar alt="Org" initials="CR" shape="rounded-square" />,
);
expect(container.firstChild).toHaveClass(
"rounded-[var(--measures-radius-200,8px)]",
);
});
it("applies outlined fallback chrome", () => {
render(
<Avatar alt="Ada Lovelace" initials="AL" variant="outlined" />,
);
const mark = screen.getByText("AL");
expect(mark).toHaveClass(
"border-[var(--color-content-default-brand-primary)]",
);
});
it("renders a button with hover treatment when onClick is set", () => {
const onClick = vi.fn();
render(
<Avatar alt="Ada Lovelace" initials="AL" onClick={onClick} />,
);
const button = screen.getByRole("button", { name: "Ada Lovelace" });
fireEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
expect(button).toHaveClass("hover:opacity-90");
});
it("hides decorative fallbacks from the accessibility tree", () => {
render(<Avatar alt="" initials="AB" />);
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
});