authorgravatar for harshith9399@gmail.comHarshith Goka <harshith9399@gmail.com> 2026-09-17 11:14:01+09:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 23:14:01-03:00
logdf466c79810046d20c0f3ccc4e51709c5703814e
tree22a737910002d2864f390e28b0a86a7950226cea
parent573a8f2f30d59c528997daf1e02bdb58e0d9dc54
signature Signed by PGP key B5690EEEBB952194

fix(web): fall back when pull request avatars fail (#11728)


2 files changed, 33 insertions(+), 3 deletions(-)

apps/web/src/components/pullRequest/pullRequestPresentation.test.tsx created+28
...@@ -0,0 +1,28 @@
1import { act } from "react";
2import { create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, expect, it, vi } from "vite-plus/test";
4
5import { PullRequestActorAvatar } from "./pullRequestPresentation";
6
7let renderer: ReactTestRenderer | undefined;
8
9afterEach(async () => {
10 await act(async () => renderer?.unmount());
11 vi.unstubAllGlobals();
12});
13
14it("falls back to the actor initial when a remote avatar fails", async () => {
15 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
16 await act(async () => {
17 renderer = create(
18 <PullRequestActorAvatar
19 actor={{ login: "octocat", name: null, avatarUrl: "https://ghe.example/octocat.png" }}
20 />,
21 );
22 });
23
24 await act(async () => renderer!.root.findByType("img").props.onError());
25
26 expect(renderer!.root.findAllByType("img")).toHaveLength(0);
27 expect(renderer!.root.findByType("span").children).toEqual(["O"]);
28});
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+5-3
...@@ -14,7 +14,7 @@ import {...@@ -14,7 +14,7 @@ import {
14 CircleXIcon,14 CircleXIcon,
15 UserCheckIcon,15 UserCheckIcon,
16} from "lucide-react";16} from "lucide-react";
17import { Children, isValidElement, type ReactNode } from "react";17import { Children, isValidElement, type ReactNode, useState } from "react";
1818
19import { cn } from "~/lib/utils";19import { cn } from "~/lib/utils";
2020
...@@ -352,8 +352,9 @@ export function PullRequestActorAvatar({...@@ -352,8 +352,9 @@ export function PullRequestActorAvatar({
352}) {352}) {
353 const login = actor?.login ?? "ghost";353 const login = actor?.login ?? "ghost";
354 const avatarUrl = actor?.avatarUrl ?? null;354 const avatarUrl = actor?.avatarUrl ?? null;
355 return avatarUrl === null ? (355 const [failedAvatarUrl, setFailedAvatarUrl] = useState<string | null>(null);
356 // Not every host reports an avatar, so the initial stands in where none arrives.356 return avatarUrl === null || failedAvatarUrl === avatarUrl ? (
357 // Not every host reports an avatar, and a private host may refuse the browser's request.
357 <span358 <span
358 aria-hidden359 aria-hidden
359 className={cn(360 className={cn(
...@@ -370,6 +371,7 @@ export function PullRequestActorAvatar({...@@ -370,6 +371,7 @@ export function PullRequestActorAvatar({
370 src={avatarUrl}371 src={avatarUrl}
371 loading="lazy"372 loading="lazy"
372 className={cn("size-4 shrink-0 rounded-full bg-muted object-cover", className)}373 className={cn("size-4 shrink-0 rounded-full bg-muted object-cover", className)}
374 onError={() => setFailedAvatarUrl(avatarUrl)}
373 />375 />
374 );376 );
375}377}