From 602e5d3478d19b05e6cb73796d4927d3ad1cbcb7 Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
<52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Date: Thu, 2 Jul 2026 01:51:03 -0700
Subject: [PATCH] fix(messages): paint owner avatar fallback and harden its e2e
assertions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Radix AvatarFallback's delayMs exists to avoid an initials flash while
an avatar image loads. With no image src there is nothing to wait for,
yet the 200ms delay still applied — so avatar-less owners painted a
blank 16px box on cold renders (caught in live-build review). Only
apply the delay when an image src is present.
Tighten the e2e so it can no longer pass on an empty wrapper: seed one
owner with a hermetic data-URL avatar and one without, assert a fully
loaded
(naturalWidth > 0) for the former and visible initials
text for the latter, and give the second-level owner-profile fetch
generous timeouts to kill the cold-run race. 18 consecutive runs green
including cold server starts.
Co-authored-by: Taylor Ho
Signed-off-by: Taylor Ho
---
.../src/features/messages/ui/MessageRow.tsx | 1 +
desktop/src/shared/ui/UserAvatar.tsx | 5 +-
.../tests/e2e/duplicate-agent-avatars.spec.ts | 52 ++++++++++++++++---
3 files changed, 49 insertions(+), 9 deletions(-)
diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx
index 1abb9130c5..1ccbc66c52 100644
--- a/desktop/src/features/messages/ui/MessageRow.tsx
+++ b/desktop/src/features/messages/ui/MessageRow.tsx
@@ -447,6 +447,7 @@ export const MessageRow = React.memo(
truncatePubkey(message.agentOwner.pubkey)
}
size="2xs"
+ testId="message-agent-owner-avatar"
/>
diff --git a/desktop/src/shared/ui/UserAvatar.tsx b/desktop/src/shared/ui/UserAvatar.tsx
index e9bcb8d9de..c4a2af0195 100644
--- a/desktop/src/shared/ui/UserAvatar.tsx
+++ b/desktop/src/shared/ui/UserAvatar.tsx
@@ -68,7 +68,10 @@ export function UserAvatar({
: "bg-secondary text-secondary-foreground",
)}
data-testid={testId ? `${testId}-fallback` : undefined}
- delayMs={200}
+ // The delay exists to avoid an initials flash while an image loads.
+ // With no image source there is nothing to wait for — render the
+ // initials immediately or the avatar paints as an empty box.
+ delayMs={src ? 200 : undefined}
>
{initials}
diff --git a/desktop/tests/e2e/duplicate-agent-avatars.spec.ts b/desktop/tests/e2e/duplicate-agent-avatars.spec.ts
index df6e15e6ce..1bab30594b 100644
--- a/desktop/tests/e2e/duplicate-agent-avatars.spec.ts
+++ b/desktop/tests/e2e/duplicate-agent-avatars.spec.ts
@@ -14,6 +14,12 @@ const SCOUT_B_PUBKEY =
const SOLO_PUBKEY =
"aaaa000000000000000000000000000000000000000000000000000000000003";
+// 8x8 solid teal PNG. A data URL keeps the avatar-image path hermetic — no
+// network fetch, no relay proxy — while still exercising Radix's real
+// image-load lifecycle (AvatarImage only renders once the img has loaded).
+const ALICE_AVATAR_DATA_URL =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAFElEQVR4nGMUWTCNARtgwio6aCUABdEBWlX6zykAAAAASUVORK5CYII=";
+
async function emitMockMessage(page: Page, content: string, pubkey: string) {
const event = await page.evaluate(
({ msg, pk }) => {
@@ -87,7 +93,8 @@ test("duplicate-named agents get an inline owner avatar; unique agents do not",
},
],
// Seeded after managed agents, so these override the default
- // owner (the mock viewer) with two distinct humans.
+ // owner (the mock viewer) with two distinct humans. Alice carries an
+ // avatar image; bob has none, exercising the initials fallback.
searchProfiles: [
{
pubkey: SCOUT_A_PUBKEY,
@@ -101,6 +108,15 @@ test("duplicate-named agents get an inline owner avatar; unique agents do not",
ownerPubkey: TEST_IDENTITIES.bob.pubkey,
isAgent: true,
},
+ {
+ pubkey: TEST_IDENTITIES.alice.pubkey,
+ displayName: "alice",
+ avatarUrl: ALICE_AVATAR_DATA_URL,
+ },
+ {
+ pubkey: TEST_IDENTITIES.bob.pubkey,
+ displayName: "bob",
+ },
],
});
await page.goto("/");
@@ -123,13 +139,33 @@ test("duplicate-named agents get an inline owner avatar; unique agents do not",
.getByTestId("message-row")
.filter({ hasText: "Ranger here, no twin." });
- // Both same-named agents carry the inline owner avatar…
- await expect(
- scoutARow.getByTestId("message-agent-owner-avatar"),
- ).toBeVisible();
- await expect(
- scoutBRow.getByTestId("message-agent-owner-avatar"),
- ).toBeVisible();
+ // Both same-named agents carry the inline owner avatar. The wrapper being
+ // visible is not enough — assert actual painted content. Owner profiles
+ // arrive via a second-level batch fetch, so give the cold first render
+ // time to settle instead of racing it.
+ const scoutABadge = scoutARow.getByTestId("message-agent-owner-avatar");
+ const scoutBBadge = scoutBRow.getByTestId("message-agent-owner-avatar");
+ await expect(scoutABadge).toBeVisible({ timeout: 15_000 });
+ await expect(scoutBBadge).toBeVisible({ timeout: 15_000 });
+
+ // Alice has an avatar image — a real, fully loaded
must render.
+ const aliceImage = scoutABadge.getByTestId(
+ "message-agent-owner-avatar-image",
+ );
+ await expect(aliceImage).toBeVisible({ timeout: 15_000 });
+ expect(
+ await aliceImage.evaluate(
+ (img: HTMLImageElement) => img.complete && img.naturalWidth > 0,
+ ),
+ ).toBe(true);
+
+ // Bob has no avatar URL — the initials fallback must paint visible text.
+ const bobFallback = scoutBBadge.getByTestId(
+ "message-agent-owner-avatar-fallback",
+ );
+ await expect(bobFallback).toBeVisible({ timeout: 15_000 });
+ await expect(bobFallback).toHaveText("B");
+
// …the uniquely named agent does not.
await expect(rangerRow.getByTestId("message-agent-owner-avatar")).toHaveCount(
0,