diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonTrigger.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonTrigger.tsx index 7a99d4bb645..be5bcc3bc6e 100644 --- a/packages/clerk-js/src/ui/components/UserButton/UserButtonTrigger.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonTrigger.tsx @@ -42,6 +42,7 @@ export const UserButtonTrigger = withAvatarShimmer( boxElementDescriptor={descriptors.userButtonAvatarBox} imageElementDescriptor={descriptors.userButtonAvatarImage} {...user} + key={`user-avatar-${user?.imageUrl || 'no-image'}`} size={theme => theme.sizes.$7} /> diff --git a/packages/clerk-js/src/ui/elements/Avatar.tsx b/packages/clerk-js/src/ui/elements/Avatar.tsx index 7f691fa032e..f1593a193f2 100644 --- a/packages/clerk-js/src/ui/elements/Avatar.tsx +++ b/packages/clerk-js/src/ui/elements/Avatar.tsx @@ -31,6 +31,10 @@ export const Avatar = (props: AvatarProps) => { } = props; const [error, setError] = React.useState(false); + React.useEffect(() => { + setError(false); + }, [imageUrl]); + const ImgOrFallback = initials && (!imageUrl || error) ? ( diff --git a/packages/clerk-js/src/ui/elements/UserPreview.tsx b/packages/clerk-js/src/ui/elements/UserPreview.tsx index 3d82d6e9dbd..3a1b3a36212 100644 --- a/packages/clerk-js/src/ui/elements/UserPreview.tsx +++ b/packages/clerk-js/src/ui/elements/UserPreview.tsx @@ -116,6 +116,7 @@ export const UserPreview = (props: UserPreviewProps) => { {...samlAccount} name={name} avatarUrl={imageUrl} + key={`user-preview-avatar-${imageUrl || 'no-image'}`} size={getAvatarSizes} sx={avatarSx} rounded={rounded} diff --git a/packages/clerk-js/src/ui/elements/__tests__/Avatar.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/Avatar.test.tsx new file mode 100644 index 00000000000..49d7945ef10 --- /dev/null +++ b/packages/clerk-js/src/ui/elements/__tests__/Avatar.test.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Avatar } from '../Avatar'; +import { AppearanceProvider } from '../../customizables'; + +const mockTheme = { + colors: { + $colorForeground: '#000000', + $colorBackground: '#ffffff', + }, + radii: { + $circle: '50%', + $avatar: '4px', + }, + sizes: { + $4: '16px', + $6: '24px', + }, + space: { + $2: '8px', + }, +}; + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe('Avatar', () => { + it('should reset error state when imageUrl changes', () => { + const { rerender } = render( + + + , + ); + + // Initially should show image + expect(screen.getByAltText("Test User's logo")).toBeInTheDocument(); + + // Simulate image load error + const img = screen.getByAltText("Test User's logo"); + img.dispatchEvent(new Event('error')); + + // Should show initials after error + expect(screen.getByText('TU')).toBeInTheDocument(); + + // Change imageUrl - should reset error state and show new image + rerender( + + + , + ); + + // Should show image again (error state reset) + expect(screen.getByAltText("Test User's logo")).toBeInTheDocument(); + expect(screen.queryByText('TU')).not.toBeInTheDocument(); + }); + + it('should handle null imageUrl properly', () => { + render( + + + , + ); + + // Should show initials when no imageUrl + expect(screen.getByText('TU')).toBeInTheDocument(); + }); + + it('should handle empty imageUrl properly', () => { + render( + + + , + ); + + // Should show initials when empty imageUrl + expect(screen.getByText('TU')).toBeInTheDocument(); + }); +});