fix(messages): paint owner avatar fallback and harden its e2e assertions

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 <img> (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 <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-07-02 01:51:03 -07:00
committed by npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f
parent 3488713763
commit 602e5d3478
3 changed files with 49 additions and 9 deletions

View File

@@ -447,6 +447,7 @@ export const MessageRow = React.memo(
truncatePubkey(message.agentOwner.pubkey)
}
size="2xs"
testId="message-agent-owner-avatar"
/>
</span>
</TooltipTrigger>

View File

@@ -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}
</AvatarFallback>

View File

@@ -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 <img> 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,