mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Made-with: Cursor # Conflicts: # api/README.md # api/controllers/console/app/workflow_draft_variable.py # api/core/agent/cot_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/app/apps/advanced_chat/app_runner.py # api/core/plugin/backwards_invocation/model.py # api/core/prompt/advanced_prompt_transform.py # api/core/workflow/nodes/base/node.py # api/core/workflow/nodes/llm/llm_utils.py # api/core/workflow/nodes/llm/node.py # api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py # api/core/workflow/nodes/question_classifier/question_classifier_node.py # api/core/workflow/runtime/graph_runtime_state.py # api/extensions/storage/base_storage.py # api/factories/variable_factory.py # api/pyproject.toml # api/services/variable_truncator.py # api/uv.lock # web/app/account/oauth/authorize/page.tsx # web/app/components/app/configuration/config-var/config-modal/field.tsx # web/app/components/base/alert.tsx # web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx # web/app/components/base/chat/chat/answer/more.tsx # web/app/components/base/chat/chat/answer/operation.tsx # web/app/components/base/chat/chat/answer/workflow-process.tsx # web/app/components/base/chat/chat/citation/index.tsx # web/app/components/base/chat/chat/citation/popup.tsx # web/app/components/base/chat/chat/citation/progress-tooltip.tsx # web/app/components/base/chat/chat/citation/tooltip.tsx # web/app/components/base/chat/chat/question.tsx # web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx # web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx # web/app/components/base/markdown-blocks/form.tsx # web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx # web/app/components/base/tag-management/panel.tsx # web/app/components/base/tag-management/trigger.tsx # web/app/components/header/account-setting/index.tsx # web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx # web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx # web/app/signin/utils/post-login-redirect.ts # web/eslint-suppressions.json # web/package.json # web/pnpm-lock.yaml
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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 avatar container', ({ size, expected }) => {
|
|
const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size }
|
|
|
|
render(<Avatar {...props} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
const wrapper = img.parentElement as HTMLElement
|
|
expect(wrapper).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(<Avatar {...props} />)
|
|
|
|
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 container', () => {
|
|
const props = {
|
|
name: 'Test',
|
|
avatar: 'https://example.com/avatar.jpg',
|
|
className: 'custom-class',
|
|
}
|
|
|
|
render(<Avatar {...props} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
const wrapper = img.parentElement as HTMLElement
|
|
expect(wrapper).toHaveClass('custom-class')
|
|
expect(wrapper).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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
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(<Avatar {...initialProps} />)
|
|
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(<Avatar name="John" avatar="https://example.com/new-avatar.jpg" />)
|
|
|
|
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(<Avatar {...initialProps} />)
|
|
|
|
// Trigger error
|
|
fireEvent.error(screen.getByRole('img'))
|
|
await waitFor(() => {
|
|
expect(screen.getByText('J')).toBeInTheDocument()
|
|
})
|
|
|
|
rerender(<Avatar name="John" avatar={null} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
// 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(<Avatar {...props} />)
|
|
|
|
expect(screen.getByText(expected)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty string avatar as falsy value', () => {
|
|
const props = { name: 'Test', avatar: '' as string | null }
|
|
|
|
render(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
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(<Avatar {...props} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
const wrapper = img.parentElement as HTMLElement
|
|
expect(img).toHaveAttribute('alt', 'Test User')
|
|
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
|
|
expect(wrapper).toHaveStyle({ width: '64px', height: '64px' })
|
|
expect(wrapper).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(<Avatar {...props} />)
|
|
|
|
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')
|
|
})
|
|
})
|
|
})
|