import { fireEvent, render, screen, waitFor } from '@testing-library/react' import Avatar from './index' describe('Avatar', () => { beforeEach(() => { vi.clearAllMocks() }) // Rendering tests - verify component renders correctly in different states describe('Rendering', () => { it('should render img element with correct alt and src when avatar URL is provided', () => { const avatarUrl = 'https://example.com/avatar.jpg' const props = { name: 'John Doe', avatar: avatarUrl } render() const img = screen.getByRole('img', { name: 'John Doe' }) expect(img).toBeInTheDocument() expect(img).toHaveAttribute('src', avatarUrl) }) it('should render fallback div with uppercase initial when avatar is null', () => { const props = { name: 'alice', avatar: null } render() expect(screen.queryByRole('img')).not.toBeInTheDocument() expect(screen.getByText('A')).toBeInTheDocument() }) }) // Props tests - verify all props are applied correctly describe('Props', () => { describe('size prop', () => { it.each([ { size: undefined, expected: '30px', label: 'default (30px)' }, { size: 50, expected: '50px', label: 'custom (50px)' }, ])('should apply $label size to img element', ({ size, expected }) => { const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size } render() expect(screen.getByRole('img')).toHaveStyle({ width: expected, height: expected, fontSize: expected, lineHeight: expected, }) }) it('should apply size to fallback div when avatar is null', () => { const props = { name: 'Test', avatar: null, size: 40 } render() const textElement = screen.getByText('T') const outerDiv = textElement.parentElement as HTMLElement expect(outerDiv).toHaveStyle({ width: '40px', height: '40px' }) }) }) describe('className prop', () => { it('should merge className with default avatar classes on img', () => { const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', className: 'custom-class', } render() const img = screen.getByRole('img') expect(img).toHaveClass('custom-class') expect(img).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600') }) it('should merge className with default avatar classes on fallback div', () => { const props = { name: 'Test', avatar: null, className: 'my-custom-class', } render() const textElement = screen.getByText('T') const outerDiv = textElement.parentElement as HTMLElement expect(outerDiv).toHaveClass('my-custom-class') expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600') }) }) describe('textClassName prop', () => { it('should apply textClassName to the initial text element', () => { const props = { name: 'Test', avatar: null, textClassName: 'custom-text-class', } render() const textElement = screen.getByText('T') expect(textElement).toHaveClass('custom-text-class') expect(textElement).toHaveClass('scale-[0.4]', 'text-center', 'text-white') }) }) }) // State Management tests - verify useState and useEffect behavior describe('State Management', () => { it('should switch to fallback when image fails to load', async () => { const props = { name: 'John', avatar: 'https://example.com/broken.jpg' } render() const img = screen.getByRole('img') fireEvent.error(img) await waitFor(() => { expect(screen.queryByRole('img')).not.toBeInTheDocument() }) expect(screen.getByText('J')).toBeInTheDocument() }) it('should reset error state when avatar URL changes', async () => { const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' } const { rerender } = render() const img = screen.getByRole('img') // First, trigger error fireEvent.error(img) await waitFor(() => { expect(screen.queryByRole('img')).not.toBeInTheDocument() }) expect(screen.getByText('J')).toBeInTheDocument() rerender() await waitFor(() => { expect(screen.getByRole('img')).toBeInTheDocument() }) expect(screen.queryByText('J')).not.toBeInTheDocument() }) it('should not reset error state if avatar becomes null', async () => { const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' } const { rerender } = render() // Trigger error fireEvent.error(screen.getByRole('img')) await waitFor(() => { expect(screen.getByText('J')).toBeInTheDocument() }) rerender() await waitFor(() => { expect(screen.queryByRole('img')).not.toBeInTheDocument() }) expect(screen.getByText('J')).toBeInTheDocument() }) }) // Event Handlers tests - verify onError callback behavior describe('Event Handlers', () => { it('should call onError with true when image fails to load', () => { const onErrorMock = vi.fn() const props = { name: 'John', avatar: 'https://example.com/broken.jpg', onError: onErrorMock, } render() fireEvent.error(screen.getByRole('img')) expect(onErrorMock).toHaveBeenCalledTimes(1) expect(onErrorMock).toHaveBeenCalledWith(true) }) it('should call onError with false when image loads successfully', () => { const onErrorMock = vi.fn() const props = { name: 'John', avatar: 'https://example.com/avatar.jpg', onError: onErrorMock, } render() fireEvent.load(screen.getByRole('img')) expect(onErrorMock).toHaveBeenCalledTimes(1) expect(onErrorMock).toHaveBeenCalledWith(false) }) it('should not throw when onError is not provided', async () => { const props = { name: 'John', avatar: 'https://example.com/broken.jpg' } render() expect(() => fireEvent.error(screen.getByRole('img'))).not.toThrow() await waitFor(() => { expect(screen.getByText('J')).toBeInTheDocument() }) }) }) // Edge Cases tests - verify handling of unusual inputs describe('Edge Cases', () => { it('should handle empty string name gracefully', () => { const props = { name: '', avatar: null } const { container } = render() // Note: Using querySelector here because empty name produces no visible text, // making semantic queries (getByRole, getByText) impossible const textElement = container.querySelector('.text-white') as HTMLElement expect(textElement).toBeInTheDocument() expect(textElement.textContent).toBe('') }) it.each([ { name: '中文名', expected: '中', label: 'Chinese characters' }, { name: '123User', expected: '1', label: 'number' }, ])('should display first character when name starts with $label', ({ name, expected }) => { const props = { name, avatar: null } render() expect(screen.getByText(expected)).toBeInTheDocument() }) it('should handle empty string avatar as falsy value', () => { const props = { name: 'Test', avatar: '' as string | null } render() expect(screen.queryByRole('img')).not.toBeInTheDocument() expect(screen.getByText('T')).toBeInTheDocument() }) it('should handle undefined className and textClassName', () => { const props = { name: 'Test', avatar: null } render() const textElement = screen.getByText('T') const outerDiv = textElement.parentElement as HTMLElement expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600') }) it.each([ { size: 0, expected: '0px', label: 'zero' }, { size: 1000, expected: '1000px', label: 'very large' }, ])('should handle $label size value', ({ size, expected }) => { const props = { name: 'Test', avatar: null, size } render() const textElement = screen.getByText('T') const outerDiv = textElement.parentElement as HTMLElement expect(outerDiv).toHaveStyle({ width: expected, height: expected }) }) }) // Combined props tests - verify props work together correctly describe('Combined Props', () => { it('should apply all props correctly when used together', () => { const onErrorMock = vi.fn() const props = { name: 'Test User', avatar: 'https://example.com/avatar.jpg', size: 64, className: 'custom-avatar', onError: onErrorMock, } render() const img = screen.getByRole('img') expect(img).toHaveAttribute('alt', 'Test User') expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg') expect(img).toHaveStyle({ width: '64px', height: '64px' }) expect(img).toHaveClass('custom-avatar') // Trigger load to verify onError callback fireEvent.load(img) expect(onErrorMock).toHaveBeenCalledWith(false) }) it('should apply all fallback props correctly when used together', () => { const props = { name: 'Fallback User', avatar: null, size: 48, className: 'fallback-custom', textClassName: 'custom-text-style', } render() const textElement = screen.getByText('F') const outerDiv = textElement.parentElement as HTMLElement expect(outerDiv).toHaveClass('fallback-custom') expect(outerDiv).toHaveStyle({ width: '48px', height: '48px' }) expect(textElement).toHaveClass('custom-text-style') }) }) })